/* ═══ §4 · SHARED · THE SHELL ═════════════════════════════════════════════════════════
   The header, the scroll container, the bottom nav and the toast — the four things present on
   every tab. Nothing below §4 may restyle them. */

/* ★ THE ACTUAL BUG BEHIND THE SWIPE-LANDS-SCROLLED BUG (operator 2026-08-15/16, four attempts —
   the first three all patched switchTab()/the overscroll guard and all failed, because none of
   them were the defect). `min-height: 100vh` is a FLOOR, not a ceiling: with no explicit `height`
   anywhere on #app, this box grows to fit whatever its content needs — and `.view-container`
   below, `flex: 1` with no `min-height: 0`, defaults to a flex item's intrinsic minimum, which is
   its OWN CONTENT'S height, not 0. Neither one ever shrinks the other, so #app simply grows past
   100vh to match whichever view is active (measured live: 4665.95px for a fully-loaded Scanners
   tab against an 844px viewport), and `.view-container`'s `overflow-y: auto` NEVER ACTUALLY
   ACTIVATES — its box was never smaller than its content in the first place. The DOCUMENT itself
   becomes the real scroller instead, which is why three straight rounds of resetting
   `.view-container.scrollTop` changed nothing observable: that property was never the one moving.
   Confirmed by injecting both fixes live and re-measuring before touching this file: `#app`
   dropped to exactly 844px, `.view-container` gained a real scrollHeight/clientHeight split
   (4666/844), and a tab switch immediately after landed scrollTop at 1, correctly, every time.
   `100dvh` (not `100vh`) is the real fix for iOS Safari specifically — its collapsing toolbar
   makes a bare `100vh` taller than the visible viewport at certain scroll states; `100dvh`
   tracks the ACTUAL visible height. The `100vh` line stays first as the fallback for anything
   that does not understand `dvh` — CSS keeps the last value it understands, so an old engine
   simply never sees the second line.
   
   NEVER CHANGE `height: 100vh;` it breaks navbar being pinned to the very bottom of my view port 
   and breaks my auto top..*/
#app {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  height: 100vh;
}

/* ── §4.1 the header: the session word + the saw · SFTi · the fraction + tape pulse + STATIK +
   the regime. The markup is EMITTED by app/system/js/shell.js (mountHeader) — system.html hangs
   the empty bar and nothing else, the same arrangement app/shared/js/footer.js uses.

   PINNED, LIKE THE NAV (operator 2026-08-11: "pinned exactly like the navbar … just in its current
   location but always on screen right there"). Measured cause of the drift, at 390x844 on Settings:
   the document was 967px in an 844px frame and scrolled as one piece, carrying this in-flow bar off
   screen and sliding cards under the iOS clock.

   `position: fixed` is the nav's own mechanism, and this takes ONLY that line from it.
   ⚠ IT DELIBERATELY DOES **NOT** COPY the nav's `translate3d` / `will-change` / `backface-visibility`.
   #218 measured what those do on iOS: they promote the element to a compositor layer that tracks the
   LAYOUT viewport instead of the VISUAL one, and it drifts off the edge. A fixed element with no
   transform of its own is pinned to the visual viewport, which is exactly what is wanted here.
   ⚠ AND NOTHING ABOUT THE NAV, #app, html OR body IS TOUCHED — #201 rules the nav correct as it is,
   and a viewport lock on the shell (tried, reverted) moved it. This bar leaves the flow; that is all. */
