/* ==========================================================================
   NAV BUTTONS — the shared look, for BOTH rails

   ⚠️ THIS APP HAD TWO NAVS AND THEY DID NOT MATCH.

     .fs-nav   (css/app-nav.css)    17 pages   GREEN #64DC83   column layout
     .nav      (css/table-stage.css) 4 pages   TEAL  #22DDBB   row layout

   The four are the ones that matter most -- Play, the Pup Tank, Hand
   Analysis -- so a student met the teal row rail on the table and the green
   column rail everywhere else, and the app read as two products stitched
   together. That is the "double nav" Eric kept seeing.

   This file is loaded AFTER both and styles them as one component. It
   deliberately does not touch either container's width, position or grid
   role: those were tuned separately (the table rail lives in a grid column,
   the shared rail is position:fixed) and re-deciding them here would undo
   two rounds of width corrections. ONLY the buttons are unified.

   ACCENT: teal. Not a preference -- #22DDBB is already the accent on
   paywall.html, merch.html, the ceremony card, every primary button and the
   gear popover. The green was a one-off match to the home-screen artwork,
   which made the artwork agree with the rail and the rail disagree with the
   entire rest of the app.
   ========================================================================== */

:root {
  --nav-accent:      #22DDBB;
  --nav-accent-soft: rgba(34, 221, 187, 0.12);
  /* ⛔ THE GLOW WAS THE RETIRED GREEN ON EVERY PAGE. `app-nav.css` wrote the
     active edge-bar's halo as a literal `rgba(100, 220, 131, 0.7)`, and
     nothing here overrode it -- the alias below only covers rules that read
     the TOKEN. So a teal bar carried a green glow on all thirty pages, which
     is the one difference no page-to-page comparison could see because every
     page had it. Third copy of the colour this accent has had. */
  --nav-accent-glow: rgba(34, 221, 187, 0.70);
  --nav-idle:        #8A9490;
  --nav-hover:       #C9A84C;   /* gold -- the warm step between silver and neon */
  --nav-gold:        #C9A84C;
  --nav-hover-bg:    rgba(201, 168, 76, 0.10);
  /* ⚠️ TIGHTENED 2026-08-24. Eric: "the spacing is too big between the symbol
     and the letters and overall it should be slightly smaller", and the word
     must be "pinned" to its own icon so "the hat doesn't run into the word
     HOME above it and Academy doesn't run into the triangle for the PLAY
     button."
     ⭐ The base sizes come down a step; the ACTIVE growth stays, because he
     likes it ("when it is lit up and bigger, great") — what changes is that
     the row now makes room for it instead of letting it spill. */
  --nav-ico:         clamp(27px, 2.5vw, 34px);
  /* ⛔⛔ 18px IS THE FLOOR AND THE RAIL WAS EXEMPT FROM IT.
     Eric, 2026-09-05: "18 px minimum must be met on all home page and app
     pages. Period. Fix them all." That pass fixed `body p` and sixteen
     shipping pages; `legibility-floor.css` writes its rule as
     `button:not(.fs-nav-item)`, so the rail was EXCLUDED BY NAME and stayed
     at 12.5-14.5px. MEASURED 2026-09-08: 12.5px at a 1024 viewport, 14.5px at
     1920 — every unlit destination in the product, below the floor.
     ⚠️ Nobody decided that. The exclusion exists so the floor cannot fight
     the rail's own sizing; it was never a decision that the rail may be
     smaller. Eric, seeing it: "the unlit ones are so small I can't read them."
     ⭐ And our reader is seventy-eight — the same sentence that killed the
     48pt avatar ramp in APP_STATES_AND_STYLE Part 0. */
  --nav-lbl:         clamp(18px, 1.25vw, 19px);
  /* ⭐ THE LIT RATIO, ONCE. It is used in two places -- the active label's
     font-size, and the line box every label reserves so lighting one does not
     move the nine below it. They were two literals, I changed one from 1.5 to
     1.25 when the base went to the 18px floor, and the reservation kept
     asking for 35px to hold a 29px line. MEASURED: the label hung 3.7-5px
     BELOW its own item box on all ten, lit and unlit, and _nav-audit caught
     it -- printing only WIDTHS, which read as a false positive. */
  --nav-lbl-lit:     1.25;
  --nav-radius:      10px;
}

/* Retire the green. Kept as an alias rather than deleted so any stray rule
   still referencing it resolves to the accent instead of to nothing --
   a dangling var() falls back to the initial value, which for `color` is
   black on a near-black rail. */
:root { --fs-nav-green: var(--nav-accent); }

/* ---- the button ---------------------------------------------------------
   COLUMN: icon above label, sentence case. Eric's mockup, 2026-08-17.

   The row form (icon beside label) was the previous pass and it lost to the
   labels: "HAND ANALYSIS" needed two lines at every rail width, and a
   two-line item in a column of one-line items is the thing your eye catches
   instead of the thing you are looking for. Shorter names fix that at the
   source -- "Hands" and "Tiger" are one line at any size -- and once the
   labels are short the column reads cleaner than the row.

   SENTENCE CASE, not caps. Caps costs about 10-15% reading speed on
   anything longer than a word or two, and this rail is read by people who
   are new to the app and over fifty. */
.fs-nav-item,
.nav .item {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  /* ⚠️ ICON-TO-LABEL GAP, TIGHTENED 5px TO ZERO. Eric, 2026-08-19: "make the
     words under each 5px CLOSER... Tighten up the spacing of each symbol/word
     pair but leave them in their current spot."

     The pairs read as spread out while the ROWS read as crowded, which is the
     same illusion in both directions: a loose icon/label pair pushes its own
     words toward the neighbouring button, so closing the pair up buys apparent
     space between rows without moving anything.

     ⚠️ min-height AND padding ARE DELIBERATELY UNCHANGED. He asked for the
     pair to tighten and the buttons to stay put -- shrinking the button (his
     first idea, which he then revised) would have moved all nine and broken the
     56px touch floor that took three attempts to get right. This closes the
     gap inside the button and moves nothing.

     The <600px bottom bar keeps its own 5px: that is a horizontal row of five
     where the pairs are not competing with anything above or below them. */
  gap: 0;
  /* ⭐ The label is pulled UP onto its icon. The icon box is square and the
     glyph inside it is optically top-heavy, so `gap:0` still leaves a visible
     band of nothing under it — which is the gap Eric is seeing. A small
     negative margin closes the OPTICAL gap rather than the box gap. */
  /* ⛔⛔ THE GUARD BAND. The icon and the label are two things and must
     never touch, in any state, at any width. _nav-audit measures the real
     gap on every rail and fails below this. */
  --nav-gap-min: 5px;
  --nav-pair-pull: 4px;   /* ⚠️ legacy; nothing reads it as a pull any more */
  width: 100%;
  min-height: 54px;
  /* ⚠️ MORE ROOM BETWEEN ROWS, LESS INSIDE THEM. The active icon grows to
     1.75x, and at the old padding that growth had nowhere to go, so it
     overflowed into the neighbours — exactly the collision reported. */
  /* ⚠️ THE BOTTOM EDGE CUT THE DESCENDER. Eric, 2026-08-24: "the bottom of
     the highlighted area should be just below the y of the lettering, it is a
     little too high." The lit background is the ITEM's box, and with
     line-height 1.08 the text box stops at the baseline — so the tail of the
     y in "Play" hung outside the glow. ⭐ Three more pixels underneath, and
     only underneath: the pair stays tight and the box closes below the tail. */
  padding: 12px 6px 16px;
  border: 0;
  background: none;
  border-radius: var(--nav-radius);
  color: var(--nav-idle);
  cursor: pointer;
  text-align: center;
  text-decoration: none;
  font-family: var(--font-display, 'Space Grotesk', sans-serif);
  /* §2.1 — inactive 500, active 700. The weight jump is half of what makes
     the active item read as active; the other half is the 1.5x size. Before
     this everything sat at 700 and the state had to carry on colour alone. */
  font-weight: 500;
  transition: color .16s ease, background-color .16s ease;
}

/* ---- the icon ---------------------------------------------------------- */
.fs-nav-item .fs-nav-ico,
.nav .item .ico {
  flex: 0 0 auto;
  width: var(--nav-ico);
  height: var(--nav-ico);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--nav-ico);
  line-height: 1;
  /* The active item grows, so the box has to be able to. */
  transition: width .18s ease, height .18s ease;
}
.fs-nav-item .fs-nav-ico svg,
.nav .item .ico svg {
  width: 100%;
  height: 100%;
  display: block;
  vector-effect: non-scaling-stroke;
}

