:root { color-scheme: dark; }
* { -webkit-tap-highlight-color: transparent; }
html { scroll-behavior: smooth; }
body { overscroll-behavior-y: none; }
/* 2026-09-08 (Reqwa, iPhone): the page could be dragged sideways and pinched
   out to ~80 %, after which the fixed nav dock sat mid-screen ("navigator too
   high"). App shells do not do this. Horizontal overflow is clipped at the root
   and the root gesture set is vertical pan only — no pinch, no x-pan; rails
   that scroll sideways opt back in with their own touch-action (see
   .navcapsule / .plans-rail / .land-plans). */
/* `clip`, not `hidden`: hidden turns html AND body into scroll containers, and a
   nested pair of root scrollers is exactly the setup that leaves a desktop page
   with a wheel that does nothing (Reqwa, PC, 2026-09-08 06:47). clip paints the
   same — nothing sideways — but creates no scroll container at all, so the
   viewport stays the one and only scroller. hidden kept as the fallback for
   engines without clip (Safari < 16). */
html, body { overflow-x: hidden; overflow-x: clip; max-width: 100%; touch-action: pan-y; }
#app { overflow-x: clip; }

::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: #101114; }
::-webkit-scrollbar-thumb { background: #2e3138; border-radius: 99px; }
::-webkit-scrollbar-thumb:hover { background: #42454e; }

.no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }

.glass {
  background: var(--ds-bar, #1C1C1E);
  border: 1px solid rgba(255, 255, 255, 0.06);
}

/* ── Surfaces ────────────────────────────────────────────────────────────
   GEOMETRY: 22px on cards, 16-18px on controls, 12px on chips. The brief asked
   for "more roundy like iOS 27 but not forced, it has to be natural". Natural
   means the radius scales with the ELEMENT, which is how iOS does it — one
   global radius applied to everything is exactly what reads as forced, because
   a 22px corner on a 32px-tall chip is a pill and a 22px corner on a full-width
   card is barely a curve.

   ELEVATION: iOS separates layers with a tiny luminance step plus a hairline,
   not with heavy gradients and shadows. The old card had a visible top-to-
   bottom gradient on every surface, which is a large part of why the layout
   read as "crowded" — every card announced itself. A card should be a quiet
   container; the CONTENT is what should have contrast. */
.card {
  background: rgba(255,255,255,0.028);
  border: 1px solid rgba(255,255,255,0.058);
  border-radius: 22px;
}
.card-hover { transition: transform .2s ease, border-color .2s ease, box-shadow .2s ease; }
.card-hover:hover { transform: translateY(-2px); border-color: rgba(198,43,36,0.38); box-shadow: 0 12px 36px -14px rgba(198,43,36,0.30); }

/* A card that is the primary surface of a screen: slightly lifted, no gradient
   novelty. Used for tables/queues where content, not chrome, should lead. */
.panel {
  background: rgba(31,31,36,0.62);
  border: 1px solid rgba(255,255,255,0.058);
  border-radius: 22px;
}
.panel-head {
  display:flex; align-items:center; gap:.6rem;
  padding:.85rem 1.1rem; border-bottom:1px solid rgba(255,255,255,.06);
  font-weight:800; font-size:.82rem; letter-spacing:.03em; text-transform:uppercase; color:var(--txt-2);
}

/* Gradient headline ("finally measured").
   It clashed for a specific reason worth keeping: the old stops ended on
   #FF8078, a SKY BLUE 6deg outside the brand family and 18deg from the
   blue-belt hue, and started on the pre-v7 brand #C62B24. So the loudest thing
   on the landing page was the one element not speaking the app's teal — and it
   sat close enough to a rank colour to read as one.
   Now three steps along the BRAND hue (188-193, 5deg of spread), worst stop
   4.99:1 on the canvas, 23deg clear of the blue belt. The belt ramp keeps its
   actual job: rank.
   Single-line only by design — background-clip is a function of the element
   BOX, so a wrapped headline renders each line a different part of the ramp. */
.grad-text {
  background: linear-gradient(96deg, #FF8A84 0%, #FF6B63 46%, #D42F28 100%);
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent;
  /* Descenders: clip-to-text on a tight display line-height slices j/y/g/p
     tails, because the clip box edge sits above the font descender. */
  padding-bottom: .12em;
  margin-bottom: -.12em;
}

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: .5rem; padding: .68rem 1.05rem;
  cursor: pointer; user-select: none;
  /* Touch target: the brief singled out phone button shapes. 44px is the
     platform minimum on both iOS and Android, and several call sites shrink
     padding with !py-2, which previously dropped them under it. */
}
.btn:active { transform: scale(.975); }
/* Semantic action buttons — confirm / deny live all over Tasks 3 and 4, and
   hand-rolling them per call site is how two screens end up disagreeing. */
.btn:disabled { opacity: .45; cursor: not-allowed; }
.btn-sm { min-height: 36px; padding: .4rem .8rem; }

.input {
  width: 100%; background: rgba(8,11,18,.72); border: 1px solid rgba(255,255,255,.095);
  border-radius: 13px; padding: .7rem .9rem; color: var(--txt-1); font-size: .94rem;
  transition: border-color .18s ease, box-shadow .18s ease;
  /* 16px on mobile stops iOS Safari zooming the whole page on focus. */
  min-height: 44px;
}
@media (max-width: 767px) { .input { font-size: 16px; } }
.input:focus { outline: none; border-color: #C62B24; box-shadow: 0 0 0 3px rgba(198,43,36,.18); }
.input::placeholder { color: var(--txt-3); }

/* belt bars */
.belt { height: 9px; border-radius: 99px; position: relative; overflow: hidden; }
.belt-white  { background: #e5e7eb; }
.belt-blue   { background: #2563eb; }
.belt-purple { background: #7c3aed; }
.belt-brown  { background: #78350f; }
.belt-black  { background: #111827; border: 1px solid #374151; }

.chip {
  display:inline-flex; align-items:center; gap:.35rem; font-size:.72rem; font-weight:700;
  padding:.28rem .6rem; border-radius:99px; letter-spacing:.02em;
}

/* Junior marker inside a belt chip.
   currentColor, so it inherits the chip's own text colour and stays legible on
   both the white-belt chip (dark text) and the black-belt chip (light text)
   without a second rule per belt. */
.belt-jr {
  font-size: .58em; font-weight: 900; letter-spacing: .06em;
  padding: .08em .3em; border-radius: 4px; margin-left: .1rem;
  background: rgba(0,0,0,.14); color: currentColor; opacity: .72;
}

/* ---------------------- promotion sheet belt picker -------------------- */
.pr-belt {
  display: flex; flex-direction: column; align-items: center; gap: .3rem;
  padding: .55rem .2rem; border-radius: 13px;
  background: rgba(255,255,255,.035); border: 1px solid rgba(255,255,255,.07);
  transition: background .15s ease, border-color .15s ease, transform .12s ease;
}
.pr-belt:hover { background: rgba(255,255,255,.07); }
.pr-belt.on {
  background: rgba(198,43,36,.16); border-color: rgba(255,107,99,.55);
}
/* A belt BELOW the athlete's current one is still tappable — corrections are
   legitimate — but dimmed, so the demote direction never looks like the
   default. The confirmation copy does the rest. */
.pr-belt.is-demote { opacity: .45; }
.pr-belt.is-demote.on { opacity: 1; }
.pr-belt-l { font-size: .62rem; font-weight: 800; letter-spacing: .01em; }
.pr-swatch {
  width: 100%; height: 7px; border-radius: 99px; display: block;
  box-shadow: inset 0 0 0 1px rgba(255,255,255,.12);
}
.belt-sw-white  { background: #e5e7eb; }
.belt-sw-grey   { background: #9ca3af; }
.belt-sw-yellow { background: #facc15; }
.belt-sw-orange { background: #f97316; }
.belt-sw-green  { background: #10b981; }
.belt-sw-blue   { background: #2563eb; }
.belt-sw-purple { background: #7c3aed; }
.belt-sw-brown  { background: #78350f; }
.belt-sw-black  { background: #111827; }

/* heatmap */
.heat-cell { width: 100%; aspect-ratio: 1; border-radius: 4px; background: #1e2025; transition: transform .12s; }
.heat-cell.on { background: linear-gradient(140deg, #C62B24, #FF8078); }
.heat-cell.on:hover { transform: scale(1.35); }

/* feed scroller */
/* .feed-scroll / .feed-item were the vertical video pager. The feed now ships
   as a not-yet-available screen with a waitlist, so both classes had zero call
   sites and were removed instead of being carried as dead weight. */

.skeleton {
  background: linear-gradient(90deg, rgba(255,255,255,.04) 25%, rgba(255,255,255,.09) 50%, rgba(255,255,255,.04) 75%);
  background-size: 500px 100%; animation: shimmer 2.4s linear infinite; border-radius: 12px;
}
@keyframes shimmer { 0% { background-position:-500px 0 } 100% { background-position:500px 0 } }

/* .toast — the legacy 3-second div that lived here (rgba(17,23,38,.95),
   semantic border tints, fadeUp) is GONE. The unified toast is theme-v2.css
   §30 (Designer, Wave 5, DS2 item 1): a raised surface with the theme's own
   hairline and semantic inks, stackable, dismissible, role="status"/"alert".
   Deleting rather than overriding, so no dead cascade is left behind. */
@keyframes fadeUp { from { opacity:0; transform: translateY(14px) } to { opacity:1; transform:none } }

.nav-item { transition: color .18s, background .18s; }
.nav-item.active { color: #FFA39C; }

/* ---------------------- radar / spiderweb ---------------------- */
@keyframes radarIn {
  0%   { transform: scale(.35) rotate(-14deg); opacity: 0; }
  60%  { opacity: 1; }
  100% { transform: scale(1) rotate(0); opacity: 1; }
}
@keyframes dotIn { from { opacity: 0; r: 0 } to { opacity: 1 } }
.radar-dot { animation: dotIn .5s ease both; }

/* ---------------------- interface polish ---------------------- */
.stat-strip {
  display: flex; align-items: center; gap: 1.25rem; flex-wrap: wrap;
}
.mini-bar { width: 100%; height: 100%; border-radius: 2px; background: #1e2025; }
.mini-bar.on { background: linear-gradient(180deg, #91dff3, #A8231D); }

.tech-card {
  position: relative; overflow: hidden;
  transition: transform .18s ease, border-color .18s ease, box-shadow .18s ease;
}
.tech-card::before {
  content:''; position:absolute; inset:0 auto 0 0; width:3px;
  background: var(--accent, #C62B24); opacity:.75;
}
.tech-card:hover { transform: translateY(-3px); border-color: rgba(255,255,255,.16); box-shadow: 0 16px 44px -18px rgba(0,0,0,.8); }

.ring-progress { transform: rotate(-90deg); transform-origin: center; transition: stroke-dashoffset .9s cubic-bezier(.22,1,.36,1); }

.hook-glow { position: relative; }
.hook-glow::after {
  content:''; position:absolute; inset:-40% -10%; z-index:-1; pointer-events:none;
  background: radial-gradient(closest-side, rgba(198,43,36,.22), transparent 70%);
  filter: blur(28px);
}

.reveal { animation: fadeUp .5s ease both; }
.reveal-1 { animation-delay: .05s }

.line-clamp-2 { display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }

@media (max-width: 767px) { .pb-safe { padding-bottom: calc(7.25rem + env(safe-area-inset-bottom)); } }

/* ── Chat input dock (Overpowered Coach) ─────────────────────────────────────
   The composer is sticky so it stays reachable mid-transcript — but
   `bottom: 0` pins it to the VIEWPORT bottom, which on phones is exactly
   where the fixed .navdock capsule floats (the main's .pb-safe padding
   cannot help a sticky element: padding lives in the scroll container,
   the stick position does not). Result: the input rendered UNDER the nav
   buttons and could not be tapped or typed into. On mobile the dock is
   lifted above the capsule (capsule ≈ 46px tabs + .8rem padding + .7rem
   gutter) and gets an opaque backdrop so the transcript never bleeds
   through the gap it now leaves. */
.chat-dock { position: sticky; bottom: 0; z-index: 39; }
@media (max-width: 767px) {
  .chat-dock {
    bottom: calc(4.6rem + env(safe-area-inset-bottom));
    background: linear-gradient(to top, rgba(10,11,14,.97) 78%, rgba(10,11,14,0));
    margin: 0 -4px; padding-left: 4px; padding-right: 4px;
  }
}


/* ===================== status pills (Tasks 3 & 4) =====================
   A roll's state is the single most repeated piece of information in the app
   now that nothing counts until it is confirmed, so it gets ONE definition
   here instead of per-view Tailwind soup that drifts between screens. */
.pill {
  display:inline-flex; align-items:center; gap:.35rem; font-size:.7rem; font-weight:750;
  padding:.26rem .55rem; border-radius:6px; letter-spacing:.03em; text-transform:uppercase;
  border:1px solid transparent; white-space:nowrap;
}
.pill-wait  { background:rgba(251,191,36,.12); color:#fcd34d; border-color:rgba(251,191,36,.28); }
.pill-coach { background:rgba(255,255,255,.06); color:var(--txt-2); border-color:rgba(255,255,255,.14); }
.pill-ok    { background:var(--good-weak); color:var(--txt-1); border-color:color-mix(in srgb, var(--good) 30%, transparent); }
.pill-no    { background:rgba(244,63,94,.12);  color:#fda4af; border-color:rgba(244,63,94,.30); }
.pill-upset { background:rgba(249,115,22,.14); color:#fdba74; border-color:rgba(249,115,22,.32); }

/* Unread/queue count on a nav item. */
.badge-dot {
  position:absolute; top:.3rem; right:.9rem; min-width:17px; height:17px; padding:0 4px;
  display:grid; place-items:center; border-radius:99px; font-size:9px; font-weight:900;
  background:#f43f5e; color:#fff; box-shadow:0 0 0 2px #101114;
}

/* ===================== mobile swipe (brief: "swiping") =====================
   Horizontal snap rails for the card strips. Native momentum + snap, so it
   feels like an app rather than a scrollable div. */
.swipe-row {
  display:flex; gap:.7rem; overflow-x:auto; scroll-snap-type:x mandatory;
  -webkit-overflow-scrolling:touch; padding-bottom:.25rem;
  scroll-padding-left:1rem;
}
.swipe-row > * { scroll-snap-align:start; flex:0 0 auto; }
.swipe-row::-webkit-scrollbar { display:none; }
.swipe-row { -ms-overflow-style:none; scrollbar-width:none; }

/* A row that can be swiped away to reveal actions is a lie unless the actions
   also exist as buttons, so the actions stay in the DOM and the transform is
   purely an affordance. */
.swipe-item { touch-action:pan-y; transition:transform .18s cubic-bezier(.22,1,.36,1); }
.swipe-item.swiped { transform:translateX(-84px); }

/* Section label used across the dashboards for a flatter, more B2B rhythm. */
.section-label {
  font-size:.7rem; font-weight:800; letter-spacing:.09em; text-transform:uppercase; color:var(--txt-3);
}
/* Tabular figures everywhere numbers are compared vertically. */
.tnum { font-variant-numeric: tabular-nums; }


/* ══════════════════════════════════════════════════════════════════════════
   DESIGN LANGUAGE v2 — "harmonized"
   ─────────────────────────────────────────────────────────────────────────
   Brief: "more up to date … insanely harmonized … B2B friendly while not
   boring for B2C", with two references supplied (one dark, one light).

   The references differ wildly in layout and palette, so copying either would
   have been the wrong read. What they SHARE is a small, strict rule set, and
   that is what is implemented here:

     1. Navigation floats. A detached capsule with the active tab wearing a
        pill, not an edge-to-edge bar with a top border.
     2. One radius language. Everything soft, nothing at 4px next to 24px.
     3. The accent is rationed. In both references exactly one element per
        screen is loud. Our accent stays indigo (`op`) — adding the neon mint
        or lime from the references would have made a fifth semantic colour
        and broken the palette split the previous pass established.
     4. Restraint over density. Big headline, tiny tracked micro-labels, and
        the numbers that survive are few and large.

   Motion is included because a static page reads dated, but every animation
   here is short, transform/opacity-only (compositor-friendly), and disabled
   under prefers-reduced-motion.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── Floating capsule navigation ──────────────────────────────────────────
   The wrapper is the hit-free gutter; the capsule is the visible surface.

   THE DOCK MUST BE REACHABLE WITHOUT SCROLLING. The brief: "you have to scroll
   all the way down to bottom to go to another page". It was already
   position:fixed here — the bug was elsewhere: #app animated `transform` on
   reveal, and a transformed ancestor becomes the containing block for its
   fixed descendants, so the dock was pinned to a moving div instead of the
   viewport. See the bootAppIn comment. Nothing that wraps the app shell may
   animate transform/filter/perspective, or this returns.

   Geometry follows the iOS 27 / One UI reading in the references: a squarer
   "squircle" radius rather than a full pill, a hairline border, a low-contrast
   fill, and ONE tinted pill marking the active tab. Explicitly NOT liquid
   glass — the brief rules it out for Android flagships, and a heavy
   backdrop-filter is also the most expensive thing you can put on a
   permanently-visible element on mid-range Android. The blur here is modest
   and degrades to a solid fill where it is unsupported. */
.navdock {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 40;
  padding: 0 calc(.6rem + env(safe-area-inset-right))
           calc(.55rem + env(safe-area-inset-bottom))
           calc(.6rem + env(safe-area-inset-left));
  pointer-events: none;                 /* only the capsule takes taps */
  display: flex; justify-content: center;
}
.navcapsule {
  pointer-events: auto;
  display: flex; align-items: center; gap: .18rem;
  width: 100%; max-width: 24rem;
  padding: .34rem .38rem;
  /* 22px on a 58px-tall bar: the ratio iOS uses for a docked container. A full
     pill (29px+) reads as a toy on a wide element; 12px reads as a toolbar.
     "roundy but not forced" is this ratio, not the maximum. */
  border-radius: 22px;
  background: var(--ds-bar, #1C1C1E);
  border: 1px solid rgba(255,255,255,.075);
  /* Hairline top highlight — the single cheapest trick that makes a dark
     surface read as glass rather than as a grey rectangle. */
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.055),
    0 2px 10px -3px rgba(0,0,0,.65),
    0 16px 40px -14px rgba(0,0,0,.9);
}
.navtab {
  flex: 1 1 0; min-width: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: .12rem;
  height: 50px; border-radius: 16px;
  color: var(--txt-3); background: transparent;
  transition: color .2s ease, background .24s ease, transform .16s ease;
}
.navtab i { font-size: 1.06rem; transition: transform .24s cubic-bezier(.34,1.56,.64,1); }
.navtab span {
  /* 10.5px, up from 9.5px. Four tabs instead of five bought the room, and the
     old size was clipping "Mastery" and "Library" into ellipses. */
  font-size: 10.5px; font-weight: 700; letter-spacing: .005em;
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.navtab:active { transform: scale(.94); }
/* Active state = ONE tinted pill. The reference's clearest single idea, and the
   reason the bar can afford to be otherwise almost invisible. */
.navtab.active {
  color: #FF8A84;
  background: rgba(255,107,99,.13);
  box-shadow: inset 0 0 0 1px rgba(255,107,99,.20);
}
.navtab.active i { transform: translateY(-1px) scale(1.04); }

/* The one loud element in the dock: the app's flagship destination as a filled
   circle. It holds ANALYSIS (AI video), per the brief — previously check-in.
   It sits INSIDE the capsule rather than straddling its edge: a centred FAB
   would have to cover one of the four tabs. */
.navfab {
  position: relative;
  pointer-events: auto; flex: 0 0 auto;
  width: 50px; height: 50px; border-radius: 17px;
  display: grid; place-items: center; color: #fff;
  background: linear-gradient(165deg, #FF6B63, #C62B24);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.28),
    0 6px 18px -6px rgba(198,43,36,.9);
  transition: transform .16s ease, filter .2s ease;
}
.navfab:active { transform: scale(.92); }
.navfab i { font-size: 1.05rem; }
.navfab.active { box-shadow: inset 0 1px 0 rgba(255,255,255,.3), 0 0 0 2px rgba(255,107,99,.35), 0 6px 18px -6px rgba(198,43,36,.9); }
/* NO breathing pulse. It was a permanent infinite animation on a permanently
   visible element: a battery cost on every screen, a distraction on all of
   them, and it fought the "minimalist" direction. A filled circle among four
   flat tabs is already the loudest thing in the bar. */

/* ── Buttons: pill variant for hero CTAs ─────────────────────────────────
   Both references use a fully-rounded CTA. Offered as a modifier so existing
   13px buttons elsewhere are untouched. */
.btn-pill { padding-left: 1.35rem; padding-right: 1.35rem; }

/* ── Segmented control ───────────────────────────────────────────────────
   The device that lets the home page carry ONE number instead of a grid of
   them: four stats share a slot and the athlete picks which one is showing. */
.seg {
  display: flex; gap: .2rem; padding: .25rem;
  background: rgba(255,255,255,.045); border: 1px solid rgba(255,255,255,.07);
  border-radius: 99px;
}
.seg-btn {
  flex: 1 1 0; min-width: 0; padding: .44rem .3rem;
  border-radius: 99px; font-size: 10.5px; font-weight: 800;
  letter-spacing: .03em; text-transform: uppercase; color: var(--txt-3);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  transition: color .2s ease, background .24s ease, transform .14s ease;
}
.seg-btn:active { transform: scale(.95); }
.seg-btn.on { color: var(--txt-1); background: rgba(255,255,255,.11); box-shadow: 0 1px 0 rgba(255,255,255,.06) inset; }

/* A .seg whose options do not fit the viewport. The base .seg-btn is
   flex:1 1 0 + min-width:0 + ellipsis, which SHRINKS rather than scrolls —
   at 390px a six-option belt rail renders "All bel…". This variant stops the
   shrink so overflow-x-auto does what the markup already asked for, and adds
   the fade that tells you there is more to the right. */
.seg-scroll { overflow-x: auto; scroll-snap-type: x proximity; }
.seg-scroll::-webkit-scrollbar { display: none; }
.seg-scroll { -ms-overflow-style: none; scrollbar-width: none; }
.seg-scroll > .seg-btn {
  flex: 0 0 auto; padding-left: .7rem; padding-right: .7rem;
  scroll-snap-align: start; text-overflow: clip;
}

/* Only the selected stat pane is in flow, so the card height is stable. */
.pane { display: none; }
.pane.on { display: block; animation: paneIn .34s cubic-bezier(.22,1,.36,1) both; }
@keyframes paneIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }

/* ── Ambient hero glow ──────────────────────────────────────────────────
   The "cool animation", kept cheap: one blurred blob drifting behind the hero.
   No layout impact, no repaint of content, ~0 CPU when composited. */
.orb {
  position: absolute; border-radius: 99px; filter: blur(42px);
  opacity: .5; pointer-events: none; will-change: transform;
  animation: orbDrift 17s ease-in-out infinite alternate;
}
@keyframes orbDrift {
  from { transform: translate3d(0,0,0) scale(1); }
  to   { transform: translate3d(14%, -12%, 0) scale(1.16); }
}

/* Progress ring: draws itself once on mount. */
.ring-track { stroke: rgba(255,255,255,.08); }
.ring-fill { animation: ringDraw 1.15s cubic-bezier(.22,1,.36,1) both; }
@keyframes ringDraw { from { stroke-dashoffset: var(--dash); } }

/* Staggered entrance for a column of sections — replaces hand-numbered
   .reveal-1/-2/-3 classes at each call site. */
.stagger { display: block; }   /* container for the staggered children below */
.stagger > * { animation: fadeUp .5s ease both; }
.stagger > *:nth-child(1) { animation-delay: .02s }
.stagger > *:nth-child(2) { animation-delay: .07s }
.stagger > *:nth-child(3) { animation-delay: .12s }
.stagger > *:nth-child(4) { animation-delay: .17s }
.stagger > *:nth-child(5) { animation-delay: .22s }
.stagger > *:nth-child(6) { animation-delay: .27s }
.stagger > *:nth-child(7) { animation-delay: .32s }

/* Any tappable non-button surface gets the same press feedback, so the whole
   UI answers touch identically. */
.tap { transition: transform .14s ease, border-color .2s ease, background .2s ease; }
.tap:active { transform: scale(.978); }
/* v3: the press state existed but nothing acknowledged a POINTER. .tap is
   already on 21 elements across the app, so upgrading the primitive here gives
   every one of them the same affordance without touching a single view file —
   and it stays inside (hover: hover) so a phone never gets a stuck hover
   state after a tap. */
@media (hover: hover) {
  .tap:hover { border-color: var(--line-2); background: rgba(255,255,255,.028); }
  .dest:hover { border-color: var(--line-2); color: var(--txt-1); }
}

/* Quiet destination chip for the secondary-actions rail. */
.dest {
  display: inline-flex; align-items: center; gap: .5rem;
  padding: .6rem .9rem; border-radius: 99px; font-size: 12.5px; font-weight: 700;
  color: var(--txt-2); background: rgba(255,255,255,.045);
  border: 1px solid rgba(255,255,255,.075); white-space: nowrap; min-height: 44px;
}
.dest i { font-size: .78rem; color: var(--txt-2); }
.dest:active { transform: scale(.96); }

/* ── DESTINATION GRID ───────────────────────────────────────────────────
   Replaces the horizontal "More" rail. A rail shows about 2.5 of eight items
   on a 390px screen and gives no hint that there is anything to swipe to, so
   most of the app was reachable only by accident.

   3-up on a phone, 4-up from 480px. Deliberately FLAT — no card, no shadow, no
   description line — because the ask was to keep it minimalist while making
   the features visible, and those are not in conflict: what was hiding the
   features was the layout, not the amount of decoration. */
.dest-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: .5rem;
}
@media (min-width: 480px) { .dest-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }

.dtile {
  position: relative;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: .42rem;
  /* 68px, comfortably past the 44px minimum in both axes — the rail's pills
     were 44px tall but only as wide as their text, so the short ones were
     awkward targets. */
  min-height: 68px; padding: .7rem .35rem;
  border-radius: 16px;
  background: rgba(255,255,255,.035);
  border: 1px solid rgba(255,255,255,.07);
  transition: background .15s ease, border-color .15s ease, transform .12s ease;
}
.dtile i { font-size: .92rem; color: var(--txt-2); }
.dtile-l {
  font-size: 10.5px; font-weight: 800; letter-spacing: .005em; color: var(--txt-2);
  text-align: center; line-height: 1.2;
  /* Two lines maximum, then ellipsis. A third line changes the tile's height
     and breaks the grid's rhythm, which is more noticeable than a clipped
     word. */
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.dtile:active { transform: scale(.96); }
@media (hover: hover) {
  .dtile:hover { background: rgba(255,255,255,.07); border-color: var(--line-2); }
}

/* Locked tiles stay VISIBLE and tappable — they open the paywall. Dimming them
   to ~55% marks them as different without hiding them: a paid feature the user
   never sees is a paid feature they never buy. */
/* Wave 4a-6: locked tiles are NOT dimmed — see theme-v2 §17.6b. This is the
   pre-theme fallback layer, so the value is corrected here too rather than
   left as a .55 that paints for the one frame before v2 applies. */
.dtile.is-locked i, .dtile.is-locked .dtile-l { opacity: 1; }
.dtile-lock {
  position: absolute; top: .4rem; right: .45rem;
  font-size: .52rem !important; color: #FF6B63 !important; opacity: .9;
}


/* The Mat Feed card styles (.fd-*) lived here until 2026-09-04. The feed was
   removed 2026-09-02 (docs/archive/FEED-REMOVAL.md) and no view rendered them since —
   deleted in Wave 3 §3H. */

/* ── Federated sign-in ──────────────────────────────────────────────────
   Deliberately NOT .btn-primary. Google's brand guidelines require their own
   mark at its own colours on a neutral surface, and an indigo gradient with a
   monochrome glyph in it is both off-brand and less recognisable — the whole
   value of this button is that it is instantly identifiable, so styling it to
   match our palette would defeat its only job.

   Light surface, because that is the variant Google specifies for a dark UI. */
.btn-google {
  width: 100%; min-height: 48px; border-radius: 13px;
  display: inline-flex; align-items: center; justify-content: center; gap: .6rem;
  font-weight: 650; font-size: .92rem; letter-spacing: .005em;
  background: #ffffff; color: #1f1f1f; border: 1px solid rgba(255,255,255,.16);
  cursor: pointer; user-select: none;
  transition: transform .14s ease, filter .18s ease, box-shadow .18s ease;
  box-shadow: 0 1px 0 rgba(0,0,0,.04) inset, 0 8px 20px -14px rgba(0,0,0,.8);
}
.btn-google:hover  { filter: brightness(.965); }
.btn-google:active { transform: scale(.978); }
.btn-google:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(198,43,36,.45); }
.btn-google:disabled { opacity: .55; cursor: not-allowed; transform: none; }
.btn-google svg { width: 18px; height: 18px; flex: 0 0 auto; }

/* "or" divider. A rule with the word sitting in it, so the two routes read as
   alternatives rather than as a sequence of steps. */
.or-split {
  display: flex; align-items: center; gap: .75rem;
  color: var(--txt-3); font-size: 11.5px; font-weight: 700;
  text-transform: uppercase; letter-spacing: .09em;
}
.or-split::before, .or-split::after {
  content: ""; flex: 1 1 0; height: 1px; background: rgba(255,255,255,.09);
}

/* Signup consent rows. Two UNTICKED boxes; the button is disabled until both
   are ticked. The empty box is deliberately NOT accent-coloured — a lime empty
   box reads as already-agreed, which is the exact misreading consent must not
   invite. Only the ticked state carries the accent. */
.consent-block { display: grid; gap: .4rem; padding-top: .15rem; }
.consent-row {
  display: flex; align-items: flex-start; gap: .7rem;
  padding: .7rem .8rem; border-radius: var(--radius-sm);
  border: 1px solid var(--line); background: var(--sunken);
  cursor: pointer; user-select: none;
}
.consent-row:has(.consent-box:checked) { border-color: var(--line-2); }
.consent-box {
  appearance: none; -webkit-appearance: none;
  flex: 0 0 auto; width: 20px; height: 20px; margin-top: .05rem;
  border-radius: 6px; border: 1.5px solid var(--txt-3); background: transparent;
  display: grid; place-content: center; cursor: pointer;
}
.consent-box::after {
  content: ""; width: 10px; height: 6px;
  border-left: 2.5px solid var(--accent-on); border-bottom: 2.5px solid var(--accent-on);
  transform: rotate(-45deg) scale(0); transition: transform .14s ease;
}
.consent-box:checked { background: var(--accent-fill); border-color: transparent; }
.consent-box:checked::after { transform: rotate(-45deg) translate(1px, -1px) scale(1); }
.consent-text { font-size: 13px; line-height: 1.45; color: var(--txt-2); }
/* D5-9 helper (COPY-WAVE2 §2.2) — sits directly under the third box, muted,
   no link. Rendered only when the box itself is. */
.consent-help { margin: -.05rem .15rem 0; line-height: 1.5; color: var(--txt-3); text-wrap: pretty; }
.consent-link { color: var(--txt-1); font-weight: 700; text-decoration: underline; text-underline-offset: 3px; }
.consent-link:hover { color: var(--txt-1); }

/* Auth-page error banner. Distinct from toast(): an OAuth failure arrives as a
   full page load from Google, and a toast that fades after 2.6s is the wrong
   surface for a message the athlete may need to act on (checking a config,
   retrying, using a password instead). This one persists. */
.auth-alert {
  display: flex; gap: .65rem; align-items: flex-start;
  padding: .8rem .9rem; border-radius: 13px; font-size: 12.5px; line-height: 1.5;
  background: rgba(244,63,94,.1); border: 1px solid rgba(244,63,94,.28);
  color: #fda4af;
}
.auth-alert i { margin-top: .12rem; }
.auth-alert.info {
  background: rgba(198,43,36,.1); border-color: rgba(198,43,36,.3); color: #FFA39C;
}

/* ── Onboarding ─────────────────────────────────────────────────────────
   Belt picker. A <select> hides the single most consequential answer a new
   athlete gives us behind a tap; these are visible, tappable, and coloured so
   the choice is obvious at a glance. */
.belt-pick {
  display: flex; align-items: center; gap: .7rem; width: 100%;
  padding: .7rem .85rem; border-radius: 15px; text-align: left;
  background: rgba(255,255,255,.04); border: 1px solid rgba(255,255,255,.08);
  color: var(--txt-1); font-weight: 650; font-size: .92rem; min-height: 52px;
  transition: transform .14s ease, border-color .2s ease, background .2s ease;
  cursor: pointer;
}
.belt-pick:active { transform: scale(.985); }
.belt-pick.on {
  background: rgba(198,43,36,.13); border-color: rgba(198,43,36,.45);
  box-shadow: 0 0 0 1px rgba(198,43,36,.25) inset;
}
.belt-pick .swatch { width: 30px; height: 10px; border-radius: 99px; flex: 0 0 auto; }
.belt-pick .tick { margin-left: auto; color: var(--txt-2); opacity: 0; transition: opacity .18s ease; }
.belt-pick.on .tick { opacity: 1; }

/* Stripe picker — 0–4, as five tappable cells rather than a dropdown. */
.stripe-pick {
  flex: 1 1 0; min-width: 0; min-height: 44px; border-radius: 12px;
  background: rgba(255,255,255,.04); border: 1px solid rgba(255,255,255,.08);
  color: var(--txt-2); font-weight: 750; font-size: .9rem; cursor: pointer;
  transition: transform .14s ease, border-color .2s ease, background .2s ease, color .2s ease;
}
.stripe-pick:active { transform: scale(.96); }
.stripe-pick.on {
  background: rgba(198,43,36,.15); border-color: rgba(198,43,36,.45); color: #d0f2fb;
}

/* Step dots. Progress the athlete can see, so a 3-step form does not feel
   open-ended. */
.step-dots { display: flex; gap: .35rem; align-items: center; }
.step-dot {
  width: 24px; height: 4px; border-radius: 99px;
  background: rgba(255,255,255,.12); transition: background .25s ease;
}
.step-dot.on   { background: #C62B24; }
.step-dot.done { background: rgba(198,43,36,.45); }

/* ── Accessibility ──────────────────────────────────────────────────────
   Decoration must never be a barrier. Everything above degrades to static. */
@media (prefers-reduced-motion: reduce) {
  .orb, .navfab::after { animation: none !important; }
  .ring-fill, .pane.on, .stagger > *, .reveal { animation: none !important; }
  html { scroll-behavior: auto; }
}

/* ══════════════════════════════════════════════════════════════════════════
   DESIGN LANGUAGE v3 — "phone-first, and Ranked stops looking like a poster"
   ─────────────────────────────────────────────────────────────────────────
   Brief: "best looking for iphone and high end android phones, especially the
   ranked needs to be improved, it looks like a thing made in canva … cool log
   in animation, interactive things, animations … theme color is good but feels
   off a little."

   Four separate problems, four separate fixes:

   1. THE THEME. Diagnosed by measurement, not taste. The old ground was
      #0d111a = hue 222 at 33% SATURATION, while the brand indigo is hue 239
      at 84%. Two strongly-tinted blues 17 degrees apart cannot separate from
      each other, so the accent looked like part of the background and the
      whole screen read plasticky. The ink ramp is now ~hue 222 at 8-13%
      saturation (src/shell.ts) and every hardcoded surface above was re-based
      onto it. Same hue family, a third of the chroma: the indigo now has the
      only saturated pixels on screen, which is what makes it read as an
      accent at all.

   2. "LOOKS LIKE CANVA". That verdict is earned by four specific tells, all
      of which were present on the leaderboard: emoji used as interface icons
      (medals, a gi), every element competing at the same visual weight, no
      real typographic hierarchy, and colour applied as decoration. Fixed with
      a genuine podium, metallic rank treatments drawn in CSS rather than
      pasted in as emoji, tabular figures, and a proportional bar so the
      numbers mean something spatially instead of just sitting there.

   3. PHONES. iPhone 12-16 are 390-430px wide with a home indicator and a
      notch/island; high-end Androids are 360-412px. Both need env() insets on
      BOTH edges, 44px targets, no horizontal overflow at 360px, and momentum
      scrolling that does not fight the browser.

   4. MOTION. Interactive and animated, but every single animation here is
      transform/opacity only (compositor-friendly, no layout thrash) and every
      one is switched off under prefers-reduced-motion at the end of the file.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── Phone chrome ────────────────────────────────────────────────────────── */
/* The notch/island side. pb-safe already handles the bottom, but a fixed
   topbar on an iPhone sits UNDER the status bar without this. */
.pt-safe { padding-top: env(safe-area-inset-top); }
/* Landscape / foldable-unfolded: the inset moves to the sides. */
.px-safe { padding-left: env(safe-area-inset-left); padding-right: env(safe-area-inset-right); }
/* Kill the iOS tap flash — we draw our own press states via .tap/:active. */
a, button, .tap, .navtab, .seg-btn { -webkit-tap-highlight-color: transparent; }
/* Stop the double-tap zoom that makes fast tapping feel broken on iOS. */
button, .tap, .navtab, .seg-btn, .rk-row { touch-action: manipulation; }

/* ── Ranked: podium ──────────────────────────────────────────────────────
   Top three, sized by placing. The point of a podium is that rank is legible
   from the SHAPE before you read a single character — that is the thing a
   flat list with medal emoji can never do. */
.podium {
  display: grid; grid-template-columns: 1fr 1.22fr 1fr;
  align-items: end; gap: .5rem;
}
.pod {
  position: relative; display: flex; flex-direction: column; align-items: center;
  padding: .85rem .4rem .7rem; border-radius: 20px; overflow: hidden;
  background: rgba(255,255,255,.035); border: 1px solid rgba(255,255,255,.07);
  transition: transform .16s ease;
}
.pod:active { transform: scale(.972); }
/* The metal is a gradient sweep, not a flat fill: one tone reads as a sticker,
   a sweep reads as a surface catching light. */
.pod-1 {
  padding-top: 1.15rem;
  background: linear-gradient(168deg, rgba(251,191,36,.17), rgba(251,191,36,.03) 62%, rgba(255,255,255,.02));
  border-color: rgba(251,191,36,.34);
  box-shadow: 0 10px 30px -14px rgba(251,191,36,.4);
}
.pod-2 {
  background: linear-gradient(168deg, rgba(203,213,225,.14), rgba(203,213,225,.02) 62%, rgba(255,255,255,.02));
  border-color: rgba(203,213,225,.26);
}
.pod-3 {
  background: linear-gradient(168deg, rgba(217,119,6,.15), rgba(217,119,6,.02) 62%, rgba(255,255,255,.02));
  border-color: rgba(217,119,6,.30);
}
/* A slow specular pass over first place only. Rationed accent, again. */
.pod-1::after {
  content: ''; position: absolute; inset: -40% -120%;
  background: linear-gradient(74deg, transparent 42%, rgba(255,255,255,.13) 50%, transparent 58%);
  animation: podSheen 5.5s ease-in-out infinite; pointer-events: none;
}
@keyframes podSheen {
  0%, 62% { transform: translateX(-46%); }
  100%    { transform: translateX(46%); }
}
/* Placing numeral, drawn as type rather than as an emoji glyph. */
.pod-rank {
  font-size: 10px; font-weight: 900; letter-spacing: .1em; text-transform: uppercase;
  margin-bottom: .5rem;
}
.pod-1 .pod-rank { color: #fbbf24; }
.pod-2 .pod-rank { color: var(--txt-2); }
.pod-3 .pod-rank { color: #d97706; }
.pod-av { position: relative; }
.pod-av img, .pod-av > span { display: block; }
/* The ring is the medal. No emoji required. */
.pod-1 .pod-av { box-shadow: 0 0 0 2px #fbbf24, 0 0 0 5px rgba(251,191,36,.16); border-radius: 14px; }
.pod-2 .pod-av { box-shadow: 0 0 0 2px #cbd5e1, 0 0 0 5px rgba(203,213,225,.14); border-radius: 14px; }
.pod-3 .pod-av { box-shadow: 0 0 0 2px #d97706, 0 0 0 5px rgba(217,119,6,.14); border-radius: 14px; }
.pod-name {
  margin-top: .6rem; font-size: 12px; font-weight: 800; line-height: 1.15;
  max-width: 100%; text-align: center;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pod-val { font-size: 17px; font-weight: 900; line-height: 1.1; margin-top: .28rem; font-variant-numeric: tabular-nums; }
.pod-1 .pod-val { font-size: 21px; }
.pod-unit { font-size: 8.5px; font-weight: 800; letter-spacing: .09em; text-transform: uppercase; color: var(--txt-3); }

/* ── Ranked: list rows ───────────────────────────────────────────────────
   A leaderboard row has four jobs and they need four different weights:
   position (quiet, tabular), identity (loud), context (tiny), value (loud,
   tabular, right-aligned so digits line up as a column you can scan). */
.rk-row {
  position: relative; display: flex; align-items: center; gap: .7rem;
  padding: .7rem .85rem; min-height: 60px;
  transition: transform .14s ease, background .2s ease;
  cursor: pointer;
}
.rk-row:active { transform: scale(.988); }
@media (hover: hover) { .rk-row:hover { background: rgba(255,255,255,.032); } }
.rk-num {
  flex: 0 0 auto; width: 26px; text-align: right;
  font-size: 13px; font-weight: 800; color: var(--txt-3);
  font-variant-numeric: tabular-nums;
}
.rk-row.is-me { background: rgba(198,43,36,.09); }
.rk-row.is-me .rk-num { color: #FFA39C; }
/* The indigo edge marker: cheaper and calmer than tinting the whole row. */
.rk-row.is-me::before {
  content: ''; position: absolute; left: 0; top: 10px; bottom: 10px; width: 3px;
  border-radius: 0 99px 99px 0; background: #C62B24;
}
.rk-main { min-width: 0; flex: 1 1 auto; }
.rk-name {
  font-size: 13.5px; font-weight: 750; line-height: 1.2;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rk-sub { display: flex; align-items: center; gap: .4rem; margin-top: .25rem; min-width: 0; }
.rk-gym { font-size: 10.5px; color: var(--txt-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rk-val { flex: 0 0 auto; text-align: right; min-width: 62px; }
.rk-fig { font-size: 15px; font-weight: 900; line-height: 1; font-variant-numeric: tabular-nums; }
.rk-unit { font-size: 8.5px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; color: var(--txt-3); margin-top: .2rem; }
/* Proportional bar. This is what turns a column of numbers into a ranking you
   can feel: a 4th place at 92% of the leader looks close, at 30% it does not.
   Absolutely positioned so it costs no row height. */
.rk-bar {
  position: absolute; left: 0; bottom: 0; height: 2px;
  background: linear-gradient(90deg, rgba(198,43,36,.15), rgba(198,43,36,.65));
  border-radius: 0 99px 99px 0;
  transform-origin: left center; animation: rkBar .75s cubic-bezier(.22,1,.36,1) both;
}
@keyframes rkBar { from { transform: scaleX(0); } }

/* Sticky "your position" bar. Scrolling to find yourself is the single most
   common thing anyone does on a leaderboard, so it stops being a scroll. */
.rk-me-dock {
  position: sticky; bottom: calc(5.5rem + env(safe-area-inset-bottom)); z-index: 20;
  display: flex; align-items: center; gap: .7rem;
  padding: .6rem .8rem; border-radius: 99px;
  background: var(--ds-bar, #1C1C1E);
  border: 1px solid rgba(198,43,36,.3);
  box-shadow: 0 14px 38px -16px rgba(0,0,0,.9);
}
@media (min-width: 768px) { .rk-me-dock { bottom: 1.25rem; } }

/* Board switch. Two ladders with different rules, so it is NOT a .seg (which
   means "views of one number"); it is two cards, one of which is armed. */
/* .board-pick — the leaderboard board picker. The red/orange tinted rules that
   lived here were superseded by theme-v2.css §37.5 (DS6) and deleted in the
   same pass so the cascade has one owner. */

/* ── "Not shipped yet" surface ───────────────────────────────────────────
   Shared by the AI roll analyser and the feed. An unfinished feature is more
   trustworthy when it says so plainly and offers a way to be told when it
   lands, than when it is hidden or faked with dummy content. */
.wip-hero {
  position: relative; overflow: hidden; border-radius: 20px; padding: 1.4rem 1.15rem;
  background: linear-gradient(168deg, rgba(251,146,60,.10), rgba(255,255,255,.014));
  border: 1px solid rgba(251,146,60,.24);
}
.wip-badge {
  display: inline-flex; align-items: center; gap: .4rem;
  padding: .3rem .6rem; border-radius: 99px;
  font-size: 9.5px; font-weight: 900; letter-spacing: .1em; text-transform: uppercase;
  background: rgba(251,146,60,.16); color: #fdba74; border: 1px solid rgba(251,146,60,.3);
}
.wip-badge .dot {
  width: 5px; height: 5px; border-radius: 99px; background: #fb923c;
  animation: wipPulse 1.9s ease-in-out infinite;
}
@keyframes wipPulse { 0%,100% { opacity: 1; transform: scale(1); } 50% { opacity: .35; transform: scale(.75); } }
/* Progress toward shipping, stated as a number so "coming soon" is falsifiable. */
.wip-meter { height: 5px; border-radius: 99px; background: rgba(255,255,255,.07); overflow: hidden; }
.wip-meter > i {
  display: block; height: 100%; border-radius: 99px;
  background: linear-gradient(90deg, #fb923c, #fbbf24);
  transform-origin: left center; animation: rkBar 1.1s cubic-bezier(.22,1,.36,1) both;
}
/* A blurred preview of the real thing. Honest: it is obviously not usable. */
.wip-peek { position: relative; border-radius: 17px; overflow: hidden; }
.wip-peek > .inner { filter: blur(5px) saturate(.55); opacity: .5; pointer-events: none; user-select: none; }
.wip-peek::after {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(180deg, rgba(16,17,20,.15), rgba(16,17,20,.92));
}
.wip-lock {
  position: absolute; inset: 0; z-index: 2; display: grid; place-items: center;
  font-size: 10px; font-weight: 900; letter-spacing: .12em; text-transform: uppercase; color: var(--txt-2);
}

/* ── Login stage ─────────────────────────────────────────────────────────
   "a cool log in animation". The mat is the brand: a slowly-breathing grappling
   grid with two drifting orbs over it. It is one element, purely decorative,
   aria-hidden, and it animates transform/opacity only. */
.auth-stage { position: relative; overflow: hidden; }
/* The auth card sits close to the top instead of centred in a tall column:
   vertical centring on a short viewport is what pushes a form under the fold
   in the first place. Small negative pull cancels the <main> py-6 so the card
   starts just under the topbar. */
.auth-shell { margin-top: -0.35rem; }
/* Identity as a ROW, not a stack. This is the ~120px the login fold gets back. */
.auth-head {
  display: flex; align-items: center; gap: .95rem;
  padding-bottom: .9rem; margin-bottom: .9rem;
  border-bottom: 1px solid rgba(255,255,255,.06);
}
/* The belt bar is now a short rule under a small wordmark rather than a
   full-width banner element, so it reads as an underline, not a divider. */
.auth-head .belt-sweep { width: 84px; }
/* Lift EVERY child above the decoration, then push the two decorative layers
   back down. Opting content in one-by-one with a .content class was the first
   attempt and it is a trap: .mat-grid is absolutely positioned with z-index 0,
   which paints above ordinary in-flow text, so a single element that forgot the
   class would silently render behind the grid. This way the safe state is the
   default and only the two decorations are special. */
/* CRITICAL: re-assert `position:absolute` here, not just z-index.
   `.auth-stage > *` above has the SAME specificity as the `.orb` rule and
   comes later in the file, so it was silently overriding `position:absolute`
   with `relative`. Two 260px blurred decorations therefore dropped into normal
   flow and pushed the sign-in form 484px down the page — measured: the form
   began at y=850 in a 600px viewport, which is the whole reason logging in
   required scrolling. The z-index reset alone could never have fixed it. */
.auth-stage > * { position: relative; z-index: 2; }

/* IDENTIFY DECORATION BY HOW IT POSITIONS, NOT BY WHAT IT IS CALLED.
   This exclusion used to name classes (`.mat-grid`, `.orb`). That works right
   up until someone writes a decorative div with raw utilities and no class —
   which is exactly what the landing hero has: `absolute -top-32 -right-24 w-96
   h-96 blur-3xl`, a 384px invisible circle with no `.orb` on it. It was
   therefore demoted to `position: relative` by the rule above and pushed 384px
   of empty space into the layout. Same defect, second time, because the
   previous fix was a list of names.
   A rule whose job is "stack the CONTENT" must match on POSITIONING. */
.auth-stage > .absolute, .auth-stage > .fixed,
.auth-stage > .mat-grid, .auth-stage > .orb,
.auth-stage > .orb-hero { position: absolute; z-index: 0; }

/* Decoration also gets a NAME instead of a pile of utilities, so it is
   greppable and cannot be mistaken for content. */
.orb-hero {
  position: absolute;
  border-radius: 99px;
  pointer-events: none;
  filter: blur(48px);
  z-index: 0;
}
.mat-grid {
  position: absolute; inset: -25%; z-index: 0; pointer-events: none;
  background-image:
    linear-gradient(rgba(198,43,36,.075) 1px, transparent 1px),
    linear-gradient(90deg, rgba(198,43,36,.075) 1px, transparent 1px);
  background-size: 46px 46px;
  /* Faded at the edges so it never reads as a hard boundary. */
  mask-image: radial-gradient(120% 78% at 50% 22%, #000 12%, transparent 74%);
  -webkit-mask-image: radial-gradient(120% 78% at 50% 22%, #000 12%, transparent 74%);
  animation: matDrift 26s linear infinite;
}
@keyframes matDrift {
  from { transform: translate3d(0,0,0); }
  to   { transform: translate3d(-46px,-46px,0); }   /* exactly one cell: seamless */
}
/* The wordmark assembles itself, then a light sweep passes over it once. */
.auth-mark { position: relative; display: inline-block; }
.auth-mark .ch {
  display: inline-block;
  animation: chIn .62s cubic-bezier(.22,1,.36,1) both;
}
.auth-mark .ch:nth-child(1) { animation-delay: .04s }
.auth-mark .ch:nth-child(2) { animation-delay: .11s }
.auth-mark .ch:nth-child(3) { animation-delay: .18s }
.auth-mark .ch:nth-child(4) { animation-delay: .25s }
.auth-mark .ch:nth-child(5) { animation-delay: .32s }
@keyframes chIn {
  from { opacity: 0; transform: translateY(16px) rotate(-7deg) scale(.9); filter: blur(5px); }
  to   { opacity: 1; transform: none; filter: none; }
}
/* Belt bar under the wordmark: white to black, the whole journey in one line.
   It draws itself on load, which is the "cool" bit and also the pitch. */
.belt-sweep {
  height: 4px; border-radius: 99px; overflow: hidden;
  background: linear-gradient(90deg, #e5e7eb 0%, #2563eb 27%, #7c3aed 52%, #78350f 76%, #0b0b0f 100%);
  transform-origin: left center;
  animation: beltDraw 1.05s .3s cubic-bezier(.22,1,.36,1) both;
}
@keyframes beltDraw { from { transform: scaleX(0); opacity: .3; } to { transform: scaleX(1); opacity: 1; } }
/* Fields arrive after the mark so the eye lands on the brand first. */
.auth-in { animation: fadeUp .55s both; }
.auth-in-1 { animation-delay: .34s } .auth-in-2 { animation-delay: .42s }
.auth-in-3 { animation-delay: .50s } .auth-in-4 { animation-delay: .58s }
/* Focus ring that grows from the field rather than snapping on. */
.input:focus-visible { outline: none; }

/* ── Interaction feedback ────────────────────────────────────────────────
   "interactive things". A press that answers you is the cheapest possible way
   to make software feel expensive. */
.lift { transition: transform .18s cubic-bezier(.22,1,.36,1), box-shadow .22s ease, border-color .2s ease; }
@media (hover: hover) {
  .lift:hover { transform: translateY(-2px); box-shadow: 0 14px 34px -18px rgba(0,0,0,.9); }
}
.lift:active { transform: scale(.985); }

/* ── Accessibility, v3 additions ─────────────────────────────────────────
   Same contract as the v2 block: every animation above degrades to its end
   state. Anything added later must be added here too. */
@media (prefers-reduced-motion: reduce) {
  /* .skeleton was the worst offender here and it predates v3: an INFINITE
     shimmer loop that never stopped for users who asked for no motion. .toast
     and .radar-dot were animating unguarded too. All three are safe to simply
     switch off — none of them starts from a hidden state, so removing the
     animation leaves the element at its natural, visible styles. */
  .skeleton, .toast, .radar-dot,
  .pod-1::after, .rk-bar, .wip-badge .dot, .wip-meter > i,
  .mat-grid, .auth-mark .ch, .belt-sweep, .auth-in {
    animation: none !important;
  }
  .lift:hover { transform: none; }
  /* Bars/marks must still be VISIBLE, not stuck at their from-state. */
  .rk-bar, .wip-meter > i, .belt-sweep { transform: none; opacity: 1; }
  .auth-mark .ch, .auth-in { opacity: 1; filter: none; }
}

/* ══════════════════════════════════════════════════════════════════════════
   ROUND CYCLING (Overpowered Coach)
   A coaching round is 8 athlete messages. The UI has to make that ceiling
   visible without turning the chat into a dashboard, so the whole affordance
   is one strip of pips plus two ghost buttons.
   ══════════════════════════════════════════════════════════════════════════ */

/* Turn pips. Deliberately tiny: the counter is reference information, not the
   subject of the screen. The last pip warms to amber so "one message left"
   is felt rather than read. */
.rnd-pip {
  width: 14px; height: 4px; border-radius: 99px;
  background: rgba(255,255,255,.10);
  transition: background .25s ease, box-shadow .25s ease;
}
.rnd-pip.on { background: #C62B24; box-shadow: 0 0 0 1px rgba(198,43,36,.28); }
.rnd-pip.on:last-of-type { background: #fb923c; box-shadow: 0 0 0 1px rgba(251,146,60,.3); }
@media (max-width: 400px) { .rnd-pip { width: 10px; } }

/* The round-complete rule. It reads as a chapter break in the transcript,
   which is exactly what it is — not an error and not a toast. */
.rnd-break {
  display: flex; align-items: center; gap: .5rem;
  margin: 1rem 0 .25rem; padding: .5rem .75rem;
  font-size: 11px; font-weight: 800; letter-spacing: .01em;
  color: #fcd34d;
  background: linear-gradient(90deg, rgba(251,146,60,.10), rgba(251,146,60,.03));
  border: 1px solid rgba(251,146,60,.22); border-radius: 12px;
}
.rnd-break i { font-size: 10px; opacity: .9; }

/* Rows inside the Past rounds sheet. */
.tap-row { transition: background .16s ease; }
.tap-row:active { background: rgba(255,255,255,.05); }
@media (hover: hover) { .tap-row:hover { background: rgba(255,255,255,.035); } }

/* ── Bottom sheet ───────────────────────────────────────────────────────
   Past rounds is archival reading, so it slides from the bottom where a
   thumb already is, instead of a centred dialog that covers the chat. On
   wide screens it centres so it does not look like a phone control stranded
   on a desktop. */
.sheet-wrap { position: fixed; inset: 0; z-index: 95; display: flex; align-items: flex-end; justify-content: center; }
.sheet-back { position: absolute; inset: 0; background: rgba(4,5,7,.66); animation: sheetFade .2s ease both; }
.sheet {
  position: relative; width: 100%; max-width: 30rem;
  background: #17181c; border: 1px solid rgba(255,255,255,.09);
  border-bottom: 0; border-radius: 20px 20px 0 0;
  box-shadow: 0 -20px 60px -20px rgba(0,0,0,.9);
  padding-top: .5rem; padding-bottom: env(safe-area-inset-bottom, 0);
  animation: sheetUp .28s cubic-bezier(.22,1,.36,1) both;
}
.sheet-grab { width: 38px; height: 4px; border-radius: 99px; background: rgba(255,255,255,.16); margin: .25rem auto .75rem; }
@media (min-width: 640px) {
  .sheet-wrap { align-items: center; padding: 1rem; }
  .sheet { border-radius: 20px; border-bottom: 1px solid rgba(255,255,255,.09); }
}
@keyframes sheetUp { from { transform: translateY(14px); opacity: 0; } to { transform: none; opacity: 1; } }
@keyframes sheetFade { from { opacity: 0; } to { opacity: 1; } }


/* ══════════════════════════════════════════════════════════════════════════
   BOOT SPLASH — the belt being TIED
   ══════════════════════════════════════════════════════════════════════════
   The brief was specific and repeated: not a belt that fades in, but the
   ANIMATION OF A BELT BEING WORN. So the sequence follows the order a pair of
   hands actually does it, and nothing in it uses opacity as its primary move:

     1. WRAP    the two bands come in from off-stage, around the waist
     2. THREAD  the crossing panels pass over / under / over — the weave
     3. CINCH   the collar cuff tightens with a slight overshoot, because
                pulling a knot tight is a snap, not a glide
     4. FALL    the tail drops under gravity and settles
     5. RANK    the rank blocks flick on along the tail
     6. WORDMARK splits open from the centre
     7. BELT RAMP draws through

   Everything is transform / opacity / clip only, so it composites on the GPU
   and will not judder on a mid-range Android while the app does its own boot
   work in parallel.

   The timings below are asserted by scripts/test-boot-splash.mjs, which checks
   the ORDER (wrap < thread < cinch < fall < rank) rather than exact values —
   so the sequence cannot silently regress into a fade. */

.boot-splash {
  position: fixed;
  inset: 0;
  z-index: 9999;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0;
  /* background-COLOR as well as the gradient, deliberately: theme-v2's
     structural `[class] { background-image: none }` reset strips the gradient,
     and a splash whose only paint was that gradient rendered fully transparent
     over the landing page. theme-v2 now exempts .boot-splash, and this opaque
     floor means even a future reset cannot make the overlay see-through. */
  background-color: #0b0c0e;
  background-image:
    radial-gradient(120% 90% at 50% 42%, #16171b 0%, #0b0c0e 55%, #07080a 100%);
  transition: opacity .38s ease, transform .42s ease, filter .38s ease;
}

/* The handoff is a camera push, not a cut: the splash scales up and blurs out
   while the app fades in underneath (see #app.boot-reveal).
   Safe HERE, unlike on #app: .boot-splash is a leaf overlay with no
   position:fixed descendants of its own, so transforming it creates a
   containing block that contains nothing. The same two properties on #app are
   what broke the bottom navigation. */
.boot-splash.boot-done {
  opacity: 0;
  transform: scale(1.14);
  filter: blur(6px);
  pointer-events: none;
}

/* THE HANDOFF IS THE SHUTTER, and it is the reason the sequence ends on a
   viewfinder rather than on a logo.
   The mark leads the exit and the brackets keep travelling outward past the
   frame edge, so the camera does not fade away — it opens and the dashboard is
   what was inside it. The mark moving FASTER than the sheet it sits on is the
   whole effect: a uniform scale on the splash alone reads as a picture being
   pulled back, which is what this used to be.
   TRANSITIONS, not keyframes, deliberately: the exit is triggered by a class
   the router adds (dismissSplash in app.js) at a time nobody can predict, and
   a keyframe would restart from its 0% frame mid-reveal on a slow session.
   Both properties are compositor-only, and .boot-stage has no fixed
   descendants — the #app containing-block bug cannot reach here. */
.boot-stage { transition: transform .40s cubic-bezier(.3,.86,.28,1); }
.boot-splash.boot-done .boot-stage { transform: scale(1.10); }
.boot-mark .bl-frame { transition: transform .38s cubic-bezier(.3,.9,.26,1); }
.boot-splash.boot-done .bl-frame-tl { transform: translate(-13%, -13%); }
.boot-splash.boot-done .bl-frame-tr { transform: translate( 13%, -13%); }
.boot-splash.boot-done .bl-frame-bl { transform: translate(-13%,  13%); }
.boot-splash.boot-done .bl-frame-br { transform: translate( 13%,  13%); }
/* The wordmark and the ramp settle DOWN and out as the frame opens, so the
   exit has a direction instead of everything leaving at once. */
.boot-word, .boot-belt { transition: transform .34s cubic-bezier(.3,.86,.28,1), opacity .22s ease; }
.boot-splash.boot-done .boot-word { transform: translateY(9px); opacity: 0; }
.boot-splash.boot-done .boot-belt { transform: translateY(9px) scaleX(1.06); opacity: 0; }

/* 232px, not 330px. The previous mark was a wide coil (~2.2:1), so 330px of
   WIDTH was ~150px of height; the 2026-09-03 mark is SQUARE, and the same
   width made it a 330px block that filled a phone screen and left the
   wordmark stranded at the bottom. The stage is sized by the mark's optical
   area rather than by a number inherited from a different drawing. */
.boot-stage {
  position: relative;
  width: min(58vw, 232px);
  display: grid;
  place-items: center;
  margin-bottom: 22px;
}

.boot-glow {
  position: absolute;
  width: 92%;
  aspect-ratio: 1;
  border-radius: 50%;
  /* The glow is the ACCENT, not a second colour. It was a leftover cyan from the
     palette the 2026-08-30 brief threw out — the only saturated hue left on the
     boot screen, and the one thing on it that did not belong to the brand. */
  background: radial-gradient(circle, rgba(206,255,86,.22), rgba(206,255,86,0) 70%);
  filter: blur(16px);
  opacity: 0;
  animation: bootGlow 1.5s ease-out both;
}
@keyframes bootGlow {
  0%   { opacity: 0; transform: scale(.7); }
  45%  { opacity: 1; transform: scale(1); }
  100% { opacity: .55; transform: scale(1.04); }
}

.boot-mark { width: 100%; height: auto; display: block; overflow: visible; }

/* THE MARK IS THE OWNER'S OWN ARTWORK, shipped as raster layers rather than a
   trace of it (owner instruction 2026-09-01: "just use the image i uploaded",
   restated 2026-09-03: "do not try to replicate the png ... upload it directly
   as a PNG"). The 2026-09-03 artwork is a NEW DRAWING, not a recolour: a
   flat-line belt knot centred inside four camera-viewfinder corner brackets.

   Two consequences that matter here:
     · there are no <path> elements to fill, so there are no cloth-tone rules —
       the pixels carry their own colour and any fill we declared on the raster
       would be ignored. What replaces them is nothing, deliberately.
     · every animated selector must target <image>, not path. `.bl-facet path`
       matched nothing after the previous swap: the cinch beat ran on zero
       elements while the boot suite stayed green, which is exactly the
       silent-stage failure the redistribution rule exists to prevent.

   WHAT CHANGED WITH THIS ARTWORK, and why the old keyframes could not simply
   be re-pointed onto it: the previous mark carried three RANK BLOCKS as
   separate layers and the sequence taped them on one at a time. This drawing
   has no rank blocks at all — so #8a4a10 / #7b28c8 / #0a66f8 are gone from
   shell.ts and from this stylesheet together, rather than left behind painting
   nothing. The four corner brackets are what this drawing genuinely has as
   separable, individually-positioned elements, so they carry the staggered
   beat the ranks used to.

   The 2048 square was cut into layers by luminance against the plate, so each
   layer is a real region of the owner's own pixels: no layer is redrawn. */
.boot-mark .bl-coil image,
.boot-mark .bl-facet image,
.boot-mark .bl-frames image { image-rendering: auto; }

/* THE LOCKUP SEQUENCE, rebuilt for the 2026-09-03 artwork.
 *
 * The old sequence animated the ACT OF TYING, and it could only do that
 * because the belt was five separately-positioned cloth layers. This drawing
 * is a single flat knot plus four brackets; there is no honest way to tie it,
 * and porting the tying keyframes onto it would animate a belt that never
 * moves. So the physical action changes with the drawing rather than being
 * faked: the brackets are a CAMERA ACQUIRING ITS SUBJECT — which is what the
 * product does — and the belt is the subject it locks onto.
 *
 *   1. WRAP   the clip rect sweeps across, revealing the belt along its own
 *             travel direction. Animating a clip reveals the real geometry
 *             instead of sliding a copy of it, so the belt never reads as a
 *             rigid object being moved into place.
 *   2. SETTLE the whole mark drops the last few pixels and takes its weight.
 *   3. CINCH  the knot pops as the wrap arrives — the moment it pulls tight.
 *   4. FRAME  the four brackets snap in from their own diagonals, one at a
 *             time: focus being acquired, corner by corner.
 *   5. WORD   the wordmark splits open from the centre.
 *   6. RAMP   the belt ramp draws through.
 *
 * Everything is transform / opacity / clip only, so it composites on the GPU
 * and will not judder on a mid-range Android while the app boots in parallel.
 * Total 0.92s (DS6, was 1.44s): the sequence is the same six beats at a
 * tighter tempo — a brand animation is a cost paid on every cold open, and
 * the second viewing is pure waiting. BOOT_MIN_MS in app.js is pinned to it
 * from both directions.
 */

/* 1 — WRAP. `width` on a clipPath rect is not a layout property — it cannot
   reflow anything, which is why animating it is safe here where animating
   width on a DOM element would not be. */
.boot-mark .bl-wipe { animation: beltWipe .46s cubic-bezier(.32,.86,.28,1) .02s both; }
@keyframes beltWipe {
  from { width: 0; }
  to   { width: 1584px; }
}

/* 2 — SETTLE, with an overshoot: cloth has weight, so the reveal ends with the
   mark settling rather than with a wipe simply finishing. */
.boot-mark { animation: beltSettle .40s cubic-bezier(.28,1.32,.44,1) .30s both; }
@keyframes beltSettle {
  0%   { transform: translateY(-2.2%) scale(.988); }
  64%  { transform: translateY(.5%) scale(1.004); }
  100% { transform: none; }
}

/* 3 — CINCH. The knot is the centre of the drawing; popping it while the wrap
   is still arriving is what makes the belt read as pulled tight rather than
   drawn on. */
.boot-mark .bl-facet image { animation: facetPop .22s cubic-bezier(.34,1.62,.5,1) .36s both; }
@keyframes facetPop {
  0%   { opacity: 0; transform: scale(.82); }
  100% { opacity: 1; transform: none; }
}
.boot-mark .bl-facet { transform-origin: 50% 50%; }

/* 4 — FRAME. Each bracket enters along its OWN diagonal, so the four together
   read as a frame closing on the belt rather than four copies of one move.
   transform-box: fill-box gives each <image> its own local origin — without it
   every bracket would rotate/scale about the SVG viewBox centre and the corners
   would swing across the mark. */
.boot-mark .bl-frame {
  transform-box: fill-box;
  transform-origin: 50% 50%;
}
.boot-mark .bl-frame-tl { animation: frameIn .20s cubic-bezier(.34,1.6,.64,1) .48s both; }
.boot-mark .bl-frame-tr { animation: frameIn .20s cubic-bezier(.34,1.6,.64,1) .52s both; }
.boot-mark .bl-frame-bl { animation: frameIn .20s cubic-bezier(.34,1.6,.64,1) .56s both; }
.boot-mark .bl-frame-br { animation: frameIn .20s cubic-bezier(.34,1.6,.64,1) .60s both; }
@keyframes frameIn {
  0%   { opacity: 0; transform: scale(.62); }
  100% { opacity: 1; transform: none; }
}

/* 6 — WORDMARK splits open from the centre. */
.boot-word {
  display: flex;
  gap: .42em;
  font-size: clamp(1.6rem, 6.4vw, 2.35rem);
  font-weight: 900;
  letter-spacing: -.035em;
  line-height: 1;
}
.boot-word-l { color: #f4f4f5; animation: bootSplitL .26s cubic-bezier(.16,.94,.2,1) .60s both; }
/* The literal is retinted to the lime, not switched to var(--accent-ink).
   theme-v2 already sets that variable on .boot-word-r, so this value is the
   PRE-THEME fallback — and the suite asserts the two mechanisms resolve to the
   same colour precisely so the splash cannot paint a different brand than the
   topbar for the one frame before v2 applies. Leaving it coral is what the
   suite caught: a coral wordmark under a lime mark. */
.boot-word-r { color: var(--txt-1); animation: bootSplitR .26s cubic-bezier(.16,.94,.2,1) .60s both; }
@keyframes bootSplitL { from { transform: translateX(.42em); opacity: 0; } to { transform: none; opacity: 1; } }
@keyframes bootSplitR { from { transform: translateX(-.42em); opacity: 0; } to { transform: none; opacity: 1; } }

/* 7 — BELT RAMP. The five ranks, drawn through left to right. */
.boot-belt {
  margin-top: 18px;
  width: min(58vw, 220px);
  height: 3px;
  border-radius: 2px;
  /* DS6: the ramp is the brand lime, not a five-belt gradient — the boot ends
     on the same object the app opens with (the one lime primary). */
  background: #CEFF56;
  transform-origin: left center;
  animation: bootBelt .26s cubic-bezier(.16,.94,.2,1) .66s both;
}
@keyframes bootBelt {
  from { transform: scaleX(0); opacity: .2; }
  to   { transform: scaleX(1); opacity: 1; }
}

/* The app rises as the splash pushes past — one continuous move. */
/* THE REVEAL MUST NOT TRANSFORM #app.
   This animation used to be `transform: translateY(10px) -> none` on #app, and
   that one property broke the whole bottom navigation. A transformed element
   becomes the CONTAINING BLOCK for its position:fixed descendants, so for the
   .56s this ran, the nav dock — "fixed to the bottom of the viewport" — was
   fixed to the bottom of a moving div instead. It flew up into the middle of
   the screen and vanished, on every single page load and route change.

   Rule for anything wrapping the app shell: animate OPACITY and nothing else.
   `filter`, `perspective`, `backdrop-filter`, `will-change: transform` and
   `contain: paint` create the same containing block and would reintroduce it.
   The lift is now applied to the page's own inner content instead (.page-rise),
   which has no fixed descendants, so the motion is kept without the bug. */
/* .34s rather than .30s, and it starts under a splash that is still on screen:
   the two now OVERLAP for most of the exit, which is what makes it one move
   instead of a fade-out followed by a fade-in. Any longer and the first frame
   the user reads is a half-transparent dashboard. */
#app.boot-reveal { animation: bootAppIn .26s cubic-bezier(.22,.9,.24,1) both; }
@keyframes bootAppIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* THE CONTENT LIFTS, THE SHELL DOES NOT. #app must never be transformed (a
   transformed element becomes the containing block for its position:fixed
   descendants, which is what flung the bottom nav into the middle of the
   screen on every route change). The lift therefore lands on the page's own
   inner content, which has no fixed descendants — same motion, no bug. */
#app.boot-reveal .page-rise { animation: bootRise .42s cubic-bezier(.22,.9,.24,1) .02s both; }
@keyframes bootRise {
  from { transform: translateY(8px); }
  to   { transform: none; }
}

/* Topbar lockup — same symbol, so it can never drift from the splash. */
/* Topbar lockup. Sizing only — the COLOURS come from presentation attributes
   on the symbol itself (shell.ts), because this mark is drawn through
   <use> and CSS cannot reach into its shadow tree. A `.logo-mark .bl-cloth`
   rule here would look correct and do nothing, which is exactly how the mark
   ended up black-on-black. */
/* The 2026-09-03 mark is SQUARE (a knot inside four viewfinder brackets), where
   the previous one was a wide coil. A height tuned for a 2.2:1 lockup renders
   this one visibly smaller than the wordmark beside it, so the height moves
   with the aspect ratio rather than being inherited from the old drawing. */
.logo-mark { height: 1.9rem; width: auto; display: block; overflow: visible; }


/* ══════════════════════════════════════════════════════════════════════════
   SOLO MASTERY — the character sheet
   ══════════════════════════════════════════════════════════════════════════
   Premium here means restraint plus one hero moment, not decoration everywhere:
   the tier number and the radar carry the page, and everything else stays
   quiet so they can. */

.sm-wrap { display: grid; gap: 1rem; }

/* ── hero ── */
.sm-hero { position: relative; overflow: hidden; padding: 1.4rem 1.25rem 1.5rem; }
.sm-hero-in { position: relative; z-index: 2; }

.sm-tier-row { display: flex; align-items: center; gap: 1rem; margin: .65rem 0 .9rem; }
.sm-tier-num {
  font-size: clamp(3.1rem, 13vw, 4.4rem);
  font-weight: 900;
  line-height: .84;
  letter-spacing: -.045em;
  background: linear-gradient(150deg, #FFE9E7 0%, #FF8A84 42%, #D42F28 100%);
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent;
  /* clip-to-text on a tight line-height slices descenders and, on digits,
     the baseline itself — pad and pull back. */
  padding-bottom: .06em; margin-bottom: -.06em;
}
.sm-tier-name { font-size: 1.22rem; font-weight: 800; letter-spacing: -.02em; }
.sm-tier-sub { font-size: .82rem; color: var(--txt-2); margin-top: .15rem; }

/* Tier rail: 15 pips. Filled ones are the brand ink so the ladder never
   borrows the belt ramp — rank belongs to belts, not to this page. */
.sm-rail { display: flex; gap: 3px; margin-bottom: .85rem; }
.sm-pip {
  flex: 1; height: 5px; border-radius: 3px;
  background: rgba(255,255,255,.09);
  transition: background .3s ease;
}
.sm-pip.on { background: linear-gradient(90deg, #FF6B63, #FF8A84); }

.sm-xp-bar {
  height: 8px; border-radius: 5px; overflow: hidden;
  background: rgba(255,255,255,.07);
}
.sm-xp-bar > span {
  display: block; height: 100%; border-radius: 5px;
  background: linear-gradient(90deg, #D42F28, #FF8A84);
  transform-origin: left center;
  animation: smFill .9s cubic-bezier(.22,.9,.24,1) both;
}
@keyframes smFill { from { transform: scaleX(0); } to { transform: scaleX(1); } }
.sm-xp-txt { margin-top: .45rem; font-size: .8rem; color: var(--txt-2); }
.sm-xp-txt em { color: #FF8A84; font-style: normal; font-weight: 700; }

.sm-note { margin-top: .9rem; font-size: .76rem; line-height: 1.5; color: var(--txt-3); }

/* ── stat strip ── */
.sm-stats {
  display: grid; gap: .5rem;
  grid-template-columns: repeat(3, minmax(0, 1fr));
}
@media (min-width: 640px) { .sm-stats { grid-template-columns: repeat(6, minmax(0, 1fr)); } }
.sm-stat {
  background: linear-gradient(157deg, rgba(255,255,255,.045), rgba(255,255,255,.015));
  border: 1px solid rgba(236,229,216,.08);
  border-radius: 16px;
  padding: .7rem .5rem;
  text-align: center;
}
.sm-stat i { color: #FF6B63; font-size: .78rem; }
.sm-stat-v { font-size: 1.12rem; font-weight: 800; margin-top: .2rem; letter-spacing: -.02em; }
.sm-stat-k { font-size: .62rem; text-transform: uppercase; letter-spacing: .07em; color: var(--txt-3); margin-top: .1rem; }

/* ── radar ── */
.sm-radar-panel { padding: 1.1rem 1rem 1.25rem; }
.sm-radar { overflow: visible; }
/* The polygon GROWS from its own centre with a spring overshoot. A fade would
   show a finished shape appearing; growing shows a shape BECOMING, which is the
   claim the page is making. */
.sm-poly {
  transform-origin: 120px 112px;
  animation: smGrow 1.05s cubic-bezier(.34,1.42,.5,1) .12s both;
}
@keyframes smGrow {
  0%   { transform: scale(.04); opacity: .25; }
  62%  { opacity: 1; }
  100% { transform: scale(1); opacity: 1; }
}
.sm-radar-note { margin-top: .85rem; font-size: .76rem; line-height: 1.55; color: var(--txt-3); }

/* ── read ── */
.sm-read-list { display: grid; gap: .5rem; margin-top: .25rem; }
.sm-read-list li {
  list-style: none; padding-left: 1rem; position: relative;
  font-size: .87rem; line-height: 1.55; color: var(--txt-2);
}
.sm-read-list li::before {
  content: ''; position: absolute; left: 0; top: .55em;
  width: 5px; height: 5px; border-radius: 50%; background: #FF6B63;
}
.sm-ai-body { margin-top: 1rem; display: grid; gap: .6rem; }
.sm-ai-body p { font-size: .88rem; line-height: 1.62; color: var(--txt-1); }
.sm-ai-off { color: var(--txt-2); }
.sm-ai-off p { color: var(--txt-2); }

/* ── milestones ── */
.sm-ms-group { margin-top: 1.1rem; }
.sm-ms-glabel {
  font-size: .64rem; text-transform: uppercase; letter-spacing: .1em;
  color: var(--txt-3); font-weight: 700; margin-bottom: .5rem;
}
.sm-ms-grid {
  display: grid; gap: .55rem;
  grid-template-columns: repeat(auto-fill, minmax(148px, 1fr));
}
.sm-ms {
  border-radius: 16px; padding: .75rem .8rem .8rem;
  transition: transform .16s var(--spring, cubic-bezier(.34,1.42,.5,1));
}
.sm-ms:active { transform: scale(.975); }

/* EARNED: solid, gold, unmistakable. */
.sm-ms.earned {
  background: linear-gradient(157deg, rgba(251,191,36,.13), rgba(251,191,36,.04));
  border: 1px solid rgba(251,191,36,.34);
}
.sm-ms.earned i { color: var(--warn-ink); }
.sm-ms-tag {
  font-size: .56rem; font-weight: 800; letter-spacing: .09em;
  color: var(--warn-ink); border: 1px solid color-mix(in srgb, var(--warn-ink) 40%, transparent);
  border-radius: 99px; padding: .12rem .4rem;
}

/* TARGET: dashed teal with REAL progress — never grey. A greyed-out locked card
   reads as failure; a dashed card at 84% reads as almost. */
.sm-ms.target {
  background: rgba(255,255,255,.02);
  border: 1px dashed var(--line-2);
}
.sm-ms.target i { color: #FF6B63; opacity: .85; }
.sm-ms-pct { font-size: .68rem; font-weight: 800; color: #FF8A84; }

.sm-ms-top { display: flex; align-items: center; justify-content: space-between; gap: .4rem; }
.sm-ms-name { font-size: .82rem; font-weight: 800; margin-top: .45rem; letter-spacing: -.01em; }
.sm-ms-blurb { font-size: .7rem; line-height: 1.45; color: var(--txt-3); margin-top: .2rem; }
.sm-ms-bar { height: 4px; border-radius: 3px; background: rgba(255,255,255,.07); margin-top: .55rem; overflow: hidden; }
.sm-ms-bar > span { display: block; height: 100%; border-radius: 3px; background: linear-gradient(90deg, #D42F28, #FF8A84); }
.sm-ms-have { font-size: .62rem; color: var(--txt-3); margin-top: .3rem; }

/* Same contract as every other block: reduced motion means end state, and
   for a sheet the end state must be fully open and opaque, not mid-slide. */

/* ====================================================================== *
 *              SUBSCRIPTION SURFACE — intro, paywall, plan cards
 *
 * ANIMATION RULE OBSERVED THROUGHOUT THIS BLOCK, and it is the one that has
 * already cost this project a whole bug round: nothing here animates transform
 * on an element that WRAPS the app shell. A transformed ancestor becomes the
 * containing block for its position:fixed descendants, which is what made the
 * bottom nav teleport to the middle of the screen during the boot reveal.
 * These overlays are leaves — they contain no fixed children of the app — so
 * transform is safe ON THEM, and only on them.
 * ====================================================================== */

/* ---------------------------- first-run intro -------------------------- */
.intro-wrap { position: fixed; inset: 0; z-index: 96; display: flex; align-items: center; justify-content: center; padding: 1rem; }
.intro-back { position: absolute; inset: 0; background: rgba(4,5,7,.78); animation: introFade .28s ease both; }
@keyframes introFade { from { opacity: 0 } to { opacity: 1 } }

.intro-card {
  position: relative; width: 100%; max-width: 380px;
  max-height: 88vh; overflow-y: auto;
  padding: 1.5rem 1.25rem 1.25rem;
  border-radius: 26px;
  background: linear-gradient(168deg, #12161d 0%, #0a0d12 62%);
  border: 1px solid rgba(255,255,255,.09);
  box-shadow: 0 30px 80px -20px rgba(0,0,0,.9), 0 0 0 1px rgba(255,107,99,.06);
  animation: introRise .42s cubic-bezier(.16,.94,.2,1) both;
}
@keyframes introRise {
  from { transform: translateY(26px) scale(.96); opacity: 0 }
  to   { transform: none; opacity: 1 }
}
.intro-wrap.leaving .intro-card { animation: introGo .22s ease-in both; }
.intro-wrap.leaving .intro-back { animation: introFade .22s ease reverse both; }
@keyframes introGo { to { transform: translateY(14px) scale(.97); opacity: 0 } }

.intro-hero { text-align: center; margin-bottom: 1.15rem; }
/* .intro-mark (the cropped belt mark over the welcome sheets) was removed
   2026-09-04 (Reqwa: not supposed to be there). Both sheets open on the title. */
.intro-title { font-size: 1.5rem; font-weight: 900; letter-spacing: -.03em; line-height: 1.05; }
.intro-sub { font-size: .78rem; color: var(--txt-3); margin-top: .38rem; line-height: 1.5; }

/* DS2-FIX 2026-09-06: the tinted free (emerald) and paid (coral) boxes, the
   coloured badges and the .intro-mini price chips are gone. The sheet now
   uses the landing pricing tiles' surface language; the rules live in
   theme-v2.css §22d (.intro-tier*). Only the list motion stays here. */
.intro-list { display: flex; flex-direction: column; gap: .48rem; }
.intro-list li {
  display: flex; align-items: flex-start; gap: .5rem;
  font-size: .795rem; line-height: 1.35; color: var(--txt-2);
  /* Staggered by index so the list assembles rather than appearing as a block.
     The stagger is what makes it feel considered; it costs nothing because
     each item animates opacity and a 6px translate only. */
  animation: introItem .34s cubic-bezier(.16,.94,.2,1) both;
  animation-delay: calc(.18s + var(--i) * .045s);
}
@keyframes introItem { from { opacity: 0; transform: translateX(-6px) } to { opacity: 1; transform: none } }
.intro-list li i { font-size: .66rem; margin-top: .28rem; flex-shrink: 0; }
.intro-free { animation: introItem .34s cubic-bezier(.16,.94,.2,1) .12s both; }
.intro-paid { margin-top: .8rem; animation: introItem .38s cubic-bezier(.16,.94,.2,1) .42s both; }

.intro-go { margin-top: 1rem; animation: introItem .38s cubic-bezier(.16,.94,.2,1) .52s both; }
.intro-later {
  width: 100%; margin-top: .5rem; padding: .5rem;
  font-size: .74rem; font-weight: 700; color: var(--txt-3);
  animation: introItem .38s cubic-bezier(.16,.94,.2,1) .58s both;
}
.intro-later:hover { color: var(--txt-2); }

/* ------------------------------- paywall ------------------------------- */
/* DS5 paywall v2 (Reqwa, 2026-09-07): same family as the landing pricing
   cards — .land-plan-name / -flag / -feats / -excl are reused verbatim so
   the sheet and the landing read as one system. Colours are tokens from
   theme-v2.css; nothing here names a hex. Phone: near-full-screen sheet with
   its own scroll region and a fixed footer; ≥640px: the bottom sheet stays
   30rem wide and sized by content. */
.pw-sheet { padding-bottom: 0; display: flex; flex-direction: column; }
.pw-sheet.pw-v2 { max-height: 92vh; max-height: 92dvh; }
.pw-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; -webkit-overflow-scrolling: touch; padding-bottom: .5rem; }
.pw-close {
  position: absolute; top: .55rem; left: .55rem; width: 44px; height: 44px; border-radius: 999px;
  display: grid; place-items: center; color: var(--txt-2); background: transparent; z-index: 2;
}
.pw-close:hover { background: var(--sunken); color: var(--txt-1); }
.pw-head { padding: .25rem 3.25rem .9rem; text-align: center; }
.pw-icon {
  width: 52px; height: 52px; margin: 0 auto .7rem; border-radius: 16px;
  display: grid; place-items: center; font-size: 1.15rem; color: var(--txt-1);
  background: var(--accent-weak); border: 1px solid var(--line);
  animation: pwPop .38s cubic-bezier(.34,1.5,.5,1) both;
}
@keyframes pwPop { from { transform: scale(.7); opacity: 0 } to { transform: none; opacity: 1 } }
.pw-title { font-size: 1.22rem; font-weight: 900; letter-spacing: -.025em; color: var(--txt-1); }
.pw-line { font-size: .82rem; color: var(--txt-3); line-height: 1.5; margin-top: .38rem; text-wrap: pretty; }

/* Tier switcher — one .seg like the rest of the app, 44px cells. */
.pw-seg { margin: 0 1.25rem .8rem; display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; }
.pw-seg-btn { min-height: 44px; font-size: .86rem; font-weight: 800; }
.pw-seg-btn i { font-size: .68rem; opacity: .7; }

/* The card — landing anatomy, compact. */
.pw-card-slot { padding: 0 1.25rem; }
.pw-card {
  position: relative; background: var(--card); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 1.25rem 1.15rem 1.15rem;
}
.pw-card.is-hi { background: var(--raised); box-shadow: var(--lift-2); }
.pw-card.is-cant { opacity: .92; }
.pw-card .land-plan-flag { top: -.7rem; left: 1.15rem; }
.pw-card-top { display: flex; align-items: flex-start; justify-content: space-between; gap: .8rem; }
.pw-card-tag { margin-top: .3rem; font-size: .84rem; line-height: 1.45; color: var(--txt-3); text-wrap: pretty; }
.pw-cant-line { margin: .8rem 0 0; font-size: .8rem; font-weight: 700; color: var(--txt-3); display: flex; gap: .45rem; align-items: center; }

/* Billing tiles — radio look, price big, term small, saving as a pill. */
.pw-bill { display: grid; grid-template-columns: 1fr 1fr; gap: .55rem; margin-top: 1rem; }
.pw-bill-tile {
  display: flex; flex-direction: column; align-items: flex-start; gap: .3rem; min-height: 64px;
  padding: .7rem .8rem; text-align: left; border-radius: var(--radius-sm);
  background: var(--sunken); border: 1.5px solid var(--line); color: var(--txt-2);
  transition: border-color .15s ease, background .15s ease;
}
.pw-bill-tile.on { border-color: var(--txt-1); background: var(--card); color: var(--txt-1); }
.pw-bill-k { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; font-size: .72rem; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; color: var(--txt-3); }
.pw-bill-tile.on .pw-bill-k { color: var(--txt-2); }
.pw-bill-v { display: inline-flex; align-items: baseline; gap: .15rem; }
.pw-bill-v b { font-size: 1.35rem; font-weight: 900; letter-spacing: -.03em; line-height: 1; font-variant-numeric: tabular-nums; }
.pw-bill-v small { font-size: .72rem; font-weight: 700; color: var(--txt-3); }
.pw-save { font-style: normal; font-size: .6rem; font-weight: 800; letter-spacing: .04em; padding: .15rem .45rem; border-radius: 999px; background: var(--accent-weak); color: var(--txt-1); }
html[data-theme="light"] .pw-save { color: var(--txt-1); }

.pw-cta { margin-top: .9rem; width: 100%; min-height: 50px; font-size: .98rem; }
.pw-cta[disabled] { opacity: .55; cursor: not-allowed; }

.pw-feats { margin-top: 1.1rem; padding-top: 1rem; }
.pw-feats li span { min-width: 0; }
.pw-feats li.is-cap { color: var(--txt-1); font-weight: 650; }
.pw-tick {
  display: inline-block; vertical-align: middle; margin-left: .35rem;
  font-style: normal; font-size: .58rem; font-weight: 900; letter-spacing: .06em; text-transform: uppercase;
  padding: .18em .5em; border-radius: 999px; background: var(--accent-fill); color: var(--accent-on);
}
.pw-excl { margin-top: .75rem; }

/* Footer stays put under the scroll region. */
.pw-foot { flex: none; padding: .4rem 1.25rem calc(.5rem + env(safe-area-inset-bottom, 0)); border-top: 1px solid var(--line); background: inherit; display: grid; gap: 0; }
.pw-foot-row { display: grid; grid-template-columns: 1fr 1fr; gap: .5rem; }
.pw-all, .pw-not { min-height: 48px; padding: .5rem; font-size: .84rem; font-weight: 750; color: var(--txt-2); border-radius: var(--radius-sm); }
.pw-all i { font-size: .7rem; margin-left: .25rem; }
.pw-all:hover, .pw-not:hover { background: var(--sunken); }
.pw-all { color: var(--txt-1); }
.pw-not { color: var(--txt-3); }
.pw-legal { display: flex; justify-content: center; align-items: center; gap: .25rem; font-size: .72rem; color: var(--txt-3); margin-top: -.15rem; }
.pw-legal a { display: inline-flex; align-items: center; min-height: 44px; padding: 0 .6rem; color: var(--txt-3); text-decoration: underline; text-underline-offset: 2px; }
.pw-next { padding: .25rem 1.25rem .75rem; }
.pw-sheet.is-message-only .pw-foot-row { grid-template-columns: 1fr; }

@media (min-width: 640px) {
  .pw-sheet.pw-v2 { max-height: min(92vh, 56rem); }
}
@media (prefers-reduced-motion: reduce) { .pw-icon { animation: none; } }

/* ------------------------------ plan cards ----------------------------- */
.plan-card {
  position: relative; padding: 1.1rem 1.15rem; border-radius: 22px;
  background: rgba(255,255,255,.028); border: 1px solid rgba(255,255,255,.075);
}
.plan-card.featured { background: rgba(198,43,36,.09); border-color: rgba(255,107,99,.4); }
.plan-card.current { border-color: rgba(16,185,129,.45); }
.plan-flag {
  position: absolute; top: -9px; left: 1.15rem;
  font-size: .55rem; font-weight: 900; letter-spacing: .08em; text-transform: uppercase;
  padding: .2em .55em; border-radius: 6px; background: #C62B24; color: #fff;
}
.plan-head { display: flex; align-items: baseline; justify-content: space-between; gap: .6rem; flex-wrap: wrap; }
.plan-name { font-size: 1.05rem; font-weight: 900; letter-spacing: -.02em; }
.pp-amt { font-size: 1.45rem; font-weight: 900; letter-spacing: -.03em; }
.pp-per { font-size: .66rem; font-weight: 700; color: var(--txt-3); margin-left: .1rem; }
.plan-save { font-size: .6rem; font-weight: 800; color: #34d399; width: 100%; text-align: right; }
.plan-tag { font-size: .73rem; color: var(--txt-3); line-height: 1.45; margin-top: .35rem; }
.plan-feats, .plan-excl { display: flex; flex-direction: column; gap: .38rem; margin-top: .8rem; }
.plan-feats li, .plan-excl li { display: flex; align-items: flex-start; gap: .5rem; font-size: .765rem; line-height: 1.35; }
.plan-feats li { color: var(--txt-2); }
.plan-feats li i { color: #FF6B63; font-size: .62rem; margin-top: .28rem; flex-shrink: 0; }
.plan-excl { margin-top: .5rem; padding-top: .5rem; border-top: 1px solid rgba(255,255,255,.06); }
.plan-excl li { color: var(--txt-3); }
.plan-excl li i { color: var(--txt-3); font-size: .58rem; margin-top: .3rem; flex-shrink: 0; }
.plan-cta {
  margin-top: .85rem; padding: .55rem; border-radius: 12px; text-align: center;
  font-size: .74rem; font-weight: 800;
}
.plan-cta.is-current { background: rgba(16,185,129,.12); color: #34d399; }
.plan-cta.is-free { background: rgba(255,255,255,.04); color: var(--txt-3); }

/* --------------------------- locked feature card ----------------------- */
.lock-card {
  padding: 1.35rem 1.2rem; border-radius: 22px;
  background: linear-gradient(165deg, rgba(198,43,36,.1), rgba(255,255,255,.02));
  border: 1px solid rgba(255,107,99,.24);
}
.lock-badge {
  display: inline-flex; align-items: center; gap: .35rem;
  font-size: .55rem; font-weight: 900; letter-spacing: .09em; text-transform: uppercase;
  color: #FF6B63; margin-bottom: .55rem;
}
.lock-title { font-size: 1.3rem; font-weight: 900; letter-spacing: -.028em; line-height: 1.1; }
.lock-line { font-size: .79rem; color: var(--txt-2); line-height: 1.5; margin-top: .45rem; }
.lock-demo {
  margin-top: 1rem; padding: .85rem .9rem; border-radius: 15px;
  background: rgba(4,6,10,.55); border: 1px solid rgba(255,255,255,.06);
}
.lock-demo-head {
  font-size: .55rem; font-weight: 900; letter-spacing: .08em; text-transform: uppercase;
  color: var(--txt-3); margin-bottom: .55rem;
}
.lock-row { display: flex; gap: .6rem; font-size: .73rem; line-height: 1.45; color: var(--txt-2); padding: .3rem 0; }
.lock-row + .lock-row { border-top: 1px solid rgba(255,255,255,.045); }
.lock-ts { font-variant-numeric: tabular-nums; font-weight: 800; color: #FF6B63; flex-shrink: 0; }
.lock-alt { width: 100%; margin-top: .55rem; padding: .5rem; font-size: .73rem; font-weight: 700; color: var(--txt-3); }
.lock-alt:hover { color: var(--txt-2); }

@media (prefers-reduced-motion: reduce) {
  .sheet, .sheet-back { animation: none !important; }
  /* The subscription overlays: every animated selector needs an explicit
     reset, because `animation: none` on an element whose keyframe START state
     is opacity:0 leaves it permanently invisible rather than simply static. */
  .intro-card, .intro-back, .intro-list li, .intro-free, .intro-paid, .intro-go, .intro-later,
  .pw-icon { animation: none !important; opacity: 1 !important; transform: none !important; }
  .pw-plan:hover { transform: none; }
  .sheet { transform: none; opacity: 1; }
  .rnd-pip { transition: none; }

  /* Reduced motion means the END STATE, never a mid-animation freeze: the belt
     is shown already tied. Every animated boot selector needs an explicit
     reset, or `both` fill leaves it stuck on its 0% frame — an invisible logo. */
  .boot-splash, .boot-splash * { animation: none !important; transition: none !important; }
  .boot-mark, .boot-mark .bl-wipe, .boot-mark .bl-facet image,
  .boot-mark .bl-frame-tl, .boot-mark .bl-frame-tr,
  .boot-mark .bl-frame-bl, .boot-mark .bl-frame-br,
  .boot-word-l, .boot-word-r { transform: none !important; opacity: 1 !important; }
  .boot-glow { opacity: .55 !important; transform: none !important; }
  /* The wipe rect keeps its ATTRIBUTE width (1584 — the artwork's own square),
     so cancelling the animation shows the mark fully revealed rather than
     clipped to nothing. */
  .boot-mark .bl-wipe { width: 1584px !important; }
  .boot-belt { transform: none !important; opacity: 1 !important; }
  #app.boot-reveal { animation: none !important; opacity: 1; transform: none; }

  /* Solo Mastery: end state means the polygon is already at full size and the
     bars are already filled — not frozen at scale(.04), which would render an
     invisible radar. */
  .sm-poly { animation: none !important; transform: none !important; opacity: 1 !important; }
  .sm-xp-bar > span { animation: none !important; transform: none !important; }
  .sm-ms { transition: none !important; }
  /* DS5 (2026-09-07): belt-progress rows + onboarding scan strip — folded here so this block stays the LAST reduced-motion block. */
  .bp-row-c { transition: none; }
  .scan-frame canvas, .scan-frame span, .scan-chip { transition: none; transform: none; }
}

/* ── Mat roll call (brief #5) ─────────────────────────────────────────────
   A list built for one repeated gesture, tapped standing up on a mat. Every
   size here is driven by that: 48px targets, high-contrast state, no hover
   affordances (there is no cursor on a phone at the front of a class). */
.rollcall-cta {
  width: 100%; display: flex; align-items: center; gap: .85rem;
  padding: .95rem 1rem; border-radius: 22px; text-align: left;
  background: linear-gradient(150deg, rgba(255,107,99,.10), rgba(255,107,99,.03));
  border: 1px solid rgba(255,107,99,.22);
  transition: transform .14s ease, border-color .2s ease;
}
.rollcall-cta:active { transform: scale(.99); }
.rc-ico {
  width: 42px; height: 42px; border-radius: 14px; flex: 0 0 auto;
  display: grid; place-items: center; color: #FF8A84;
  background: rgba(255,107,99,.14); font-size: 1.05rem;
}

/* Attendance meter. A bar, not a percentage: the coach reads "most of them" at
   a glance and never needs the exact number mid-class. */
.rc-meter {
  height: 5px; border-radius: 99px; overflow: hidden;
  background: rgba(255,255,255,.07);
}
.rc-meter > span {
  display: block; height: 100%; border-radius: 99px;
  background: linear-gradient(90deg, #C62B24, #FF6B63);
  transition: width .35s cubic-bezier(.22,.9,.24,1);
}

.rc-row {
  display: flex; align-items: center; gap: .7rem;
  padding: .55rem .7rem; border-radius: 16px;
  background: rgba(255,255,255,.028);
  border: 1px solid rgba(255,255,255,.055);
  transition: background .2s ease, border-color .2s ease, opacity .2s ease;
}
/* Marked rows RECEDE rather than disappear. A coach who mis-taps has to be
   able to see and undo it; removing the row would hide the mistake. */
.rc-row.is-here {
  background: rgba(255,107,99,.07);
  border-color: rgba(255,107,99,.20);
  opacity: .62;
}
.rc-tick {
  width: 48px; height: 48px; flex: 0 0 auto; border-radius: 16px;
  display: grid; place-items: center;
  color: var(--txt-3); font-size: 1rem;
  background: rgba(255,255,255,.05);
  border: 1px solid rgba(255,255,255,.08);
  transition: transform .14s ease, background .2s ease, color .2s ease;
}
.rc-tick:active { transform: scale(.9); }
.rc-tick.on {
  color: #06251f; background: linear-gradient(165deg, #34d399, #10b981);
  border-color: transparent;
}
/* Analysis timestamp trimmer. Both range inputs share the same track; the
   server remains authoritative for the resulting Stream clip duration.

   DESIGNER, RETOKENISED ON MERGE 2026-09-03. This block arrived using
   `--brand-fill` / `--brand-ink`. Those tokens DO NOT EXIST — they are
   `--accent-fill` / `--accent-ink` (theme-v2.css:63). An undefined custom
   property makes the whole declaration invalid at computed-value time, so
   every range thumb fell back to the UA default and the selection window
   painted with no background at all: the two handles a user drags were
   invisible against the filmstrip. Same shape as the boot-splash
   transparency bug found this morning, one layer down — a declaration that
   reads correctly and computes to nothing.

   Sizes also raised to trimmer-visual-spec.md §3: the thumb was 18px on a
   28px track. These are dragged with a thumb on a phone, and 44px+ is the
   floor `.belt-pick` already sets in this app — so the visible grip is 12px
   on a full-height 56px track, `--txt-1` at rest and `--accent-fill` while
   dragging. The window tint is `--accent-weak`, the app's existing
   selected-row tint, rather than a color-mix of a token that was not there. */
.ra-filmstrip{display:grid;grid-template-columns:repeat(6,1fr);gap:1px;height:44px;overflow:hidden;border-radius:var(--radius-sm);background:var(--sunken)}
.ra-filmstrip img{width:100%;height:44px;object-fit:cover;filter:saturate(.7)}
.ra-filmstrip.is-placeholder{background:repeating-linear-gradient(90deg,var(--sunken) 0 16%,var(--raised) 16% 16.5%)}
.ra-range{height:56px;position:relative;background:var(--sunken);border:1px solid var(--line);border-radius:var(--radius-sm)}
.ra-range input{position:absolute;inset:0;width:100%;margin:0;background:transparent;pointer-events:none;appearance:none}
.ra-range input::-webkit-slider-thumb{appearance:none;width:12px;height:56px;border:0;border-radius:var(--radius-sm);background:var(--txt-1);pointer-events:auto;cursor:ew-resize}
.ra-range input:active::-webkit-slider-thumb{background:#FFFFFF}
.ra-range input::-moz-range-thumb{width:12px;height:56px;border:0;border-radius:var(--radius-sm);background:var(--txt-1);pointer-events:auto;cursor:ew-resize}
.ra-range input:active::-moz-range-thumb{background:#FFFFFF}
.ra-window{position:absolute;top:3px;bottom:3px;background:var(--accent-weak);border-block:1px solid var(--accent-ink);pointer-events:none}
/* Analysis result states, polled from /roll-analysis/:id. Colour only on the
   border so the card reads as a status, never as a decoration. */
.ra-status{border-left:3px solid var(--line-2)}
.ra-status.is-running{border-left-color:var(--txt-3)}
.ra-status.is-done{border-left-color:var(--accent-ink)}
.ra-status.is-failed{border-left-color:#f43f5e}
.ra-summary{white-space:pre-wrap;line-height:1.55}

/* ── Coach page modes (Wave 1 item 5, Engineer). Two PEER pills; the locked
   one stays visible with a lock mark — that is the upgrade read. Token-only;
   Designer polishes after. ── */
.mode-switch{display:grid;grid-template-columns:1fr 1fr;gap:6px;padding:4px;margin-bottom:.9rem;border-radius:var(--radius-pill);background:var(--sunken);border:1px solid var(--line)}
.mode-pill{display:flex;align-items:center;justify-content:center;gap:.5rem;min-height:44px;padding:.55rem .9rem;border-radius:var(--radius-pill);border:1px solid transparent;background:transparent;color:var(--txt-2);font-weight:700;font-size:14px;letter-spacing:-.01em;transition:background .16s,color .16s,transform .12s var(--ease)}
.mode-pill:active{transform:scale(.975)}
.mode-pill.is-active{background:var(--k-seg-on);color:#FFFFFF}
.mode-pill.is-locked{color:var(--txt-3)}
.mode-pill.is-locked.is-active{background:var(--raised);color:var(--txt-1)}
.mode-lock{font-size:11px;opacity:.85}
.comp-choices{display:grid;gap:8px}
.comp-choice{display:flex;flex-direction:column;align-items:flex-start;gap:2px;text-align:left;padding:.8rem .95rem;border-radius:var(--radius-sm);border:1px solid var(--line);background:var(--sunken);color:var(--txt-1);min-height:52px;transition:border-color .16s,background .16s}
.comp-choice.is-on{border-color:var(--txt-1);background:var(--k-fill)}
.comp-choice-l{font-weight:700;font-size:14px}
.comp-choice-s{font-size:12px;color:var(--txt-3)}
.comp-clips{display:grid;gap:6px}
.comp-clip{display:flex;align-items:center;gap:.75rem;padding:.7rem .9rem;border-radius:var(--radius-sm);border:1px solid var(--line);background:var(--sunken);cursor:pointer;min-height:48px}
.comp-clip.is-on{border-color:var(--txt-1);background:var(--k-fill)}
.comp-clip-box{appearance:none;-webkit-appearance:none;width:18px;height:18px;border-radius:5px;border:1.5px solid var(--txt-3);flex:0 0 auto;display:grid;place-content:center}
.comp-clip-box:checked{background:var(--accent-fill);border-color:transparent}
.comp-clip-box:checked::after{content:"";width:9px;height:5px;border-left:2.5px solid var(--accent-on);border-bottom:2.5px solid var(--accent-on);transform:rotate(-45deg) translate(1px,-1px)}
.comp-clip-t{flex:1 1 auto;min-width:0;font-weight:600;font-size:14px;color:var(--txt-1);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.comp-clip-s{font-size:12px;color:var(--txt-3)}
.comp-dossier{display:flex;align-items:center;gap:.75rem;width:100%;text-align:left;padding:.7rem .25rem;min-height:44px}
.comp-new,.comp-session{min-width:0}

/* ------------------------- Progress · the story (DS5) ------------------------
   Reqwa via Tech Lead 3787012/3787115: Progress opens on the athlete's story
   — last Roll IQ + trend on a 20-block bar, streak / this-week / strongest /
   leaking as stat cells — then Game tools, then the three doors; Belt Progress
   is a single collapsed row at the very bottom with a small belt glyph. Tokens
   only; lime is INK in dark and a FILL behind dark ink in light (D9-3). ---- */
.story-hero { padding: 1.05rem 1.15rem 1.1rem; margin-bottom: 1rem; }
.story-iq { display: block; width: 100%; text-align: left; min-height: 44px; }
.story-iq-k { display: flex; align-items: baseline; justify-content: space-between; gap: .6rem; }
.story-iq-k .section-label { margin: 0; }
.story-iq-when { font-size: .74rem; color: var(--txt-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
.story-iq-row { display: flex; align-items: flex-end; gap: .75rem; margin-top: .25rem; }
.story-iq-num { font-size: 3.4rem; line-height: .92; font-weight: 900; letter-spacing: -.045em; color: var(--txt-1); }
html[data-theme="light"] .story-iq-num { color: var(--txt-1); }
.story-iq-trend { display: inline-flex; align-items: center; gap: .35rem; padding-bottom: .3rem; font-size: .95rem; font-weight: 800; color: var(--txt-2); }
.story-iq-trend small { font-size: .74rem; font-weight: 600; color: var(--txt-3); }
.story-iq-trend.is-flat { color: var(--txt-3); }
.story-iq-trend.is-up { color: var(--txt-1); }
.story-iq-trend.is-down { color: var(--danger-ink); }
html[data-theme="light"] .story-iq-trend.is-up { color: var(--txt-1); }
.story-iq-c { margin-left: auto; padding-bottom: .45rem; font-size: .8rem; color: var(--txt-3); }
.story-iq-empty { font-size: .95rem; font-weight: 700; color: var(--txt-1); padding: .35rem 0; }
.story-iq-bar { display: grid; grid-template-columns: repeat(20, minmax(0, 1fr)); gap: 3px; margin-top: .75rem; height: 10px; }
.story-iq-bar i { display: block; height: 100%; border-radius: 2px; background: var(--sunken); border: 1px solid var(--line); }
.story-iq-bar i.on { background: var(--accent-fill); border-color: transparent; }
html[data-theme="light"] .story-iq-bar i.on { background: var(--txt-1); border-color: var(--txt-1); }
.story-cells { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .5rem; margin-top: 1rem; }
.story-cell { min-height: 68px; }
.story-cell .stat-cell-v { font-size: 1.25rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.story-cell.is-strong .stat-cell-v { color: var(--txt-1); }
.story-cell.is-weak .stat-cell-v { color: var(--danger-ink); }
html[data-theme="light"] .story-cell.is-strong .stat-cell-v, html[data-theme="light"] .story-cell.is-weak .stat-cell-v { color: var(--txt-1); }
@media (min-width: 640px) { .story-cells { grid-template-columns: repeat(4, minmax(0, 1fr)); } }

/* Belt Progress as ONE row: glyph · title + summary · caret. The glyph is the
   belt bar the app already uses (.belt-{colour}) with four stripe ticks. */
.bp-card.is-row { padding: 0; overflow: hidden; }
.bp-row { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: .85rem; width: 100%; min-height: 60px; padding: .85rem 1.15rem; text-align: left; }
.bp-glyph { position: relative; display: flex; align-items: center; justify-content: flex-end; gap: 3px; width: 44px; height: 12px; border-radius: 3px; padding: 0 5px; box-sizing: border-box; }
.bp-glyph i { display: block; width: 3px; height: 8px; border-radius: 1px; background: rgba(255,255,255,.28); }
.bp-glyph i.on { background: var(--ink-on-dark); }
.bp-glyph.belt-white i { background: rgba(20,21,26,.2); }
.bp-glyph.belt-white i.on { background: var(--accent-on); }
.bp-row-t { min-width: 0; display: grid; gap: .15rem; }
.bp-row-t .section-label { margin: 0; }
.bp-row-s { font-size: .86rem; line-height: 1.4; color: var(--txt-2); text-wrap: pretty; }
.bp-row-s b { font-weight: 800; color: var(--txt-1); }
.bp-row-c { font-size: .8rem; color: var(--txt-3); transition: transform .2s var(--ease); }
.bp-row-c.is-open { transform: rotate(180deg); }
.bp-more { padding: 0 1.15rem 1.05rem; border-top: 1px solid var(--line); }
.bp-more .bp-frame { margin-top: .9rem; }

/* Allowance strip in its compact Me form: tighter rows, no bar. */
.allow-strip.is-compact { padding: 1rem 1.15rem; margin-top: 1rem; }
.allow-strip.is-compact .allow-row { grid-template-areas: "l v" "r r"; min-height: 40px; padding: .45rem 0; }
.allow-strip.is-compact .allow-bar { display: none; }
.allow-strip.is-compact .allow-l, .allow-strip.is-compact .allow-v { font-size: .86rem; }
/* reduced-motion rule for .bp-row-c lives in the final reduced-motion block (test:boot contract: that block must stay last). */
/* Inside the expanded row the five state pills read as five verdicts; the
   state is a quiet dot + short word (.bp-sig-w — its own class, so theme-v2's
   pill rules for .bp-sig-s no longer apply) and the figures carry the row. */
.bp-card.is-row .bp-sig { grid-template-columns: 1fr auto; grid-template-areas: "l v" "s s"; gap: .15rem .6rem; }
.bp-card.is-row .bp-sig-l { grid-area: l; } .bp-card.is-row .bp-sig-v { grid-area: v; color: var(--txt-1); }
.bp-sig-w { grid-area: s; font-size: .74rem; font-weight: 650; color: var(--txt-3); }
.bp-sig-w::before { content: ""; display: inline-block; width: 6px; height: 6px; border-radius: 50%; margin-right: .4rem; background: var(--txt-3); vertical-align: middle; }
.bp-sig.is-met .bp-sig-w::before { background: var(--accent-fill); }
html[data-theme="light"] .bp-sig.is-met .bp-sig-w::before { background: var(--txt-1); }

/* --------------------- onboarding · preparing your first roll (DS5) ---------
   Reqwa via Tech Lead 3787115: the pass shows real work — facts read off the
   file, five frames pulled off the <video> into a contact strip that fills one
   by one, the athlete's own answers as chips. Tokens only. --------------- */
.scan-facts { position: absolute; left: 12px; bottom: 12px; display: flex; gap: .4rem; }
.scan-facts span { display: inline-block; padding: .2rem .5rem; border-radius: 999px; font-size: .68rem; font-weight: 750; color: var(--ink-on-dark); background: rgba(0,0,0,.55); border: 1px solid rgba(255,255,255,.18); }
.scan-strip { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: .4rem; margin-top: .7rem; }
.scan-frame { position: relative; aspect-ratio: 4 / 3; border-radius: 8px; overflow: hidden; background: var(--sunken); border: 1px solid var(--line); }
.scan-frame canvas { display: block; width: 100%; height: 100%; object-fit: cover; opacity: 0; transform: scale(1.04); transition: opacity .45s var(--ease), transform .45s var(--ease); }
.scan-frame.is-in canvas { opacity: 1; transform: none; }
.scan-frame.is-in { border-color: var(--line-2); }
.scan-frame span { position: absolute; left: 0; right: 0; bottom: 0; padding: .1rem 0; text-align: center; font-size: .58rem; font-weight: 750; color: var(--ink-on-dark); background: rgba(0,0,0,.5); opacity: 0; transition: opacity .3s var(--ease); }
.scan-frame.is-in span { opacity: 1; }
.scan-frame.is-miss::after { content: "—"; position: absolute; inset: 0; display: grid; place-items: center; font-size: .8rem; color: var(--txt-3); }
.scan-chips { display: flex; flex-wrap: wrap; gap: .4rem; margin-top: .7rem; }
.scan-chip { display: inline-flex; align-items: center; gap: .4rem; min-height: 30px; padding: 0 .65rem; border-radius: 999px; font-size: .76rem; font-weight: 700; color: var(--txt-1); background: var(--raised); border: 1px solid var(--line); opacity: 0; transform: translateY(4px); transition: opacity .3s var(--ease), transform .3s var(--ease); }
.scan-chip small { font-size: .75rem; font-weight: 600; letter-spacing: 0; text-transform: none; color: var(--txt-3); }
.scan-chip.is-in { opacity: 1; transform: none; }
.scan-chip.is-more { color: var(--txt-3); background: none; }
.ob .scan .scan-stage { max-height: 26dvh; }
.scan .scan-meta { margin-top: .85rem; }
.scan .scan-steps { margin-top: .9rem; gap: .35rem; }
.scan .scan-step { font-size: .8rem; }
.scan-step.is-skipped { text-decoration: line-through; text-decoration-color: var(--txt-3); }
.scan.is-reduced .scan-frame canvas, .scan.is-reduced .scan-frame span, .scan.is-reduced .scan-chip { transition: none; transform: none; }
/* reduced-motion rule for .scan-frame / .scan-chip lives in the final reduced-motion block (test:boot contract). */

/* ---------------------- Roll IQ score card, visual (DS5) --------------------
   Reqwa via Tech Lead 3787012: not a bare number — a 20-block bar filled to
   the score under the figure, and the six axes as label / value / thin bar
   pairs in a two-column grid. Lime is ink in dark, fill in light (D9-3). -- */
.rl-sc-seg { display: grid; grid-template-columns: repeat(20, minmax(0, 1fr)); gap: 3px; margin-top: .8rem; height: 12px; }
.rl-sc-seg i { display: block; height: 100%; border-radius: 2px; background: var(--sunken); border: 1px solid var(--line); }
.rl-sc-seg i.on { background: var(--accent-fill); border-color: transparent; }
html[data-theme="light"] .rl-sc-seg i.on { background: var(--txt-1); border-color: var(--txt-1); }
.rl-sc-axes.is-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .7rem .9rem; }
.rl-sc-axes.is-grid .rl-sc-axes-h { grid-column: 1 / -1; }
.rl-sc-axes.is-grid .rl-sc-axis { grid-template-columns: 1fr auto; grid-template-areas: "l v" "bar bar"; gap: .25rem .5rem; min-height: 44px; align-content: center; }
.rl-sc-axes.is-grid .rl-sc-axis-l { grid-area: l; font-size: .66rem; }
.rl-sc-axes.is-grid .rl-sc-axis-v { grid-area: v; font-size: 1.05rem; font-weight: 850; color: var(--txt-1); }
.rl-sc-axes.is-grid .rl-sc-bar { grid-area: bar; height: 6px; }
.rl-sc-axes.is-grid .rl-sc-axis.is-top .rl-sc-axis-v { color: var(--txt-1); }
html[data-theme="light"] .rl-sc-axes.is-grid .rl-sc-axis.is-top .rl-sc-axis-v { color: var(--txt-1); }
/* The map is the second chapter of Progress: a hairline and a label mark the
   turn, and the ruleset switcher takes the full width so "No-Gi" never
   truncates at 390 px. */
.gm-head { margin-top: 1.5rem; padding-top: 1.5rem; border-top: 1px solid var(--line); }
.gm-head .section-label { margin-bottom: .2rem; }
.gm-head > .flex { flex: 1 1 100%; }
.gm-head .gm-seg { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); width: 100%; }
@media (min-width: 640px) { .gm-head > .flex { flex: 0 0 auto; } .gm-head .gm-seg { display: inline-flex; width: auto; } }
/* Roll Card preview is the square now (DS5: the square is the primary share). */
.rc-preview { aspect-ratio: 1 / 1; height: auto; width: min(100%, 46dvh); }