.status-bar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  /* ★ A THREE-TRACK GRID, AND `minmax(0, 1fr)` IS THE LOAD-BEARING PART. A plain `1fr` means
     `minmax(auto, 1fr)`, and that AUTO MINIMUM lets whichever side holds more content grow its own
     track and shove the mark off the true viewport centre. `0` as the minimum makes the two side
     tracks equal by construction, which is what puts SFTi on the centre of the screen. */
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  gap: 10px;
  /* min-height, not height: the bar's real height is whatever the safe-area inset and the content
     make it, and syncHeaderClearance() MEASURES it. A fixed height could only clip. */
  min-height: var(--status-height);
  padding: max(var(--sp-md), var(--safe-top)) var(--sp-lg) var(--sp-md) var(--sp-lg);
  border-bottom: 1px solid var(--glass-border);
  flex-shrink: 0;
  /* above the scrolling cards, far below the nav (9999) and every modal, so a sheet still covers it */
  z-index: 100;
  /* ★★ A FLAT FILL, AND IT REPLACES `glass-heavy` ON THIS BAR (operator, iOS 27.6 beta: "still
     blurry in pwa only. not in safari").
     The markup used to read `<header class="status-bar glass-heavy">`, and `.glass-heavy` is
     `backdrop-filter: blur(30px)` over a TRANSLUCENT fill. On a `position: fixed` element that is
     the exact combination #218 measured as fatal on iOS, and the exact one
     `app/shared/css/header.css` had already deleted from the PUBLIC header — its own words:
     "Scrolling header + blur is safe; fixed is not", and "a FLAT `background: var(--bg-void)` …
     needs no blur, triggers none of #218's compositor promotion".
     ★ THE DIAGNOSIS WAS TWO SCREENSHOTS OF ONE INSTALL, not a theory: at the same minute, on the
     same phone, index.html's bare <header> rendered CRISP in the standalone app while this bar
     rendered washed out. Same OS, same status bar, same launch — the only difference was the
     class. Apple's own status-bar layer was never the cause; the first two attempts chased it and
     both failed, because an opaque backdrop cannot un-blur text that is inside the blurred
     element.
     ⚠ NOT `@media (display-mode: standalone)`. The fill is what this bar is now made of — scoping
     it would leave the browser with a transparent bar and no background at all.
     ⚠ AND NEVER A backdrop-filter HERE AGAIN, in any form. The colour is the canvas TOKEN rather
     than a literal, so the strip, the page and the manifest's own `background_color` agree by
     construction instead of by three hand-copied hex values. */
  background: var(--bg-void);
}
/* ★ EQUAL CLEARANCE AND A VIEWPORT-CENTRED MARK ARE ONLY SIMULTANEOUSLY SATISFIABLE WHEN BOTH SIDES
   ARE EDGE-ANCHORED, and that is the whole reason this rule exists. The side tracks are EQUAL (see
   above), so unequal CONTENT becomes unequal CLEARANCE: measured at 147px of left against 115px of
   right, the mark had 5px beside it on one side and 36px on the other — the congestion the operator
   saw. `stretch` + `space-between` makes each side fill its own track, anchoring its outer item to
   the frame edge and its inner item to the mark, so both clearances equal the grid gap BY
   CONSTRUCTION. Nothing to tune, and it cannot drift when a state word changes width. */
.status-side {
  display: flex;
  align-items: center;
  gap: 9px;
  justify-self: stretch;
  justify-content: space-between;
  min-width: 0;
  overflow: hidden;
}
/* the right side's inner cluster — pulse · STATIK · regime travel together against the mark */
.status-pair {
  display: flex;
  align-items: center;
  gap: 9px;
  min-width: 0;
  flex-shrink: 0;
}

/* THE SESSION — a holo glyph, not a word (operator 2026-08-11) ────────────────────────────────
   ★ IT PAID FOR THE WHOLE REBALANCE. `● After Hours` measured 71px — the widest single item in the
   bar — while the RIGHT track was 2.2px SHORT of the 44px finger floor its four gaps need and the
   LEFT carried 55.6px of surplus. A ~22px glyph frees almost exactly the deficit, so the two
   complaints ("right-heavy", "taps land wrong") close on the same edit. It also has no font size,
   which is the other half of "a bit small".
   The glyph set is app/internal.svg/session.svg, in the nav's own holo language: stroke-only,
   currentColor, and the same drop-shadow bloom .holo-btn svg wears. */
.status-session {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: 0;
  padding: 0;
  flex-shrink: 0;
  cursor: pointer;
  color: var(--text-dim);
  transition: color 0.3s ease, filter 0.3s ease;
}
.status-session svg {
  width: 22px;
  height: 22px;
  display: block;
  filter: drop-shadow(0 0 5px currentColor);
}
/* ★ TWO INKS PER STATE, AND THE SECOND ONE IS WHY `--ses-accent` EXISTS (operator 2026-08-11:
   "sunset red/red, sunrise yellow orange, intraday bright red/dark orange"). `currentColor` can
   carry exactly one, so the ARC takes it and the PIP — the diamond marking NOW on the day's arc —
   takes `--ses-accent`. The pip is always the BRIGHTER of the pair: it is the mark the eye is meant
   to find, and the arc is the field it sits on.
   ⚠ INTRADAY LEFT GREEN FOR RED ON HIS INSTRUCTION. Green said "open" by convention; his palette
   says the session's HEAT, and heat is what this bar reports. The state is still unmistakable
   because the GLYPH differs, not only the tint — the pip sits at the apex and only this one breathes. */
