/*
  The two navigation rows (#485) -- one stylesheet shared by every page
  that loads it (study.html, settings.html, tutorial.html, ... -- #1907 and
  #1915 took the reader, the queue page and the compare page out of that
  list), the same shape and for the same reason as shared/build_stamp.css
  beside it: the row has to look and behave identically everywhere, and a
  fix here has to fix it everywhere.

  The rule this file exists to draw, in the owner's words:

      top = act on what is in front of you
      bottom = go somewhere else

  So the BOTTOM row (`.milim-nav`) is the destinations -- built entirely by
  shared/nav.js from its own `DESTINATIONS` list, identical in position and
  content on every page -- and the only thing this file contributes to the TOP
  row is `.milim-flag`, the feedback control (#480), which is an action on the
  current screen and therefore lives up there with the source picker and the
  audio toggle. Everything else in a top rail is page-specific and stays in
  that page's own stylesheet.

  ## Every colour below is this file's own -- #509, #530, #532

  This used to say the opposite: the six host pages do not agree on token
  NAMES (study.css calls the page ground `--ground`, the hairline `--line`,
  the accent `--accent` and the alarm hue `--no`; the other five call the same
  four `--chalk`, `--rule`, `--blue` and `--clay`), so every colour below used
  to be a `var()` fallback CHAIN -- `var(--rule, var(--line, #D9D5CB))` -- that
  picked whichever host's name happened to exist and fell to a literal only as
  a last resort.

  That shape is a defect, not a design, and #509/#530/#532 are the same
  defect found three times: a shared component whose colour is decided by
  which host happens to omit a variable has no defined behaviour. #509 fixed
  it for the current-page indicator by giving that one property nav.css's own
  token; #530 (the flag's alarm state, falling through to a light-tuned
  literal on tutorial.css's dark page, the one host defining neither `--clay`
  nor `--no`) and #532 (the feedback submit button, white text on a
  background that inverted the wrong way for dark mode on every host, and
  landed on a third colour again by omission on study.css) are the same shape
  in two more selectors that nobody had measured against all six hosts yet.
  Rather than patch those two and leave the next selector free to reintroduce
  it, every colour this file declares is now its own: a literal palette below,
  read bare, with nothing left for a host page to shadow or fall through to.
  `tests/test_nav_contract.py::test_no_var_read_by_this_file_is_left_undefined`
  is the guard that keeps it that way -- it does not enumerate selectors, it
  parses this file and asserts every `var(--x)` it reads is a name this file
  itself defines above.

  The literals were chosen to keep rendering the SAME colour the majority of
  hosts already resolved their end of the old chain to, not to invent a new
  palette -- see the PR for #530/#532 for the six-host, both-theme measurement
  before and after.

  ## What is, and is not, self-contained

  The bottom row paints its own `background` (`--milim-nav-surface`) and the
  feedback sheet does too, so both are now fully opaque, self-contained
  surfaces: nothing about their contrast depends on which page they sit on.
  The top row's flag is different -- it has no background of its own by
  design (a transparent hit target over whatever the host page painted), so
  it still sits directly on each host's own ground, and its contrast still has
  to be checked against all six, in both themes. That is still true after this
  file owns every colour it declares; owning the FOREGROUND does not create a
  background where the layout never painted one. `tests/test_nav_contract.py`
  keeps checking the flag's alarm colour (and its muted rest colour) against
  every real host ground for exactly that reason.

  ## The floors

  44px minimum on every tap target in both rows -- the same floor
  tests/test_beat_word_match.py and tests/test_beat_reward.py already hold the
  match board and the reward dismiss to, and the one #482 was about. Pinned
  here by tests/test_api_chrome.py's
  `test_every_tap_target_in_the_shared_nav_clears_the_44px_floor` (parses this
  file with the real CSS parser rather than grepping the string, because a
  comment can satisfy a grep) -- not by the JS harness, whose own header now
  says plainly that jsdom runs no layout engine and leaves size assertions to
  that Python test; this comment used to claim otherwise and was wrong.

  `env(safe-area-inset-bottom)` is not optional on a bar that sits exactly
  where iOS draws the home indicator. All six pages set `viewport-fit=cover`,
  which is what makes the inset non-zero in standalone mode; it resolves to
  0px in an ordinary browser tab, so the desktop layout is unchanged.

  #542: on a phone in standalone mode this inset used to be pure dead space --
  all of it landed in `.milim-nav`'s own `padding-bottom`, below a content row
  that was centred as if the inset did not exist, so the icon+label block sat
  stranded near the top of a bar that grew a blank footer band underneath it
  ("wasted footer area", the owner's own words, confirmed on a real iPhone in
  standalone -- see the PR for #542). The bar's total height must still equal
  `--milim-nav-h` + `env(safe-area-inset-bottom)`, because #buildStamp's own
  `bottom:` below and `app/static/study/audio.js`'s `AUDIO_DEBUG` panel
  (outside this file's fence) both compute their own position from exactly
  that sum -- so the fix cannot shrink the total, only redistribute it. It now
  splits the inset
  evenly between `padding-top` and `padding-bottom` instead of dumping all of
  it below: `.milim-nav-item` itself is untouched (still stretched to exactly
  `--milim-nav-h`, still centred within that box per the comment at its own
  padding below), so the icon+label ink ends up equidistant from the bar's
  true top and bottom edges for ANY inset, not just the 0px a desktop tab
  measures. The tap target (the anchor, `.milim-nav-item`) never grows into
  the inset -- it stays exactly `--milim-nav-h` tall, positioned inset/2 away
  from the physical edge on both sides, so it does not gain a hit-testable
  strip flush against where iOS reads its own edge-swipe gesture. That is a
  judgement call, not a proof: giving the tap target the FULL inset as
  clearance (rather than half) is not simultaneously achievable with
  symmetric ink at a fixed total bar height -- the two are in tension, and the
  PR body has the arithmetic. inset/2 is comfortably more than the home
  indicator pill itself needs (it is a few px tall, drawn near the physical
  edge), which is the basis for calling this safe.

  #596: the owner's very next comment after #542 merged -- "buttons are now
  centered instead of raised above the center line... I did like that they
  were raised a bit above from center." #542's round-1/round-2 split above is
  UNCHANGED by this: it is still what makes the ink's offset from the bar's
  true edges equal for any inset. What changes is `.milim-nav-item`'s own
  (non-inset) padding, which #542 round 1 deliberately made equal -- #596
  makes it deliberately unequal by one named amount, `--milim-nav-ink-bias`
  (declared below, with the measurement that picked its value). See that
  token's own comment and `.milim-nav-item`'s padding-top/padding-bottom.

  #597: the sentence above -- "the bar's total height must equal
  `--milim-nav-h` + `env(safe-area-inset-bottom)`" -- was NOT TRUE, and had
  never been true on a phone. Every host page declares `* { box-sizing:
  border-box }`, so `.milim-nav`'s `min-height: var(--milim-nav-h)` was a
  minimum on the BORDER box, not on the content row: the inset padding was
  absorbed INTO the 58px until the intrinsic content outgrew it, instead of
  adding to it. Measured in Chrome on `/study` with the inset simulated at
  34px (the owner's iPhone 15), the bar rendered **84px**, not 92px --
  content row 49px + inset 34px + 1px border -- and `--milim-nav-h` was
  therefore not controlling the bar's height at all at any inset above 8px.
  It only controlled the two things that READ it, which is how three
  dependents ended up positioned off a number the bar did not have:

    - `#buildStamp`'s `bottom:` resolved to 98px against a bar whose top edge
      was at 84px, so the stamp floated 14px above the bar rather than the
      6px its own rule asks for (visible in #772's phone screenshot).
    - `AUDIO_DEBUG`'s panel offset in `app/static/study/audio.js` has the
      same 8px error, from the same sum.
    - `.milim-nav-spacer` reserved 58px in flow for an 84px bar, so the last
      26px of every page's content sat under it -- the drift this file's own
      token comment says a repeated literal would cause, arriving instead
      through a `box-sizing` rule in a different file.

  The fix is to stop inferring the bar's height and state it: `--milim-nav-
  total` below is the ONE expression of the bar's rendered height, and the
  bar, the flow spacer and #buildStamp all read that single token. The bar
  now carries `height`, not `min-height`, because a minimum is exactly what
  let the rendered height and the reserved height disagree -- see the
  token's own comment for what that trades away.

  #2714 SHRINKS the total, which #542 above said could not be done, because
  the thing that pinned it moved: the build stamp no longer sits in the strip
  above the row, it sits at the row's floor under the labels. With nothing in
  the upper strip it is dropped (`padding-top: 0`), the lower strip -- the
  home indicator's -- keeps its inset/2, and the row is trimmed to the
  content it holds (`--milim-nav-h` 58px -> 52px). Measured, bar height:
  58px -> 52px in a browser tab (plus the 17px band above the bar the stamp
  used to cost every page), and 92px -> 69px at the owner's 34px inset. The
  ink is no longer centred between two equal strips (#542) -- it sits near
  the top, with the stamp and then the home indicator below it; #596's
  "raised a bit above centre" is kept in spirit and exceeded in fact, and the
  owner has the screenshots on #2714 to overrule it. `.milim-nav`'s own
  padding and `#buildStamp` carry the numbers.
*/