.fs-nav-item .fs-nav-lbl,
.nav .item .lbl {
  flex: 0 0 auto;
  max-width: 100%;
  font-size: var(--nav-lbl);
  letter-spacing: 0.01em;
  line-height: 1.08;
  /* ⛔⛔ A GUARD BAND, NOT A PULL. Eric, 29 Aug: "look at HOME, how it is
     smashed up against the symbol... this NEVER happens again."
     ⚠️ MEASURED: the icon and the label were ONE PIXEL apart at rest and
     MINUS ONE when lit — the label sat on the icon. The pull was -4px and
     the active state pulled -6.4px while the icon grew to 1.75x, so the two
     moved toward each other from both directions at once.
     ⭐ `--nav-gap-min` is a FLOOR the state cannot eat: whatever the icon
     does, the label starts at least this far below it. */
  margin-top: var(--nav-gap-min);
  text-align: center;
  /* Short names, so they should never wrap. If one does, the name is too
     long -- fix the NAME, not the rail. */
  white-space: nowrap;
  transition: font-size .18s ease;
}

/* ---- states -------------------------------------------------------------
   idle silver -> hover gold -> active neon teal. Eric's ladder: the item
   warms as you approach it and arrives at the brand colour when it is
   where you are.

   Every state changes TWO things, never colour alone: roughly one man in
   twelve has some red/green deficiency and this rail's whole job is saying
   where you are. */
.fs-nav-item:hover,
.nav .item:hover {
  color: var(--nav-hover);              /* gold */
  background: var(--nav-hover-bg);
}

.fs-nav-item:focus-visible,
.nav .item:focus-visible {
  outline: 3px solid var(--nav-accent);
  outline-offset: -3px;
  color: var(--nav-hover);
}

/* ---- ACTIVE: neon, and 60% bigger --------------------------------------
   "50%-75% bigger" -- 1.6x on the icon, which is the part that carries the
   size read. The label goes up only 1.18x: at 1.6x it would be wider than
   the rail and the nowrap above would clip it. */
.fs-nav-item.is-active,
.nav .item.active {
  color: var(--nav-accent);
  background: var(--nav-accent-soft);
}
.fs-nav-item.is-active .fs-nav-ico,
.nav .item.active .ico {
  width: calc(var(--nav-ico) * 1.75);
  height: calc(var(--nav-ico) * 1.75);
  /* The neon. A drop-shadow on the element bleeds the glow off the strokes
     rather than boxing it, which is what makes it read as lit rather than
     as a rectangle behind the icon. */
  filter: drop-shadow(0 0 7px rgba(34, 221, 187, .72))
          drop-shadow(0 0 15px rgba(34, 221, 187, .34));
}
.fs-nav-item.is-active .fs-nav-lbl,
.nav .item.active .lbl {
  /* §2.1 wanted 1.5x, and it was right WHEN THE BASE WAS 11pt — the icon was
     doing all the work and the lit label had room to grow into. With the base
     at the 18px floor, 1.5x is 27px, and "Progress" at 27px is 108px inside a
     102px content box on the 124px rail. ⛔ The spec's own escape hatch is
     "shorten the NAME", which is not available: these are the ten
     destinations Eric fixed in order on 2026-09-01.
     ⭐ So the RATIO yields, not the floor. 1.25x is 22.5-23.75px, still a
     clear step up, and the lit item already carries weight 700, the accent
     colour, a glow and two edge bars — the size was never carrying it alone.
     ⚠️ MEASURED at 1920/1440/1280/1024/901/800: nothing clips. */
  font-size: calc(var(--nav-lbl) * var(--nav-lbl-lit));
  font-weight: 700;
  /* ⛔ AND THE LIT PAIR NEEDS MORE ROOM, NOT LESS. This used to pull the
     label UP by 1.6x the resting pull, on the one item whose icon had just
     grown by 1.75x — the two changes worked against each other and the
     result was a label printed over a house.
     ⚠️ The old comment's reasoning was sound and its sign was wrong: a
     bigger icon does want a proportional gap, so the floor scales UP with
     the icon rather than the label climbing into it. */
  margin-top: calc(var(--nav-gap-min) * 1.5);
  text-shadow: 0 0 9px rgba(34, 221, 187, .55);
}

/* ⚠️ NO LEFT BAR. Eric, 2026-08-18: "I don't like the neon LINE on the left.
   I like the way it is illuminated." The bar was a second, redundant signal --
   the active item is already 1.75x and lit. Two signals for one state is the
   noise the caret was removed for earlier; this is the same mistake one layer
   down. The GLOW stays, because that is the part that reads.

   Colour is still not doing this alone: size carries it (1.75x on the icon,
   1.5x on the label) plus the background wash, so a red/green-deficient
   student sees the state as clearly as anyone. */

/* The old text caret is redundant now that the item is neon and 60% bigger:
   three signals for one state is noise. Kept in the markup for the
   aria-current hook, hidden from view. */
.fs-nav-item .fs-nav-caret { display: none; }

.fs-nav-item.is-disabled,
.nav .item.is-disabled { opacity: .38; cursor: not-allowed; }
.fs-nav-item.is-disabled:hover,
.nav .item.is-disabled:hover { background: none; color: var(--nav-idle); }

/* ---- the divider above Settings ----------------------------------------
   Eric's mockup separates the thing you configure from the things you do. */
.fs-nav-item.nav-sep,
.nav .item.nav-sep {
  margin-top: 9px;
  padding-top: 13px;
  /* the divider itself is the background-image immediately below */
}
/* ⛔⛔ THE DIVIDER AND THE "YOU ARE HERE" BAR WANTED THE SAME PSEUDO-ELEMENT.
   Eric, 2026-09-13: "why does Tour only have one vertical green line and the
   rest have two?" -- Tour is the ONLY rail item with `sep: true`
   (`app-nav.js` RAIL_ORDER), and `app-nav.css` draws the active marker as
   `.fs-nav-item.is-active::before` (left bar) AND `::after` (right bar).
   This rule was `.fs-nav-item.nav-sep::after`: the same 0,2,1 specificity,
   in the file that links later, so on tour.html the 1px grey divider WON the
   ::after and the right-hand green bar never painted. One element has one
   ::after; that is not a contest specificity can settle -- raising it here
   would win the divider and lose the bar, lowering it loses the divider.
   ⭐ So the divider stops being a pseudo-element. It is a 1px line across the
   top inset 14% each side, which is exactly a background-image, and the rail
   already draws its own trailing edge this way further down this file.
   ⚠️ The longhands are deliberate: `.fs-nav-item.is-active` above sets
   `background:` as a SHORTHAND, which resets background-image to none. Equal
   specificity, and this rule is later, so the image survives -- specificity
   settles each PROPERTY separately, never the rule as a whole. */
.fs-nav-item.nav-sep,
.nav .item.nav-sep {
  background-image: linear-gradient(to right,
      transparent 0, transparent 14%,
      rgba(255, 255, 255, .13) 14%, rgba(255, 255, 255, .13) 86%,
      transparent 86%, transparent 100%);
  background-repeat: no-repeat;
  background-position: 0 0;
  background-size: 100% 1px;
}

/* ---- locked ------------------------------------------------------------
   NOT the disabled style: greyed-to-38% reads as broken or coming-soon, and
   a paid level is available for money. It stays legible and gains a
   padlock, so it looks like a door rather than a fault. */
.fs-nav-item.is-locked,
.nav .item.is-locked { color: var(--nav-idle); }
.fs-nav-item.is-locked:hover,
.nav .item.is-locked:hover {
  color: var(--nav-hover);
  background: var(--nav-hover-bg);
}
/* ⛔⛔ THIS BOX WAS SIZED FOR AN SVG AND NOW HOLDS TEXT. The rule below it
   styles `.fs-nav-lock svg`, and locked-previews.js sets
   `textContent = '🔒 ' + need` — so a two-or-three-character string was being
   asked to live in a 14x14 icon box and spilled onto the label.
   MEASURED at 667x375: "Drills" x "🔒 3", "Tiger" x "🔒 1" and "Hands" x
   "🔒 7" each overlapped by 14x3px. Text on text, on every locked rail item,
   on every page.
   ⚠️ Two ideas of what one element contains — the recurring defect at the size
   of a padlock. It is sized by its CONTENT now, which is the only thing that
   cannot disagree with what is put in it. */
