/* SnS mobile tokens — spec 2026-07-17 §3. Semantic colors are LOCKED: one meaning each,
   always dual-encoded with a word/number. --action appears on exactly one primary per screen. */
/* Round 5 T7 — deep-space retune (Ali: "the color doesn't scream Sci-fi"). VALUES only, never
   names: surfaces shift from neutral near-black to a committed deep-space navy ramp, --hairline
   becomes the accent at low alpha so every 1px border reads as a luminous console edge, and two
   new helper tokens carry the accent's soft fill + glow. --action (#35e0ff) and --gm-push
   (#f472b6) are pinned by other suites and unchanged. Contrast audited in round5-theme.test.js:
   --text and --muted both clear 4.5:1 on every surface tier (muted is 7.4:1 on --card). */
:root {
  --bg: #040a18; --surface: #081126; --card: #0c1731; --raised: #13224a; --hairline: rgba(53, 224, 255, 0.16);
  --text: #e9f1ff; --muted: #94a8cf; --disabled: #5b6a8a;
  --action: #35e0ff; --hp-good: #3ddc84; --hp-warn: #ffb547; --hp-crit: #ff5d5d;
  --barrier: #4aa8ff; --cdu: #b07aff; --credits: #ffd166;
  --damage: #ff5d5d; --heal: #3ddc84; --crit: #ffd76a; --miss: #8fa0bd; --gm-push: #f472b6;
  /* Round 6 (Zee, acceptance-panel MAJOR): --strip-h was 24px, making the sticky status strip —
     a full-width tappable link back to PLAY on every page — the one interactive element under the
     app's own --sns-touch (44px) floor (measured live at 351x24). Raised to the floor itself;
     every dependent offset (.sns-vitals top, .sns-help-pill top, .sns-sheet max-height,
     .sns-rules-search top) reads var(--strip-h), so the layout moves as one. */
  --nav-h: 64px; --strip-h: 44px;
  --action-soft: rgba(53, 224, 255, 0.12);
  --action-glow: 0 0 16px rgba(53, 224, 255, 0.3);
}
/* min-height, NOT height: a fixed 100dvh clamps the document's scrollable area to exactly one
   screen, so any page taller than the viewport (a data-heavy codex planet, a long PLAY sheet) is
   painted below the fold but unreachable — the window can't scroll. min-height lets the body grow
   with its content so the page scrolls normally, while short pages still fill the screen. The
   fixed bottom nav / sticky strip / fixed actionbar are viewport-anchored and unaffected. */
html, body { min-height: 100dvh; }
/* background-COLOR, not the `background:` shorthand — this rule loads AFTER theme.css's own
   equal-specificity body.sns-body rule (layout.njk link order), so the shorthand would reset
   background-image to none and silently erase theme.css's starfield layers. The longhand keeps
   the color authoritative here while the image layers survive the cascade. */
body.sns-body { background-color: var(--bg); color: var(--text); padding-bottom: calc(var(--nav-h) + env(safe-area-inset-bottom)); }
.sns-appnav { position: fixed; inset: auto 0 0 0; height: calc(var(--nav-h) + env(safe-area-inset-bottom));
  padding-bottom: env(safe-area-inset-bottom); background: var(--card); border-top: 1px solid var(--hairline);
  display: flex; justify-content: space-around; align-items: stretch; z-index: 100; }
.sns-appnav a { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 2px; color: var(--muted); text-decoration: none; font-size: 10.5px; letter-spacing: .1em;
  text-transform: uppercase; min-width: 48px; touch-action: manipulation; }
.sns-appnav a.active { color: var(--text); }
.sns-appnav a.sns-nav-play { position: relative; }
.sns-appnav .sns-nav-play-disc { width: 48px; height: 48px; border-radius: 50%; margin-top: -22px;
  background: var(--surface); border: 2px solid var(--muted); display: flex; align-items: center; justify-content: center; }
.sns-appnav a.sns-nav-play.active .sns-nav-play-disc,
.sns-appnav a.sns-nav-play.combat .sns-nav-play-disc { border-color: var(--action); box-shadow: 0 0 14px rgba(53,224,255,.35); }
/* Round 6 (Zee) — min-height floor rides along with the --strip-h raise above: the strip is an
   interactive link, so it must clear --sns-touch (44px, theme.css) even if --strip-h ever moves. */
.sns-statusstrip { position: sticky; top: 0; z-index: 90; height: var(--strip-h); min-height: var(--sns-touch);
  display: flex; gap: 10px;
  align-items: center; justify-content: space-between; padding: 0 10px; font-size: 11.5px;
  background: var(--surface); border-bottom: 1px solid var(--hairline); color: var(--muted); }