:root {
  /* The bar's own height, before the safe-area inset. Declared once and used
     twice -- as the fixed bar's `min-height` and as the flow spacer's `height`
     -- so the two cannot drift apart. They are two elements that must always
     be exactly the same size, and a repeated literal is precisely how that
     stops being true: the bar would grow with its content while the spacer
     stayed put, and the page's last line would slide under it by however much
     the label grew. Stated as a `min-height` rather than a `height` so the bar
     can still grow if a future icon needs it -- at which point the spacer
     grows with it, because it is the same number.

     #2714: 58px -> 52px. The row's own content is 49px -- `.milim-nav-item`'s
     4px + 10px padding, the 22px icon, the 3px gap and the 10px label -- and
     the row is this minus the 1px border-top, so 52px leaves 2px over, which
     the item's `justify-content: center` splits 1px above the icon and 1px
     under the label. That 1px is for the build stamp, which now sits in the
     10px under the labels (see `#buildStamp` below): measured in Chrome, 50px
     put the stamp's pill flush against the labels' ink and it read as one
     smudge; 52px leaves a visible gap. The anchor is 51px tall against its
     44px floor (it was 57px -- 8px of the old 58 was empty row the
     icon+label were centred in). The owner, on /study once #2683's top band
     had taken 28px from the column: "Can we trim some of its height". */
  --milim-nav-h: 52px;

  /* #597: the bar's TOTAL rendered height, inset included -- the one number
     the fixed bar's own `height`, the flow spacer's `height` and
     #buildStamp's `bottom:` all read, so the rendered height and the
     reserved height cannot disagree. They used to: the bar inferred its
     height from its content under `box-sizing: border-box` while the spacer
     read `--milim-nav-h` directly, and on a phone those were 84px and 58px
     (see the #597 section of this file's header for the measurement).

     This is why the bar below says `height` and not `min-height`. That
     trades away the "the bar can still grow if a future icon needs it"
     property the token comment above claims -- a property that was never
     real, because the spacer could not grow with it: growing the bar past
     `--milim-nav-h` is precisely what produced the 26px of covered content.
     A taller row is now a deliberate edit to `--milim-nav-h`, which moves
     the bar and everything positioned off it together.
     `tests/js/test_nav_behaviour.mjs` pins that these four declarations
     keep reading this one token, and `tests/test_nav_geometry.py` pins that
     the row it leaves for `.milim-nav-item` still clears the 44px floor.

     Declared on `body` below rather than here, for a reason that is not
     cosmetic -- see that rule. */

  /* #530/#532: this file's own palette for everything that used to be a
     host-variable chain. `--milim-nav-ink`, `--milim-nav-muted`,
     `--milim-nav-surface` and `--milim-nav-rule` are the literals five of the
     six hosts already resolved their `--ink`/`--muted`/`--surface`/
     `--rule`-or-`--line` to (study.css's own spellings differ by a few points
     of luminance at most -- see the PR's measurement table for the actual
     ratios against this file's now-fixed values on all six). `--milim-nav-
     alarm` is `--clay`'s literal, the four-host majority for the flag's
     pressed/hovered state -- picked over `--no`'s (study's) because it is the
     value already failing to reach two of the six hosts (tutorial.css) rather
     than a fresh third colour. */
  --milim-nav-ink: #17191e;
  --milim-nav-muted: #6e6a60;
  --milim-nav-surface: #fbfaf7;
  --milim-nav-rule: #d9d5cb;
  --milim-nav-alarm: #b44a32;

  /* #532: white text on a background that must clear AA in BOTH themes.
     Deliberately one literal, not a light/dark pair -- see the `@media`
     block below for why giving this a dark override would just reintroduce
     the defect under a new name. */
  --milim-nav-submit-bg: #2a4fc4;
  --milim-nav-submit-fg: #ffffff;

  /* #509: the ONE colour this file owned outright before #530/#532 rather
     than borrowing from a host page. Everything above now follows the same
     shape; this token's own history is worth keeping because it explains
     WHY amber and not blue -- "where you already are" used to read
     `var(--blue, var(--accent, #2A4FC4))` -- study.css is the only page
     stylesheet that never defined `--blue`, so it alone fell through to its
     own `--accent` and rendered amber while the other four, which all define
     `--blue`, rendered blue. A shared component whose current-page colour is
     decided by which host happens to omit a variable has no defined
     behaviour, so the lookup is gone: this token is the whole answer, on
     every page, and no page stylesheet may define it (see
     tests/test_nav_contract.py).

     Amber, not blue: `--blue` means "an unknown word" everywhere it is
     spelled in this codebase (flashcards.css's `.pill.unknown` is the
     surviving example) -- a word-status colour, not UI chrome. Amber is the app's
     actual control accent (the AUDIO pill, the flag's pressed state, the
     study progress rail), so the current-page indicator uses the same two
     literals study.css's own `--accent` already does, just reattached to the
     OS theme signal the other four pages use it under (their default is
     light, study.css's default is dark, but `prefers-color-scheme` is the
     same real signal in both cases) -- verified >=4.5:1 against every page
     this file loads on, both themes, in
     tests/test_nav_contract.py::test_the_current_item_colour_clears_aa_on_every_page_both_themes.

     #530/#532: also reused below as the general focus-ring / "selected"
     accent for `:focus-visible` outlines and the feedback sheet's pressed
     label, in place of the same `var(--blue, var(--accent, ...))` chain that
     broke the submit button -- one already-proven-AA accent token, not a
     second one minted to match. */
  --milim-nav-current: #8a6410;

  /* #596: follow-up to #542. #542 split the safe-area inset evenly between
     `.milim-nav`'s own padding-top/padding-bottom, which put the icon+label
     ink at exact geometric centre of the bar (measured 25px above / 24px
     below at a real 34px inset). The owner, immediately after that merged:
     "I did like that they were raised a bit above from center" -- dead
     centre is not what was wanted; a few px ABOVE centre is, which is the
     normal instinct for an icon-plus-label row (the label's descenders and
     the icon's stroke weight both read as visual mass sitting low in the
     box, even when the box itself is symmetric).

     This is a taste value, not a derived one, so it is named and isolated
     here rather than baked into two padding literals someone would have to
     recompute in lockstep: `.milim-nav-item`'s own padding-top and
     padding-bottom below both read this ONE property, one subtracting it
     and one adding it, off the same base literal -- moving this number is
     the entire retune. It lives on `.milim-nav-item`'s own (non-inset)
     padding, not on `.milim-nav`'s inset split above, so it applies
     identically at zero inset (desktop) and at any safe-area inset, and
     touches neither `--milim-nav-h` nor the inset split -- the bar's total
     height, #buildStamp's `bottom:` and AUDIO_DEBUG's own offset (both
     outside this file) are all unaffected.

     3px chosen by measuring in a real Chrome window at both a 0px and a
     simulated 34px safe-area inset (see the PR body for #596 for the
     numbers) against the ticket's own suggested range (roughly 3-5px more
     space below the ink than above it) -- NOT confirmed by the owner on a
     physical device yet, so treat this literal as the one candidate the
     ticket asked for, not a settled answer. Retune by editing only this
     line. */
  --milim-nav-ink-bias: 3px;
}