.fs-nav-item .fs-nav-lock,
.nav .item .lock {
  position: absolute;
  /* ⭐ VERTICALLY CENTRED ON THE ICON — Eric, 2026-09-08: "Drills and hands
     that are locked should have a lock and a number next to it that is
     CENTERED vertically on the Drills/symbol and Hands/symbol. It is
     justified at the TOP."
     MEASURED before: the badge sat 7-8px ABOVE the icon's centre line at
     1440 and 1280, so it read as hanging off the corner rather than sitting
     beside the symbol.
     ⛔ DERIVED, NOT A MAGIC NUMBER. The icon's centre is the item's padding
     plus half the icon, both of which are tokens, so this stays right when
     either moves -- and both DO move: the icon steps down under 900px tall
     and the padding under 900 and 780. */
  top: calc(6px + var(--nav-ico) / 2);
  transform: translateY(-50%);
  right: 8px;
  width: auto; height: auto;
  font-size: max(10px, .62rem);
  line-height: 1;
  white-space: nowrap;
  color: var(--nav-gold);
  opacity: .9;
}
/* ⛔⛔ AND THE RESERVATION BELONGS TO THE ROW LAYOUT ONLY — IT WAS GLOBAL,
   AND IT PUSHED THE WORD OFF CENTRE ON EVERY DESKTOP RAIL.
   Eric, 2026-09-08: "the word Drills is not centered". MEASURED at 1280x900:
   the three LOCKED labels sat 20px left of centre while the seven unlocked
   ones were 0 — one rule, three items, and nothing else on the rail moved.

   ⭐ THE REASON IT BOUGHT NOTHING IN A COLUMN: the badge is absolutely
   positioned in the item's TOP-RIGHT corner and, in the vertical rail, the
   label is on the row BELOW the icon. Measured with the reservation off, the
   two rects never meet — `Drills` label 61,449..93,475 against a badge at
   104,411..141,425. It was reserving horizontal space against a collision
   that only exists when the item is a ROW.
   ⚠️ Same family as the two `max()` floors this file records: a declaration
   that reads as working and cannot take effect. Here it took effect on the
   wrong axis.
   ⭐ The drawer keeps its own copy at (0,3,0) — there the item IS a row, the
   label runs left-to-right into the badge (measured 37x1px), and the label is
   left-aligned by design rather than centred, so padding costs nothing. */

/* ⚠️ AND SYMMETRIC PADDING IS NOT THE FIX EITHER, WHICH IS WORTH WRITING DOWN
   BECAUSE IT IS THE OBVIOUS ONE. It restores the centring and CLIPS the label:
   the rail is 124px at 1024 and below, so 46px on each side leaves 32px for
   "Drills". It also cannot move a centred box — padding is equal on both
   sides, the centre does not shift — so it does nothing about the one real
   collision in a column: at rails under ~130px the ICON reaches the badge.
   MEASURED at 1024: icon right 76, badge left 75. The badge yields the 5px,
   because it is the element with somewhere to go. */
@media (min-width: 601px) and (max-width: 1120px) and (min-height: 501px) {
  .fs-nav .fs-nav-item .fs-nav-lock,
  .nav .item .lock { right: 3px; }
}
/* ⛔⛔ AND BELOW 900px THE RAIL IS 96px, WHERE THE ARITHMETIC SIMPLY DOES NOT
   CLOSE. `--fs-nav-w` drops from `clamp(124px, 12vw, 176px)` to a flat 96px,
   which leaves an 86px content box. The icon is 27px and CENTRED — 30..57 —
   and the badge is 36px hard against the right edge — 47..83. They overlap by
   ten pixels, and no amount of padding fixes it: padding is symmetric, so it
   does not move a centred box at all.
   ⭐ THE BADGE IS THE ELEMENT WITH SOMEWHERE TO GO. Tightened it measures
   ~20px and clears the icon. ⛔ The PADLOCK AND THE NUMBER BOTH STAY —
   CLAUDE.md records the rail as `Drills 🔒 3 · Tiger 🔒 1 · Hands 🔒 7`,
   "with their level numbers", and a badge that drops the number is a
   different and weaker thing than a small one.
   ⚠️ This band is 601-900px WIDE and over 500px TALL — small laptops and
   tablets in landscape. It is not the drawer, and it is not the wide rail;
   it is the band this file has already had to fix twice for having neither. */
@media (min-width: 601px) and (max-width: 900px) and (min-height: 501px) {
  .fs-nav .fs-nav-item .fs-nav-lock,
  .nav .item .lock {
    font-size: max(9px, .55rem);
    top: 3px; right: 2px;
    letter-spacing: -.04em;
  }
}
.fs-nav-item .fs-nav-lock svg,
.nav .item .lock svg { width: 100%; height: 100%; display: block; }

/* ---- narrow: icons only ------------------------------------------------
   The label is hidden, so the icon must centre or the rail goes lopsided. */
@media (max-width: 900px) {
  .fs-nav-item .fs-nav-lbl,
  .nav .item .lbl { display: none; }
  .fs-nav-item, .nav .item { gap: 0; min-height: 52px; }
}

@media (prefers-reduced-motion: reduce) {
  .fs-nav-item, .nav .item,
  .fs-nav-item .fs-nav-caret { transition: none; }
}

/* ---- SPACED AND SEPARATED (Eric, 2026-08-18) ---------------------------
   "Each nav button should take up space and be centered so they don't seem
   to run together, almost like they have a small thin silver line
   separating them."

   The items were flush against each other, so eight of them read as one
   striped column rather than eight targets. A hairline between each gives
   the eye somewhere to stop — and it is a HAIRLINE, at 8% alpha: any
   heavier and the rail becomes a table of contents competing with the
   screen it sits beside. */
.fs-nav-item + .fs-nav-item,
.nav .item + .item {
  border-top: 1px solid rgba(214, 228, 224, .08);
}
/* The divider above Settings is a SECTION break and must still out-rank the
   hairlines, or the grouping disappears into the rhythm. */
.fs-nav-item.nav-sep, .nav .item.nav-sep { border-top: 0; }

/* Breathing room, without touching the left-to-right spacing Eric approved. */
/* ⛔⛔ THE PADDING WAS BUDGETED FOR 12.5px LABELS.
   The rail is height-constrained -- ten items at 76px is 760px plus the brand
   against an 800px window -- so flex SHRINKS each item, measured 64.6px
   against a declared 76. The CONTENT does not shrink with it: icon 32 +
   label 29 + 26px of padding is 87px of contents in a 64.6px box, and the
   label hung below its own item on all ten.
   ⚠️ It passed at 12.5px only because the label's line box was 5px shorter
   and the overhang landed inside _nav-audit's 1px tolerance. Raising the type
   to the 18px floor did not create this; it revealed it.
   ⭐ THE PADDING YIELDS, NOT THE WORD. Same call as the short-viewport rule
   further down: the icon and the whitespace are what a reader can lose. */
.fs-nav-item, .nav .item { padding-top: 6px; padding-bottom: 6px; }

/* ==========================================================================
   NAV_BAR_SPEC_v7 §2.3 + PART 2b — THE ITEMS FILL THE VERTICAL

   ⭐ "Do not cluster the items at the top." The spec calls this the single
   biggest accessibility decision in the interface, and it was the one part
   of the rail that had never been built: items were natural-height and sat
   in a clump at the top with the rest of the column empty.

   THE FLEX RULE (§2.3): each item is `flex: 1` within the rail, so the rail
   always fills its column whatever the viewport, items grow and shrink
   together, and the layout never needs a media query for height.

   Why this size at all -- and it is roughly six times the platform minimum:
   Siteimprove's finding that undersized targets do not merely slow users
   with motor impairment or arthritis, they stop them completely. A
   meaningful share of a 50+ recreational-poker audience is in that group.
   "At this size, they cannot miss."
   ========================================================================== */
.fs-nav {
  /* §2b — top/bottom padding is 3.7% of viewport height, floored so a short
     laptop window does not squeeze it to nothing. */
  padding-top: max(10px, 3.7vh);
  padding-bottom: max(10px, 3.7vh);
}