.sns-statusstrip .sns-pip { width: 8px; height: 8px; border-radius: 50%; background: var(--hp-good); display: inline-block; }
.sns-statusstrip .sns-pip.reconnecting { background: var(--hp-warn); }
.sns-statusstrip .sns-pip.offline { background: var(--disabled); }
.sns-credits, .sns-strip-credits { color: var(--credits); font-variant-numeric: tabular-nums; }
.sns-primary-btn { display: block; width: 100%; min-height: 56px; border-radius: 12px; border: 1px solid var(--action);
  background: linear-gradient(180deg, #0e3a55, #082338); color: var(--action); font-size: 17px; font-weight: 700;
  letter-spacing: .06em; text-transform: uppercase;
  box-shadow: var(--action-glow), inset 0 1px 0 rgba(233, 241, 255, 0.08); touch-action: manipulation; }
.sns-tile { display: flex; flex-direction: column; gap: 4px; justify-content: center; min-height: 96px;
  background: var(--card); border: 1px solid var(--hairline); border-radius: 12px; padding: 16px;
  color: var(--text); text-decoration: none; touch-action: manipulation; }
.sns-tile .sns-tile-sub { color: var(--muted); font-size: 13px; }
.sns-fab { position: fixed; right: 14px; bottom: calc(var(--nav-h) + env(safe-area-inset-bottom) + 14px);
  width: 52px; height: 52px; border-radius: 50%; background: var(--card); border: 1px solid var(--hairline);
  color: var(--text); z-index: 95; touch-action: manipulation; }
/* Plan 23a T2 — Task 1 carryover: the plan promised .sns-sheet (the bottom-sheet stub the '?'
   pill and dice FAB pop open) but never defined it. */
.sns-sheet { position: fixed; inset: auto 0 calc(var(--nav-h) + env(safe-area-inset-bottom)) 0;
  background: var(--surface); border-top: 1px solid var(--hairline); border-radius: 16px 16px 0 0;
  padding: 16px; z-index: 96;
  /* Panel-loop round 1 (UI lens): a fixed bottom sheet grows UPWARD, so a sheet taller than the
     viewport pushed its own top rows off-screen with no way to reach them (fixed elements don't
     scroll with the page). Cap the height and let the sheet itself scroll — the equip-picker,
     pay-dues, and ship-storage sheets can be long. */
  max-height: calc(100dvh - var(--strip-h) - var(--nav-h) - env(safe-area-inset-bottom) - 24px);
  overflow-y: auto; overscroll-behavior: contain; }
.sns-help-pill { position: fixed; top: calc(var(--strip-h) + 8px); right: 10px; z-index: 95;
  /* The sole way to open help on each screen — must clear the --sns-touch (44px) floor. */
  width: 44px; height: 44px; border-radius: 50%; background: var(--card); border: 1px solid var(--hairline);
  color: var(--muted); touch-action: manipulation; }
@media (prefers-reduced-motion: reduce) { .sns-appnav a, .sns-primary-btn { transition: none !important; } }

/* Plan 23b — legacy bridge: old --sns-* names now resolve to the spec palette. Full markup
   migration lands in 23c; until then both vocabularies MUST agree or the app is two-toned. */
:root {
  --sns-bg: var(--bg); --sns-card: var(--card); --sns-line: var(--hairline);
  --sns-tx: var(--text); --sns-tx-dim: var(--muted); --sns-tx-faint: var(--disabled);
  --sns-cy: var(--action); --sns-gold: var(--credits); --sns-amber: var(--hp-warn);
  --sns-pos: var(--heal); --sns-neg: var(--damage);
}

/* Plan 23b T1 — sticky vitals header (play.njk) + expandable sheet panel (design spec
   2026-07-17 §4). Word-state HP + HP/barrier bars + CDU/Armor/Credits cells stay visible in the
   sticky row; tapping it expands the full-sheet panel below (a plain x-show, no x-collapse — see
   play.njk's own header comment for why the collapse plugin isn't available). */
.sns-vitals { position: sticky; top: var(--strip-h); z-index: 10; background: var(--surface); border-bottom: 1px solid var(--hairline); }
/* Ali 2026-07-20 — the row is now a vertical stack (HP over stats), not a single horizontal line.
   The old single flex row let the fixed secondary cells (Moves / Augment Power / Armor / Credits)
   consume the whole width and squeeze .sns-vitals-hp (flex + min-width:0) down to ~0px on a phone,
   so the HP bar, HP amount, and barrier bar all collapsed to invisible — worst on augmented
   characters, whose extra "Augment Power" cell tipped the row past overflow. Now the HP block owns
   the full width on top; the stat cells sit in their own horizontally-scrollable strip below, so
   the HP bar is always full-width/visible and the header stays two lines at any screen width. */
.sns-vitals-row { display: flex; flex-direction: column; align-items: stretch; gap: 8px; width: 100%; min-height: 48px;
  padding: 8px 12px; background: none; border: none; color: inherit; text-align: left;
  font: inherit; touch-action: manipulation; }
.sns-vitals-hp { min-width: 0; }
/* The stat cells: one horizontal row under the HP bar, scrolling if too wide to fit (never wrapping
   down and never squeezing the HP bar above). Cells grow to share the width when there's room. */
.sns-vitals-cells { display: flex; gap: 12px; overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: none; }
.sns-vitals-cells::-webkit-scrollbar { display: none; }
.sns-vitals-cells > .sns-vitals-cell { flex: 1 0 auto; }
.sns-vitals-word { display: block; font-size: 13px; font-weight: 700; margin-bottom: 3px; }
.sns-vitals-cell { display: flex; flex-direction: column; align-items: center; gap: 2px; padding: 0 2px; flex: 0 0 auto; }
.sns-vitals-cell .lbl { font-size: 11px; text-transform: uppercase; letter-spacing: .12em; color: var(--muted); }
.sns-sheet-panel { padding: 12px; border-top: 1px solid var(--hairline); }

/* Plan 23b T2 — weapon carousel + pinned primary + verdict results (mobile spec §2/§4). */
.sns-weapon-carousel { display: flex; overflow-x: auto; scroll-snap-type: x mandatory; gap: 12px;
  padding: 4px 2px 10px; -webkit-overflow-scrolling: touch; }
.sns-weapon-card { scroll-snap-align: center; min-width: 86%; flex: 0 0 auto; }
.sns-weapon-card--selected { border-color: var(--action); }
/* The bar is fixed above the bottom nav; the SCROLLING container that holds it and everything
   below the carousel needs matching bottom padding so the last cards/cards aren't hidden behind
   it. Scoped via :has() to pages that actually render .sns-actionbar (today just /c/:id) rather
   than padding every page's <main> unconditionally. */
main.container:has(.sns-actionbar) { padding-bottom: 84px; }
.sns-actionbar { position: fixed; left: 0; right: 0; bottom: calc(var(--nav-h) + env(safe-area-inset-bottom));
  padding: 8px 16px; background: linear-gradient(transparent, var(--bg) 40%); z-index: 20; }
.sns-primary-btn--glow { box-shadow: 0 0 14px rgba(53,224,255,.35); border-color: var(--action);
  color: var(--action); min-height: 64px; width: 100%; font-size: 17px; font-weight: 700; }
/* Plan 23b T2 review fix (double-fire guard) — reduced opacity + no glow pulse while a
   roll/fullAuto/reload/rollDamage request is in flight (bar's own :disabled="isBusy()" binding
   in play.njk). The button KEEPS the glow class while disabled — it's still the identified
   primary, just temporarily inert. */
.sns-primary-btn--glow:disabled { opacity: .55; box-shadow: none; }
.sns-ammo-pips { letter-spacing: 3px; font-size: 14px; color: var(--muted); margin: 6px 0; }
.sns-firemode { display: flex; gap: 8px; margin: 8px 0; }
.sns-firemode button { flex: 1; padding: 8px; border-radius: 10px; border: 1px solid var(--hairline);
  background: var(--card); color: var(--text); font-size: 13px; touch-action: manipulation; }
.sns-firemode button.active { border-color: var(--action); color: var(--action); }
.sns-firemode .tradeoff { display: block; font-size: 11px; color: var(--muted); margin-top: 2px; }
.sns-verdict { text-align: center; padding: 8px 0; }
.sns-verdict-num { font-size: 52px; font-weight: 800; font-variant-numeric: tabular-nums; line-height: 1; }
.sns-verdict-word { font-size: 22px; letter-spacing: .1em; font-weight: 700; margin-top: 2px; }
.sns-why { background: none; border: none; color: var(--muted); text-decoration: underline;
  padding: 6px 12px 6px 0; font-size: 13px; touch-action: manipulation;
  display: inline-flex; align-items: center; min-height: var(--sns-touch); }
.sns-verdict-damage { text-align: center; }
.sns-damage-total { font-size: 40px; font-weight: 800; font-variant-numeric: tabular-nums; line-height: 1; color: var(--damage); }
.sns-shot-chip { display: inline-flex; align-items: baseline; gap: 3px; font-size: 13px; font-weight: 700; margin: 0 10px 4px 0; }
@media (prefers-reduced-motion: reduce) { .sns-weapon-carousel { scroll-behavior: auto; } }

/* Plan 23b T3 — moves-left pips, inline action costs, "I got hit" soak receipt, aid/round card
   restyle (design spec 2026-07-17 §2/§4). ONE-glow invariant: none of these carry
   sns-primary-btn--glow — that class stays reserved for the actionbar's single weapon-fire
   primary (Task 2). Dots are dual-encoded (shape/fill, not color alone): spent = hollow ring in
   --disabled, remaining = solid --text — no second cyan/--action element introduced here. */
.sns-pips { display: flex; align-items: center; gap: 4px; }
.sns-pip-dot { width: 14px; height: 14px; border-radius: 50%; background: var(--text); flex: 0 0 auto; }
.sns-pip-dot.spent { background: none; border: 1px solid var(--disabled); }
.sns-pips-lbl { font-size: 11px; color: var(--muted); margin-left: 4px; white-space: nowrap; }

/* Actions card — every button is a >=56px row: name (+ its previously hover-only description,
   now always visible per the "no silent dead taps" rule) on the left, cost/cooldown/CDU text
   (already-composed prose off ActionDef.cost — never re-parsed/invented) right-aligned muted. */
.sns-action-row { display: flex; align-items: center; justify-content: space-between; gap: 10px;
  width: 100%; min-height: 56px; padding: 10px 14px; margin-bottom: 8px; border-radius: 12px;
  border: 1px solid var(--hairline); background: var(--card); color: var(--text); text-align: left;
  touch-action: manipulation; }
.sns-action-row:disabled { opacity: .55; }
.sns-action-row-main { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.sns-action-row-name { font-size: 15px; font-weight: 600; }
.sns-action-row-desc { font-size: 12px; color: var(--muted); }
.sns-cost { font-size: 12px; color: var(--muted); text-align: right; flex: 0 0 auto; white-space: nowrap; }

/* "I got hit" (takeDamageUI) — big numeric amount, an AP stepper (no bare number spinner), and a
   solid --damage-bordered Apply (NOT the cyan actionbar glow — that's the weapon-fire primary's
   alone, per the ONE-glow invariant above). */
.sns-hit-amount { width: 100%; min-height: 56px; font-size: 24px; text-align: center;
  border-radius: 12px; border: 1px solid var(--hairline); background: var(--card); color: var(--text); }
.sns-ap-stepper { display: flex; align-items: center; gap: 8px; }
.sns-ap-stepper button { width: 44px; height: 44px; border-radius: 10px; border: 1px solid var(--hairline);
  background: var(--card); color: var(--text); font-size: 18px; line-height: 1; touch-action: manipulation; }
.sns-ap-stepper-value { min-width: 32px; text-align: center; font-variant-numeric: tabular-nums; }
.sns-damage-btn { display: block; width: 100%; min-height: 56px; margin-top: 12px; border-radius: 12px;
  border: 2px solid var(--damage); background: var(--card); color: var(--damage); font-size: 16px;
  font-weight: 700; touch-action: manipulation; }
.sns-damage-btn:disabled { opacity: .55; }
[data-role="soak-receipt"] { margin-top: 10px; font-size: 14px; }
[data-role="soak-receipt"] div { margin-bottom: 2px; }

/* Aid inbox + Round tracker — plain card-row restyle only, no logic changes: 48px buttons, inline
   outcome/error text stays exactly where each handler already writes it. */
.sns-card-row { display: flex; align-items: center; justify-content: space-between; gap: 10px;
  min-height: 48px; padding: 8px 12px; margin-bottom: 8px; border-radius: 10px;
  border: 1px solid var(--hairline); background: var(--card); flex-wrap: wrap; }
.sns-card-row-actions { display: flex; gap: 8px; flex: 0 0 auto; }
.sns-card-row-actions .btn { min-height: 48px; }
.sns-card-row-btn { display: block; width: 100%; min-height: 48px; padding: 8px 14px;
  border-radius: 10px; border: 1px solid var(--hairline); background: var(--card); color: var(--text);
  text-align: left; touch-action: manipulation; text-decoration: none; }
/* A whole .sns-card-row that IS a navigation link (e.g. each shop row → its own page). Resets the
   anchor's UA underline/link-colour so it reads as a card row, and gives a press affordance. */
a.sns-nav-row { color: var(--text); text-decoration: none; touch-action: manipulation; cursor: pointer; }
a.sns-nav-row:active { border-color: var(--action); }
.sns-round-btn-sub { display: block; font-size: 11px; color: var(--muted); margin-top: 2px; font-weight: 400; }

/* Plan 23b T4 — party strip (crew HP at a glance, directly under the vitals header) + roll log
   (collapsed <details>, bottom of the combat region). Chips scroll horizontally rather than wrap,
   same idiom as the weapon carousel above, so a full ship's crew never pushes the vitals header
   taller. The sliver is a plain 2-stop bar (no ok/warn/danger banding, unlike the owner's own HP
   bar above) — a crewmate's exact HP band isn't this glance's job, just "roughly how hurt". */
.sns-party-strip { display: flex; gap: 10px; overflow-x: auto; padding: 8px 12px; -webkit-overflow-scrolling: touch; }
.sns-party-chip { flex: 0 0 auto; min-width: 64px; max-width: 84px; }
.sns-party-chip .nm { display: block; font-size: 11px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-bottom: 3px; }
/* Round 6 (Petra) — the exact HP figure under the name, dual-encoding the sliver below it. */
.sns-party-chip .hp { display: block; font-size: 11px; color: var(--text); font-variant-numeric: tabular-nums; margin-bottom: 2px; }
.sns-party-chip .sliver { height: 4px; border-radius: 2px; background: var(--hairline); overflow: hidden; }
.sns-party-chip .sliver div { height: 100%; background: var(--hp-good); }

.sns-roll-log { margin-bottom: 12px; padding: 10px 14px; border: 1px solid var(--hairline); border-radius: 12px; background: var(--card); }
.sns-roll-log summary { cursor: pointer; touch-action: manipulation; }
.sns-roll-log-row { display: flex; align-items: center; gap: 10px; padding: 6px 0; border-top: 1px solid var(--hairline); font-size: 13px; }
.sns-roll-log-row:first-of-type { border-top: none; margin-top: 8px; }
.sns-roll-log-time { flex: 0 0 auto; width: 40px; color: var(--muted); font-variant-numeric: tabular-nums; }
.sns-roll-log-label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sns-roll-log-total { flex: 0 0 auto; font-variant-numeric: tabular-nums; font-weight: 700; }
.sns-roll-log-verdict { flex: 0 0 auto; font-size: 11px; font-weight: 700; letter-spacing: .06em; }

/* Plan 23b T5 — dice tray (_appshell.njk's diceFab() macro, every shell page). d100 is the big
   "raised card" default — background:var(--raised), a token defined since Task 1 but unused until
   now, deliberately NOT .sns-primary-btn--glow (the cyan glow stays reserved for PLAY's own
   actionbar primary, the ONE-glow invariant Plan 23b T2 established). d20/d10/d6 sit in a smaller
   row underneath as quick alternates; both rows share the same "active = last die rolled" cue
   (border/text -> --action) as the weapon card's own fire-mode segment (.sns-firemode) already
   establishes, not a new color vocabulary. */
.sns-dice-primary { display: block; width: 100%; min-height: 64px; margin-bottom: 10px; border-radius: 14px;
  border: 1px solid var(--hairline); background: var(--raised); color: var(--text); font-size: 20px;
  font-weight: 800; letter-spacing: .04em; box-shadow: 0 4px 10px rgba(0,0,0,.35); touch-action: manipulation; }
.sns-dice-primary.active { border-color: var(--action); color: var(--action); }
.sns-dice-primary:disabled { opacity: .55; }
.sns-dice-row { display: flex; gap: 8px; margin-bottom: 10px; }
.sns-dice-row button { flex: 1; min-height: 48px; border-radius: 10px; border: 1px solid var(--hairline);
  background: var(--card); color: var(--text); font-size: 15px; font-weight: 700; touch-action: manipulation; }
.sns-dice-row button.active { border-color: var(--action); color: var(--action); }
.sns-dice-row button:disabled { opacity: .55; }
.sns-dice-stepper { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
.sns-dice-stepper button { width: 44px; height: 44px; border-radius: 10px; border: 1px solid var(--hairline);
  background: var(--card); color: var(--text); font-size: 18px; line-height: 1; touch-action: manipulation; }
.sns-dice-stepper-value { min-width: 40px; text-align: center; font-variant-numeric: tabular-nums; font-size: 16px; }

/* Plan 23b T5 — stacking: PLAY's own .sns-actionbar (Task 2) sits fixed just above the bottom nav;
   the dice FAB's default bottom offset (14px above the nav) would land UNDER that bar on PLAY
   while a weapon is equipped. play.njk's playUI() adds `has-actionbar` to <body> on mount (only
   inside its own `{% if equipment.weapons | length %}` branch — see that component's own comment)
   so ONLY that page/state raises the FAB; every other shell page, and PLAY with no weapon
   equipped, never gets the class and the FAB keeps tokens.css's default .sns-fab position above. */
body.has-actionbar .sns-fab { bottom: calc(var(--nav-h) + 92px + env(safe-area-inset-bottom)); }

/* Plan 23c T1 — GEAR screen (Loadout/Backpack/Chips). Segmented control reuses the exact visual
   idiom .sns-firemode already established (flex row, 1px hairline border, --action border+text on
   the active segment) — no new color vocabulary, just a 3-way version of the same 2-way control. */
.sns-segmented { display: flex; gap: 8px; margin-bottom: 14px; }
.sns-segmented button { flex: 1; min-height: 48px; border-radius: 10px; border: 1px solid var(--hairline);
  background: var(--card); color: var(--muted); font-size: 14px; font-weight: 700; touch-action: manipulation; }
.sns-segmented button.active { border-color: var(--action); color: var(--action); }
.sns-card-row-main { flex: 1 1 auto; min-width: 0; }
.sns-card-row-name { font-size: 15px; font-weight: 600; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.sns-chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 4px 0; }
.sns-chip { display: inline-block; padding: 2px 8px; border-radius: 8px; background: var(--surface);
  border: 1px solid var(--hairline); color: var(--muted); font-size: 12px; }
.sns-gear-section { margin-bottom: 18px; }
/* P-Chip grids (Chips segment) — 12 numbered cells per grid, filled/empty dual-encoded (fill color
   AND a dot vs number, same "never color alone" rule the moves-left pips already establish above). */
.sns-pchip-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 6px; margin: 8px 0 16px; }
.sns-pchip-cell { aspect-ratio: 1; border-radius: 8px; border: 1px solid var(--hairline); background: var(--card);
  display: flex; align-items: center; justify-content: center; font-size: 11px; color: var(--disabled); }
.sns-pchip-cell.filled { background: var(--raised); border-color: var(--action); color: var(--text); }
/* Sticky "Pay dues" bar — same fixed-above-nav placement as PLAY's own .sns-actionbar, but GEAR
   never renders that bar (PLAY-only), so there is no stacking conflict to guard against here. */
.sns-duesbar { position: fixed; left: 12px; right: 12px; bottom: calc(var(--nav-h) + env(safe-area-inset-bottom) + 12px);
  min-height: 48px; border-radius: 12px; border: 1px solid var(--damage); background: var(--card);
  color: var(--damage); font-size: 15px; font-weight: 700; z-index: 21; touch-action: manipulation; }
.sns-dot { min-width: 8px; min-height: 8px; padding: 0; border-radius: 50%; display: inline-block; vertical-align: middle; }
/* Plan 23c T1 review fix — damage-state visibility restored on GEAR's card rows (armor/barrier/
   gear/backpack all dropped it when the old flat tables became cards; only weapons kept a "Broken"
   chip). Outline pill (not filled, unlike .sns-chip) so it visually reads as a WARNING distinct
   from a plain stat chip at a glance — amber for any non-functional state short of "broken", red
   for "broken". Weapons' pre-existing "Broken — repair required" chip is retargeted onto the same
   --broken modifier (inventory.njk) rather than keeping its own one-off inline style. */
.sns-state-chip { display: inline-block; padding: 2px 8px; border-radius: 8px; border: 1px solid currentColor;
  font-size: 12px; font-weight: 600; }
.sns-state-chip--damaged { color: var(--sns-amber); }
.sns-state-chip--broken { color: var(--sns-neg); }

/* Plan 23c T2 — Shop + Missions phone restyle. */
/* An extra ramp step: E/D/C/A already have a home in the existing palette (muted/hp-good/hp-warn/
   hp-crit), but the 5-step grey->green->amber->orange->red risk ramp this task's brief calls for
   needs a genuine ORANGE distinct from --hp-warn's amber, which nothing above defines. */
:root { --risk-orange: #ff8c42; }

/* Lock chips (shop license requirements, missions' captain-only reason) — same outline-pill shape
   as .sns-state-chip above, reusing .sns-chip's base sizing/padding rather than a third one-off
   size. Plain (held or informational-only, e.g. missions' captain-only reason) reads muted like
   any other .sns-chip; the --missing modifier (a shop license the character does NOT hold) reads
   amber, same "explain, don't hide" language as .sns-state-chip's damaged tier. */
.sns-lock-chip { border: 1px solid var(--hairline); }
.sns-lock-chip--missing { border-color: var(--sns-amber); color: var(--sns-amber); }
/* Can't-afford shop card — dimmed, never hidden or removed; the shortfall line (own style, inline
   in shop.njk) and the Buy button both stay fully legible/tappable underneath. */
.sns-card-row--dim { opacity: .6; }

/* Plan 23g T1 — selectable card row (characters.njk's Race wizard step, a <label> wrapping a
   radio input). Same "border-color -> --action" active/selected language every other selectable
   control in this file already uses (.sns-firemode button.active, .sns-segmented button.active,
   .sns-dice-primary.active, .sns-weapon-card--selected) — no new color vocabulary. */
.sns-card-row--selected { border-color: var(--action); }

/* Plan 23g T1 — sticky remaining-budget bar (buildout.njk, the build-out wizard's top-of-page
   truth). characters.njk/buildout.njk are the two screens rendered OUTSIDE the shared appshell
   (_charnav.njk) — neither carries a .sns-statusstrip — so this sticks at top:0 rather than under
   var(--strip-h) the way .sns-vitals (play.njk) does; same surface/hairline sticky-header
   treatment otherwise, for visual consistency with the rest of the app's sticky headers. Updates
   via the existing full-page reload after every buy/confirm (buildout()'s own location.reload()),
   never a live-JS binding — the value is just server-rendered from character.credits each render. */
.sns-budgetbar { position: sticky; top: 0; z-index: 10; display: flex; align-items: center;
  justify-content: space-between; gap: 10px; padding: 10px 14px; margin: 0 0 14px;
  background: var(--surface); border: 1px solid var(--hairline); border-radius: 12px; }
.sns-budgetbar-label { font-size: 11px; text-transform: uppercase; letter-spacing: .12em; color: var(--muted); }
.sns-budgetbar-amount { font-size: 20px; font-weight: 800; }

/* Risk chips (missions board/accepted) — outline pill matching .sns-state-chip's shape, one class
   per tier (E safest -> A deadly). Always dual-encoded: the chip's own text carries "Risk X —
   <word>", per tokens.css's file-level rule up top ("always dual-encoded with a word/number") —
   color is never the only signal. */
.sns-risk-chip { display: inline-block; padding: 2px 10px; border-radius: 8px; border: 1px solid currentColor;
  font-size: 12px; font-weight: 700; }
.sns-risk-chip.sns-risk-e { color: var(--muted); }
.sns-risk-chip.sns-risk-d { color: var(--hp-good); }
.sns-risk-chip.sns-risk-c { color: var(--hp-warn); }
.sns-risk-chip.sns-risk-b { color: var(--risk-orange); }
.sns-risk-chip.sns-risk-a { color: var(--hp-crit); }

/* Standalone interactive chips — the related-rule navigation chips (rule sheet), the mission
   risk-tier chip, and the shop license-lock chip — are real tap targets sitting in their own chip
   rows / cards, so they clear the --sns-touch (44px) floor via an inline-flex box that keeps the
   compact pill look while giving a full-height hit area. The INLINE glossary term chips
   (.sns-term-chip: CDU/Recoil/Shield/Armor embedded mid-sentence) deliberately stay compact under
   WCAG 2.5.5's inline-target exception — forcing 44px on a chip inside a running sentence would
   break the text line. */
.sns-rules-related-chip, .sns-risk-chip, .sns-lock-chip {
  display: inline-flex; align-items: center; min-height: var(--sns-touch); }

/* Plan 23c T3 — Codex poster tiles: the .sns-system-fallback planet list, restyled from bootstrap
   list-group rows into image-forward tiles (thumbnail + name + zone/intel counts). Same tile
   shape/touch semantics as .sns-tile (world.njk's hub grid) plus an image slot up top; a mapless
   planet gets a plain gradient placeholder (.sns-poster-thumb--empty) rather than a broken <img>. */
.sns-poster-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 12px; margin-bottom: 16px; }
.sns-poster-tile { display: flex; flex-direction: column; background: var(--card); border: 1px solid var(--hairline);
  border-radius: 12px; overflow: hidden; color: var(--text); text-decoration: none; touch-action: manipulation; }
.sns-poster-thumb { aspect-ratio: 4 / 3; background: var(--surface); overflow: hidden; }
.sns-poster-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.sns-poster-thumb--empty { width: 100%; height: 100%; background: linear-gradient(160deg, var(--surface), var(--raised)); }
.sns-poster-tile-body { padding: 10px 12px; display: flex; flex-direction: column; gap: 2px; }
.sns-poster-tile-body strong { font-size: 15px; }

/* Plan 23d T2 — RULES tab (search field, result/shelf rows, First Session/Recently-viewed rails,
   card sheet's "At the table" tint). Rows are <button>s, not the GEAR idiom of a <div class=
   "sns-card-row"> with an inner action button — here the WHOLE row is the one tap target (no
   separate per-row action), so they get their own thin button-reset shell instead of fighting
   <button> UA defaults layered onto .sns-card-row's flex-div rules. The rail reuses the SAME
   overflow-x:auto/flex/gap idiom .sns-party-strip and .sns-weapon-carousel already establish
   (horizontal scroll rather than wrap, so a full shelf's worth of cards never pushes the page
   wider than the 360px mobile viewport) — a new class because the card content shape (title +
   2-line ruling) doesn't match either existing rail's card shape, not because the pattern itself
   is new. */
.sns-rules-search { position: sticky; top: var(--strip-h); z-index: 15; background: var(--bg); padding: 10px 0 8px; }
.sns-rules-search-input { display: block; width: 100%; min-height: 48px; padding: 0 14px; border-radius: 12px;
  border: 1px solid var(--hairline); background: var(--card); color: var(--text); font-size: 16px; touch-action: manipulation; }
.sns-rules-search-input::placeholder { color: var(--muted); }
.sns-rules-results { margin-top: 4px; }
.sns-rules-row { display: block; width: 100%; text-align: left; min-height: 48px; padding: 8px 12px;
  margin-bottom: 8px; border-radius: 10px; border: 1px solid var(--hairline); background: var(--card);
  color: var(--text); font: inherit; touch-action: manipulation; }
.sns-rules-row-title { display: block; font-size: 15px; font-weight: 600; }
.sns-rules-row-ruling { display: block; font-size: 13px; color: var(--muted); margin-top: 2px; }
.sns-rules-shelf { margin-bottom: 18px; }
.sns-rules-rail { display: flex; gap: 10px; overflow-x: auto; padding: 2px 2px 8px; -webkit-overflow-scrolling: touch; }
.sns-rules-rail-card { flex: 0 0 auto; width: 200px; max-width: 78vw; min-height: 72px; padding: 10px 12px;
  border-radius: 12px; border: 1px solid var(--hairline); background: var(--card); color: var(--text);
  text-align: left; touch-action: manipulation; }
.sns-rules-rail-card.read { border-color: var(--action); }
.sns-rules-rail-title { display: block; font-size: 14px; font-weight: 600; }
.sns-rules-rail-ruling { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; font-size: 12px; color: var(--muted); margin-top: 3px; }
/* The example tint (distinct background from the sheet's own --surface, per this task's brief) —
   .sns-sheet already sets --surface as ITS background, so the example block needs a genuinely
   different token to read as visually distinct; --raised (defined since Task 1, used elsewhere for
   the dice tray's own "raised card" default) is that second surface tone already in the palette. */
.sns-rules-example { margin-top: 8px; padding: 10px 12px; border-radius: 10px; background: var(--raised);
  border: 1px solid var(--hairline); font-size: 14px; }
/* Rule-card body/full text is authored prose that can carry intentional line breaks (worked lists,
   and the pipe-tables the generator flattens into " · "-joined rows). x-text sets textContent, so
   without pre-wrap those newlines collapse to spaces and the rows run together. pre-wrap keeps the
   line structure; overflow-wrap guards a long unbroken token from forcing horizontal scroll. */
.sns-rules-body { white-space: pre-wrap; overflow-wrap: anywhere; }
/* The "← Back" control appears in the shared rule-card sheet's drill-down history AND the
   new-character wizard's step-back — a reused one-thumb navigation control, so it clears the
   --sns-touch floor. inline-flex keeps it left-aligned and text-styled while giving a 44px hit
   height; a touch of right padding widens the horizontal target past the bare glyph. */
.sns-rules-back { background: none; border: none; color: var(--muted); padding: 0 12px 0 0; margin-bottom: 4px;
  font-size: 13px; touch-action: manipulation; display: inline-flex; align-items: center; min-height: var(--sns-touch); }
.sns-rules-related-chip { border: 1px solid var(--hairline); background: var(--surface); touch-action: manipulation; }

/* Plan 23d T3 — the three doors + iOS splash. */
/* Glossary term chips (play/inventory/shop/missions "jargon" sites): inline dotted-underline TEXT,
   deliberately NOT a boxed chip like .sns-chip/.sns-lock-chip/.sns-risk-chip below — these sit
   inside an existing label (e.g. "CDU", "Recoil", "Shield") and must read as part of that label's
   own text, not as a separate control. Resets every <button> UA default (background/border/
   padding/margin) except font (Bootstrap's Reboot already sets font: inherit/margin:0 on all
   <button>s — see this section's own comment on .sns-risk-chip below for what Reboot does and
   doesn't cover) so only the dotted underline itself signals "tappable". Tapping opens the shared
   rule-card sheet via window.__snsOpenTerm(word) (_appshell.njk's ruleSheet() macro). */
.sns-term-chip { background: none; border: none; padding: 0; color: inherit;
  text-decoration: underline dotted; text-underline-offset: 2px; cursor: pointer; touch-action: manipulation; }
/* The rule-card sheet (RULES page's own sheet AND _appshell's shared over-sheet) sits ONE above the
   base .sns-sheet z-index (96): a glossary chip tapped while a page confirm sheet — shop buy,
   missions reserve, gear equip — is already open must paint the rule card ON TOP of that sheet, not
   silently underneath it (the shared sheet is emitted first in DOM, so equal z-index would lose the
   paint order). Dismissing the rule card returns the player to whatever sheet was underneath. */
.sns-rules-sheet { z-index: 97; }
/* Existing chip-shaped components (.sns-chip and its .sns-lock-chip modifier) becoming term-chip
   TAP TARGETS too (shop's license lock chips, GEAR's P-Chip heading reuses plain .sns-chip): both
   already declare their own background+border, which — same as every other custom <button> in this
   file (.sns-firemode button, .sns-segmented button) — overrides the UA button defaults on their
   own, no extra reset needed here. */
/* .sns-risk-chip (missions) becoming a term-chip tap target too: unlike .sns-chip, it never set its
   own background (outline-pill, transparent over the card), which a <button>'s UA default
   (buttonface) would otherwise leak through — added here rather than duplicated per risk tier. */
.sns-risk-chip { background: none; }

/* Plan 23e T3 — the player turn banner (play.njk, below the sticky vitals header). A STATUS
   HEADER, not a second glowing primary: the base rule below carries NO box-shadow at all (unlike
   .sns-primary-btn--glow), and --you only ever touches border-color/color, never adds one — this
   app's ONE glow stays reserved for the roll actionbar (web.test.js's own repeated "exactly one
   .sns-primary-btn--glow" assertions). --deck is deliberately the SAME neutral treatment as the
   base rule (brief's own "neutral accent" for on-deck) — no separate color block needed. */
.sns-turn-banner { display: block; width: 100%; padding: 10px 16px; text-align: center;
  font-size: 14px; font-weight: 700; letter-spacing: .03em; background: var(--card);
  border-bottom: 1px solid var(--hairline); color: var(--text); }
.sns-turn-banner--you { background: var(--raised); border-bottom-color: var(--action); color: var(--action); }

/* Plan 23e T3 — combat gating (deliverable D): dimmed, not disabled/removed — the underlying
   link/button still functions when tapped through (neither carries preventDefault), so this is
   purely a visual de-emphasis cue, paired with the tap-time toast (window.__snsToast,
   app-shell.js) for the "never a silent dead tap" half of the rule. Reduced opacity is enough on
   its own for a card-shaped tile/bar (unlike a single icon, there's no color-only-signal risk —
   the sub-label TEXT itself also changes, on the Shop tile, to spell out why). */
.sns-tile--dim, .sns-duesbar--dim { opacity: .5; }

/* Plan 23f T3 — the required-acknowledge push overlay (handout / rule card / planet reveal /
   spotlight / roll request). Reuses the shared .sns-sheet bottom-sheet shell verbatim (same dvh-
   capped max-height, safe-area inset, z-index 96 tier — see .sns-sheet's own comment) with ONE
   addition: a --gm-push (magenta) top border, this push channel's OWN accent, used for nothing
   else in this app (this task's own binding constraint). z-index stays the shared 96 (not a new,
   higher tier) — the overlay's on-top-of-everything-at-that-tier behavior comes from DOM ORDER
   instead (_charnav.njk emits pushOverlay() AFTER helpPill()'s sheet but BEFORE diceFab()'s tray),
   so: an incoming push surfaces above an already-open help sheet, the dice tray opens ON TOP of a
   still-showing overlay when a roll-request's "Roll" button is tapped (generalRoller pre-filled via
   window.__snsOpenRollRequest, below), and the shared rule sheet (.sns-rules-sheet, z-index 97)
   still paints over the push overlay when "Open rule" is tapped from inside it — no z-index math
   needed for that last case, 97 already beats 96. Never a second glow: only the roll-request
   branch's OWN Roll button ever carries .sns-primary-btn--glow; "Got it" always stays a plain
   .sns-card-row-btn regardless of push type (this task's own binding one-glow rule). */
.sns-push-overlay { border-top: 3px solid var(--gm-push); }
.sns-push-overlay .sns-push-image { max-width: 100%; display: block; border-radius: 8px;
  touch-action: manipulation; cursor: zoom-in; }
/* Tap-to-expand: a real full-viewport lightbox, not a same-width no-op (an earlier draft toggled
   max-width:100% <-> width:100%, which resolve to the identical rendered width inside the sheet's
   own content box — no visible zoom at all). z-index 150 clears every sheet tier (96-98) and the
   fixed nav (100); it stays under the rare service-worker-update/install-coach toasts (200, both
   app-shell.js) on purpose — neither can appear while a required-ack overlay is up. */
.sns-push-overlay .sns-push-image--zoomed { position: fixed; inset: 0; z-index: 150;
  width: 100vw; height: 100dvh; max-width: none; object-fit: contain; background: rgba(0,0,0,.92);
  border-radius: 0; cursor: zoom-out; }

/* Plan 23g T2 — The Range (first-run tutorial overlay). Reuses the shared .sns-sheet shell verbatim
   (same idiom as .sns-push-overlay above) with ONE addition: a cyan --action top border, so it reads
   as a friendly practice run, visually distinct from the push channel's own magenta accent — this
   task's own binding constraint (no --gm-push anywhere in the Range). No new color vocabulary. */
.sns-range-overlay { border-top: 3px solid var(--action); }
/* The Range's own ammo pictogram — a SEPARATE class from the real weapon card's .sns-ammo-pips
   (play.njk), identical styling, deliberately NOT the same class name: this overlay renders
   unconditionally on every character page (_charnav.njk), and existing web.test.js assertions pin
   "sns-ammo-pips" as ABSENT from the whole page for a melee/unlimited-ammo weapon — reusing the
   literal class name here would collide with that pre-existing invariant regardless of this
   overlay's own x-show/x-cloak state (which only ever matters post-hydration, not to a raw SSR grep). */
.sns-range-ammo-pips { letter-spacing: 3px; font-size: 14px; color: var(--muted); margin: 6px 0; }

/* Round 5 T7 — loading emblem + sci-fi finish. */
/* The full-screen loader overlay (_appshell.njk's loader() macro; app-shell.js builds a fallback
   copy on pages outside the character shell). Hidden by default via the `hidden` attribute; the
   base rule sets display:flex, which would beat `hidden`'s UA display:none, so the attribute
   selector below re-asserts it explicitly. Shown by window.__snsLoading(true) — wired to
   beforeunload/pagehide in app-shell.js so every page transition paints the emblem over the old
   page instead of a white gap. z-index 300: above the sheets (96-98), nav (100), push-image
   lightbox (150) and the SW-update/install toasts (200) — nothing may paint over "loading". */
.sns-loader { position: fixed; inset: 0; z-index: 300; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 16px;
  background: radial-gradient(ellipse 420px 420px at 50% 42%, #0a1734 0%, var(--bg) 75%);
  color: var(--action); }
.sns-loader[hidden] { display: none; }
.sns-emblem { display: block; }
.sns-loader .sns-emblem { width: 64px; height: 64px;
  filter: drop-shadow(0 0 14px rgba(53, 224, 255, 0.45));
  animation: sns-loader-pulse 1.8s ease-in-out infinite; }
/* The dashed orbit ring + its satellite dot rotate as one group. transform-box: view-box (not
   fill-box): the satellite sits off-axis, so fill-box's bbox center would wobble the spin — the
   view-box's own 50%/50% is the emblem's true center (32,32 in a 64x64 viewBox). */
.sns-emblem-orbit { transform-box: view-box; transform-origin: 50% 50%;
  animation: sns-orbit-spin 6s linear infinite; }
.sns-loader-caption { font-size: 11px; font-weight: 700; letter-spacing: 0.5em; text-indent: 0.5em;
  text-transform: uppercase; color: var(--muted); }
/* Small inline variant — for compact "loading" states inside cards/sheets. */
.sns-emblem--inline { display: inline-block; width: 18px; height: 18px; vertical-align: -4px; color: var(--action); }
@keyframes sns-loader-pulse { 0%, 100% { opacity: 0.65; transform: scale(0.97); } 50% { opacity: 1; transform: scale(1); } }
@keyframes sns-orbit-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .sns-loader .sns-emblem, .sns-emblem-orbit { animation: none; }
}

/* Stat numerals — monospace-flavored via font-variant-numeric (tabular columns + slashed zero),
   per the sci-fi console brief; no font-family swap, so the native stack stays. Additive: several
   of these already declare tabular-nums individually, this consolidates the vocabulary. */
.sns-vitals-cell, .sns-verdict-num, .sns-damage-total, .sns-budgetbar-amount,
.sns-dice-stepper-value, .sns-ap-stepper-value, .sns-hit-amount,
.sns-credits, .sns-strip-credits, .sns-roll-log-total, .sns-roll-log-time {
  font-variant-numeric: tabular-nums slashed-zero;
}

/* Login hero (login.njk) — the first impression: the emblem large over the letterspaced identity,
   one accent CTA. The eyebrow is the same letterspaced-uppercase treatment the section labels use. */
.sns-login-hero .sns-emblem--hero { width: 92px; height: 92px; margin: 0 auto;
  color: var(--action); filter: drop-shadow(0 0 20px rgba(53, 224, 255, 0.35)); }
/* Round 13 Lane C — the login hero's eyebrow is a letterspaced uppercase section label, so it
   picks up the self-hosted display face (--sns-font-display, defined + @font-face'd in theme.css)
   alongside the hero's app name (.sns-title). Display accent only; body text is untouched. */
.sns-login-eyebrow { font-family: var(--sns-font-display); font-size: 11px; letter-spacing: 0.32em; text-indent: 0.32em;
  text-transform: uppercase; color: var(--muted); margin-top: 6px; }
.sns-login-cta { box-shadow: var(--action-glow); min-height: 48px; font-weight: 700; letter-spacing: 0.05em; }