@media (prefers-color-scheme: dark) {
  :root {
    --milim-nav-ink: #e7e4dd;
    --milim-nav-muted: #9a9487;
    --milim-nav-surface: #1d2027;
    --milim-nav-rule: #31353e;
    --milim-nav-alarm: #de8168;
    --milim-nav-current: #f2c14e;

    /* #532: no --milim-nav-submit-bg/-fg override here, on purpose. The
       defect this ticket fixed was a background that INVERTS for dark mode
       the way a text colour must: `--blue`'s dark value (#8AA0F0) and
       `--accent`'s dark value (#F2C14E) are both light "text on a dark
       ground" tints, and white text on either one measures 2.51:1 / 1.68:1,
       both under AA. A button BACKGROUND is not text -- what has to stay
       legible is the white text sitting on it, and #2A4FC4 already clears AA
       against white in both themes (6.97:1; contrast math has no theme).
       Overriding it here for "consistency" would re-introduce the exact
       inversion that broke it, just spelled with a new token. */
  }
}
/* #2617: the in-app theme override, same reasoning as settings.css's own
   `[data-theme]` pair -- an unconditional attribute selector on `:root`
   outranks both the bare rule and the `@media` rule above on specificity
   alone, so whichever is present always wins over the OS and neither is
   present for `auto`. This file's base `:root` above is light (the
   default), so the light block here just restates the base's own values
   for the six tokens the dark block overrides; the dark block is that
   `@media` block above, unchanged -- `--milim-nav-submit-bg`/`-fg` stay out
   of both, for the identical reason the dark block's own comment gives. */