.fs-nav-item {
  /* ⚠️ `flex: 1 1 0` and NOT `flex: 1 1 auto`: with auto, an item whose label
     wraps claims more of the column than its neighbours and the rhythm goes
     ragged. Zero basis makes every item an equal share of whatever is left. */
  flex: 1 1 0;
  /* ⚠️ THE FLOORS ARE NOT NEGOTIABLE (§2b). Item never below 56px, icon never
     below 24px, label never below 11px. On a very short viewport the rail
     SCROLLS rather than shrinking past these -- .fs-nav already has
     overflow-y:auto, which is what makes that safe. */
  min-height: 56px;
}

/* The brand is not one of the destinations and must not take an equal share
   of the column. */
.fs-nav-brand { flex: 0 0 auto; }

/* §2b floors for the content, restated here because the clamps above are
   viewport-relative and a narrow-but-short window can drive both to their
   minimum at once. */
.fs-nav-item .fs-nav-ico  { min-width: 24px; min-height: 24px; }
/* ⚠️ AND THIS `max(11px, ...)` WAS THE SECOND EXEMPTION, one floor down: a
   guard band written when the base could legitimately reach 10.5px. It now
   floors at the real floor, so a media query that lowers `--nav-lbl` cannot
   quietly take the rail under 18 again. */
.fs-nav-item .fs-nav-lbl  { font-size: max(18px, var(--nav-lbl)); }

/* ---- PART 4 — the trailing hairline ------------------------------------
   §4: a 1px trailing edge in the accent at 25%, fading out top and bottom.
   The rail had a flat white 8% border, which reads as a panel seam rather
   than as part of the component. Fading it at both ends is what stops it
   looking like a ruled line. */
.fs-nav {
  border-right: 0;
  background-image:
    linear-gradient(to bottom,
      transparent 0%,
      rgba(34, 221, 187, .25) 12%,
      rgba(34, 221, 187, .25) 88%,
      transparent 100%);
  background-repeat: no-repeat;
  background-position: 100% 0;
  background-size: 1px 100%;
}

/* ---- §2b.3 — RESPONSIVE -------------------------------------------------
   ≥900px   full rail, icons + labels
   600-900  rail at a fixed 96px, items at the minimum floors, icons only
   <600     ⚠️ the spec collapses the rail to a BOTTOM BAR here. Not built --
            see the note in js/app-nav.js. The rail stays, narrowed, so the
            app is usable at phone width rather than half-migrated. */
@media (max-width: 900px) {
  :root { --fs-nav-w: 96px; }
  .fs-nav-item { min-height: 56px; }
  /* ⛔ THE BRAND GOES. Below 900px the rail is 96px of icons, and the
     wordmark is 30px of rail that six 56pt destinations need more.

     ⚠️ TWO CLASSES, AND THAT IS THE WHOLE POINT. `.fs-nav-brand` alone is
     (0,1,0), and this file declares `.fs-nav-brand { display: flex }`
     LATER — at the brand-mark section below — so an equally specific rule
     up here loses on order, inside one file, exactly as it lost across two.
     `.fs-nav .fs-nav-brand` is (0,2,0) and does not depend on load order at
     all. The same trick this file already uses for .fs-nav-item's 56pt
     floor, for the same reason. */
  .fs-nav .fs-nav-brand { display: none; }

  /* ⚠️ AND THE LAST THREE PIXELS. With the brand gone the rail measured
     378px of content in a 375px box at 667x375 — six destinations that very
     nearly fit. The 56pt floor is not negotiable and six is Eric's number,
     so the only thing left to give is the rail's own padding: 14px top and
     bottom is generous on a laptop and is three destinations' worth of
     nothing on a phone. */
  .fs-nav { padding-top: 8px; padding-bottom: 8px; }
}

/* §2b.2b — 150ms ease-out on the state change, and NEVER on the glow.
   "Pulsing navigation is exhausting." Under reduced motion the state still
   changes, it just arrives instantly. */
.fs-nav-item .fs-nav-ico,
.fs-nav-item .fs-nav-lbl {
  transition: width .15s ease-out, height .15s ease-out,
              font-size .15s ease-out, color .15s ease-out;
}
@media (prefers-reduced-motion: reduce) {
  .fs-nav-item .fs-nav-ico,
  .fs-nav-item .fs-nav-lbl { transition: none; }
}

/* ---- PART 6 — the badge dot --------------------------------------------
   "Badge dot on Academy only. A finished level, a pending gate. NEVER on
   Play -- that's a nag." One dot, one place, and it is earned information
   rather than a prompt to come back. */
.fs-nav-item .fs-nav-dot {
  position: absolute;
  top: 18%; right: 22%;
  width: 10px; height: 10px;
  border-radius: 50%;
  background: var(--nav-gold);
  box-shadow: 0 0 8px rgba(201, 168, 76, .8);
  pointer-events: none;
}
@media (max-width: 900px) {
  .fs-nav-item .fs-nav-dot { top: 12%; right: 14%; }
}

/* ---- THE BRAND MARK -----------------------------------------------------
   A drawn logo, not a wordmark button. Kept small and given a hard cap on
   height: §2.3 wants the destinations filling the column, and every pixel
   the mark takes is a pixel off nine touch targets. */