.status-session .s-mark        { fill: var(--ses-accent, currentColor); stroke: none; }
.status-session .s-mark-stroke { fill: none; stroke: var(--ses-accent, currentColor); stroke-width: 1.5;
                                 stroke-linecap: round; stroke-linejoin: round; }
.status-session .s-dot         { fill: var(--ses-accent, currentColor); }

.status-session.premarket  { color: #f0a500; --ses-accent: #ffd447; }
.status-session.intraday   { color: #b3450a; --ses-accent: #ff2d20; }
.status-session.afterhours { color: #8f1d18; --ses-accent: #ff3b30; }
.status-session.overnight  { color: #4a4a58; --ses-accent: #8a8a9a; }
.status-session.unknown    { color: var(--accent-red); --ses-accent: var(--accent-red); }
/* the market is OPEN — the one state that earns motion */
.status-session.intraday svg { animation: session-breathe 2.6s ease-in-out infinite; }
@keyframes session-breathe {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.55; }
}

.status-brand {
  justify-self: center;
  font-size: 1rem;
  font-weight: 800;
  letter-spacing: 0.1em;
  white-space: nowrap;
  text-decoration: none;
  /* the storefront's wordmark gradient — the SAME red→gold the shared header carries, so the
     brand reads as one mark across index · shop · system rather than three tints of it. */
  background: linear-gradient(135deg, var(--accent-red), var(--accent-gold));
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}
/* the regime chip — the Market Regime modal's only door, and it drops to the session word's size
   so the two ends of the bar weigh the same. */
.status-clock {
  font-family: var(--font-mono);
  font-size: 0.57rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  color: var(--text-secondary);
  white-space: nowrap;
  flex-shrink: 0;
  cursor: pointer;
}

/* THE MEMORY CHIP — the regime chip's opposite number, at the other frame edge ─────────────────
   Same typography, same four letters, opposite subject: `Bull` is what the MARKET is doing, `GROW`
   is what the ORGANISM is doing. That pairing is the whole reason the bar reads as one arrangement
   instead of eight widgets, so this rule INHERITS .status-clock's metrics rather than restating
   them — a second set of numbers here is how two ends of a bar drift apart.
   The colour is the STATE's, the way the session glyph's is: gold while memory is forming (the same
   ink the learn strip beside it uses), quiet when held, and RED when the instrument could not read —
   because an unread probe must never look like a measured empty store. */
.status-engram {
  font-family: var(--font-mono);
  font-size: 0.57rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  white-space: nowrap;
  flex-shrink: 0;
  cursor: pointer;
  color: var(--text-dim);
  transition: color 0.3s ease;
}
.status-engram.grow {
  color: var(--accent-gold);
  animation: engram-breathe 2.6s ease-in-out infinite;
}
.status-engram.held { color: var(--text-secondary); }
.status-engram.void { color: var(--text-dim); }
.status-engram.null { color: var(--accent-red); }
@keyframes engram-breathe {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.6; }
}

/* THE SAW CHIP — what the board is looking at right now, one ticker per race poll. The black box
   saying what it sees, in the chrome. */
.status-saw {
  display: inline-flex;
  align-items: baseline;
  gap: 4px;
  white-space: nowrap;
  flex-shrink: 0;
  font-size: 0.58rem;
  letter-spacing: 0.04em;
  color: var(--text-dim);
}
.status-saw b {
  color: var(--accent-cyan);
  font-weight: 700;
  font-size: 0.63rem;
  animation: saw-swap 0.45s cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes saw-swap {
  from { opacity: 0; transform: translateY(-3px); }
  to   { opacity: 1; transform: none; }
}

/* THE FRACTION — rows clearing the board's own floor, over the size of the gaze. */
.status-frac {
  font-size: 0.62rem;
  letter-spacing: 0.02em;
  color: var(--accent-cyan);
  font-weight: 700;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  flex-shrink: 0;
}
.status-frac s {
  text-decoration: none;
  color: var(--text-dim);
  font-weight: 500;
  font-size: 0.55rem;
}
.status-frac.flash { animation: frac-flash 0.5s ease; }
@keyframes frac-flash { from { color: #fff; } to {} }

/* THE TWO STRIPS — LEARNING on the left, SEEING on the right (operator 2026-08-11) ────────────
   One rule, two skins, four bars each, both doors to the movie. They are a PAIR and they are read
   as a pair: it SEES and it LEARNS. Side by side that is the single most useful sentence about the
   organism's state, and it sits on the first screen.
       LEFT  · learn · engram.claims           — the memory it has formed
       RIGHT · see   · feed.severity           — the tape landing, the feed owner's own verdict
   ★ BOTH ARE RENDERED, NEVER DERIVED. `see` reads the verdict the wheel computes once in
   apisurface.feed_view (verdicts.js §3 is the law); `learn` reads a COUNT, and a count is not a
   verdict — it is lit iff the number MOVED between two reads, which is an observation, not an
   opinion about health.
   ★ AND `learn` IS DARK TODAY ON PURPOSE. engram.claims is 0: the organism is watching and has not
   yet learned. A dark strip beside a lit one is that fact, honestly, at a glance — it is not a
   broken element, and its tooltip says which counter and what value. It lights itself the moment
   the engram fills; nothing has to be changed for that to happen.
   The motion is CSS, so neither strip runs a timer of its own. */
.status-strip {
  display: flex;
  align-items: flex-end;
  gap: 3px;
  height: 13px;
  flex-shrink: 0;
  cursor: pointer;
}
.status-strip i {
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: #3a3a46;
  transition: height 0.35s ease, background 0.35s ease, border-radius 0.35s ease;
}
/* ══ EACH STRIP IS A FOUR-STEP METER, NOT A LAMP ═══════════════════════════════════════════════
   (operator 2026-09-10: "the dots that sit stale forever and the lines that never stop doing the
   wave… they dont make sense and are not informative")
   BOTH READINGS WERE BINARY AND BOTH WERE PINNED. `.live` raised all four bars and ran
   `strip-run` INFINITELY; without it all four fell to dead dots. So `see` — driven off a feed
   severity that reads OK all day — waved for ever and said only "still OK", and `learn` — driven
   off an engram total that moves once a minute at most — sat dark for ever and said only "not
   yet". Neither could report HOW MUCH of anything, which is the one thing a strip of four is
   shaped to say.
   ★ THE BARS NOW COUNT. `.on` is set per bar by `_paintStrip`, so the strip reads 0-4 at a glance
   and an unlit bar keeps the dot it always was — the rest state is unchanged, what is new is that
   there are states BETWEEN the two.
   ★★ AND THE MOTION IS A ONE-SHOT ON A REAL EVENT. `strip-beat` fires once, when the counter
   underneath ACTUALLY MOVED, and the strip is still between events. A perpetual animation is
   indistinguishable from a frozen one — both are "always doing that" — so the wave was carrying no
   information at exactly the moments it looked busiest. */
.status-strip i.on {
  height: 12px;
  border-radius: 1.5px;
}
.status-strip.beat i.on { animation: strip-beat 0.75s ease-out 1; }
.status-strip.beat i.on:nth-child(1) { animation-delay: 0s; }
.status-strip.beat i.on:nth-child(2) { animation-delay: 0.08s; }
.status-strip.beat i.on:nth-child(3) { animation-delay: 0.16s; }
.status-strip.beat i.on:nth-child(4) { animation-delay: 0.24s; }
@keyframes strip-beat {
  0%   { opacity: 0.30; transform: scaleY(0.55); }
  45%  { opacity: 1;    transform: scaleY(1.18); }
  100% { opacity: 1;    transform: scaleY(1); }
}
@media (prefers-reduced-motion: reduce) {
  .status-strip.beat i.on { animation: none; }
}
/* SEEING — solid cyan bars, the tape landing. */
.status-strip.see i.on {
  background: var(--accent-cyan);
  box-shadow: 0 0 6px rgba(106, 208, 200, 0.75);
}
/* LEARNING — GOLD and HOLLOW, so the pair is never read as one thing duplicated. A different
   shape is what makes two abstract strips legible as two different facts. */
.status-strip.learn i.on {
  background: transparent;
  border: 1px solid var(--accent-gold);
  box-shadow: 0 0 6px rgba(212, 175, 55, 0.6);
}
.status-strip.learn i { border: 1px solid transparent; }

/* THE STATIK CONTROL — the boot brain itself, the same file the organism draws while it wakes.
   28px of ink inside a 44px finger target (tap-targets.css grows the hit box, not the mark). */
.statik-chat-trigger {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: 0;
  padding: 9px 8px;
  margin: -9px -4px;
  flex-shrink: 0;
  cursor: pointer;
  opacity: 0.92;
  transition: opacity 0.18s ease;
}
.statik-chat-trigger:hover { opacity: 1; }
.statik-chat-trigger img { display: block; width: 28px; height: 26px; }

/* ── §4.2 the scroll container + view visibility ── */
/* THE PAGE NEVER FOLLOWS A HORIZONTAL FINGER (operator 2026-08-10, caught mid-drag in his own
   shot): a sideways flick SWITCHES the tab (swipe.js) and the entry motion is .view.active's
   viewFadeIn below — the document itself must not pan. `touch-action` binds between the finger
   and the NEAREST scroller, so carousels and the chart box (their own overflow-x scrollers,
   below this) still own their own gesture. */
html,
body {
  overflow-x: hidden;
  touch-action: pan-y;
  overscroll-behavior: none;
}
.view-container {
  flex: 1;
  /* THE OTHER HALF of the #app fix above — a flex item's default min-height is its own content's
     size, not 0, so without this override the box below never actually shrinks to fit #app's now-
     real 100dvh ceiling and `overflow-y: auto` has nothing to clip. This line is what makes it. */
  min-height: 0;
  /* ★ THE FOOTER-DISCONNECTS-FROM-NAV BUG (operator 2026-08-20: "when race is loading the footer
     disconnects from nav... every page in system.html does this when they are short"). footer.js
     appends ONE #site-footer as the LAST CHILD of this box, after whichever `.view` is `.active`
     (§4.1 below) — not inside it. In plain block flow the footer paints immediately after that
     view's own content, wherever that lands; on a quiet/short view (a 504 note, a handful of
     rows) that is far above this box's own bottom edge, leaving a dead gap between the footer and
     the floating nav it is supposed to sit against. On a tall view the footer already lands at the
     true bottom via ordinary overflow, which is why the defect is invisible there and only shows
     "when they are short". `display: flex; flex-direction: column` turns this box into the
     classic sticky-footer container — `footer.in-shell`'s own `margin-top: auto` (footer.css) then
     claims whatever space is left in THIS box's content area (already net of `--nav-clearance`
     above) and pushes the footer down to it; when a view's content already fills or exceeds this
     box the free space is zero and the footer sits exactly where it always did, so tall views are
     unaffected by this change. `.view { display: none }` items are not flex items at all and do
     not participate, so only the one active view + the footer are ever laid out here. */
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  overflow-x: hidden;
  -webkit-overflow-scrolling: touch;
  /* ★ THE REAL FIX (2026-08-12): kill the rubber-band on the ACTUAL scroller. This box is what
     scrolls — not body. Pulling past its top or bottom bounced it and let the body's --bg-void
     bleed OVER the fixed header (top) and nav (bottom). `body` already has overscroll-behavior:none
     but that governs the body scroll, which never happens here, so it did nothing. The lock has to
     be on the element that actually scrolls. This was the whole bug — not the mesh, not cache. */
  overscroll-behavior: none;
  scrollbar-width: none;
  /* THE CARD COLUMN. This box's content width is what every card is wide, on every surface —
     so it is the only box that states one. */
  max-width: var(--shell-w);
  margin: 0 auto;
  padding: var(--sp-md);
  /* THE TOP RESERVE — `--header-clearance` (tokens.css), the ONE declaration of how far down the
     fixed header reaches; the toast reads the same token. It is MEASURED rather than hardcoded
     because the bar's height is `max(48px, safe-area-top + padding)` — 51px on a desktop frame,
     98px at a 59px notch inset, and a constant would be wrong on one of them. */
  padding-top: calc(var(--header-clearance) + var(--sp-md));
  padding-bottom: var(--nav-clearance);
  padding-left: max(var(--sp-md), var(--safe-left));
  padding-right: max(var(--sp-md), var(--safe-right));
}
.view-container::-webkit-scrollbar { display: none; }

.view { display: none; }
.view.active { display: block; animation: viewFadeIn 0.3s ease-out; }
@keyframes viewFadeIn {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* THE CARD FAMILIES — ONE WIDTH, ONE DECLARATION. Every top-level card on every surface is
   exactly the card column wide. A family that wants to be narrower has to say so here, in
   front of the others, rather than by quietly adding an inset three hundred lines away. */
.race-bar,
.scanner-card,
.home-section,
.r-panel,
details.settings-section,
.discover-hdr,
.discover-section {
  width: var(--card-w);
  max-width: 100%;
}

/* ── §4.3 the bottom nav. #201: the nav is HOLLOW BY DESIGN and content scrolls clear behind
   it. Do not make it opaque and do not change its padding. ── */
.holo-nav {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 9999;
  transform: translate3d(0, 0, 0);
  -webkit-transform: translate3d(0, 0, 0);
  will-change: transform;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  display: flex;
  justify-content: center;
  align-items: flex-end;
  /* THE ROW MUST HOLD EVERY BUTTON. At eight tabs the old `3.5vw` gap + 24px side pad needed
     460px of row inside a 390px phone, and the two end labels were clipped mid-word — "OME" and
     "SETTING". The gap and the SIDE pad now shrink with the viewport so the row fits at 390px
     and still opens up on a wide screen.
     ⚠ The BOTTOM pad below is untouched: #201 ruled the hollow nav correct and #218 measured the
     home-indicator inset, and neither is what overflowed — this is the horizontal axis only. */
  gap: clamp(4px, 2vw, 28px);
  padding: 14px clamp(6px, 2vw, 24px) 28px;
  /* the 28px is OUR design margin; the inset is the DEVICE's and only it may state one. A 28px
     FALLBACK would assert 28px of home indicator on exactly the screens that report none. */
  padding-bottom: max(28px, env(safe-area-inset-bottom, 0px));
  /* SEMI-TRANSPARENT AT THE BOTTOM ON PURPOSE. `to top` means 0% is the BOTTOM edge, so an
     opaque first stop paints a solid slab under the labels — and the labels necessarily sit one
     home-indicator inset above that edge, which then reads as the nav hovering. */
  background: linear-gradient(to top,
      rgba(4, 8, 14, 0.92) 0%,
      rgba(4, 8, 14, 0.78) 40%,
      rgba(4, 8, 14, 0.0) 100%);
  pointer-events: none;
}
.holo-btn {
  pointer-events: all;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 5px;
  text-decoration: none;
  background: none;
  border: none;
  /* ★ COLOUR ONLY (operator 2026-08-11: "the navbar in all index/shop/system/ needs to match
     theme"). Silver at rest, GOLD when current — the same two inks `app/shared/css/nav.css`
     gives index and shop, so the three navs are one design in three documents.
     ⚠ NOT ONE BYTE OF GEOMETRY MOVES. #201 ruled this nav correct as it is: the gap, the
     padding, the 34px icons, the float and the hollow gradient are all untouched below. The
     operator authorised the PALETTE, and only the palette. */
  color: rgba(192, 192, 192, 0.72);
  transition: color 0.3s ease;
  animation: holoFloat 3.2s ease-in-out infinite;
  -webkit-tap-highlight-color: transparent;
}
.holo-btn:nth-child(1) { animation-delay: 0s; }
.holo-btn:nth-child(2) { animation-delay: 0.15s; }
.holo-btn:nth-child(3) { animation-delay: 0.3s; }
.holo-btn:nth-child(4) { animation-delay: 0.45s; }
.holo-btn:nth-child(5) { animation-delay: 0.6s; }
.holo-btn:nth-child(6) { animation-delay: 0.75s; }
.holo-btn:nth-child(7) { animation-delay: 0.9s; }
.holo-btn:nth-child(8) { animation-delay: 1.05s; }
/* the travel here IS --nav-float; app.js adds it back when it measures the occlusion. */
@keyframes holoFloat {
  0%, 100% { transform: translateY(0px); }
  50%      { transform: translateY(-5px); }
}
.holo-btn svg {
  width: 34px;
  height: 34px;
  stroke: currentColor;
  fill: none;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  /* the resting glow stays SUBTLE and silver — eight lit icons at rest is what made this row read
     as a strip of blue rather than as navigation. The CURRENT tab is what earns the bloom. */
  filter: drop-shadow(0 0 6px rgba(192, 192, 192, 0.35));
  transition: filter 0.3s ease;
}
.holo-btn span {
  font-family: 'Orbitron', monospace;
  font-size: 0.5rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: rgba(192, 192, 192, 0.5);
  transition: color 0.3s ease;
}
.holo-btn.active { color: var(--accent-gold); }
.holo-btn.active svg {
  /* gold core, red outer — shop's two emphasis accents in the order the wordmark uses them */
  filter: drop-shadow(0 0 8px rgba(212, 175, 55, 0.85))
          drop-shadow(0 0 18px rgba(212, 175, 55, 0.55))
          drop-shadow(0 0 36px rgba(230, 57, 70, 0.30));
}
.holo-btn.active span { color: var(--accent-gold); }
@media (hover: hover) and (pointer: fine) {
  .holo-btn:hover {
    color: var(--accent-gold);
    animation-play-state: paused;
    transform: translateY(-8px) scale(1.1);
  }
  .holo-btn:hover svg {
    filter: drop-shadow(0 0 10px rgba(212, 175, 55, 0.95))
            drop-shadow(0 0 22px rgba(212, 175, 55, 0.55))
            drop-shadow(0 0 40px rgba(230, 57, 70, 0.28));
  }
  .holo-btn:hover span { color: var(--accent-gold); }
}

/* ── §4.4 the toast ── */
/* ★ IT SLIDES IN BELOW THE HEADER, NOT ONTO IT. `top: safe-top + 8px` put it INSIDE the bar at every
   inset by construction — the bar's height is `safe-top + padding + content`, so a box anchored at
   `safe-top + 8` can only land inside it. Measured at 390x844, inset 59px, transition settled: the
   toast sat at y 67..104 while the header spanned 0..98 and its ink 62..82 — it cut the SFTi
   wordmark in half and hid the saw and fraction chips entirely.
   ⚠ AND NO z-index ON `.status-bar` CAN EVER FIX IT: #app is `position:relative; z-index:1` (§4 top),
   so the bar's `z-index:100` is scoped INSIDE that stacking context, while #toast is a sibling of
   #app and its 300 competes against #app's 1 in the root context. The toast wins at ANY header
   z-index. The anchor is the defect, so the anchor is what moves.
   It reads the SAME `--header-clearance` `.view-container` reserves, so the toast and the first card
   can never disagree about where the chrome ends. `pointer-events: none` also means an
   elementFromPoint occlusion checker is structurally blind to this — it is a purely visual cover. */
.toast {
  position: fixed;
  top: calc(var(--header-clearance) + var(--sp-sm));
  left: 50%;
  transform: translateX(-50%) translateY(-200px);
  padding: 10px 20px;
  border-radius: 12px;
  font-size: 0.75rem;
  font-weight: 600;
  z-index: 300;
  transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
  pointer-events: none;
}
.toast.visible { transform: translateX(-50%) translateY(0); }
.toast.error {
  background: rgba(199, 78, 10, 0.85);
  color: white;
  backdrop-filter: blur(10px);
}
.toast.success {
  background: rgba(33, 133, 75, 0.2);
  color: var(--accent-green-bright);
  border: 1px solid rgba(33, 133, 75, 0.4);
  backdrop-filter: blur(10px);
}
.toast.info {
  background: rgba(0, 229, 255, 0.12);
  color: var(--accent-cyan);
  border: 1px solid rgba(0, 229, 255, 0.25);
  backdrop-filter: blur(10px);
}
.toast.warning {
  background: rgba(255, 215, 0, 0.12);
  color: var(--accent-gold);
  border: 1px solid rgba(255, 215, 0, 0.25);
  backdrop-filter: blur(10px);
}