:root[data-theme="dark"] {
  --milim-nav-ink: #e7e4dd;
  --milim-nav-muted: #9a9487;
  --milim-nav-surface: #1d2027;
  --milim-nav-rule: #31353e;
  --milim-nav-alarm: #de8168;
  --milim-nav-current: #f2c14e;
}
:root[data-theme="light"] {
  --milim-nav-ink: #17191e;
  --milim-nav-muted: #6e6a60;
  --milim-nav-surface: #fbfaf7;
  --milim-nav-rule: #d9d5cb;
  --milim-nav-alarm: #b44a32;
  --milim-nav-current: #8a6410;
}

/* ---------------------------------------------------------------------------
   The bottom row: where you can go.
   --------------------------------------------------------------------------- */

/* #597: `--milim-nav-total` is declared HERE, on `body`, and deliberately not
   on `:root` beside `--milim-nav-h`. A custom property's `var()` references
   are substituted at computed-value time on the element that DECLARES it, and
   the result is what inherits -- so `--milim-nav-total` declared on `:root`
   would resolve `var(--milim-nav-h)` against `:root`'s 58px once and hand
   every descendant that baked-in number. A page (or, since #885, the labels
   setting below) retuning `--milim-nav-h` on `body` would then move the bar
   and NOT the spacer or the stamp -- which is the identical class of defect
   this ticket is fixing, reintroduced by where a declaration sits. On
   `body`, both properties resolve on the same element, so the retune and the
   total cannot come apart.

   Everything that reads it -- `.milim-nav`, `.milim-nav-spacer`, `#buildStamp`
   and audio.js's AUDIO_DEBUG panel -- is a descendant of `body`, so nothing
   loses access by the move. */
body {
  /* #2714: `+ inset / 2`, not `+ inset`. The bar used to carry the WHOLE
     inset -- half above its row and half below (#542) -- and the half above
     held nothing but the build stamp (#1409). The stamp now sits at the
     bar's floor instead, so that upper strip is gone and only the lower half
     -- the home indicator's strip -- is still reserved. See `.milim-nav`'s
     own padding below for the measurement and for what this does to #542. */
  --milim-nav-total: calc(var(--milim-nav-h) + env(safe-area-inset-bottom) / 2);

  /* #2714: where the bar's own icon+label ROW ends, measured from the
     viewport's bottom edge -- which is exactly `.milim-nav`'s own
     `padding-bottom` (the home indicator's strip), read from the far side:

         --milim-nav-total      =  --milim-nav-h + inset/2
         --milim-nav-row-bottom =                  inset/2

     It replaces #1409's `--milim-nav-row-top`, which named the TOP of the row
     because the stamp stood there, in the strip of inset the bar kept above
     its icons. That strip is gone (see `--milim-nav-total` above) and the
     stamp moved to the other end of the row -- the owner's words, "put the
     stamp at the floor of the page instead of the top of the bottom bar UI".
     Read by `#buildStamp`'s `bottom` below and by nothing else. On `body` for
     the same reason as `--milim-nav-total`, although it does not read
     `--milim-nav-h` today: a token that names a boundary of the bar belongs
     beside the one that names its height. */
  --milim-nav-row-bottom: calc(env(safe-area-inset-bottom) / 2);

  /* #2714. THE CLEARANCE, overridden for pages that have a bottom bar: none.

     shared/build_stamp.css publishes 23px on `:root` -- 6px of lift plus the
     ink -- for the pages with nothing but their own content under the stamp.
     On a page with a bar the stamp now sits INSIDE the bar, in the row's own
     bottom padding under the labels (`#buildStamp` below), at every inset:
     its ink runs from `--milim-nav-row-bottom` up by 8.8px (that rule's own
     8px x 1.1, no vertical padding), inside the row's 10px of bottom padding
     (`7px + --milim-nav-ink-bias`), so no part of it reaches above the bar's
     top edge and no page has anything to hold out of its way.

     History, because the number moved three times: 30px hand-typed (#572),
     23px published (#1196), `max(0px, ink - inset/2)` = 17px in a browser tab
     and 0px on the owner's phone (#1409, which stood the stamp on TOP of the
     row), and now 0px everywhere. The token stays, rather than every page
     dropping its `var()`, because the bar-less pages still need the `:root`
     value and a page that reads the token moves with the stamp the next time
     it moves -- #952's whole lesson. `tests/test_build_stamp_keepout.py`
     re-derives the 0 from the geometry rather than trusting this line. */
  --milim-stamp-keepout-block: 0px;
}

/* #885: the label-hiding history, kept rather than deleted, because the
   reasoning in both directions is the expensive part to re-derive.

   #597 (2026-08-16, "labels_then_autohide") dropped the destination LABELS
   on `/study` only, keeping the icons, to give the study screen back the
   ~10px the row's content no longer needed once the label was gone --
   `body[data-milim-page="study"] { --milim-nav-h: 48px; }` plus
   `body[data-milim-page="study"] .milim-nav-label { display:none; }`,
   scoped to that one page. PR #780 shipped it and measured an 18px gain on
   the study screen.

   #885 overruled it two days later, by the owner's own words: "This has been
   made overly complex. The bottom button bar should be consistent. Icons and
   labels are all present. But the text should be an optional item." His
   reasoning is decluttering a screen with "a lot of distractions", not
   reclaiming space -- and he wants it OFF BY DEFAULT ON, app-wide, with an
   opt-out in settings, not a page that silently differs from the other five.
   #889 (auto-hide, "the real answer" half of #597's original two-step plan)
   is superseded by this and closed alongside it.

   So the bar is identical on every page again -- no `data-milim-page`
   selector reads `--milim-nav-h` or hides `.milim-nav-label` any more, and
   the 18px PR #780 measured goes back, which is the owner's explicit trade
   and is not clawed back here by an auto-hide or any other clever
   alternative. What replaces the page-scoped rule is a SETTING-scoped one,
   same two-halves-are-one-change shape #597 already established (retuning
   `--milim-nav-h` only together with hiding the label): `shared/nav.js`
   reads the labels preference from `localStorage`
   (`milim.settings.navLabels`, default ON) and sets
   `data-milim-nav-labels="off"` on `body` at boot, before the bar is even
   built, so there is nothing to flash -- see nav.js's own comment at the
   read. `app/static/settings.js` owns the control that writes that key.

   Kept in nav.css rather than settings.css on purpose, same argument #597's
   comment already made for the page-scoped version: `tests/test_nav_contract.
   py`'s `test_no_page_stylesheet_shadows_the_current_item_rule` states the
   principle that this component is styled in exactly one file. */