.fs-nav-brand {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 2px 6px 10px;
  color: var(--nav-idle);
  font-weight: 700;
  font-size: clamp(12px, 1.4vw, 15px);
  letter-spacing: .06em;
  text-align: center;
  line-height: 1.05;
  /* ⭐ THE LOGO IS A LINK HOME — Eric, 2026-09-12: "It should ALWAYS be
     clickable to take users back to the home button." It was a <div> with
     `cursor: default`, which is the one property that tells a visitor a thing
     is NOT clickable, so it has to move in the same commit as the element. */
  cursor: pointer;
  text-decoration: none;
}
.fs-nav-brand:hover img, .fs-nav-brand:focus-visible img { opacity: .85; }
.fs-nav-brand:focus-visible {
  outline: 2px solid var(--fs-focus, #22DDBB);
  outline-offset: 2px;
}
.fs-nav-brand img {
  display: block;
  /* The full rail width, minus the brand's own padding: at 3.19:1 every
     pixel of width is three pixels of legibility, so capping it smaller
     (it was 86px) threw away the only room available. */
  width: 100%;
  /* ⭐ 2x — Eric, 2026-08-25. Was 8vh. The width was already the full rail,
     so the cap is what actually limited it. */
  max-height: 16vh;
  height: auto;
  object-fit: contain;
}
@media (max-width: 900px) {
  .fs-nav-brand { padding: 2px 3px 8px; }
  .fs-nav-brand img { max-height: 12vh; }   /* 2x, was 6vh */
}

/* ---- ⚠️ THE FLOOR HAD BEEN LOSING A SPECIFICITY FIGHT -------------------
   css/touch-targets.css carries

       button[class], button:not([class]) { min-height: 44px }

   at (0,1,1), deliberately, so it beats a page rule like `.kbtn` without
   needing !important. It also loads AFTER this file. So every `.fs-nav-item`
   floor written as a single class -- 56px here, 58px in touch-targets.css
   itself -- was being beaten by that 44px, and the nav items resolved to
   min-height:44px.

   It never showed at desktop size because `flex: 1` gives each item far more
   than any floor. It only bit on a SHORT viewport, which is exactly when the
   floor is the only thing left protecting the target: measured 44px at
   800x400, against a spec that says 56 and an audience the spec chose 56 FOR.

   Two classes takes this to (0,2,0), which beats (0,1,1) on specificity and
   so does not depend on load order at all. */
.fs-nav .fs-nav-item { min-height: 56px; }
/* ⭐ THE RESERVED HEIGHT. Every item is this tall, lit or not, so lighting
   one cannot move the eight below it. The number is the ACTIVE height, which
   is what has to fit; an unlit item simply has room to spare. */
:root { --fs-nav-item-h: 76px; }
/* ⭐ AND ON A SHORT SCREEN THE ICON YIELDS TOO. This file already carries
   that rule below 780px; the squeeze starts earlier now that the label is
   18px, so the icon steps down at 900 rather than waiting for 780. */
/* ⭐⭐ AS LARGE AS THE BOX ALLOWS — Eric, 2026-09-08: "Users have to be able to
   see the nav buttons nice and clear and as large as possible."
   ⚠️ MEASURED SLACK inside a rail item (item height minus icon, label and
   padding): 1920 +12 · 1600 +5 · 1440 +8 · 1280 +1 · 1024 +3 · and NEGATIVE at
   1366x768 (-11), 900x700 (-5) and 800x620 (-12), where the content already
   over-fills the box.
   ⛔ So there is no single number that is "as large as possible": the rail is
   height-constrained and ten items share it. The icon steps DOWN in two bands
   instead of one, so the screens with room keep a big symbol and the short
   ones are not pushed further over.
   ⚠️ 27px was one cap for everything from 900px tall down to 620, chosen when
   the labels went to 18px. It cost 1440x900 six pixels of symbol for a
   constraint that only bites below 800. */
@media (max-height: 900px) {
  :root { --nav-ico: clamp(24px, 2.3vw, 31px); }
  .fs-nav-item, .nav .item { padding-top: 4px; padding-bottom: 4px; }
}
@media (max-height: 800px) {
  :root { --nav-ico: clamp(22px, 2vw, 26px); }
}
@media (max-height: 780px) { :root { --fs-nav-item-h: 62px; } }
@media (max-height: 560px) { :root { --fs-nav-item-h: 54px; } }
@media (max-width: 600px), (max-height: 500px) { :root { --fs-nav-item-h: auto; } }
@media (max-width: 900px) { .fs-nav .fs-nav-item { min-height: 56px; } }


/* ---- THE PAGE MAKES ROOM FOR THE RAIL ----------------------------------
   Owned by CSS so it tracks the breakpoint. js/app-nav.js only sets the
   attribute; it used to write body.style.paddingLeft from --fs-nav-w read
   once at load, which froze the value at whatever the viewport was when the
   script ran. Crossing 600px then put the page under the rail, or left a
   gutter with no rail in it.

   body[data-fs-nav="on"] is (0,1,1), so it beats a page's own `body { }`
   rule the way the inline style used to. */
body[data-fs-nav="on"] { padding-left: var(--fs-nav-w); }

/* ==========================================================================
   §2b.3 — UNDER 600px THE RAIL BECOMES A BOTTOM BAR

   "A vertical rail on a phone steals width the table cannot spare." At this
   width the rail is ~16% of the screen and the felt is the product, so the
   nav moves to the bottom edge and the five destinations sit in a row.

   ⚠️ FIVE, NOT NINE. Material: six destinations is the documented CEILING,
   and at 390pt six gives 65pt each -- 19% under its own 80dp minimum action
   width. Five gives 78pt. So Tour, Skills, Progress and Settings leave the
   bar; §2b.3 sends Settings to a gear on Home and Material says to give the
   rest "alternative locations", which is the utility row on Home.

   Nothing becomes unreachable. That was the condition for doing this at all.
   ========================================================================== */
/* ==========================================================================
   ⛔⛔ THE BAR TRIGGERED ON WIDTH, AND THE PROBLEM IS HEIGHT.

   Every rule below fired at `max-width: 600px`. A LANDSCAPE PHONE IS WIDE
   AND SHORT — 844x390, 740x360 — so it never matched, and the app is
   landscape-LOCKED, which means the one device shape this product actually
   runs on was the one shape the bottom bar never reached.

   ⚠️ MEASURED by _nav-audit before this note existed: the vertical rail came
   to 417px against a 360px viewport. Nine items, 57px of overflow, the last
   one unreachable. Shrinking items to fit dropped targets to 44.9px, under
   the 56pt floor — and that floor is not a preference, it is the whole
   reason the rail was measured in the first place.

   ⭐ THE ARITHMETIC SETTLES IT. Nine items at 56px is 504px of rail. A
   360px-tall screen cannot hold that vertically and can hold it easily
   across 740px of width. So the bar mode is right; it was simply asking the
   wrong question.

   ⭐ `(max-width: 600px), (max-height: 500px)` — a narrow phone in portrait
   OR any short viewport. Both get the bar, and the bar was already built.
   ========================================================================== */
/* ==========================================================================
   THE PHONE NAV IS A DRAWER, NOT A BAR.

   ⛔⛔ THE BOTTOM BAR IS DELETED. Eric, 2026-09-01: "you are building just
   ONE vertical nav bar and no horizontal bars should exist any longer."
   00_NO_ACCOUNT_v8 §4.3 says the same thing after he hit it in the product:
   "UNIVERSAL_LAYOUT_v8: LEFT rail, or the drawer at phone sizes. Never a top
   bar." The bar was a SECOND layout for one component -- the recurring defect
   in this codebase wearing a media query -- and it is why ten items rendered
   horizontally when he clicked Home.

   ⭐ UNIVERSAL_LAYOUT_v8 §9.4 writes what replaces it:
       - delete the 10pt sliver
       - ONE visible button, ≥56pt, that opens the nav as a full drawer
       - all items, SCROLLABLE -- never items below the fold with no scroll
       - lock badges render IN THE DRAWER: Drills 🔒 3 · Tiger 🔒 1 · Hands 🔒 7

   ⚠️ The drawer is the SAME rail. It keeps its column direction, its labels,
   its locks and its active state; only its position and its visibility
   change. That is the whole point -- one component, one order, one copy of
   the truth.

   ⛔ EXCEPT ON A SCENE PAGE. table-layout and pup-tank-layout mount the rail
   into a column of their own grid (`data-fs-nav-mount`) and drive it with
   responsive-chrome.js's `.peek`; they are left alone here.
   ========================================================================== */
@media (max-width: 600px), (max-height: 500px) {
  :root {
    /* the rail no longer holds a gutter open: the drawer floats over the page */
    --fs-nav-w: 0px;
    /* ⭐ wide enough for a label at full size, capped so it never becomes the
       whole screen -- a drawer that covers everything is a page, not a drawer. */
    --fs-drawer-w: min(78vw, 300px);
  }

  .fs-nav:not([data-fs-nav-mount]) , .fs-nav.fs-nav-drawerable {
    position: fixed;
    top: 0; bottom: 0; left: 0; right: auto;
    width: var(--fs-drawer-w);
    height: auto;
    /* ⛔ STILL A COLUMN. This is the same vertical rail, moved. */
    display: flex;
    flex-direction: column;
    align-items: stretch;
    justify-content: flex-start;
    gap: 2px;
    padding: 10px 8px calc(10px + env(safe-area-inset-bottom, 0px));
    /* §9.4 — "all items, scrollable". Ten items at 56pt is 560px and a
       landscape phone is 375px tall, so this is load-bearing, not defensive. */
    overflow-y: auto;
    overflow-x: hidden;
    -webkit-overflow-scrolling: touch;
    border-right: 1px solid rgba(120, 210, 190, .28);
    background: var(--fs-nav-bg, #071613);
    box-shadow: 6px 0 28px rgba(0, 0, 0, .55);
    z-index: 9200;
    transform: translateX(-102%);
    transition: transform .22s ease;
    visibility: hidden;
  }
  .fs-nav:not([data-fs-nav-mount]).is-open , .fs-nav.fs-nav-drawerable.is-open {
    transform: translateX(0);
    visibility: visible;
  }
  @media (prefers-reduced-motion: reduce) {
    .fs-nav:not([data-fs-nav-mount]), .fs-nav.fs-nav-drawerable { transition: none; }
  }

  /* ⭐ Labels are ON in the drawer. They are hidden on the narrow desktop rail
     because it is too thin for them; the drawer is 300px and
     NAVIGATION_SPEC Part 1 is unambiguous that unlabelled icons lose. */
  .fs-nav:not([data-fs-nav-mount]) .fs-nav-item , .fs-nav.fs-nav-drawerable .fs-nav-item {
    flex: 0 0 auto;
    width: 100%;
    max-width: none;
    min-height: 56px;                 /* the floor, in the drawer too */
    flex-direction: row;
    align-items: center;
    justify-content: flex-start;
    gap: 12px;
    padding: 8px 12px;
    border-radius: 10px;
    text-align: left;
  }
  /* ⛔ THE LOCKED ITEM RESERVES THE BADGE'S SPACE HERE TOO. The rule above
     sets `padding: 8px 12px` at (0,3,0)+ and `.fs-nav-item.is-locked` is
     (0,2,0), so the base rule lost inside the drawer and the label ran under
     the badge — measured 37x1px on Drills, Tiger and Hands at 667x375.
     ⚠️ CLAUDE.md's rule, third instance today: when two rules disagree, raise
     specificity; never reorder and hope. */
  .fs-nav:not([data-fs-nav-mount]) .fs-nav-item.is-locked ,
  .fs-nav.fs-nav-drawerable .fs-nav-item.is-locked { padding-right: 46px; }

  .fs-nav:not([data-fs-nav-mount]) .fs-nav-lbl , .fs-nav.fs-nav-drawerable .fs-nav-lbl {
    display: block;
    font-size: max(14px, .95rem);      /* Eric: "min font size is 14 please!" */
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    height: auto;
    margin: 0;
    text-align: left;
    flex: 1 1 auto;
  }
  .fs-nav:not([data-fs-nav-mount]) .fs-nav-item .fs-nav-ico , .fs-nav.fs-nav-drawerable .fs-nav-item .fs-nav-ico {
    width: 28px; height: 28px; flex: 0 0 auto;
  }
  .fs-nav:not([data-fs-nav-mount]) .fs-nav-item.is-active .fs-nav-ico , .fs-nav.fs-nav-drawerable .fs-nav-item.is-active .fs-nav-ico {
    width: 28px; height: 28px;
  }
  /* §9.4 — the lock badge and its LEVEL NUMBER render in the drawer. */
  .fs-nav:not([data-fs-nav-mount]) .fs-nav-lock , .fs-nav.fs-nav-drawerable .fs-nav-lock {
    flex: 0 0 auto; margin-left: auto; font-size: max(13px, .85rem);
  }
  /* the caret points along a vertical rail, which is what this is again */
  .fs-nav:not([data-fs-nav-mount]) .fs-nav-caret, .fs-nav.fs-nav-drawerable .fs-nav-caret { display: none; }
  .fs-nav:not([data-fs-nav-mount]) .fs-nav-brand, .fs-nav.fs-nav-drawerable .fs-nav-brand { display: none; }

  /* ⭐⭐ THE ONE VISIBLE BUTTON, ≥56pt. §9.4: "delete the 10pt sliver -- a
     control that cannot be tapped is worse than no control, it is a control
     they will BLAME THEMSELVES for missing." app-nav.js injects this on any
     page that is not a scene page. */
  /* ⚠️ TWO CLASSES, FOR THE SAME REASON .fs-nav .fs-nav-item needs them.
     touch-targets.css carries `button[class] { min-height: 44px }` at
     (0,1,1) and loads AFTER this file, so a single-class floor LOSES:
     measured, this opener resolved to 44px against a §9.4 that says >=56.
     (0,2,0) beats (0,1,1) on specificity and does not depend on load order.
     ⛔ Do not "fix" this by reordering the link tags. */
  .fs-nav-opener.fs-nav-opener {
    position: fixed;
    left: 0; top: 50%; transform: translateY(-50%);
    width: 56px; min-height: 56px;
    display: flex; align-items: center; justify-content: center;
    border: 1px solid rgba(120, 210, 190, .42); border-left: 0;
    border-radius: 0 12px 12px 0;
    background: rgba(8, 26, 24, .95); color: var(--teal, #22DDBB);
    font-size: 22px; line-height: 1; cursor: pointer;
    z-index: 9199;
  }
  .fs-nav-opener[hidden] { display: none; }

  .fs-nav-scrim {
    position: fixed; inset: 0;
    background: rgba(0, 0, 0, .5);
    z-index: 9150;
    border: 0; padding: 0;
  }
  .fs-nav-scrim[hidden] { display: none; }
}

/* ⛔ The opener and the scrim belong to the drawer breakpoint ONLY. Above it
   the rail is always visible and an opener would be a control that opens
   something already open. */
@media (min-width: 601px) and (min-height: 501px) {
  .fs-nav-opener, .fs-nav-scrim { display: none !important; }
}

/* The page reserves NOTHING at this size: the drawer floats over the content
   and the opener is 56px of fixed chrome on the left edge. Driven off the
   attribute app-nav.js already sets. */
@media (max-width: 600px), (max-height: 500px) {
  body[data-fs-nav="on"] {
    padding-left: 0 !important;
    padding-bottom: 0 !important;
  }
}

/* ---- THE BRAND MARK IN THE SCENE RAILS ---------------------------------
   table-layout, pup-tank-layout and hand-analysis draw their own rail
   inside the scene. They carried the wordmark as TEXT ("FELT / SHARK");
   they now carry the compact logo, and this sizes it in one place for all
   three rather than three inline copies that drift.

   Capped deliberately: NAV_BAR_SPEC §2.3 wants the destinations filling the
   column, and every pixel the mark takes is a pixel off a touch target. */
.nav .brand img {
  display: block;
  width: 100%;
  height: auto;
  margin: 0 auto;
}

/* ==========================================================================
   THE CHARCOAL PLATE — Eric, 2026-08-18: "If there is ever a white background,
   we need a charcoal-ish square behind it? This is programmable, right?"

   ⚠️ HALF OF IT IS. Two different problems wearing one name:

   IN THE APP -- yes, and this is it. Any logo can be given `.fs-plate` and it
   gets the charcoal ground behind it, whatever the surface underneath. Pure
   CSS, no second asset, and it follows the logo file if that changes.

   ON THE HOME SCREEN -- no. No code of ours runs there: the launcher
   composites the PNG we shipped and that is the end of it. The ground has to
   be BAKED IN, which is why apple-touch-icon.png and the two maskable icons
   are opaque #14191C files rather than transparent ones.

   Same charcoal in both places so they never read as two different blacks.
   ========================================================================== */
:root { --fs-plate: #14191C; }

.fs-plate {
  background: var(--fs-plate);
  border-radius: 10px;
  padding: 8px 10px;
  display: inline-block;
}
.fs-plate img { display: block; }

/* Square marks want a squircle rather than a soft rectangle. */
.fs-plate.is-square { border-radius: 18%; padding: 6%; }

/* ⚠️ USE IT ONLY WHERE THE GROUND IS LIGHT OR UNKNOWN. On the app's own
   near-black surfaces the plate renders as a slightly lighter rectangle
   around the mark, which reads as an accidental box rather than a design
   choice. Every in-app surface today is dark, so nothing carries this class
   by default -- it exists for the moment one is not. */

/* ⭐ "SOON" RAIL ITEMS — FRIDAY_SCOPE Part 2's cut list, shown and disabled.
   Seeing what is coming is the pacing promise; a rail that grows new items
   between releases reads as an unfinished app rather than a deliberate one.
   ⚠️ Dimmed, never hidden, and never so dim it reads as a rendering fault. */
.fs-nav .fs-nav-item.is-soon { opacity: .38; cursor: default; }
.fs-nav .fs-nav-item.is-soon:hover { background: none; color: var(--fs-nav-idle); }

/* ==========================================================================
   ⚠⚠ A SHORT VIEWPORT HAS TO FIT ELEVEN ITEMS.

   The pair-tightening of 2026-08-24 added 8px of vertical padding per item to
   close the gap under the descender — right on a laptop, and 88px of extra rail
   across eleven items, which pushed SIX of them off the bottom at phone
   landscape (375px tall). The rail does scroll, so they were reachable, but a
   navigation you have to discover by scrolling is a navigation most people do
   not know they have.

   ⭐ Height, not width, is what runs out here — so this keys off max-height.
   ⛔ The icon/label PAIRING is untouched: what shrinks is the space around each
   item, never the gap that binds a word to its symbol.
   ========================================================================== */
@media (max-height: 780px) {
  .fs-nav-item, .nav .item { min-height: 46px; padding: 7px 6px 9px; }
  /* ⛔ THE ICON SHRINKS ON A SHORT SCREEN; THE WORD DOES NOT. Height is what
     is scarce here, and the icon is the part a reader can lose — the label is
     the part that says where the button goes. */
  :root { --nav-ico: clamp(22px, 2.1vw, 27px); --nav-lbl: 18px; }
}
@media (max-height: 560px) {
  .fs-nav-item, .nav .item { min-height: 40px; padding: 5px 6px 7px; }
  :root { --nav-ico: clamp(19px, 1.9vw, 23px); --nav-lbl: 18px; }
  /* ⛔ at this height the active item's 1.75x growth is what breaks the rail */
  .fs-nav-item.is-active .fs-nav-ico, .nav .item.active .ico {
    width: calc(var(--nav-ico) * 1.35); height: calc(var(--nav-ico) * 1.35);
  }
}
/* ==========================================================================
   THE MOUNTED RAIL KEEPS THE 56pt FLOOR — measured 2026-09-05.

   `play/pup-tank-layout.html` and `play/table-layout.html` mount the shared
   rail into the table grid with `data-fs-nav-mount`. The short-viewport 56px
   floor above is written `.fs-nav:not([data-fs-nav-mount]) .fs-nav-item`, so
   the scene pages are excluded from it by name and fell through to the 40px
   band. Measured at 800x420 and 568x320: TEN nav buttons at 40px on both
   Pup Tank rooms and both full-table rooms, where academy.html at the same
   viewport measures 56px.

   The exclusion itself is correct — that rule also switches the item to the
   drawer's row layout — so this restores the HEIGHT only, and nothing else.

   And the 40px band buys no reachability here: the scene rail already
   scrolls (scrollHeight 449 against clientHeight 324, and it carries
   `has-more`), so the target was being shrunk below the floor for nothing.

   `.fs-nav[data-fs-nav-mount] .fs-nav-item` is (0,3,0) and beats the 40px
   band's (0,2,0) regardless of order — raise specificity, never reorder and
   hope. #19, #21 and the note further down this file are all that lesson.
   ========================================================================== */
@media (max-height: 780px) {
  .fs-nav[data-fs-nav-mount] .fs-nav-item,
  .fs-nav[data-fs-nav-mount] .fs-nav-uitem { min-height: 56px; padding: 6px 6px 8px; }
}
/* ==========================================================================
   ⭐ THE ALWAYS-REACHABLE PAIR — Tour and Settings.
   Eric, 2026-08-26: "all levels should have TOUR and Settings even L0."
   They were only on Home's utility row, so from the Academy, a drill or the
   table there was no way to reach either — at any level, not just L0.

   ⚠️ DELIBERATELY SMALLER THAN A DESTINATION. The rail is six destinations
   and that is measured: at a 56pt floor eleven items is 705px against a
   375-430px landscape phone. These are ~40pt utilities, visually separated,
   so six destinations stays true while the two utilities are always there.
   ========================================================================== */
.fs-nav-util { margin-top: auto; padding-top: 6px;
  border-top: 1px solid rgba(255,255,255,.10); display: grid; gap: 2px; }
/* ⚠️ ONLY WHAT DIFFERS FROM A DESTINATION. These render with
   .fs-nav-item, so they already have the right size, spacing, hover and
   focus. Restating those here would be a second copy of the rail metrics —
   the exact defect this repo keeps paying for. The one real difference is
   that an <a> needs the button reset. */
.fs-nav-uitem { text-decoration: none; }

/* ⛔ NO `.fs-nav` RULE HERE. app-nav.css already sets
   `display:flex; flex-direction:column` on it, so `margin-top:auto` above
   works with no help — and adding a fourth `.fs-nav` block in a second
   file is defect #19 verbatim: same selector, two files, winner decided by
   <link> order. It added nothing and could only ever take something away. */

/* ==========================================================================
   ⛔⛔ SHORT VIEWPORTS: THE UTILITIES GO HORIZONTAL.

   Eric asked for Tour, Feedback and Settings on every nav at every level,
   at the same size as a destination. At desktop and tablet heights that is
   exactly what happens.

   ⚠️ BUT SIX DESTINATIONS PLUS THREE UTILITIES AT ~96px IS 604px OF RAIL,
   AND A LANDSCAPE PHONE IS 430px TALL. Measured at 932×430: Tour, Contact Us
   and Settings sat at y=470, 528 and 586 — all three below the fold. The rail
   scrolls, so they were technically reachable; a rail with no scroll
   affordance means nobody reaches them, and _p0-geometry gates on exactly
   that ("cut off where it cannot be scrolled to").

   ⭐ SO THE TRIO BECOMES A ROW, ICON-ONLY, AT THE 44pt FLOOR — only where
   the height demands it. Three across costs one row instead of three, and
   the whole rail fits without scrolling. Nothing is hidden and nothing moves
   at the sizes where there was room all along.
   ========================================================================== */
@media (max-height: 620px) {
  .fs-nav-util {
    display: flex;
    justify-content: space-around;
    gap: 2px;
    padding-top: 4px;
  }
  .fs-nav-uitem {
    flex: 1 1 0;
    min-height: 44px;
    padding: 4px 2px;
    gap: 0;
  }
  /* ⚠️ the LABEL goes, not the target. Same trade the destinations make at
     900px, and the accessible name still carries the word. */
  .fs-nav-uitem .fs-nav-lbl { display: none; }
  .fs-nav-uitem .fs-nav-ico { width: 22px; height: 22px; }
}

/* ⛔ AND THE BRAND GOES ON A SHORT VIEWPORT, TOO.
   Measured at 932×430 after the utilities went horizontal: brand 39 +
   six destinations at 56 + the utility row 61 + 32px of padding + gaps
   = 482px of rail in 430px. Still 52 over.

   The wordmark is 39 of those 52. It is the one element on the rail that
   navigates nowhere — the Home button is right beneath it and does the same
   job — so on a screen with no room it is the first thing to go.

   ⚠️ `.fs-nav .fs-nav-brand` IS (0,2,0) ON PURPOSE. Defect #21: a
   `.fs-nav-brand` rule further down this same file beats an earlier one at
   equal specificity regardless of media query, which is how the wordmark
   shipped on every phone for months while a stylesheet insisted it was
   hidden. When two rules disagree, raise specificity; do not reorder. */
@media (max-height: 620px) {
  .fs-nav .fs-nav-brand { display: none; }
  .fs-nav { padding-top: 6px; padding-bottom: 6px; }
}

/* ==========================================================================
   ⭐ THE RAIL, MEASURED — Eric, 2026-08-29: "triple check the NAV panel is
   spaced PERFECTLY... properly spaced whether they are unlit or lit, all fit."
   `_nav-audit` measures all of it at five viewports. Two real defects, both
   invisible in a screenshot:

   ⛔⛔ 1 · THE SHORT-VIEWPORT RULES NEVER APPLIED. `@media (max-height:780px)`
   and `(max-height:560px)` above set `.fs-nav-item { min-height: 46px }` and
   `40px` — (0,1,0). `.fs-nav .fs-nav-item { min-height:56px }` is (0,2,0) and
   beats them on specificity regardless of order, so a 360px-tall landscape
   phone kept the full 56px item and the rail measured 417px against a 360px
   viewport. Nine items, 57px of overflow, and the last one unreachable.
   ⚠️ #19 and #21 AGAIN, third time: a rule that looks like it wins because it
   is later, losing to one that is more specific. Raise specificity; do not
   reorder and hope.

   ⛔⛔ 2 · LIGHTING AN ITEM MOVED EVERY ITEM BELOW IT. Measured 75px unlit vs
   77px lit. The active icon scales 1.75x and the label 1.5x — both deliberate
   (§2.1: "the icon was doing all the work") — but scaling WIDTH and FONT-SIZE
   changes layout, so the rail twitched on every navigation. A fixed rail
   exists so the target does not move; two pixels of drift on every item is
   the thing it is there to prevent.
   ⭐ The growth is now a TRANSFORM on the icon and a RESERVED line box on the
   label: identical appearance, no reflow.
   ========================================================================== */

/* 1 · the height rules, at a specificity that actually wins */
@media (max-height: 780px) {
  .fs-nav .fs-nav-item, .fs-nav .fs-nav-uitem {
    min-height: 46px; padding: 7px 6px 9px;
  }
}
@media (max-height: 560px) {
  .fs-nav .fs-nav-item, .fs-nav .fs-nav-uitem {
    min-height: 40px; padding: 5px 6px 7px;
  }
}

/* 2 · lit and unlit occupy exactly the same box */
.fs-nav .fs-nav-ico {
  width: var(--nav-ico);
  height: var(--nav-ico);
  display: grid;
  place-items: center;
  /* ⭐ the glow must not be clipped by the box it now lives in */
  overflow: visible;
}
.fs-nav .fs-nav-item.is-active .fs-nav-ico {
  /* ⛔ NOT width/height — that is what reflowed the rail */
  width: var(--nav-ico);
  height: var(--nav-ico);
}
.fs-nav /* ⛔ AND THE LIT ICON GREW 1.75x INSIDE A BOX THAT DID NOT.
   Eric, 2026-09-08: "the Academy name is not centered on the nav ... it needs
   to be centered vertically." MEASURED at 1920: the lit `<svg>` draws 60x60
   inside a 34x34 box, so it overflows ~13px UPWARD past its own slot while the
   icon+label group is still centred on the 34px box. The group is centred; the
   INK is not, and the eye centres on ink.
   ⭐ 1.25x keeps the lit icon inside its own box (42px against 34) and the
   pair reads centred. The emphasis is not lost — weight, accent, glow, two
   edge bars and a 1.25x label all still fire. */
.fs-nav-item.is-active .fs-nav-ico > svg {
  transform: scale(1.25);
  transform-origin: center;
}
/* ⭐ THE LABEL RESERVES ITS LIT SIZE. The active label is 1.5x, so every
   label — lit or not — gets a line box that tall. ⚠️ The TEXT does not grow
   when unlit; only the space it sits in is already the right size. */
/* ⛔⛔ AND THE HEIGHT IS PINNED ON THE ITEM ITSELF.
   Reserving the label box took the lit/unlit difference from 2px to 1px and
   then back to 2px as other rules moved -- which is the signal that chasing
   it through the children is the wrong approach. The ITEM is what must not
   change size; what happens inside it is presentation.
   ⚠️ min-height stays as the 56pt floor; this fixes the BOX so an active
   item cannot push the rail around. */
.fs-nav .fs-nav-item {
  height: var(--fs-nav-item-h, auto);
  box-sizing: border-box;
}
.fs-nav .fs-nav-lbl {
  display: flex;
  align-items: center;
  justify-content: center;
  /* ⛔ HEIGHT, NOT min-height. min-height still let the box GROW when the
     active label went to 1.5x, which left a 1px difference between lit and
     unlit — small, and it still moved every item below it on every
     navigation. A fixed line box is reserved once and never changes. */
  /* ⚠️ RESERVED FOR THE LIT SIZE, and the ratio is the SAME TOKEN the lit
     rule uses -- a second literal here is what let the two drift. */
  height: calc(var(--nav-lbl) * var(--nav-lbl-lit) * 1.3);
  line-height: 1.15;
  overflow: visible;
}

/* ==========================================================================
   ⭐ THE MOUNTED SCENE RAIL, ON A SHORT SCREEN.
   table-layout and pup-tank-layout mount the rail into a column of their own
   grid, so it keeps the vertical layout even where the standalone rail
   becomes a bottom bar — the bottom of a landscape phone belongs to the
   action row, and putting a fixed bar there covered all sixteen nav controls
   (_p0-detail, 932×430).

   ⚠️ BUT NINE ITEMS AT 56pt IS 504px AND AN iPHONE 8 IN LANDSCAPE IS 375px
   TALL. Something has to give, and of the three candidates only one is
   honest: dropping items breaks "always reachable", shrinking below 56pt
   breaks the floor this audience was measured against, so the rail SCROLLS.
   ⭐ _p0-geometry excludes content inside a scrollable panel by design —
   "reachable, just not right now" is a different thing from cut off — but
   only if it genuinely scrolls, which needs a height to scroll WITHIN.
   ⛔ A column that grows to fit its content never scrolls; it just overflows
   the grid, which is what was happening. */
.fs-nav[data-fs-nav-mount] {
  min-height: 0;              /* a grid item's default is min-content */
  max-height: 100%;
  overflow-y: auto;
  overscroll-behavior: contain;
  /* ⚠️ A scrollable rail must SHOW that it scrolls, or a student who cannot
     see Settings will conclude it is not there. */
  scrollbar-width: thin;
  /* ⚠️ AND THE LAST ITEM MUST CLEAR THE SCENE'S OWN BOTTOM CHROME.
     With the rail scrolling, Settings sat flush against the end of the
     column and the action row overlapped it — _p0-detail listed exactly it,
     on both scene pages, at 932x430. ⛔ A control that is scrolled INTO view
     and then covered is worse than one below the fold: it looks reachable. */
  padding-bottom: 12px;
}
/* ⛔⛔ AND THE RAIL MUST END ABOVE THE ACTION BAR.
   _p0-detail, precisely: Settings "blocked by div.action-bar BET SIZE 1/3"
   on both scene pages at 932x430. ⚠️ padding-bottom did NOT fix it, and the
   reason is worth keeping: padding only moves the last item when the rail
   SCROLLS. Where the content already fits, the padding just extends the
   rail box down behind the action bar and the item stays exactly where it
   was. ⭐ Capping the HEIGHT is what actually reserves the space — the rail
   ends above the bar, and if that means it scrolls, it scrolls. */
@media (max-height: 520px) {
  /* ⚠️ 100% RESOLVES TO auto IN A GRID COLUMN WITH NO DEFINITE HEIGHT,
     so the first cap did nothing at all. vh is definite. */
  .fs-nav[data-fs-nav-mount] { max-height: calc(100vh - 96px); }
}

/* ⛔ AND THE MOUNTED RAIL SITS ABOVE THE FELT.
   With the rail capped short of the action bar, _p0-detail moved the finding
   rather than clearing it: Settings went from "blocked by div.action-bar" to
   "blocked by div#area.table-area" — the felt itself. The rail is a grid
   sibling of the scene, so at equal stacking the later element wins and the
   table paints over the rail's last item.
   ⚠️ A control that is visible, correctly sized and in the right place, and
   simply has the felt drawn on top of it, is still a control nobody can
   press — and it is the hardest kind to see in a screenshot, because it
   looks completely normal. */
.fs-nav[data-fs-nav-mount] {
  position: relative;
  /* ⭐ THE NUMBER THE FIXED RAIL USES, WHATEVER THAT NUMBER IS (app-nav.css).
     60 was a guess and it lost: .table-area sets container-type:size, which
     forms a containment context, and the felt painted over the rail's last
     item. Matching the rail's own layer is what stops the re-guessing.
     ⛔⛔ AND IT MOVED TO 99000 WITH IT — Eric, 2026-09-11: "the nav panel
     NEVER disappears ... Even when we ask them to give feedback and contact
     us." The app's overlays sit at 9000-10000; at 400 the feedback survey and
     the stat explanation buried the mounted rail on table-layout, and
     check_nav_always's overlay pass named both. ⚠️ Two rules, one number:
     raising app-nav.css alone fixed ask-tiger and left this page behind. */
  z-index: 99000;
}

/* ==========================================================================
   ⛔⛔ THE RAIL DOES NOT STAND DOWN. IT IS ALWAYS PRESENT.

   Eric, 2026-09-03, on meeting it: "there is no DEV option in the bottom
   right either. so with no nav, I cannot escape the pup tank!!!!" — then,
   plainly: "the nav panel is ALWAYS PRESENT in PLAY mode (actually ALWAYS
   NO MATTER WHAT)."

   ⚠️ That REVERSES NAVIGATION_SPEC_v8 Part 5 ("Hidden during a hand at the
   Play table — Focus. Returns when the hand resolves."). The app is right
   and the spec is now the stale copy; Part 5 is Eric's to strike, per
   SPEC_HYGIENE S2.

   ⛔⛔ AND WHAT SHIPPED NEVER DID WHAT PART 5 SAID EITHER. nav-focus.js read
   "a decision is live" off the action buttons' own `disabled` state, which
   sounds like the right abstraction and is not — for a reason CLAUDE.md had
   ALREADY recorded one screen away, about action-reasons.js: **the rooms
   keep FOLD live for as long as the hero is in the hand**. So the flag was
   true from the first deal onward — through every villain's turn, between
   streets, and after the hand ended. Measured: `.fs-nav` collapsed to width
   0 on the first deal and was still 0 three hands later, with the grid
   column still holding its 190px.
   ⭐ "Returns when the hand resolves" was never once true, and the only way
   off the page went with it.

   ⚠️ The collapse lived HERE and the attribute was set by js/nav-focus.js.
   Both are gone. `check_nav_always.py` is the guard and it DRIVES REAL
   HANDS — source is exactly where this looked correct.
   ========================================================================== */
.fs-nav {
  transition: width .18s ease, transform .18s ease, visibility .18s;
}
@media (prefers-reduced-motion: reduce) {
  .fs-nav { transition: none; }
}