body[data-milim-nav-labels="off"] {
  --milim-nav-h: 48px;
}

body[data-milim-nav-labels="off"] .milim-nav-label {
  display: none;
}

.milim-nav {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  /* Under the build stamp (2147483647) and above everything else. The stamp is
     `pointer-events:none`, so the two never fight over a touch. */
  z-index: 2147483000;
  display: flex;
  justify-content: center;
  /* #597: `height`, not `min-height` -- see `--milim-nav-total`'s comment.
     A minimum let the bar's rendered height be decided by its content plus
     the inset (84px on a phone) while every dependent read 58px + the inset
     (92px) and the flow spacer reserved 58px. */
  height: var(--milim-nav-total);
  background: var(--milim-nav-surface);
  border-top: 1px solid var(--milim-nav-rule);
  /* #542: split, not all-bottom. `.milim-nav-item` (below) is stretched to
     fill exactly `--milim-nav-h` regardless of this padding -- these two
     declarations are pure chrome around that fixed-height row, so putting
     half the inset above it and half below moves the row itself to the
     vertical centre of the bar's full height (--milim-nav-h + the inset)
     instead of leaving it pinned to the top with the whole inset as dead
     space underneath. `padding-top` and `padding-bottom` must stay identical
     expressions -- that identity is the whole fix, see
     tests/js/test_nav_behaviour.mjs. */
  padding-top: 0;
  padding-bottom: calc(env(safe-area-inset-bottom) / 2);
  /* #2714 ENDS #542's identity above, on purpose, and only the upper half of
     it. #542 split the inset evenly so the row sat centred in the bar; #1409
     then put the build stamp in the upper strip, and that strip was the one
     thing on the owner's phone the stamp's move frees. Measured, real Chrome
     at 393x852 with the owner's 34px inset (`Emulation.setSafeAreaInsetsOverride`):
     the bar was 92px -- 1px border + 17px strip + 57px row + 17px strip --
     and is now 69px: the border + a 51px row + the 17px lower strip. The lower
     strip stays exactly as #542 sized it, because it is the home indicator's
     (~8..13px off the floor) and #1409 measured it as the one band ink must
     not enter. What changes visually is that the icon+label sit higher than
     centre instead of 3px above it (#596) -- with the stamp and the home
     indicator now filling the space below them. The owner has not seen this
     on the device yet; #2714 carries the screenshots. */
  /* The bar is chrome, not content: it must never be the thing that produces a
     horizontal scrollbar on a narrow phone. */
  overflow-x: hidden;
}

.milim-nav-inner {
  display: flex;
  width: 100%;
  /* study.css's `.phone` is 430px and the bar is four icons wide at most --
     stretching it across a page's full-width column (skills.css's and
     flashcards.css's `.page` run to 44rem) would put the destinations
     a hand's width apart on a desktop and nowhere near a thumb on a phone. */
  max-width: 430px;
  padding: 0 calc(env(safe-area-inset-left)) 0 calc(env(safe-area-inset-right));
}

.milim-nav-item {
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  /* The 44px floor. `min-height` on the anchor itself, not on an inner box:
     the anchor is what receives the tap. */
  min-height: 44px;
  /* #542, round 1 (superseded by #596 below): measured live (Chrome,
     875px-tall viewport, safe-area-inset 0) at top-offset 10.5px /
     bottom-offset 11.5px for the icon+label ink with EQUAL 7px top/bottom
     padding -- `justify-content: center` on this column splits whatever
     space is left evenly on both sides regardless of the padding split, so
     equal padding was what "optically centred" reduced to at zero inset.
     `.milim-nav`'s own padding above is round 2, the actual #542 fix, and is
     still untouched by anything below: it stays the even inset split: this
     rule's box is still flush-symmetric within its own --milim-nav-h box,
     the inset-driven centring still happens one level up.

     #596: the owner did not want dead centre -- "I did like that they were
     raised a bit above from center", immediately after #542 merged. Equal
     top/bottom padding is exactly what round 1 above measured as producing
     that centre, so raising the ink means UNEQUAL padding here on purpose:
     less above, more below, by `--milim-nav-ink-bias` (declared once, at the
     top of this file, with the measurement that produced its value). Both
     sides still read the same 7px base literal -- the bias is the only
     difference between them -- and their SUM is still 14px, so this rule's
     own box height and the 44px floor below are both exactly as unaffected
     by this as round 1 always was. Guarded by tests/js/test_nav_behaviour.mjs
     and tests/js/test_css_contract.mjs, both updated by #596 for the new
     invariant: padding-top's base equals padding-bottom's base, and their
     signs on the bias are opposite. */
  padding-top: calc(7px - var(--milim-nav-ink-bias));
  padding-right: 4px;
  padding-bottom: calc(7px + var(--milim-nav-ink-bias));
  padding-left: 4px;
  text-decoration: none;
  color: var(--milim-nav-muted);
  font-family: var(--ui, system-ui, -apple-system, sans-serif);
  -webkit-tap-highlight-color: transparent;
  /* #2555: the containing block for the hit strip below. */
  position: relative;
}

/* #2555: the whole bar is the tap target, not only its middle.

   The owner, on /study on his iPhone: taps on the bar "intermittently do
   nothing ... navigating to another section can take many taps". Measured
   in real Chrome at 393x852 with the safe-area inset set to his iPhone 15's
   34px (`Emulation.setSafeAreaInsetsOverride`,
   `scripts/bar_hit_probe_2555.py`): the bar is painted from 760 to 852 but
   `.milim-nav-item` only spans 778..835, because #542 puts half the inset
   in `.milim-nav`'s own padding ABOVE the row and half BELOW it. The 17px
   strip over the icons and the 17px strip under the labels hit-tested to
   `.milim-nav` itself, which is not a link: 38% of the bar's painted area
   took a tap and did nothing. Nothing covers the bar -- the probe walked
   every /study card state and found no element over it -- the dead area
   was the bar's own padding. In a browser tab the inset is 0px, both
   strips vanish and the bar is one row of links, which is why no desktop
   check ever saw it.

   So each link's hit area is stretched over both strips with a
   pseudo-element: it hit-tests as the link, so a tap anywhere in the link's
   column of the bar goes where that column's icon says. Nothing moves --
   `.milim-nav`'s padding, the icon+label position (#542/#596), the bar's
   height (#597) and #buildStamp's band (#1409) are all untouched; only
   the area that answers a tap grows.

   This reverses one sentence of #542's comment above, on purpose: #542
   kept the link out of the lower strip so it would not be "a hit-testable
   strip flush against where iOS reads its own edge-swipe gesture", and
   called that a judgement call. It cost more than it bought: the home
   gesture is a swipe, and a swipe is a drag, which never produces a
   `click` -- so a link reaching the edge adds no way to navigate by
   accident, while a dead strip under the labels is exactly where a thumb
   aiming at a label lands short. NOT verified on a device: this is Chrome
   with the inset simulated, and #2555 stays open for the phone check.

   `.milim-nav` is `overflow-x: hidden`, which makes it clip its content to
   the padding box -- both strips are padding, so the stretch is inside the
   clip, and the 1px border-top stays as it was. */
.milim-nav-item::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  /* #2714: `0`, was `calc(env(safe-area-inset-bottom) / -2)` -- the strip
     above the row is gone (see `.milim-nav`'s padding), so the row's own top
     edge IS the bar's top edge inside its border, and there is nothing
     above it to stretch over. The lower stretch is unchanged. */
  top: 0;
  bottom: calc(env(safe-area-inset-bottom) / -2);
}

.milim-nav-icon {
  width: 22px;
  height: 22px;
  display: block;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  flex: 0 0 auto;
}

/* #485's own warning: "icons on a bottom bar are a discoverability risk in
   exactly the way a long-press was rejected for". The label is the answer, and
   it is not optional -- a bar of three unlabelled glyphs is a puzzle. */
.milim-nav-label {
  font-size: 10px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1;
  white-space: nowrap;
}

/* Where you already are. `aria-current="page"` is the state -- the styling
   hangs off the attribute rather than off a class, so the thing a screen
   reader announces and the thing an eye sees cannot come apart. */
.milim-nav-item[aria-current="page"] {
  color: var(--milim-nav-current);
}

.milim-nav-item[aria-current="page"] .milim-nav-icon {
  stroke-width: 2;
}

.milim-nav-item:focus-visible {
  outline: 2px solid var(--milim-nav-current);
  outline-offset: -2px;
  border-radius: 6px;
}

@media (hover: hover) {
  .milim-nav-item:hover {
    color: var(--milim-nav-ink);
  }
}

/* ---------------------------------------------------------------------------
   The flow spacer: the bar is fixed, so the page has to reserve its height at
   the end of its own scrolling content or the last paragraph sits under it.
   Appended to `[data-milim-main]` -- each page's own top-level column -- and
   not to `<body>`, because study.css's `.phone` is `min-height:100dvh`: a
   spacer after it would add scroll height below a full-viewport column while
   leaving `.foot` exactly where it was, i.e. still under the bar.
   --------------------------------------------------------------------------- */

.milim-nav-spacer {
  /* #597: `--milim-nav-total`, not `--milim-nav-h`. The bar overlays its own
     height PLUS the safe-area inset; reserving only the former left the last
     26px of content under the bar on a phone, which is what the owner
     reported twice ("still has a gap", #597; the occluded advance control in
     #772). This is the same token the bar's own `height` reads. */
  height: var(--milim-nav-total);
  flex: 0 0 auto;
}

/* The build stamp (#416) is `position:fixed; bottom:6px; right:9px`, which on
   a phone is exactly on top of the bottom row's rightmost destination -- at
   430px the bar's inner column reaches the right edge, so the stamp lands over
   that item's label. It does not block the tap (`pointer-events:none`), which
   is the worse failure: the destination is still hittable while looking
   defaced, so nothing is obviously broken and the corner just reads as a bug.

   Lifted here rather than in shared/build_stamp.css because this is not a fact
   about the stamp -- it is a fact about pages that have a bottom bar, and the
   stamp is on four pages, all of which now load this file after that one.
   Flagged on #485 rather than moved silently. */
#buildStamp {
  /* #2714 (the owner, 2026-09-28, on /study: "put the stamp at the floor of
     the page instead of the top of the bottom bar UI"): the stamp sits at the
     FLOOR of the bar's icon row, in the row's own 10px of bottom padding under
     the labels. Its bottom edge is `--milim-nav-row-bottom` -- the top of the
     home indicator's strip -- so:

       in a browser tab (inset 0)   it sits on the viewport's bottom edge;
       on the owner's phone (34px)  it sits 17px up, just above the home
                                    indicator (~8..13px off the floor).

     Measured, real headless Chrome, every bar page (/study, /skills,
     /progress, /settings). At 393x852 with a 34px bottom inset set by
     `Emulation.setSafeAreaInsetsOverride`: bar 783..852, label boxes
     814..824, stamp 826.2..835, the lower strip 835..852 (the home
     indicator draws ~839..844). At 375x667, inset 0: bar 615..667, label
     boxes 646..656, stamp 658.2..667 -- on the viewport's floor.

     SMALLER than the bar-less stamp, on purpose, and only here: 8px type on
     a 1.1 line box with no vertical padding is 8.8px of ink, which fits the
     11px under the label boxes (the item's 10px bottom padding plus the 1px
     `--milim-nav-h` leaves), so it never paints over a label however long
     the text gets -- a worktree build's stamp runs to ~270px and crosses two
     items. 8px mono at a phone's 3x is 24 device pixels a glyph; it read
     cleanly in the #2714 crops. 9px (9.9px of ink) was tried first and sat
     flush on the labels. The 17px stamp #1409 stood on the row
     could not fit here: `bottom:0` with it clipped every label (#1409's own
     negative control). The bar-less pages keep build_stamp.css's 10px/1.5
     stamp at `bottom:6px`, which is what `--milim-stamp-ink-block` and the
     `:root` keep-out still describe.

     History this replaces, kept short: #597 lifted the stamp 6px above the
     bar, which cost every bar page 23px of its column; #1409 stood it ON the
     row, in the strip of inset the bar kept above its icons, which took the
     owner's phone to zero but left 17px above the bar in a browser tab. #2714
     moves it to the other end of the row, which is zero at every inset, and
     lets `.milim-nav` drop the upper strip entirely (see its padding).

     Unchanged and load-bearing: `pointer-events: none` (#973/#575) -- it now
     sits over the bottom of the bar's own links, so a tap on it must still
     reach the link underneath, and it does. It is still the real
     `<a href="/release-notes">` build_stamp.js builds, focusable by keyboard;
     the pointer door into the release notes is the settings row. */
  bottom: var(--milim-nav-row-bottom);
  font-size: 8px;
  line-height: 1.1;
  padding: 0px 5px;
  border-radius: 4px;
}

/* ---------------------------------------------------------------------------
   The top row's one shared member: the feedback flag (#480, placed by #485).
   --------------------------------------------------------------------------- */

/* A FLAG, not a bug. Settled on #485's comment: a bug glyph reads as "software
   defect", which is narrower than the channel -- the report that prompted #480
   was a wrong gloss, a content problem -- and an icon that pre-classifies what
   the learner saw is the same mistake as a report form whose options are all
   remedies.

   Outline at rest, filled on press. The owner asked for "a red flag ... easy to
   get to"; a permanently saturated red mark in the corner of every screen is an
   alarm on a page whose whole design is about the cue, so the red is spent on
   the press instead. That press state is a transient acknowledgement of a
   touch and is deliberately the SAME whether or not the tap actually files
   something -- ADR 0034's distinction. Since #480 it always does file
   something, but the flash still reads as "acknowledged", not as a progress
   indicator for the network call underneath it (which is fire-and-forget and
   may still be in flight, or queued, after the flash has already faded). */
/* #1281 drew `.milim-rail-icon` here -- a sibling of `.milim-flag` rather
   than a second class on it, boxed and stroked to match it but deliberately
   NOT sharing its `:active` alarm-red, which was right for "report a
   problem" and wrong for a navigation link. 2026-09-09 retires the icon it
   was drawn for: the owner asked Progress back into the bottom row and nav.js
   no longer mounts anything into the top rail in its place ("I like that it
   is one less thing in the top bar" -- see shared/nav.js's own #1281/
   2026-09-09 comments). The box (`.milim-rail-icon` itself, its
   `[aria-current="page"]`, `:active` and `:focus-visible` states -- four
   rules nothing draws any more) is deleted with it.

   #2092 deleted `.milim-rail-icon-glyph` too, in the same change: it was
   kept alive past 2026-09-09 only because `mountAudioToggle` (shared/nav.js)
   still painted the audio toggle's speaker/waves glyph through it, and that
   toggle -- the box below it, `.milim-audio-toggle` and its two states --
   is gone with the top-bar control the owner asked removed. Five rules in
   all; `tests/test_nav_geometry.py`'s `_PARSED_SHAPE["shared/nav.css"]`
   carries this file's own arithmetic. */

.milim-flag {
  appearance: none;
  -webkit-appearance: none;
  background: transparent;
  border: 0;
  padding: 0;
  margin: 0;
  /* The same 44px floor as the bottom row. This one is load-bearing in a
     different way: study.css's `.rail` is a 10px-font row, so left to its
     content this button would be about 20px square. */
  min-width: 44px;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  color: var(--milim-nav-muted);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.milim-flag-icon {
  width: 21px;
  height: 21px;
  display: block;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* The pennant's body, the one part that fills. The pole never does. */
.milim-flag-body {
  fill: none;
  transition: fill 0.12s ease;
}

.milim-flag:active {
  color: var(--milim-nav-alarm);
}

.milim-flag:active .milim-flag-body {
  fill: var(--milim-nav-alarm);
}

.milim-flag:focus-visible {
  outline: 2px solid var(--milim-nav-current);
  outline-offset: 1px;
  border-radius: 8px;
}

@media (hover: hover) {
  .milim-flag:hover {
    color: var(--milim-nav-alarm);
  }
}

/* A press that paints instantly is the point; honour the reader's setting all
   the same -- the fill still happens, it just does not animate. Same call the
   reward beat's hearts already make. */
@media (prefers-reduced-motion: reduce) {
  .milim-flag-body {
    transition: none;
  }
}

/* ---------------------------------------------------------------------------
   #480: the sheet a tap opens after filing the one-tap report. Built once by
   nav.js, on the first tap, and shown/hidden with `hidden` rather than
   added/removed from the DOM.
   --------------------------------------------------------------------------- */

.milim-feedback-backdrop {
  position: fixed;
  inset: 0;
  /* Above the bottom nav (2147483000) -- this is a modal, the one thing on
     any page allowed to sit over it.

     #1942: NOT above the build stamp's corner, and that is not fixable by
     raising this number -- `#buildStamp` is `z-index: 2147483647`
     (build_stamp.css), the ceiling a browser will resolve at all, so there
     is no higher value to give this backdrop. Two owner reports named the
     stamp sitting over this sheet's own Send button as friction ("the build
     number and date and time are in the way ... difficult to send"), and
     the fix is not z-index at all: `.milim-feedback-open #buildStamp` below
     hides the stamp for as long as this sheet is open, which is the
     ticket's own other suggestion ("Is that possible to have the report
     draw on top of it maybe? ... the stamp hides while the sheet is open").
     The stamp is still fetched and still recorded on every report --
     nav.js's `feedbackBuildStamp` comes from `/api/v1/build`, never from
     this corner's own DOM. */
  z-index: 2147483400;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  background: rgba(0, 0, 0, 0.4);
}

/* #1942: hides the build stamp's corner for exactly as long as the sheet is
   open. `document.body` because the class has to be visible to
   build_stamp.css's own `#buildStamp` rule, which nav.js does not touch and
   should not have to -- see nav.js's own comment on
   `FEEDBACK_OPEN_BODY_CLASS`/`openFeedbackSheet`/`closeFeedbackSheet` for
   why hiding beats trying to out-z-index a value already at the ceiling.
   `display: none` rather than `visibility: hidden`: the stamp is already
   `pointer-events: none` (build_stamp.css, #973/#575) so it was never
   stealing the tap, only the sight -- this removes it from paint entirely
   rather than merely making it non-interactive a second way. */
.milim-feedback-open #buildStamp {
  display: none;
}

.milim-feedback-backdrop[hidden] {
  display: none;
}

.milim-feedback-sheet {
  position: relative;
  width: 100%;
  max-width: 430px;
  /* #2568: `100%` is the backdrop, which nav.js fits to the space ABOVE the
     phone keyboard while the sheet is open; `80vh` alone is the whole screen,
     taller than that space, so the sheet ran off its top. With no keyboard
     the backdrop is the viewport and this is the old 80vh. */
  max-height: min(80vh, 100%);
  overflow-y: auto;
  background: var(--milim-nav-surface);
  border-top: 1px solid var(--milim-nav-rule);
  border-radius: 14px 14px 0 0;
  padding: 18px 18px calc(18px + env(safe-area-inset-bottom));
  box-sizing: border-box;
  font-family: var(--ui, system-ui, -apple-system, sans-serif);
  color: var(--milim-nav-ink);
}

.milim-feedback-heading {
  /* #1942: was `0 36px 14px 0` -- the right margin cleared the old
     absolutely-positioned "x" button that used to sit in this corner. That
     control lives in `.milim-feedback-actions` now (see `.milim-feedback-
     cancel` below), so nothing in this corner needs clearing any more. */
  margin: 0 0 14px 0;
  font-size: 14px;
  font-weight: 600;
}

/* #1942: was `.milim-feedback-close`, a bare "x" absolutely positioned
   top-right -- close enough to `.milim-feedback-submit` below (same corner,
   opposite row) that a misclick could cancel a report someone meant to
   send. His words: "I don't want a misclick to cancel." Now a labelled
   Cancel button, laid out as the FIRST child of `.milim-feedback-actions`,
   whose `justify-content: space-between` (below) is what puts it far from
   Send -- the panel's own width is the separation, not a margin that a
   narrow phone could shrink to nothing. Same 44px minimum hit target as
   every other control on this sheet, and deliberately NOT styled like
   `.milim-feedback-submit`: an outline rather than a filled background
   keeps Send reading as the one affirmative action on the row. */
.milim-feedback-cancel {
  appearance: none;
  -webkit-appearance: none;
  min-height: 44px;
  padding: 0 16px;
  border: 1px solid var(--milim-nav-rule);
  border-radius: 8px;
  background: transparent;
  color: var(--milim-nav-muted);
  font-family: inherit;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
}

/* The label chips (#480, #1058) were removed on 2026-09-25 at the owner's
   request, for space -- see the comment where FEEDBACK_LABELS was in nav.js. */
.milim-feedback-note {
  width: 100%;
  min-height: 64px;
  box-sizing: border-box;
  margin-bottom: 12px;
  padding: 10px;
  border: 1px solid var(--milim-nav-rule);
  border-radius: 8px;
  background: var(--milim-nav-surface);
  color: var(--milim-nav-ink);
  font-family: inherit;
  /* #554: iOS Safari force-zooms the page on focus for any text entry whose
     COMPUTED font-size is under 16px, and nothing undoes that zoom on blur --
     the learner is stuck pinching back out by hand. 13px tripped it; 16px is
     the floor, not a design pick. This is the only <textarea>/<input>/<select>
     this file or shared/phone.css style -- see the PR body for the sweep that
     confirmed it, and tests/js/test_nav_behaviour.mjs, which parses this
     file's own declared rules (not a grep) so a future text entry added below
     16px fails on its own, not only by someone remembering this comment. */
  font-size: 16px;
  resize: vertical;
}

.milim-feedback-actions {
  display: flex;
  align-items: center;
  /* #1942: was `flex-end`, which packed Send alone against the right edge.
     Now Cancel (appended first, in nav.js) is the row's only other child,
     so `space-between` pushes it flush left and holds Send flush right --
     "Cancel far left, Send far right, real distance between them" is the
     owner's own wording, and this is what makes the gap the panel's full
     interior width rather than a fixed margin. */
  justify-content: space-between;
}

.milim-feedback-submit {
  appearance: none;
  -webkit-appearance: none;
  min-height: 44px;
  padding: 0 18px;
  border: 0;
  border-radius: 8px;
  background: var(--milim-nav-submit-bg);
  color: var(--milim-nav-submit-fg);
  font-family: inherit;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
}
