/* ---------------------------------------------------------------- the type
 *
 * ONE FAMILY, ONE FILE, AND IT IS A NORMAL WIDTH. That last part is the
 * whole lesson.
 *
 * This shipped as Barlow Condensed for anything "scanned" and Barlow for
 * anything read, with Syne on the landing page. It was reported back as
 * stretched and hard to read, and measuring says exactly why — the advance
 * width of one sentence at 700/40px, against the system UI face at 100%:
 *
 *     Syne                173%
 *     Barlow Condensed     79%
 *     Figtree             100%
 *
 * Two DISTORTED widths, in opposite directions, differing from each other by
 * more than a factor of two. Condensed type is for fitting a label in a
 * column; it is not a house style, and a whole interface set in it reads as
 * squeezed. A very wide display face is the same mistake mirrored.
 *
 * Figtree is a variable font, so 300 to 900 is ONE 20KB file — less than the
 * six static faces it replaces, with more weights available. Self-hosted for
 * the same reason as before: gym wifi, no asset pipeline, and OFL, with the
 * licence beside it.
 */
@font-face {
  font-family: "Figtree";
  font-style: normal;
  font-weight: 300 900;
  font-display: swap;
  src: url("/static/fonts/figtree.woff2") format("woff2");
}

:root {
  /* ------------------------------------------------------------ surfaces
   *
   * THE SAME NEUTRAL AS THE LANDING PAGE, and a ladder that is actually a
   * ladder. Every value below was solved for a target contrast ratio against
   * the page floor and then checked on the rendered page, rather than picked
   * by nudging hex digits.
   *
   * WHAT WAS WRONG: a card was #171c22 on a page painting rgb(15,19,25) — a
   * ratio of 1.09, which is nothing. Every surface in the app sat within
   * about ten values of every other, so a card was not an object, it was a
   * 1px border around the same colour as the floor. A screen of them reads as
   * a wall of outlines, and no amount of type or spacing fixes that.
   *
   * A card is now 1.44 against the floor. That is deliberately not higher:
   * pushing it to 1.75 was tried and it drags --muted, --accent and --bad
   * under 4.5:1 for text sitting ON the card. The rest of the separation
   * comes from a shadow, which is what actually says "this is on top" — a
   * lighter rectangle with no shadow reads as a HOLE, not as a card.
   *
   * Re-measure if you change one. The full set is checked against both the
   * card and the floor, and every token clears 4.5 on both.
   */
  --bg: #0c0c0e;
  --wash-hi: #202023;
  --wash-mid: #141417;
  --surface: #27272a;
  --surface-2: #353538;
  --card-hi: #2e2e31;
  --card-lo: #212124;
  --line: #424245;
  --line-2: #57575a;
  --thumb-hi: #3a3a3f;
  --thumb-lo: #191a1d;

  --text: #edeef1;
  --text-2: #c6c7d1;
  /* Brightened twice, and the second time for the right reason. #8b97a6
     measured 4.1 on the new card; #95969e fixed the card and still failed at
     4.15 on --surface-2, which is the LIGHTEST surface muted text ever lands
     on — folder chips, count badges, inset hints. Solved against that one
     instead, so it clears 4.5 everywhere: 7.98 on the floor, 5.53 on a card,
     4.99 on --surface-2. A muted tone is for stepping back, not for being
     unreadable. */
  --muted: #a4a5ae;
  /* Half a step brighter than the landing page's, because it sits on a
     lighter surface here. */
  --accent: #4fb2ff;
  --accent-ink: #04121f;
  --warn: #ffb020;
  --bad: #ff7777;
  --ok: #34d399;

  /* A card is meant to be an object on the page, so it carries a shadow.
     Regions of the page — a stat panel, a bar track, a report section — do
     not: if everything is raised, nothing is. */
  --shadow: 0 1px 2px rgba(0,0,0,.45), 0 6px 20px rgba(0,0,0,.3);
  --shadow-hi: 0 2px 5px rgba(0,0,0,.5), 0 16px 38px rgba(0,0,0,.45);
  --radius: 14px;
  /* ONE FAMILY AT DIFFERENT WEIGHTS, not two families at different widths.
   *
   * `--font-display` still exists because the rest of this file uses it, and
   * because headings still want a different WEIGHT and tighter tracking than
   * body text. What it no longer does is change the shape of the letters.
   */
  --font: "Figtree", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-display: var(--font);
}

* { box-sizing: border-box; }

/* The `hidden` attribute is only `display: none` from the UA stylesheet, so
   ANY explicit display in this file silently beats it. That is how the
   selection bar and the folder row ended up visible on a page where the
   markup said hidden — the attribute was doing nothing.
   
   One rule rather than remembering to write `.thing[hidden]` beside every
   flex container, because the failure is invisible until somebody looks. */
[hidden] { display: none !important; }
html, body { margin: 0; padding: 0; }
body {
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  padding-bottom: env(safe-area-inset-bottom);
  min-height: 100%;
}

/* The app's ground.
 *
 * QUIETER THAN THE LANDING PAGE, deliberately. That page is selling and can
 * afford scenery; this one is a tool somebody uses after training, and every
 * bit of decoration here competes with a video thumbnail or a number they
 * came to read. So: one soft light source at the top, a barely-there mat
 * grid that fades out before it reaches the content, and grain to stop the
 * gradient banding.
 *
 * Fixed rather than scrolling, so the light stays where a light would be
 * instead of sliding up the screen with the list.
 */
body:has(.topbar)::before {
  content: ""; position: fixed; inset: 0; z-index: -2; pointer-events: none;
  /* Measured, not guessed. The first cut moved the page from rgb(17,25,33)
     to rgb(12,14,17) — a five-point difference per channel, which is real in
     a pixel sample and invisible on an actual screen. Reported, correctly,
     as "the background still looks the same".
     
     This is roughly four times the range, plus a mat glow and a vignette, so
     there is somewhere for the eye to land while still being quiet enough to
     sit behind a list of thumbnails. */
  background:
    radial-gradient(70% 42% at 50% 0%, rgba(59,168,255,.20), transparent 68%),
    radial-gradient(120% 70% at 50% 118%, rgba(59,168,255,.07), transparent 62%),
    linear-gradient(180deg, var(--wash-hi) 0%, var(--wash-mid) 34%, var(--bg) 78%);
}
body:has(.topbar)::after {
  content: ""; position: fixed; inset: 0; z-index: -1; pointer-events: none;
  opacity: .45;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)' opacity='.03'/%3E%3C/svg%3E");
}

/* The landing page's 1180px shell plus gutters; it was 900px, which left a
   third of a 1440 screen empty either side of the list. */
.wrap { max-width: 1228px; margin: 0 auto; padding: 20px 16px 64px; }
.wrap.center { display: flex; flex-direction: column; align-items: center; text-align: center; padding-top: 48px; }

/* ---------- brand ---------- */
.brand-lockup { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
.brand-mark {
  width: 44px; height: 44px; border-radius: 12px; display: grid; place-items: center;
  background: linear-gradient(140deg, var(--accent), #1d6fd0);
  color: var(--accent-ink); font-weight: 800; letter-spacing: -0.5px; font-size: 17px;
}
.brand-mark.sm { width: 32px; height: 32px; font-size: 13px; border-radius: 9px; }
.brand { font-size: 26px; margin: 0; letter-spacing: -0.6px; }
.tagline { color: var(--muted); margin: 4px 0 28px; max-width: 34ch; }

/* ---------- topbar ---------- */
.topbar {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 16px; border-bottom: 1px solid var(--line);
  /* Scrolls away; the tab row below is what sticks. Two stacked sticky bars
     cost a sixth of a phone screen for a name and an avatar. */
  position: relative; z-index: 10;
  padding-top: calc(12px + env(safe-area-inset-top));
  /* Translucent, so content passes UNDER the bar rather than vanishing at a
     hard line. The solid fallback comes first for anything without
     backdrop-filter — an unreadable transparent bar would be a bad trade for
     an effect. */
  background: var(--surface);
  background: color-mix(in srgb, var(--surface) 78%, transparent);
  backdrop-filter: saturate(150%) blur(14px);
  -webkit-backdrop-filter: saturate(150%) blur(14px);
}
.topbar .who { flex: 1; display: flex; flex-direction: column; line-height: 1.2; }
.topbar .who strong { font-size: 15px; }
.tier { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.7px; }

/* ---------- sections ---------- */
.section { margin-top: 32px; }
.section h2 { font-size: 18px; margin: 0 0 4px; letter-spacing: -0.3px; }
.section h3 { font-size: 14px; color: var(--muted); margin: 24px 0 8px; text-transform: uppercase; letter-spacing: 0.6px; }
.sub, .hint, .footnote { color: var(--muted); font-size: 13px; }
.sub { margin: 0 0 14px; }
.footnote { max-width: 42ch; margin-top: 32px; }

/* ---------- login ---------- */
.card {
  background: linear-gradient(180deg, var(--card-hi) 0%, var(--card-lo) 100%);
  border: 1px solid var(--line); border-radius: var(--radius); padding: 20px;
  position: relative; overflow: hidden;
}
.card::before {
  content: ""; position: absolute; inset: 0 0 auto 0; height: 1px;
  background: linear-gradient(90deg, transparent, rgba(255,255,255,.10), transparent);
}
.login { width: 100%; max-width: 380px; text-align: left; }
.login label { display: block; font-size: 13px; color: var(--muted); margin-bottom: 6px; }
.login input {
  width: 100%; padding: 13px 14px; font-size: 16px; border-radius: 10px;
  border: 1px solid var(--line); background: var(--bg); color: var(--text); margin-bottom: 12px;
}
.login input:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: transparent; }
.login .hint { margin: 10px 0 0; text-align: center; }

/* ---------- buttons ---------- */
.btn {
  font: inherit; font-weight: 600; border-radius: 10px; border: 1px solid transparent;
  padding: 12px 16px; cursor: pointer; width: 100%;
}
.btn.primary { background: var(--accent); color: var(--accent-ink); }
.btn.primary:active { filter: brightness(0.92); }
.btn.ghost { background: transparent; color: var(--muted); border-color: var(--line); }
.btn.sm { padding: 8px 12px; font-size: 13px; width: auto; }
/* Keep is a toggle, so the "on" state has to read as on. */
#keep-clip.is-on { border-color: var(--accent); color: var(--accent); }
.btn.tiny { padding: 5px 10px; font-size: 12px; width: auto; background: var(--warn); color: #241a00; border: 0; border-radius: 7px; font-weight: 700; }
.btn[disabled] { opacity: 0.5; pointer-events: none; }

/* ---------- steps ---------- */
.steps { display: grid; gap: 10px; margin-top: 32px; width: 100%; max-width: 380px; text-align: left; }
.step { display: flex; gap: 12px; align-items: center; }
.step .n {
  flex: none; width: 26px; height: 26px; border-radius: 50%; display: grid; place-items: center;
  background: var(--surface-2); color: var(--accent); font-size: 13px; font-weight: 700;
}
.step p { margin: 0; font-size: 14px; color: var(--muted); }

/* ---------- clip grid ---------- */
/* 205px rather than 240px, which is four across a 900px page instead of
   three. This is a list somebody scans backwards through their own training,
   so how many rounds are on screen at once is the thing that matters — and
   the card below lost a full-width strip and a redundant line, so it is
   shorter as well as narrower.

   Measured against one member's real nine rounds at 1280px: cards 280x282
   and 280x253 before, 208x205 and 208x176 after, three per row to four. The
   list went from 1175px of page to 940px.

   260px since the shell widened to 1180px: still four across on a laptop,
   but each picture is a third larger, and the newest round now sits above
   the grid at full width — so density is no longer the only job here. */
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 12px; }
.clip-card {
  background: linear-gradient(180deg, var(--card-hi) 0%, var(--card-lo) 100%);
  border: 1px solid var(--line); border-radius: var(--radius);
  overflow: hidden; display: flex; flex-direction: column;
  position: relative;
  transition: border-color .15s ease, transform .15s ease;
}
/* A hairline along the top edge, the same trick as the landing cards: the
   cheapest way to make a flat rectangle read as a raised surface. */
.clip-card::before {
  content: ""; position: absolute; inset: 0 0 auto 0; height: 1px; z-index: 1;
  background: linear-gradient(90deg, transparent, rgba(255,255,255,.11), transparent);
}
.clip-card:hover { border-color: var(--line-2); }
.thumb {
  position: relative; aspect-ratio: 16 / 9; cursor: pointer;
  background:
    radial-gradient(120% 80% at 50% 20%, var(--thumb-hi) 0%, var(--thumb-lo) 70%),
    repeating-linear-gradient(45deg, rgba(255,255,255,.02) 0 10px, transparent 10px 20px);
  display: grid; place-items: center;
}
/* The poster frame. Sits UNDER the play button and the duration chip, both
   of which are positioned, so it needs no z-index of its own — but it does
   need to escape the grid that centres the ▶. */
.thumb .shot {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; display: block;
}
.thumb .play {
  width: 46px; height: 46px; border-radius: 50%; display: grid; place-items: center;
  background: rgba(255,255,255,.12); backdrop-filter: blur(4px);
  /* The `padding-left: 3px` that used to be here was optical centring for a
     TEXT triangle, which sits left in its glyph box. The SVG carries its own
     balance — the path's centroid is already on the box centre — so keeping
     the padding pushed it visibly off to the right. */
}
.thumb .dur {
  position: absolute; right: 8px; bottom: 8px; font-size: 11px; font-variant-numeric: tabular-nums;
  background: rgba(0,0,0,.65); padding: 2px 6px; border-radius: 5px;
}
.meta { padding: 10px 12px; display: flex; flex-direction: column; gap: 2px; flex: 1; }
.meta strong { font-size: 14px; }
.meta span { font-size: 12px; color: var(--muted); }
.claim-btn { margin: 0 12px 12px; width: calc(100% - 24px); }

.banner { padding: 8px 12px; font-size: 12px; display: flex; align-items: center; gap: 8px; justify-content: space-between; }
.banner.warn { background: rgba(255,176,32,.12); color: var(--warn); border-top: 1px solid rgba(255,176,32,.25); }
.banner.muted { color: var(--muted); border-top: 1px solid var(--line); }
.banner.kept { background: rgba(52,211,153,.1); color: var(--ok); border-top: 1px solid rgba(52,211,153,.25); }

.empty {
  border: 1px dashed var(--line); border-radius: var(--radius); padding: 28px 20px;
  text-align: center; color: var(--muted);
}
.empty p { margin: 4px 0; font-size: 14px; }

/* ---------- consent ---------- */
.consent .switch { display: flex; align-items: center; gap: 12px; padding: 12px 0; border-bottom: 1px solid var(--line); font-size: 14px; }
.consent .switch input { width: 20px; height: 20px; accent-color: var(--accent); flex: none; }

/* ---------- admin ---------- */
.health { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; }
.health-card { background: var(--surface); border: 1px solid var(--line); border-left-width: 3px; border-radius: 10px; padding: 14px; display: flex; flex-direction: column; gap: 2px; }
.health-card.ok { border-left-color: var(--ok); }
.health-card.bad { border-left-color: var(--bad); }
.health-card .label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.7px; color: var(--muted); }
.health-card strong { font-size: 18px; }
.health-card .detail { font-size: 12px; color: var(--muted); }

.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); gap: 10px; }
.stat { background: var(--surface); border: 1px solid var(--line); border-radius: 10px; padding: 14px; text-align: center; }
.stat span { display: block; font-size: 24px; font-weight: 700; letter-spacing: -0.5px; }
.stat label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.6px; }

.table { width: 100%; border-collapse: collapse; font-size: 13px; margin-top: 8px; }
.table th { text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: 0.6px; color: var(--muted); padding: 8px; border-bottom: 1px solid var(--line); }
.table td { padding: 10px 8px; border-bottom: 1px solid var(--line); }
.table .mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; color: var(--muted); }
.muted-cell { color: var(--muted); text-align: center; padding: 20px !important; }
.pill { font-size: 11px; padding: 2px 8px; border-radius: 20px; background: var(--surface-2); }
.pill.ok { background: rgba(52,211,153,.15); color: var(--ok); }
.pill.bad { background: rgba(255,92,92,.15); color: var(--bad); }

/* ---------- player ---------- */
dialog#player {
  border: 0; padding: 0; background: var(--surface); color: var(--text);
  border-radius: var(--radius); max-width: min(94vw, 860px); width: 100%; overflow: hidden;
}
dialog#player::backdrop { background: rgba(0,0,0,.8); }
dialog#player video { width: 100%; display: block; background: #000; aspect-ratio: 16/9; }
.player-actions { display: flex; gap: 10px; padding: 12px; }

/* ---------- toast ---------- */
#toast {
  position: fixed; left: 50%; bottom: 24px; transform: translate(-50%, 20px);
  background: var(--surface-2); border: 1px solid var(--line); color: var(--text);
  padding: 11px 16px; border-radius: 10px; font-size: 14px; opacity: 0;
  transition: opacity .18s, transform .18s; pointer-events: none; z-index: 100; max-width: 90vw;
}
#toast.show { opacity: 1; transform: translate(-50%, 0); }

@media (prefers-reduced-motion: reduce) { * { transition: none !important; } }

/* ================= restructured app ================= */

/* ---------- tabs ---------- */
.tabs {
  display: flex; gap: 2px; padding: 0 12px;
  border-bottom: 1px solid var(--line); position: sticky; top: 57px; z-index: 9;
  overflow-x: auto;
  /* Matches the top bar, so the two read as one surface rather than two
     stacked panels. Solid fallback first, as above. */
  background: var(--surface);
  background: color-mix(in srgb, var(--surface) 78%, transparent);
  backdrop-filter: saturate(150%) blur(14px);
  -webkit-backdrop-filter: saturate(150%) blur(14px);
}
.tab { background: none; border: 0; border-bottom: 2px solid transparent; color: var(--muted);
  font: inherit; font-size: 14px; font-weight: 600; padding: 12px 14px; cursor: pointer;
  white-space: nowrap; display: flex; align-items: center; gap: 7px; }
.tab.is-active { color: var(--text); border-bottom-color: var(--accent); }
.badge { background: var(--accent); color: var(--accent-ink); font-size: 11px;
  font-weight: 700; border-radius: 20px; padding: 1px 7px; }
.view { display: none; }
.view.is-active { display: block; }
.view h2 { font-size: 18px; margin: 28px 0 4px; letter-spacing: -0.3px; }
.view h2:first-child { margin-top: 4px; }

/* ---------- clip card additions ---------- */
.meta .mine { color: var(--accent); font-weight: 600; }
.src-tag { position: absolute; left: 8px; top: 8px; font-size: 10px; letter-spacing: .06em;
  text-transform: uppercase; background: rgba(59,168,255,.16); color: var(--accent);
  padding: 2px 6px; border-radius: 4px; }

/* Where a round is filed, on the card itself. Deliberately quieter than
   `.mine` — which shelf something is on is orientation, not the reason
   anybody opened the tab, and it sits under the line that says whose roll
   it is. Wraps rather than ellipsising: a clip can be on two shelves and
   hiding the second would misreport the thing this exists to report. */
.filed { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 5px; }
.filed-chip {
  font-size: 10px; font-weight: 600; letter-spacing: .02em;
  color: var(--muted); background: var(--surface-2);
  border: 1px solid var(--line); border-radius: 4px;
  padding: 1px 6px; max-width: 100%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ---------- class rows ---------- */
.class-row { background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); margin-bottom: 10px; overflow: hidden; }
.class-row summary { padding: 14px 16px; cursor: pointer; display: flex;
  flex-direction: column; gap: 2px; list-style: none; }
.class-row summary::-webkit-details-marker { display: none; }
.class-row summary strong { font-size: 15px; }
.class-row summary span { font-size: 12.5px; color: var(--muted); }
.class-row .grid { padding: 0 14px 14px; }

/* ---------- player ---------- */
dialog#player { max-width: min(96vw, 900px); max-height: 92vh; overflow-y: auto; }
.player-head { display: flex; align-items: center; justify-content: space-between;
  padding: 12px 14px; border-bottom: 1px solid var(--line); }
.icon-btn { background: var(--surface-2); border: 1px solid var(--line); color: var(--text);
  border-radius: 8px; width: 34px; height: 34px; font-size: 14px; cursor: pointer; }
.player-controls { display: flex; align-items: center; gap: 8px; padding: 10px 14px;
  border-bottom: 1px solid var(--line); flex-wrap: wrap; }
.speeds { display: flex; gap: 2px; background: var(--surface-2); border-radius: 8px;
  padding: 2px; margin-right: auto; }
.speeds button { background: none; border: 0; color: var(--muted); font: inherit;
  font-size: 12px; font-weight: 600; padding: 6px 10px; border-radius: 6px; cursor: pointer; }
.speeds button.is-active { background: var(--accent); color: var(--accent-ink); }

/* ---------- claim your roll ----------
   The range slider that lived here is gone — a clip is one round of a whole
   mat, so everybody's range was 0 to the end and dragging it answered
   nothing. What is left is one row: what to do, and the controls to do it. */
.marker {
  padding: 14px 16px; border-bottom: 1px solid var(--line);
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--accent) 7%, transparent), transparent 80%);
}
.marker-row {
  display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
}
.marker-copy { flex: 1 1 220px; min-width: 0; }
.marker-copy strong { display: block; font-size: 15px; letter-spacing: -0.2px; }
.marker-copy .hint { margin: 3px 0 0; }
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px;
  color: var(--muted); font-variant-numeric: tabular-nums; }
.marker-actions { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.marker-actions select {
  min-width: 200px; padding: 9px 11px; font-size: 14px; font-family: inherit;
  border-radius: 10px; border: 1px solid var(--line);
  background: var(--surface-2); color: var(--text);
}
.marker-actions select:hover { border-color: var(--accent); }
/* Once the roll is claimed the prompt has done its job and should stop
   shouting — the tint goes and it reads as a saved state, not a task. */
.marker.is-claimed { background: none; }

/* ---------- subtabs + panels ---------- */
.subtabs { display: flex; gap: 2px; padding: 0 14px; border-bottom: 1px solid var(--line); }
.subtab { background: none; border: 0; border-bottom: 2px solid transparent;
  color: var(--muted); font: inherit; font-size: 13px; font-weight: 600;
  padding: 11px 12px; cursor: pointer; }
.subtab.is-active { color: var(--text); border-bottom-color: var(--accent); }
.panel { display: none; padding: 14px; }
.panel.is-active { display: block; }

/* ---------- timeline ---------- */
.tl-item { border: 1px solid var(--line); border-left: 3px solid var(--accent);
  border-radius: 9px; margin-bottom: 8px; overflow: hidden; background: var(--surface); }
.tl-item.bad { border-left-color: var(--warn); }
.tl-head { width: 100%; display: flex; align-items: center; gap: 12px; padding: 11px 13px;
  background: none; border: 0; color: var(--text); font: inherit; cursor: pointer;
  text-align: left; }
.tl-time { flex: none; }
.tl-label { flex: 1; font-weight: 600; font-size: 14.5px; }
.tl-dur { font-size: 12px; color: var(--muted); }
.tl-body { padding: 0 13px 12px; }
.first-priority { font-size: 13px; color: var(--text-2); margin: 0 0 10px;
  padding-left: 10px; border-left: 2px solid var(--line); }

/* ---------- option dropdowns ---------- */
.opt-group { border: 1px solid var(--line); border-radius: 8px; margin-bottom: 6px;
  background: var(--bg); }
.opt-group summary { padding: 9px 12px; cursor: pointer; font-size: 13px; font-weight: 600;
  display: flex; justify-content: space-between; align-items: center; list-style: none; }
.opt-group summary::-webkit-details-marker { display: none; }
.opt-group summary::after { content: "▾"; color: var(--muted); font-size: 11px; }
.opt-group[open] summary::after { content: "▴"; }
.opt-group .count { background: var(--surface-2); color: var(--text-2); font-size: 11px;
  border-radius: 20px; padding: 1px 7px; margin-left: auto; margin-right: 10px; }
.opt-list { list-style: none; margin: 0; padding: 0 12px 10px; }
.opt-list li { display: grid; grid-template-columns: 1fr auto; gap: 2px 8px;
  padding: 8px 0; border-top: 1px solid var(--line); }
.opt-name { font-size: 14px; font-weight: 600; }
.opt-detail { grid-column: 1 / -1; font-size: 12.5px; color: var(--muted); }
.lvl { font-size: 10px; text-transform: uppercase; letter-spacing: .06em; padding: 2px 6px;
  border-radius: 4px; align-self: start; }
.lvl.fundamental { background: rgba(52,211,153,.14); color: var(--ok); }
.lvl.intermediate { background: rgba(255,176,32,.14); color: var(--warn); }
.lvl.advanced { background: rgba(255,92,92,.13); color: var(--bad); }

/* ---------- notes ---------- */
.note { display: flex; gap: 11px; padding: 11px 0; border-bottom: 1px solid var(--line); }
.note.from-coach .note-who { color: var(--accent); }
.note-t { flex: none; background: var(--surface-2); border: 1px solid var(--line);
  border-radius: 6px; padding: 3px 8px; cursor: pointer; height: fit-content; }
.note-who { font-size: 12px; color: var(--muted); margin-bottom: 2px; }
.note-text { font-size: 14px; }
.note-form { margin-top: 14px; }
.note-at { font-size: 12px; color: var(--muted); margin-bottom: 6px; }
.note-form textarea { width: 100%; padding: 11px; font: inherit; font-size: 15px;
  border-radius: 9px; border: 1px solid var(--line); background: var(--bg);
  color: var(--text); resize: vertical; }
.note-actions { display: flex; gap: 10px; margin-top: 9px; }
.note-actions select { flex: 1; padding: 9px; border-radius: 8px; border: 1px solid var(--line);
  background: var(--bg); color: var(--text); font-size: 14px; }

/* ---------- progress ---------- */
.stat-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
  gap: 10px; margin: 16px 0 22px; }
.bars { display: flex; flex-direction: column; gap: 7px; }
.bar-row { display: grid; grid-template-columns: 150px 1fr 44px; gap: 10px; align-items: center; }
.bar-label { font-size: 13px; }
.bar-track { height: 20px; background: var(--surface-2); border-radius: 5px; overflow: hidden; }
.bar-fill { height: 100%; background: var(--accent); }
.bar-fill.bottom { background: var(--warn); }
.bar-fill.neutral { background: var(--muted); }
.bar-val { font-size: 12.5px; color: var(--muted); text-align: right;
  font-variant-numeric: tabular-nums; }
@media (max-width: 520px) { .bar-row { grid-template-columns: 110px 1fr 40px; } }

.drill { margin-top: 26px; background: var(--surface); border: 1px solid var(--line);
  border-left: 3px solid var(--warn); border-radius: var(--radius); padding: 18px; }
.drill-kicker { font-size: 11px; text-transform: uppercase; letter-spacing: .1em;
  color: var(--warn); font-weight: 700; margin-bottom: 6px; }
.drill h3 { margin: 0 0 10px; font-size: 17px; }

/* ---------- coach queue ---------- */
.queue-item { background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 14px; margin-bottom: 10px; }
.queue-item .btn { margin-top: 10px; }

/* ---------- check-in result ---------- */
.checkin-ok, .checkin-bad { display: flex; flex-direction: column; align-items: center;
  gap: 10px; margin: 8px 0 4px; }
.tick { width: 74px; height: 74px; border-radius: 50%; display: grid; place-items: center;
  font-size: 36px; font-weight: 700; background: rgba(52,211,153,.14); color: var(--ok);
  border: 2px solid rgba(52,211,153,.4); }
.tick.bad { background: rgba(255,176,32,.13); color: var(--warn);
  border-color: rgba(255,176,32,.4); }
.checkin-ok h2, .checkin-bad h2 { font-size: 21px; margin: 4px 0 0; letter-spacing: -0.3px; }
.checkin-ok .sub, .checkin-bad .sub { margin: 0; }

/* ==========================================================================
   Landing page
   ========================================================================== */
/* The hero is a lit room, not a flat panel. Three layers: a mat receding to
   a horizon, a warm-to-dark vertical wash over it, and a fine grain that
   stops large gradients banding on cheap screens. All painted, nothing
   fetched. */
.hero {
  position: relative; overflow: hidden;
  border-bottom: 1px solid var(--line);
  background:
    radial-gradient(120% 80% at 50% 110%, #16202b 0%, transparent 60%),
    linear-gradient(180deg, var(--wash-mid) 0%, var(--bg) 100%);
}
.hero::after {
  /* Grain. A data-URI so it costs no request, and low opacity so it reads as
     texture rather than noise. */
  content: ""; position: absolute; inset: 0; pointer-events: none; opacity: .5;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)' opacity='.035'/%3E%3C/svg%3E");
}
.hero-art { position: absolute; inset: 0; z-index: 0; pointer-events: none; }
.mat { width: 100%; height: 100%; display: block; }
.hero-nav {
  position: relative; z-index: 1; display: flex; align-items: center;
  justify-content: space-between; gap: 12px;
  max-width: 1100px; margin: 0 auto; padding: 18px 20px;
}
.brand-word { font-weight: 700; letter-spacing: -0.4px; font-size: 17px; }
.hero-nav .brand-lockup { margin-bottom: 0; gap: 10px; }

.hero-inner {
  position: relative; z-index: 1; max-width: 1100px; margin: 0 auto;
  padding: 28px 20px 56px; display: grid; gap: 40px;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr); align-items: center;
}
.eyebrow {
  display: inline-flex; align-items: center; gap: 7px;
  color: var(--accent); font-size: 12px; font-weight: 700; letter-spacing: 1.1px;
  text-transform: uppercase; margin: 0 0 14px;
  background: rgba(59,168,255,.10); border: 1px solid rgba(59,168,255,.25);
  padding: 5px 11px; border-radius: 999px;
}
.eyebrow::before {
  content: ""; width: 6px; height: 6px; border-radius: 50%;
  background: var(--accent); box-shadow: 0 0 8px var(--accent);
}
.hero-title {
  font-size: clamp(32px, 5.4vw, 56px); line-height: 1.04; letter-spacing: -1.8px;
  margin: 0 0 16px; font-weight: 800;
  /* A slight top-down wash so the headline has weight rather than sitting
     flat on the background. */
  background: linear-gradient(180deg, #ffffff 20%, #b9c6d4 100%);
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
.hero-sub { color: var(--text-2); font-size: 16px; max-width: 46ch; margin: 0 0 26px; }
.hero-form { display: flex; gap: 10px; flex-wrap: wrap; max-width: 480px; }
.hero-form input {
  /* NOT `flex: 1 1 220px`. The auth panes lay out as a COLUMN, so a grow
     factor stretches the field down the page instead of across it — it
     rendered as a 250px-tall email box. Fixed height, full width. */
  flex: 0 0 auto; width: 100%;
  padding: 13px 14px; border-radius: 11px; font-size: 15px;
  background: var(--surface-2); border: 1px solid var(--line); color: var(--text);
}
.hero-form input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.hero-hint { margin: 12px 0 0; max-width: 46ch; }
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* the authored phone mock */
.hero-shot { display: flex; justify-content: center; }
.phone {
  width: 260px; border-radius: 32px; padding: 10px;
  background: linear-gradient(160deg, var(--surface-2), var(--card-lo));
  border: 1px solid #333c47; position: relative;
  /* Two shadows: a tight one for the edge and a long soft one for the drop,
     which is what stops a mock reading as a sticker. */
  box-shadow:
    0 2px 6px rgba(0,0,0,.6),
    0 44px 80px -20px rgba(0,0,0,.75),
    0 0 90px -30px rgba(59,168,255,.35);
  transform: rotate(-1.5deg);
}
@media (prefers-reduced-motion: reduce) { .phone { transform: none; } }
.phone-notch {
  width: 92px; height: 5px; border-radius: 3px; background: #0a0d11;
  margin: 2px auto 8px;
}
.phone-screen { background: var(--bg); border-radius: 22px; padding: 14px; }
.ps-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.ps-title { font-weight: 700; font-size: 14px; }
.ps-belt { width: 26px; height: 9px; border-radius: 2px; background: var(--b); display: block; }
.ps-card { display: flex; gap: 10px; align-items: center; margin-bottom: 14px; }
.ps-thumb { position: relative; width: 96px; border-radius: 8px; overflow: hidden; flex: none; }
.ps-thumb svg { display: block; width: 100%; height: auto; }
.ps-dur {
  position: absolute; right: 4px; bottom: 3px; font-size: 10px;
  background: rgba(0,0,0,.7); padding: 1px 4px; border-radius: 4px;
}
.ps-meta { display: flex; flex-direction: column; font-size: 12px; }
.ps-meta span { color: var(--muted); }
.ps-bars { display: flex; flex-direction: column; gap: 8px; }
.ps-bar { position: relative; }
.ps-bar span {
  display: block; height: 7px; border-radius: 4px;
  background: linear-gradient(90deg, var(--accent), #1d6fd0);
}
.ps-bar em { font-style: normal; font-size: 10px; color: var(--muted); }

.belt-rail {
  position: relative; z-index: 1; display: flex; height: 6px;
  box-shadow: 0 -10px 30px -10px rgba(0,0,0,.8);
}
.belt-rail span { flex: 1; background: var(--b); }
/* Each belt carries its bar, the way a real one does. Small, and the sort of
   detail that tells a grappler somebody who trains built this. */
.belt-rail span::after {
  content: ""; display: block; height: 100%; width: 14%; margin-left: auto;
  background: rgba(0,0,0,.45);
}
.belt-rail span:nth-child(5)::after { background: #b3202e; }

/* pitch cards */
/* FOUR cards, and the track count said three — so the fourth would have
   dropped onto a row of its own beside two empty columns. That had never
   shown up because `.pitch` was not a grid at all until the name collision
   further down this file was fixed.

   TWO ACROSS RATHER THAN FOUR, and the measurement is why. Four columns in a
   900px page is 205px each, which is a 26-character line — half of what body
   copy wants, and the last card's paragraph is twice the length of the
   others. Two across gives 426px and 54 characters, and costs 46px of page
   height. Wide enough to read beats narrow enough to fit on one row.

   auto-fit rather than `repeat(2, 1fr)`: it drops to one column on a phone
   by itself, with no breakpoint to keep in sync with anything else. */
.pitch { display: grid; gap: 16px; margin: 44px 0 8px;
         grid-template-columns: repeat(auto-fit, minmax(330px, 1fr)); }
.pitch-card {
  background: linear-gradient(180deg, var(--card-hi) 0%, var(--card-lo) 100%);
  border: 1px solid var(--line); border-radius: var(--radius); padding: 22px;
  position: relative; overflow: hidden;
  transition: transform .16s ease, border-color .16s ease;
}
/* A hairline catching the light along the top edge. It is the cheapest way
   to make a flat rectangle read as a raised surface. */
.pitch-card::before {
  content: ""; position: absolute; inset: 0 0 auto 0; height: 1px;
  background: linear-gradient(90deg, transparent, rgba(255,255,255,.13), transparent);
}
.pitch-card:hover { transform: translateY(-2px); border-color: var(--line-2); }
@media (prefers-reduced-motion: reduce) { .pitch-card:hover { transform: none; } }
.pitch-ico { color: var(--accent); width: 26px; height: 26px; margin-bottom: 12px; }
.pitch-ico svg { width: 100%; height: 100%; }
.pitch-card h3 { margin: 0 0 6px; font-size: 16px; letter-spacing: -0.2px; }
.pitch-card p { margin: 0; color: var(--muted); font-size: 14px; }

.band-title { font-size: 20px; letter-spacing: -0.4px; margin: 0 0 6px; }
.band-sub { color: var(--muted); font-size: 14px; margin: 0 0 4px; max-width: 60ch; }
.steps-band { margin: 48px 0; }
/* The landing page's four-across "How it works" row USED TO BE HERE, and it
   was the first of three class collisions in this file — it redeclared
   `.steps`, which is also the sign-in screen's 380px column, and inherited
   that column's `max-width`.

   It is gone rather than fixed: the landing page has its own stylesheet now
   and uses `.lp-steps`, so this was dead code that existed only to keep
   colliding. `tests/test_css_collisions.py` fails if anything reintroduces a
   pair like it. The sign-in screen's `.steps` is the only one left. */

.upload-band {
  display: flex; align-items: center; justify-content: space-between; gap: 24px;
  flex-wrap: wrap; background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 24px; margin-bottom: 40px;
}
.upload-band .lock, .kind .lock { color: var(--accent); }
.land-foot { border-top: 1px solid var(--line); padding-top: 20px; margin-bottom: 40px; }
.land-foot p { margin: 0 0 6px; }

@media (max-width: 860px) {
  .hero-inner { grid-template-columns: 1fr; gap: 28px; padding-bottom: 36px; }
  .hero-shot { order: -1; }
  .phone { width: 210px; }
  /* `.pitch` is not here: it sizes itself with auto-fit. */
  .steps { grid-template-columns: 1fr; }
}

/* ==========================================================================
   Top bar: name menu
   ========================================================================== */
.topbar-gym { flex: 1; color: var(--muted); font-size: 13px; }
.menu { position: relative; }
.menu-trigger {
  display: flex; align-items: center; gap: 9px; background: none; border: 0;
  color: inherit; cursor: pointer; padding: 4px 6px; border-radius: 10px;
  font: inherit;
}
.menu-trigger:hover { background: var(--surface-2); }
.menu-trigger:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.menu-trigger .who { text-align: left; }
.chev { width: 12px; height: 12px; color: var(--muted); transition: transform .15s; }
.menu-trigger[aria-expanded="true"] .chev { transform: rotate(180deg); }

.avatar {
  width: 32px; height: 32px; border-radius: 50%; display: grid; place-items: center;
  background: var(--surface-2); border: 2px solid var(--line);
  font-weight: 700; font-size: 13px; flex: none;
}
/* The ring is the belt. Cheap, and instantly readable to anyone who trains. */
.avatar[data-belt="white"]  { border-color: #ece7dd; }
.avatar[data-belt="blue"]   { border-color: #2f6fd0; }
.avatar[data-belt="purple"] { border-color: #6b46c1; }
.avatar[data-belt="brown"]  { border-color: #6b4423; }
.avatar[data-belt="black"]  { border-color: #4a4f57; }
.avatar[data-belt="coral"]  { border-color: #e2554e; }
.avatar[data-belt="red"]    { border-color: #b3202e; }

.dropdown {
  position: absolute; right: 0; top: calc(100% + 8px); min-width: 240px; z-index: 40;
  background: var(--surface); border: 1px solid var(--line); border-radius: 12px;
  box-shadow: 0 18px 40px rgba(0,0,0,.5); padding: 6px; animation: dd-in .12s ease-out;
}
@keyframes dd-in { from { opacity: 0; transform: translateY(-4px); } }
@media (prefers-reduced-motion: reduce) { .dropdown { animation: none; } }
.dd-head { padding: 10px 10px 12px; border-bottom: 1px solid var(--line); margin-bottom: 6px; }
.dd-head strong { display: block; font-size: 14px; }
.dd-email { display: block; color: var(--muted); font-size: 12px; word-break: break-all; }
.dd-item {
  display: flex; align-items: center; gap: 10px; width: 100%; text-align: left;
  background: none; border: 0; color: var(--text); font: inherit; cursor: pointer;
  padding: 9px 10px; border-radius: 8px;
}
.dd-item svg { width: 17px; height: 17px; color: var(--muted); flex: none; }
.dd-item:hover { background: var(--surface-2); }
.dd-item:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.dd-item.danger:hover { background: rgba(255,92,92,.12); color: var(--bad); }
.dd-item.danger:hover svg { color: var(--bad); }
.dd-sep { height: 1px; background: var(--line); margin: 6px 0; }
.dropdown form { margin: 0; }

.belt-chip {
  display: inline-flex; align-items: center; gap: 6px; margin-top: 8px;
  font-size: 11px; text-transform: uppercase; letter-spacing: .6px; color: var(--text-2);
}
.belt-chip::before {
  content: ""; width: 22px; height: 8px; border-radius: 2px;
  background: var(--belt, var(--line)); border: 1px solid rgba(255,255,255,.14);
}

/* ==========================================================================
   Forms, settings, belt pickers
   ========================================================================== */
.field { margin-bottom: 18px; }
.field > label { display: block; font-size: 13px; font-weight: 600; margin-bottom: 6px; }
.field input[type=text], .field input[type=number], .field input[type=email], .field select {
  width: 100%; padding: 11px 12px; border-radius: 10px; font: inherit;
  background: var(--surface-2); border: 1px solid var(--line); color: var(--text);
}
.field input:disabled { color: var(--muted); }
.field input:focus-visible, .field select:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.row2 { display: grid; gap: 12px; grid-template-columns: 1fr 1fr; }
.settings-card { padding: 20px; margin-bottom: 16px; }
.settings-card h3 { margin: 0 0 16px; font-size: 15px; text-transform: none; letter-spacing: -0.2px; color: var(--text); }
.kv { display: flex; justify-content: space-between; gap: 12px; margin: 0 0 8px; font-size: 14px; }
.kv span { color: var(--muted); }
.saved { color: var(--ok); font-size: 13px; margin-left: 10px; }
.err { color: var(--bad); font-size: 13px; }

.belt-pick { display: flex; gap: 8px; flex-wrap: wrap; }
.belt-opt {
  cursor: pointer; border: 1px solid var(--line); background: var(--surface-2);
  border-radius: 10px; padding: 8px 12px 9px; display: flex; flex-direction: column;
  gap: 6px; align-items: center; min-width: 62px; color: var(--text-2); font-size: 12px;
}
.belt-opt .swatch {
  width: 40px; height: 13px; border-radius: 3px; background: var(--b);
  border: 1px solid rgba(255,255,255,.16); position: relative; overflow: hidden;
}
/* The bar on the belt — black on colours, red on black and above. */
.belt-opt .swatch::after {
  content: ""; position: absolute; right: 4px; top: 0; bottom: 0; width: 7px;
  background: var(--bar);
}
.belt-opt[aria-pressed="true"] { border-color: var(--accent); color: var(--text); background: var(--surface); }
.belt-opt:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.stripe-pick { display: flex; gap: 6px; }
.stripe-opt {
  cursor: pointer; width: 40px; height: 34px; border-radius: 8px;
  background: var(--surface-2); border: 1px solid var(--line); color: var(--text-2);
  font: inherit; font-size: 13px;
}
.stripe-opt[aria-pressed="true"] { border-color: var(--accent); color: var(--text); }

.toggle { display: flex; gap: 12px; align-items: flex-start; margin-bottom: 16px; cursor: pointer; }
.toggle input { margin-top: 3px; width: 17px; height: 17px; flex: none; accent-color: var(--accent); }
.toggle strong { display: block; font-size: 14px; font-weight: 600; }
.toggle em { font-style: normal; color: var(--muted); font-size: 13px; }

/* The round timer card: a number with its unit, and a choice of two. */
fieldset.field { border: 0; padding: 0; margin: 0 0 18px; min-width: 0; }
fieldset.field > legend { font-size: 13px; font-weight: 600; margin-bottom: 8px; padding: 0; }
.field .inline { display: flex; align-items: center; gap: 10px; }
.field .inline input[type=number] { width: 88px; }
.field .inline span { color: var(--muted); font-size: 14px; }
/* `.field > label.radio`, not just `.radio`: these labels sit directly in a
   `fieldset.field`, and the general `.field > label { display: block }`
   outranks a single class — which stacked every radio ABOVE its text. Seen in
   a screenshot; the behaviour checks all passed. */
.radio, .field > label.radio {
  display: flex; gap: 12px; align-items: flex-start; margin-bottom: 12px; cursor: pointer;
  font-size: 14px; font-weight: 400;
}
.radio input { margin-top: 3px; width: 17px; height: 17px; flex: none; accent-color: var(--accent); }
.radio strong { display: block; font-size: 14px; font-weight: 600; }
.radio em { font-style: normal; color: var(--muted); font-size: 13px; }
/* Account: label on the left, value on the right, hairline between rows.
   Password is one of the rows — changed rarely, so it opens in place rather
   than sitting open as two empty boxes at the top of Settings. */
.acct-rows { border-top: 1px solid var(--line); }
.acct-row {
  display: flex; align-items: center; gap: 12px; min-height: 48px;
  border-bottom: 1px solid var(--line); font-size: 14px;
}
.acct-row > span { color: var(--muted); flex: 0 0 88px; }
.acct-row > strong { flex: 1 1 auto; font-weight: 600; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.acct-row .mask { letter-spacing: 2px; color: var(--text-2); font-weight: 400; }
.acct-row .btn { flex: none; }
.pw-panel { padding: 16px 0 4px; border-bottom: 1px solid var(--line); }
.pw-panel .field { margin-bottom: 14px; }
.pw-panel input[type=password] {
  width: 100%; padding: 11px 12px; border-radius: 10px; font: inherit;
  background: var(--surface-2); border: 1px solid var(--line); color: var(--text);
}
.pw-panel input[type=password]:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.pw-panel .hint { margin: 6px 0 0; }
.pw-actions { display: flex; justify-content: flex-end; gap: 8px; margin-bottom: 12px; }
.settings-card .footnote { margin-top: 14px; }
fieldset.own-timer { border: 0; padding: 0 0 0 29px; margin: 0; min-width: 0; }
fieldset.own-timer:disabled { opacity: .45; }

/* ==========================================================================
   Upload
   ========================================================================== */
.kind-pick { display: grid; gap: 12px; grid-template-columns: 1fr 1fr; margin-bottom: 22px; }
.kind { cursor: pointer; }
.kind input { position: absolute; opacity: 0; pointer-events: none; }
.kind-body {
  display: flex; flex-direction: column; gap: 6px; height: 100%;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 16px;
}
.kind input:checked + .kind-body { border-color: var(--accent); background: var(--surface-2); }
.kind input:focus-visible + .kind-body { outline: 2px solid var(--accent); outline-offset: 2px; }
.kind-ico { display: block; width: 22px; height: 22px; color: var(--accent); flex: none; }
.kind-ico svg { width: 100%; height: 100%; }
.kind strong { font-size: 15px; }
.kind em { font-style: normal; color: var(--muted); font-size: 13px; }

/* `.field > label.drop`, NOT `.drop`, and that is a fix rather than a flourish.
 *
 * The drop zone is a <label> inside a .field, and `.field > label` sets
 * `display: block` at one class plus one type — which beats a bare `.drop` at
 * one class. So this rule's `display: flex` never applied: the zone was a
 * block, its icon was therefore never a flex item, an inline span ignores
 * width and height, and the 30px arrow inside rendered at 824x824. Measured,
 * not guessed. The upload screen has had a giant blue arrow on it for as long
 * as `.field > label` has existed.
 *
 * The lesson is the same one `.steps`, `.pitch` and `.life` each taught: a
 * general rule quietly outranking a component's own. Here it is a
 * specificity contest rather than a duplicate name, which is why the
 * collision test cannot see it. */
.field > label.drop,
.drop {
  display: flex; flex-direction: column; align-items: center; gap: 6px; cursor: pointer;
  border: 2px dashed var(--line); border-radius: var(--radius); padding: 34px 20px;
  background: var(--surface); text-align: center;
}
.drop:hover, .drop.is-over { border-color: var(--accent); background: var(--surface-2); }
/* `display: block` on the wrapper as well as a size. An icon should not be
   the right size only while its parent happens to be a flex container —
   width and height do nothing at all on an inline box, so that arrangement
   fails silently and spectacularly the moment the parent changes. */
.drop-ico {
  display: block; width: 30px; height: 30px; color: var(--accent); flex: none;
}
.drop-ico svg { width: 100%; height: 100%; display: block; }
.drop em { font-style: normal; color: var(--muted); font-size: 13px; }

.up-progress { margin-bottom: 16px; }
.up-progress .bar { height: 7px; border-radius: 4px; background: var(--surface-2); overflow: hidden; }
.up-progress .bar span {
  display: block; height: 100%; width: 0;
  background: linear-gradient(90deg, var(--accent), #1d6fd0); transition: width .2s;
}

/* ==========================================================================
   Welcome
   ========================================================================== */
#welcome {
  border: 1px solid var(--line); border-radius: 16px; background: var(--surface);
  color: var(--text); max-width: 460px; width: calc(100% - 32px); padding: 24px;
}
#welcome::backdrop { background: rgba(0,0,0,.72); }
.welcome-head { text-align: center; margin-bottom: 20px; }
.welcome-head .brand-mark { margin: 0 auto 12px; }
.welcome-head h2 { margin: 0 0 4px; font-size: 20px; letter-spacing: -0.4px; }
.welcome-actions { display: flex; gap: 10px; justify-content: flex-end; }

@media (max-width: 620px) {
  .kind-pick, .row2 { grid-template-columns: 1fr; }
  .topbar-gym { display: none; }
}

/* ---------- sign in / join ---------- */
.auth { max-width: 480px; }
.auth-tabs { display: flex; gap: 4px; margin-bottom: 14px; }
.auth-tab {
  background: none; border: 0; border-bottom: 2px solid transparent;
  color: var(--muted); font: inherit; font-size: 14px; font-weight: 600;
  padding: 6px 10px; cursor: pointer;
}
.auth-tab.is-active { color: var(--text); border-bottom-color: var(--accent); }
.auth-tab:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.auth-pane { display: none; }
.auth-pane.is-active { display: flex; flex-direction: column; gap: 10px; }
.auth-pane .two { display: grid; gap: 10px; grid-template-columns: 1fr 1fr; }
.auth-pane input {
  width: 100%; padding: 13px 14px; border-radius: 11px; font: inherit; font-size: 15px;
  background: var(--surface-2); border: 1px solid var(--line); color: var(--text);
}
.auth-pane input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.auth-pane input[name="join_code"] { text-transform: uppercase; letter-spacing: 2px; font-weight: 600; }
.auth-pane .hint { margin: 2px 0 0; }
#login-form.auth-pane.is-active { flex-direction: column; }
@media (max-width: 520px) { .auth-pane .two { grid-template-columns: 1fr; } }

.auth-pane select {
  width: 100%; padding: 13px 14px; border-radius: 11px; font: inherit; font-size: 15px;
  background: var(--surface-2); border: 1px solid var(--line); color: var(--text);
}
/* The code field, revealed by picking "My gym isn't listed". It used to be a
   <details> labelled "skip the wait", which framed it as an optional extra —
   for a member of an unverified gym it is the only route in. */
.code-block { margin: -2px 0 2px; }
.code-block input { text-transform: uppercase; letter-spacing: .12em; }
.code-block input::placeholder { text-transform: none; letter-spacing: normal; }
.code-block .hint { margin-top: 7px; }
/* [hidden] must beat any explicit display here — three elements were once
   permanently visible for exactly this reason. */
.code-block[hidden] { display: none; }

/* pending / waiting-room banner */
.pending-banner {
  background: linear-gradient(140deg, rgba(255,176,32,.14), rgba(255,176,32,.05));
  border: 1px solid rgba(255,176,32,.35); border-radius: var(--radius);
  padding: 18px 20px; margin-bottom: 22px;
}
.pending-banner h2 { margin: 0 0 6px; font-size: 17px; }
.pending-banner p { margin: 0 0 6px; color: var(--text-2); font-size: 14px; }

/* coach: join requests */
.req { display: flex; align-items: center; gap: 12px; padding: 12px 0; border-bottom: 1px solid var(--line); }
.req:last-child { border-bottom: 0; }
.req .who { flex: 1; display: flex; flex-direction: column; line-height: 1.3; }
.req .who strong { font-size: 15px; }
.req .who span { color: var(--muted); font-size: 12px; }
.req .acts { display: flex; gap: 8px; }

/* ---------- camera compatibility ---------- */
.cameras { margin: 48px 0; }
.cam-grid { display: grid; gap: 16px; grid-template-columns: repeat(3, 1fr); margin-top: 20px; }
.cam-card {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 20px;
}
.cam-ico { color: var(--accent); width: 26px; height: 26px; margin-bottom: 12px; }
.cam-ico svg { width: 100%; height: 100%; }
.cam-card h3 { margin: 0 0 4px; font-size: 16px; letter-spacing: -0.2px; }
.cam-models {
  margin: 0 0 8px; font-size: 12px; color: var(--accent);
  font-weight: 600; letter-spacing: 0.2px;
}
.cam-card p { margin: 0; color: var(--muted); font-size: 14px; }
.cam-card em { font-style: normal; color: var(--text-2); }
.cam-note {
  margin: 20px 0 0; padding: 14px 16px; font-size: 13px; color: var(--muted);
  border-left: 2px solid var(--line); background: var(--surface);
  border-radius: 0 10px 10px 0;
}
@media (max-width: 860px) { .cam-grid { grid-template-columns: 1fr; } }

/* ---------- join vs start a gym ---------- */
.role-pick { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 14px; }
.role {
  text-align: left; cursor: pointer; font: inherit; color: var(--text-2);
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 11px; padding: 11px 13px; display: flex; flex-direction: column; gap: 2px;
}
.role strong { font-size: 14px; color: var(--text); }
.role em { font-style: normal; font-size: 12px; color: var(--muted); }
.role.is-active { border-color: var(--accent); background: var(--surface-2); }
.role:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.role-pane { display: none; }
.role-pane.is-active { display: flex; flex-direction: column; gap: 10px; }
@media (max-width: 520px) { .role-pick { grid-template-columns: 1fr; } }
.joincode {
  text-align: center; font-size: 28px; font-weight: 800; letter-spacing: 5px;
  margin: 4px 0 10px; color: var(--accent); font-variant-numeric: tabular-nums;
}

/* ---------- tap yourself ---------- */
.stage { position: relative; line-height: 0; }
/* Sized in JS to the video's rendered content box — see fitPeopleLayer.
   `inset: 0` was right only while the video filled the stage exactly. */
.people { position: absolute; inset: 0; pointer-events: none; }
/* Only interactive while marking — otherwise it would eat the video's own
   controls, and someone just watching a round is not picking anybody. */
.people.is-picking { pointer-events: auto; cursor: crosshair; }
.person {
  position: absolute; border: 2px solid rgba(59,168,255,.85); border-radius: 8px;
  background: rgba(59,168,255,.10); cursor: pointer; padding: 0;
  transition: background .12s, border-color .12s;
}
/* Nothing is drawn until a tap lands, so `.person` is now only ever the
   confirmation outline — never a target to hunt among. On a competition mat
   the old per-detection boxes framed the referee, the next match and a
   spectator at the scoring table, and the picture disappeared under labels. */
.person { pointer-events: none; }
.person.is-picked { border-color: var(--ok); background: rgba(52,211,153,.28); }
/* Where they tapped when nobody was highlighted. */
.pin {
  position: absolute; width: 26px; height: 26px; margin: -13px 0 0 -13px;
  border: 2px solid var(--ok); border-radius: 50%;
  background: rgba(52,211,153,.3); pointer-events: none;
}

/* ---------- folders ---------- */
.section-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.section-head h2 { margin: 0; }
/* ---------- folders ----------
   These used to be inline pills, which made them read as filter chips —
   the same shape the rest of the app uses for "show me only X". A folder is
   a PLACE you put things, not a predicate, and the shape should say so
   before the label does.

   So: a real folder silhouette, tab and all. The tab is a ::before sitting
   above the body with its bottom border removed, so the two read as one
   piece of card stock rather than a box with a hat. */
.folders {
  /* 190 rather than 150: auto-fill packs in as many columns as fit, so a
     narrow minimum made six thin columns on a desktop width and
     ellipsised "Competition 2026" down to "Competition 2…". Folder names
     are the whole point of a folder. */
  /* min(190px, 45%) rather than a flat 190: on a phone a flat minimum
     gives ONE folder per row, which turns a shelf label into a
     full-width banner and leaves the tab looking lost on a 358px card.
     The percentage floor keeps two columns there and four on a
     desktop, with no media query. */
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(190px, 45%), 1fr));
  gap: 20px 12px; margin: 22px 0 22px;
}
.folder {
  position: relative; margin-top: 13px; cursor: pointer; font: inherit;
  display: block; text-align: left; color: var(--text);
  background: var(--surface); border: 1px solid var(--line);
  /* Square at the top-left, where the tab meets it. */
  border-radius: 0 12px 12px 12px;
  padding: 13px 13px 12px;
  transition: transform .12s ease, border-color .12s ease, background .12s ease;
}
.folder::before {
  content: ""; position: absolute; left: -1px; top: -13px;
  width: 54%; max-width: 78px;
  /* 14px against a -13px offset, so the tab overlaps the body's own top
     border by exactly 1px and paints over it. Without that the border runs
     straight under the tab and the two read as a box wearing a hat rather
     than as one piece of card stock. */
  height: 14px;
  background: inherit;
  border: 1px solid var(--line); border-bottom: 0;
  border-radius: 9px 10px 0 0;
}
.folder:hover, .folder:focus-visible {
  border-color: var(--accent); transform: translateY(-2px);
}
.folder:hover::before, .folder:focus-visible::before { border-color: var(--accent); }
.folder:focus-visible { outline: none; }

/* Open is a genuinely different state — this folder's contents are what the
   grid below is showing — so it gets fill rather than just a border. */
.folder.is-open { border-color: var(--accent); background: var(--surface-2); }
.folder.is-open::before { border-color: var(--accent); }

.folder .name {
  display: block; font-weight: 700; font-size: 14px; line-height: 1.3;
  /* Long folder names must not reflow the grid — and must not run underneath
     the delete button, which is absolutely positioned and so does not push
     text out of its way. "Leg locks — drilling" ellipsised straight through
     the ✕ before this padding existed. */
  padding-right: 20px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.folder .n {
  display: block; margin-top: 3px;
  color: var(--muted); font-size: 12px; font-weight: 500;
  font-variant-numeric: tabular-nums;
}
/* Delete stays out of the way until wanted: it sits beside the name in a
   card you click to open, and a visible ✕ on every folder invites the
   misclick it is most important to avoid. */
.folder .x {
  position: absolute; top: 8px; right: 7px;
  border: 0; background: none; color: var(--muted); cursor: pointer;
  font-size: 13px; line-height: 1; padding: 3px 4px; border-radius: 6px;
  opacity: 0; transition: opacity .12s ease, color .12s ease;
}
.folder:hover .x, .folder:focus-within .x { opacity: 1; }
.folder .x:hover { color: var(--bad); background: var(--surface-2); }
/* Touch has no hover, so there it is simply always there. */
@media (hover: none) {
  .folder .x { opacity: .65; }
}
/* Unfiled is a computed view, not a shelf somebody made, and it should not
   pass for one — a dashed edge reads as "the leftovers" rather than as a
   folder you could rename or delete. It has no ✕ for the same reason, so it
   also gets the name padding back. */
/* Keeps `background: var(--surface)` and lets ::before inherit it, exactly
   like a real folder. Painting the tab with `--bg` looked right on a flat
   page and wrong here — My rolls has a gradient header behind the strip, so
   an opaque --bg tab read as a dark block punched out of it. */
.folder.is-unfiled { border-style: dashed; }
.folder.is-unfiled::before { border-style: dashed; }
.folder.is-unfiled .name { color: var(--muted); padding-right: 0; }
.folder.is-unfiled.is-open {
  border-style: solid; background: var(--surface-2); border-color: var(--accent);
}
.folder.is-unfiled.is-open::before { border-style: solid; }
.folder.is-unfiled.is-open .name { color: var(--text); }

@media (prefers-reduced-motion: reduce) {
  .folder { transition: none; }
  .folder:hover, .folder:focus-visible { transform: none; }
}

/* ---------- calendar ---------- */
.cal { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); padding: 14px; margin-bottom: 18px; }
.cal-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
.cal-head strong { font-size: 15px; letter-spacing: -0.2px; }
.cal-dow, .cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; }
.cal-dow span { text-align: center; font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: .6px; padding-bottom: 4px; }
.cal-cell {
  aspect-ratio: 1; border-radius: 9px; border: 1px solid transparent;
  background: none; color: var(--muted); font: inherit; font-size: 13px;
  display: grid; place-items: center; position: relative; padding: 0;
}
.cal-cell.pad { visibility: hidden; }
.cal-cell.today { border-color: var(--line); color: var(--text); }
/* A day with footage is the whole point of the grid, so it is the loudest
   thing in it — filled, not merely dotted. */
.cal-cell.has {
  background: rgba(59,168,255,.16); border-color: rgba(59,168,255,.45);
  color: var(--text); font-weight: 700; cursor: pointer;
}
.cal-cell.has:hover { background: rgba(59,168,255,.3); }
.cal-cell.is-open { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); }
.cal-cell .dot {
  position: absolute; bottom: 5px; left: 50%; transform: translateX(-50%);
  font-size: 9px; color: inherit; opacity: .75;
}
.cal-cell:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.day-head { display: flex; align-items: baseline; gap: 10px; margin: 4px 0 12px; }
.day-head h3 { margin: 0; font-size: 16px; }

/* ==========================================================================
   3D belts
   --------------------------------------------------------------------------
   Real geometry, not a picture of geometry: preserve-3d with separate faces,
   so the belt has thickness and the light lands differently on the top edge
   than on the front.

   No library. three.js or model-viewer would be a CDN dependency and hundreds
   of KB on a PWA that has to cold-start on gym wifi — for a shape that is a
   strap, a knot and a tail. The browser already has a 3D transform engine.

   Colour comes from --b, and the bar from --bar, so the same markup renders
   any rank: see belts.py, which is the single source of both.
   ========================================================================== */
.belt3d {
  --b: #2f6fd0;
  --bar: #1a1a1a;
  --deep: rgba(0, 0, 0, .45);
  width: 128px; height: 74px;
  perspective: 520px;
  perspective-origin: 50% 40%;
}
.belt3d-inner {
  position: relative; width: 100%; height: 100%;
  transform-style: preserve-3d;
  transform: rotateX(16deg) rotateY(-26deg);
  transition: transform .5s cubic-bezier(.22, .61, .36, 1);
}
.belt3d:hover .belt3d-inner,
.belt3d:focus-within .belt3d-inner { transform: rotateX(10deg) rotateY(16deg); }
@media (prefers-reduced-motion: reduce) {
  .belt3d-inner, .belt3d:hover .belt3d-inner { transition: none; }
}

/* The strap. Front face plus a top edge, which is where the thickness reads. */
.belt3d .strap {
  /* Thicker than the first cut, which read as a coloured bar with a lump on
     it. A belt is a substantial thing and the depth only shows if there is
     enough face for the light to fall across. */
  position: absolute; left: 4%; top: 36%; width: 92%; height: 30%;
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--b) 88%, white 12%) 0%,
    var(--b) 45%,
    color-mix(in srgb, var(--b) 82%, black 18%) 100%);
  border-radius: 2px;
  transform-style: preserve-3d;
  box-shadow: 0 10px 22px -8px rgba(0, 0, 0, .8);
}
.belt3d .strap::before {           /* top edge, catching the light */
  content: ""; position: absolute; inset: 0 0 auto 0; height: 30%;
  background: linear-gradient(180deg, rgba(255,255,255,.28), transparent);
}
.belt3d .strap::after {            /* the far edge, in shadow */
  content: ""; position: absolute; left: 0; right: 0; bottom: -6px; height: 6px;
  background: var(--deep); transform: rotateX(-88deg); transform-origin: top;
}

/* The knot, sitting proud of the strap — this is what makes it read as a
   belt rather than a coloured bar. */
.belt3d .knot {
  position: absolute; left: 36%; top: 26%; width: 28%; height: 48%;
  background: linear-gradient(145deg,
    color-mix(in srgb, var(--b) 92%, white 18%),
    color-mix(in srgb, var(--b) 78%, black 22%));
  border-radius: 4px;
  transform: translateZ(13px) rotate(-4deg);
  box-shadow: 0 6px 14px -4px rgba(0, 0, 0, .75);
}
.belt3d .knot::after {             /* the crossing-over crease */
  content: ""; position: absolute; inset: 34% 8% auto 8%; height: 12%;
  background: rgba(0, 0, 0, .3); border-radius: 2px;
}

/* The tail hanging from the knot, tilted back so it sits behind the strap. */
.belt3d .tail {
  /* Long enough for the bar and its stripes to be countable. The stubby
     version was geometrically fine and useless — you could not tell a
     two-stripe blue from a four. */
  position: absolute; left: 43%; top: 64%; width: 18%; height: 46%;
  background: linear-gradient(180deg, var(--b),
    color-mix(in srgb, var(--b) 70%, black 30%));
  border-radius: 0 0 3px 3px;
  transform: translateZ(6px) rotate(6deg);
}
/* The rank bar, which is black on the coloured belts and red from black up.
   Getting that backwards is the detail that tells a grappler nobody who
   trains built this. */
.belt3d .tail::after {
  content: ""; position: absolute; inset: auto 0 6% 0; height: 40%;
  background: var(--bar);
}

/* Stripes on the bar, 0-4. */
.belt3d .stripes {
  position: absolute; inset: auto 0 10% 0; height: 32%;
  display: flex; gap: 12%; justify-content: center; align-items: center;
  transform: translateZ(5px);
}
/* Wide enough to actually count at the size these render. Thin hairlines
   looked correct in isolation and vanished on the page. */
.belt3d .stripes i {
  width: 15%; height: 100%; background: #f4f4f4; border-radius: 1px;
  box-shadow: 0 0 2px rgba(0,0,0,.5);
}

/* A row of them on the landing page, replacing the flat rail. */
.belt-shelf {
  position: relative; z-index: 1;
  display: flex; gap: 14px; justify-content: center; flex-wrap: wrap;
  align-items: flex-end; padding: 30px 16px 34px;
}
.belt-shelf .belt3d { width: 138px; height: 84px; }
@media (max-width: 620px) {
  .belt-shelf { gap: 6px; }
  .belt-shelf .belt3d { width: 92px; height: 58px; }
}

/* ---------- expandable progress rows ---------- */
.bar-item { border-bottom: 1px solid var(--line); }
.bar-item:last-child { border-bottom: 0; }
.bar-item > summary { cursor: pointer; list-style: none; padding: 9px 0; }
.bar-item > summary::-webkit-details-marker { display: none; }
.bar-item > summary::after {
  content: "＋"; color: var(--muted); font-size: 13px; margin-left: 8px; flex: none;
}
.bar-item[open] > summary::after { content: "－"; }
.bar-item > summary:hover .bar-label { color: var(--text); }
.bar-item > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.bar-advice { padding: 4px 0 16px; }
.bar-meta {
  color: var(--muted); font-size: 12px; margin: 0 0 10px;
  font-variant-numeric: tabular-nums;
}

/* ---------- the belt picker, in 3D ---------- */
.belt-opt {
  padding: 10px 8px 11px; gap: 4px; min-width: 84px;
}
.belt-opt .belt3d { width: 76px; height: 46px; pointer-events: none; }
/* The picker is a row of small belts, so ease the tilt off — five objects
   all rotated hard reads as clutter rather than depth. */
.belt-opt .belt3d-inner { transform: rotateX(14deg) rotateY(-16deg); }
.belt-opt[aria-pressed="true"] .belt3d-inner { transform: rotateX(10deg) rotateY(8deg); }

/* ---------- personal coaching in Progress ----------
   Visibly distinct from the curated options below it. The library is what
   the advice SHOULD be; this is one reading of one roll, and it must not
   borrow the library's authority by looking identical to it. */
.observed {
  border-left: 2px solid var(--accent);
  background: rgba(59,168,255,.06);
  border-radius: 0 10px 10px 0; padding: 11px 13px; margin: 0 0 12px;
}
.observed-kicker {
  font-size: 10px; text-transform: uppercase; letter-spacing: .8px;
  color: var(--accent); font-weight: 700; margin-bottom: 5px;
}
.observed-mistake { margin: 0 0 4px; font-size: 14px; font-weight: 600; }
.observed-why { margin: 0 0 6px; font-size: 13px; color: var(--muted); }
.observed-fix { margin: 0; font-size: 13px; color: var(--text-2); }
.observed-fix strong { color: var(--ok); }

.takeaways {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 18px 20px; margin: 18px 0 22px;
}
.takeaways h3 { margin: 0 0 12px; font-size: 14px; text-transform: none;
  letter-spacing: -0.2px; color: var(--text); }
.takeaway { padding: 9px 0; border-bottom: 1px solid var(--line); }
.takeaway:last-child { border-bottom: 0; padding-bottom: 0; }
.takeaway-thing { margin: 0 0 3px; font-size: 14px; }
.takeaway-from { margin: 0; font-size: 12px; color: var(--muted); }
.btn.ghost.danger { color: var(--bad); border-color: rgba(255,92,92,.35); }
.btn.ghost.danger:hover { background: rgba(255,92,92,.12); }

/* ---------- selecting rolls to delete ---------- */
.head-actions { display: flex; gap: 8px; }
.select-bar {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  background: var(--surface-2); border: 1px solid var(--accent);
  border-radius: 11px; padding: 10px 14px; margin: 12px 0 16px;
  font-size: 14px;
}
.select-actions { display: flex; gap: 8px; }
.btn.danger-solid { background: var(--bad); color: #2a0000; border: 0; }
.btn.danger-solid[disabled] { opacity: .45; }

.clip-card.is-selectable { cursor: pointer; position: relative; }
.clip-card.is-selectable:hover { border-color: var(--accent); }
.clip-card.is-picked { border-color: var(--bad); box-shadow: 0 0 0 1px var(--bad) inset; }
/* Dimmed rather than hidden: a camera clip you cannot delete should still be
   visible in your list, just plainly not pickable. */
.clip-card.is-locked { opacity: .38; }
.clip-card .pick {
  position: absolute; top: 10px; left: 10px; z-index: 2;
  width: 24px; height: 24px; border-radius: 50%;
  border: 2px solid rgba(255,255,255,.55); background: rgba(0,0,0,.55);
  display: grid; place-items: center; font-size: 13px; font-weight: 700;
  color: #fff;
}
.clip-card.is-picked .pick { background: var(--bad); border-color: var(--bad); }
.clip-card.is-locked .pick { display: none; }

/* ---------- quote ---------- */
.quote-band {
  margin: 56px 0; padding: 40px 0;
  border-top: 1px solid var(--line); border-bottom: 1px solid var(--line);
  position: relative;
}
.quote-band blockquote { margin: 0; max-width: 30ch; }
.quote-band p {
  margin: 0 0 16px; font-size: clamp(22px, 3.4vw, 34px); line-height: 1.22;
  letter-spacing: -0.9px; font-weight: 700;
  background: linear-gradient(180deg, #fff 25%, #9fb0c2 100%);
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
.quote-band cite {
  font-style: normal; font-size: 13px; color: var(--muted); display: block;
}
.quote-band cite em { font-style: italic; color: var(--text-2); }
/* A belt laid across the rule, because a quote band with nothing in it is a
   horizontal line pretending to be a design. */
.quote-band::after {
  content: ""; position: absolute; right: 0; top: 50%; transform: translateY(-50%);
  width: 190px; height: 12px; border-radius: 2px;
  background: linear-gradient(90deg, transparent, #6b46c1 22%, #6b46c1 78%, #1a1a1a 78%);
  opacity: .55;
}
@media (max-width: 860px) { .quote-band::after { display: none; } }

/* ---------- what you get back ---------- */
.tracks-band { margin: 56px 0; }
.tracks {
  margin-top: 22px; background: linear-gradient(180deg, var(--card-hi), var(--card-lo));
  border: 1px solid var(--line); border-radius: var(--radius); padding: 22px 24px;
  position: relative; overflow: hidden;
}
.tracks::before {
  content: ""; position: absolute; inset: 0 0 auto 0; height: 1px;
  background: linear-gradient(90deg, transparent, rgba(255,255,255,.12), transparent);
}
.track {
  display: grid; grid-template-columns: 168px 1fr 44px; gap: 14px;
  align-items: center; padding: 9px 0;
}
.track-label { font-size: 14px; color: var(--text-2); }
.track-bar { height: 9px; border-radius: 5px; background: rgba(255,255,255,.05); overflow: hidden; }
.track-bar i {
  display: block; height: 100%; border-radius: 5px;
  background: linear-gradient(90deg, var(--accent), #1d6fd0);
}
.track-bar i.top { background: linear-gradient(90deg, var(--ok), #1f9c72); }
.track-bar i.bottom { background: linear-gradient(90deg, var(--warn), #c07a06); }
.track-val {
  text-align: right; font-size: 13px; color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.tracks-note {
  margin: 14px 0 0; padding-top: 14px; border-top: 1px solid var(--line);
  color: var(--muted); font-size: 13px;
}
@media (max-width: 620px) { .track { grid-template-columns: 118px 1fr 40px; gap: 10px; } }

/* ==========================================================================
   The room the app sits in
   --------------------------------------------------------------------------
   Tinted by the member's BELT. A blue-grey gradient is what every dark app
   looks like; rank is the one piece of identity this product already knows
   and the most recognisable thing in the sport, so a purple belt's app is
   not the same room as a white belt's.

   Still restrained: this sits behind a grid of thumbnails and a table of
   numbers somebody came to read, and must not compete with either.
   ========================================================================== */

/* Fallback first, belts after — a member with no belt set is a real state,
   not an error, and gets the house blue. */
body:has(.topbar) { --room: var(--accent); }
body:has(.mat-bg[data-belt="white"])  { --room: #d9d2c5; }
body:has(.mat-bg[data-belt="blue"])   { --room: #2f6fd0; }
body:has(.mat-bg[data-belt="purple"]) { --room: #8257e0; }
body:has(.mat-bg[data-belt="brown"])  { --room: #9a6a3c; }
/* A black belt tints GREY, because black on black is nothing at all. */
body:has(.mat-bg[data-belt="black"])  { --room: #8c93a0; }
body:has(.mat-bg[data-belt="coral"])  { --room: #e2554e; }
body:has(.mat-bg[data-belt="red"])    { --room: #c8353f; }

/* THE BELT WASH HAS TO STAY UNDER THE CARDS, and the first version of this
   restyle did not. Raising the card colour while leaving a 34% belt tint
   glowing over the top third of the page produced a card at rgb(25,31,39)
   sitting on a page painting rgb(30,41,50) — the CARD WAS DARKER THAN THE
   PAGE, measured, which is why it read as a dent rather than a tile.

   So the tint is dimmer and shorter: it sits behind the topbar and the tabs,
   which is where a light source at the top of a room would be, and it has
   faded before the first row of cards. The belt still colours the room; it
   just no longer competes with the content. */
body:has(.topbar)::before {
  background:
    radial-gradient(64% 30% at 50% 0%,
      color-mix(in srgb, var(--room) 17%, transparent), transparent 70%),
    radial-gradient(130% 60% at 50% 118%,
      color-mix(in srgb, var(--room) 7%, transparent), transparent 60%),
    linear-gradient(180deg, var(--wash-hi) 0%, var(--wash-mid) 30%, var(--bg) 66%);
}

.mat-bg {
  position: fixed; inset: 0 0 auto 0; height: 62vh; min-height: 380px;
  z-index: -1; pointer-events: none;
  color: var(--room, var(--accent));
  /* Fades out before it reaches the content, so the lines never run under
     text — the structure is atmosphere, not a grid to read against. */
  mask-image: linear-gradient(180deg, #000 0%, #000 34%, transparent 88%);
  -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 34%, transparent 88%);
}
.mat-bg svg { width: 100%; height: 100%; display: block; }

/* ---------- identifying yourself is a moment, not a mode ---------- */
.people { transition: opacity .35s ease; }
/* Once answered the layer leaves. It is not hidden outright, so the fade
   reads as "done" rather than as the target blinking out of existence. */
.people.is-done { opacity: 0; pointer-events: none; }
@media (prefers-reduced-motion: reduce) {
  .people { transition: none; }
}

/* The confirmation: the outline snaps to green and pulses once. */
.person.is-confirming {
  border-color: var(--ok); background: rgba(52,211,153,.28);
  animation: confirm-pulse .5s ease-out;
}
@keyframes confirm-pulse {
  0%   { box-shadow: 0 0 0 0 rgba(52,211,153,.65); }
  100% { box-shadow: 0 0 0 16px rgba(52,211,153,0); }
}
.confirm-label {
  position: absolute; left: 50%; top: -10px; transform: translate(-50%, -100%);
  background: var(--ok); color: #04231a; font-size: 11px; font-weight: 800;
  letter-spacing: .3px; padding: 3px 8px; border-radius: 999px; white-space: nowrap;
}
.pin.is-confirming { animation: confirm-pulse .5s ease-out; }

/* ---------- the identify banner ----------
   Sits over a PLAYING round, so it has to be readable against moving video
   without hiding it: bottom-anchored, blurred plate, never full-width. */
.id-banner {
  position: absolute; left: 50%; bottom: 14px; transform: translateX(-50%);
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  justify-content: center; max-width: min(92%, 560px);
  padding: 10px 14px; border-radius: 12px;
  background: rgba(9, 12, 18, .82); backdrop-filter: blur(8px);
  border: 1px solid rgba(255,255,255,.14);
  color: #f2f5f8; font-size: 14px; font-weight: 600; line-height: 1.35;
  text-align: center; pointer-events: auto;
  animation: id-banner-in .22s ease-out;
}
@keyframes id-banner-in {
  from { opacity: 0; transform: translate(-50%, 8px); }
  to   { opacity: 1; transform: translate(-50%, 0); }
}
.id-banner-text { flex: 1 1 auto; min-width: 180px; }

/* The last call stops the picture, so it earns a stronger treatment than the
   passive countdown — and the declined state is a receipt, not a prompt. */
.id-banner.is-lastcall { border-color: var(--ok); background: rgba(9,12,18,.92); }
.id-banner.is-confirm  { border-color: var(--ok); }
.id-banner.is-declined {
  background: rgba(9,12,18,.7); font-weight: 500; font-size: 13px;
  color: #c9d1d9; border-color: rgba(255,255,255,.10);
}

.id-btn {
  font: inherit; font-size: 13px; font-weight: 700; cursor: pointer;
  padding: 7px 13px; border-radius: 999px; white-space: nowrap;
  border: 1px solid rgba(255,255,255,.22);
  background: transparent; color: #f2f5f8;
}
.id-btn.primary {
  background: var(--ok); border-color: var(--ok); color: #04231a;
}
.id-btn:hover { filter: brightness(1.1); }
.id-btn:focus-visible { outline: 2px solid var(--ok); outline-offset: 2px; }

/* Once declined the layer stops soliciting taps, but stays tappable —
   "won't be analysed" with no way to fix it is a dead end. */
.people.is-declined .person { opacity: .28; }
.people.is-declined .person:hover { opacity: .8; }
@media (prefers-reduced-motion: reduce) {
  .id-banner { animation: none; }
}
@media (prefers-reduced-motion: reduce) {
  .person.is-confirming, .pin.is-confirming { animation: none; }
}

/* ---------- week to week ----------
   Sits ABOVE the all-time breakdown: the tab is opened to ask "am I getting
   better?", and the aggregate answers a different question underneath. */
.trend { margin: 0 0 26px; }
.trend .section-head { margin-bottom: 10px; }
.trend h3 { margin: 0; font-size: 16px; }
.week {
  display: grid; grid-template-columns: 90px 1fr auto; gap: 10px 12px;
  align-items: center; padding: 9px 0;
  border-bottom: 1px solid var(--line);
}
.week-when { font-size: 13px; font-weight: 700; color: var(--text); }
.week-bar {
  height: 9px; border-radius: 999px; background: var(--surface);
  border: 1px solid var(--line); overflow: hidden;
}
/* The fill is time UNDERNEATH — shorter is better, and the label beside it
   says so, because a bar nobody can orient is worse than no bar. */
.week-fill {
  height: 100%; border-radius: 999px;
  background: linear-gradient(90deg, #f59e0b, #ef4444);
}
.week-num { font-size: 12px; font-weight: 700; white-space: nowrap; }
.week-meta {
  grid-column: 2 / -1; font-size: 12px; color: var(--muted);
}
.changes { list-style: none; margin: 14px 0 0; padding: 0; }
.change {
  display: flex; gap: 9px; align-items: baseline;
  padding: 7px 0; font-size: 13.5px; line-height: 1.45;
}
.change-dot { font-size: 11px; }
.change.good .change-dot { color: var(--ok); }
.change.bad  .change-dot { color: #ef4444; }
.change.flat { color: var(--muted); }

/* ---------- lifetime + grain switcher ---------- */
.lifetime { display: flex; gap: 10px; flex-wrap: wrap; margin: 0 0 20px; }
.life {
  flex: 1 1 130px; padding: 12px 14px; border-radius: 12px;
  background: var(--surface); border: 1px solid var(--line);
}
.life span { display: block; font-size: 22px; font-weight: 800; line-height: 1.1; }
.life label { display: block; margin-top: 3px; font-size: 11.5px; color: var(--muted); }
/* Gi / no-gi / both. Shaped like the time-scale switch below it, because it
   does the same kind of thing — reframes the numbers rather than navigating
   anywhere — and two different-looking controls doing one job is how people
   miss one of them. */
.attire-pick { display: flex; gap: 4px; margin: 4px 0 18px; }
.attire {
  background: var(--surface); border: 1px solid var(--line); color: var(--muted);
  font: inherit; font-size: 13px; font-weight: 600; cursor: pointer;
  padding: 6px 14px; border-radius: 8px;
}
.attire:hover { color: var(--text); }
.attire.is-on { background: var(--accent); border-color: var(--accent);
  color: var(--accent-ink); }
.attire:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The same control, smaller, inside a class row. */
.attire-pick.sm { margin: 0 0 12px; padding: 0 14px; }
.attire-pick.sm .attire { font-size: 12px; padding: 4px 10px; }
.attire-tag {
  margin-left: 8px; font-size: 11px; font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase; color: var(--muted);
  border: 1px solid var(--line); border-radius: 4px; padding: 1px 6px;
}

.grains { display: flex; gap: 4px; }
.grain {
  font: inherit; font-size: 12px; font-weight: 700; cursor: pointer;
  padding: 5px 11px; border-radius: 999px; color: var(--muted);
  background: transparent; border: 1px solid var(--line);
}
.grain.is-on { background: var(--surface); color: var(--text); border-color: var(--muted); }
.grain:focus-visible { outline: 2px solid var(--ok); outline-offset: 2px; }

/* A single-scale switcher is noise, and a hidden grain must actually go. */
.grain[hidden] { display: none; }

/* ---------- training partner record ----------
   Every number here comes from the validated layer. It is rendered as plain
   sentences rather than a dashboard because two people read it together and
   argue about it, and a sentence is what survives being argued with. */
.partners { margin: 0 0 26px; }
.partners h3 { margin: 0; font-size: 16px; }
.partner-chips { display: flex; gap: 8px; flex-wrap: wrap; margin: 10px 0 0; }
.pchip {
  display: flex; flex-direction: column; align-items: flex-start; gap: 1px;
  cursor: pointer; font: inherit; text-align: left; color: var(--text);
  background: var(--surface); border: 1px solid var(--line);
  border-left: 3px solid var(--muted);
  border-radius: 10px; padding: 8px 13px;
}
/* The belt is the fastest way to recognise somebody you train with. */
.pchip[data-belt="white"]  { border-left-color: #d9d2c5; }
.pchip[data-belt="blue"]   { border-left-color: #2f6fd0; }
.pchip[data-belt="purple"] { border-left-color: #8257e0; }
.pchip[data-belt="brown"]  { border-left-color: #9a6a3c; }
.pchip[data-belt="black"]  { border-left-color: #24262b; }
.pchip:hover { border-color: var(--accent); }
.pchip.is-on { background: var(--surface-2); border-color: var(--accent); }
.pchip-name { font-weight: 700; font-size: 13.5px; }
.pchip-n { color: var(--muted); font-size: 11.5px; }

.partner-record {
  margin-top: 14px; padding: 16px 18px;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius);
}
.prec-head { display: flex; flex-direction: column; gap: 2px; margin-bottom: 12px; }
.prec-head strong { font-size: 16px; }
.prec-line { margin: 0 0 8px; font-size: 14px; line-height: 1.5; }
.prec-line strong { font-size: 15px; }
/* Not a warning — a caveat. The numbers below it are real; what is missing
   is the right to draw conclusions from them. */
.prec-thin {
  margin: 0 0 12px; padding: 9px 12px; border-radius: 9px;
  background: var(--surface-2); color: var(--muted);
  font-size: 12.5px; line-height: 1.5;
}
.prec-subs { margin-top: 14px; }
.prec-subs h4 {
  margin: 0 0 6px; font-size: 11.5px; text-transform: uppercase;
  letter-spacing: .7px; color: var(--muted);
}
.prec-subs ul { list-style: none; margin: 0; padding: 0; }
.prec-subs li {
  display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap;
  padding: 5px 0; font-size: 13.5px; border-bottom: 1px solid var(--line);
}
.prec-subs li:last-child { border-bottom: 0; }
.prec-subs.bad li strong { color: #f0a4a4; }
.prec-subs.good li strong { color: var(--ok); }
.prec-subs li span { color: var(--muted); font-size: 12.5px; }
.prec-late {
  background: rgba(245,158,11,.16); color: #f2b45a !important;
  border-radius: 999px; padding: 1px 8px; font-size: 11px !important;
  font-weight: 700;
}

/* ---------- tournament scoreboard ----------
   A LENS, not a result. A class round has no score and no winner, so this
   never declares one — it leads with what it is, and every point below is a
   button that scrubs to the moment it happened. */
.scoreboard {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 14px 16px; margin-bottom: 16px;
}
.sb-head { display: flex; flex-direction: column; gap: 1px; margin-bottom: 10px; }
.sb-title { font-size: 11.5px; font-weight: 800; letter-spacing: .8px;
            text-transform: uppercase; color: var(--muted); }
.sb-line { display: flex; gap: 10px; }
.sb-side {
  flex: 1; text-align: center; padding: 10px 8px; border-radius: 11px;
  background: var(--surface-2); border: 1px solid var(--line);
}
/* Ahead, not "winning" — the distinction is the whole framing. */
.sb-side.is-ahead { border-color: var(--accent); }
.sb-n { display: block; font-size: 30px; font-weight: 800; line-height: 1; }
.sb-side label { display: block; margin-top: 4px; font-size: 11px;
                 color: var(--muted); text-transform: uppercase; letter-spacing: .6px; }
.sb-events { list-style: none; margin: 12px 0 0; padding: 0; }
.sb-events li {
  display: flex; align-items: baseline; gap: 9px; padding: 5px 0;
  font-size: 13.5px; border-bottom: 1px solid var(--line);
}
.sb-events li:last-child { border-bottom: 0; }
.sb-t {
  font: inherit; font-size: 12px; cursor: pointer; padding: 1px 6px;
  border-radius: 6px; background: var(--surface-2);
  border: 1px solid var(--line); color: var(--muted);
}
.sb-t:hover { color: var(--text); border-color: var(--accent); }
.sb-what { flex: 1; }
.sb-p { font-weight: 800; font-variant-numeric: tabular-nums; }
.sb-events li.good .sb-p { color: var(--ok); }
.sb-events li.bad .sb-p { color: #f0a4a4; }
.sb-caveats { margin-top: 12px; }
.sb-caveats summary {
  cursor: pointer; color: var(--muted); font-size: 12px; list-style: none;
}
.sb-caveats summary::-webkit-details-marker { display: none; }
.sb-caveats summary::before { content: "＋ "; }
.sb-caveats[open] summary::before { content: "－ "; }
.sb-caveats ul { margin: 7px 0 0; padding-left: 18px; }
.sb-caveats li { color: var(--muted); font-size: 12px; line-height: 1.55; padding: 2px 0; }

/* ---------- player: a bit more modern ----------
   The dialog is the screen people spend the most time on, so it earns
   hierarchy: a real header, a video that stays put while the breakdown
   scrolls under it, and controls that read as controls. */
#player .player-head {
  display: flex; align-items: center; gap: 12px;
  padding: 14px 16px; border-bottom: 1px solid var(--line);
  background: var(--surface);
  position: sticky; top: 0; z-index: 6;
}
#player .player-head strong { font-size: 16px; letter-spacing: -0.3px; }
#player .player-head .sub { color: var(--muted); font-size: 12.5px; }
#player .player-head .spacer { margin-left: auto; }

/* The video stays visible while the timeline scrolls, because every entry
   below it is a seek into the video above — scrolling away from the thing
   you are seeking is the wrong way round. */
#player .stage {
  /* ABOVE everything that scrolls under it. It shared z-index 2 with the
     sticky subtabs, and on a tie the LATER element paints on top — so the
     tab bar slid across the middle of the playing video. Reported from a
     phone; reproduced at 430px. */
  position: sticky; top: 0; z-index: 5; background: #000;
}
#player .stage video { display: block; width: 100%; }

.player-controls { gap: 8px; padding: 10px 16px; }
.player-controls .icon-btn {
  border-radius: 9px; border: 1px solid var(--line); background: var(--surface-2);
}
.player-controls .icon-btn:hover { border-color: var(--accent); color: var(--text); }
.speeds { border: 1px solid var(--line); }

.subtabs { padding: 0 16px; gap: 4px; position: sticky; z-index: 2;
           background: var(--bg); }
.subtab { padding: 12px 10px; font-size: 13.5px; }
.panel { padding: 16px; }

@media (max-width: 620px) {
  /* On a phone the header sticking as well costs a quarter of the picture. */
  #player .player-head { position: static; }
  .marker-actions { width: 100%; }
  .marker-actions select { flex: 1; min-width: 0; }
}

/* Cap the picture so the breakdown under it is reachable without scrolling
   past a full screen of video. Everything below is a seek INTO this video,
   so both have to be on screen at once to be usable at all. */
#player .stage { max-height: 52vh; display: flex; }
#player .stage video { max-height: 52vh; object-fit: contain; background: #000; }
@media (max-width: 620px) {
  #player .stage, #player .stage video { max-height: 42vh; }
}

/* ---------- the coach's read ----------
   Generated, validated and stored on every analysed roll since the coaching
   pass existed — and never shown on the round until now. It belongs here
   rather than in a report because every timestamp is a seek away from the
   thing being described. */
.coachread {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 16px 18px; margin-bottom: 16px;
}
.cr-kicker {
  display: block; font-size: 11px; font-weight: 800; letter-spacing: .9px;
  text-transform: uppercase; color: var(--muted); margin-bottom: 4px;
}
.cr-head { margin-bottom: 14px; }
.cr-head strong { font-size: 16px; line-height: 1.4; letter-spacing: -0.2px; }
.cr-sec { margin-top: 16px; }
.cr-sec h4 {
  margin: 0 0 8px; font-size: 11px; font-weight: 800; letter-spacing: .9px;
  text-transform: uppercase; color: var(--muted);
}
/* Every timestamp is a button, because a claim you can go and look at is a
   different kind of claim. */
.cr-t {
  flex: 0 0 auto; font: inherit; font-size: 11.5px; cursor: pointer;
  padding: 2px 7px; border-radius: 6px; background: var(--surface-2);
  border: 1px solid var(--line); color: var(--muted);
}
.cr-t:hover { color: var(--text); border-color: var(--accent); }
.cr-audit { list-style: none; margin: 0; padding: 0; }
.cr-audit li {
  display: flex; gap: 10px; padding: 8px 0;
  border-bottom: 1px solid var(--line);
}
.cr-audit li:last-child { border-bottom: 0; }
.cr-audit strong { display: block; font-size: 13.5px; }
.cr-audit p { margin: 3px 0 0; font-size: 13px; color: var(--muted); line-height: 1.5; }
.cr-line { display: flex; gap: 10px; align-items: baseline; }
.cr-line strong { font-size: 13.5px; line-height: 1.45; }
.cr-flaw, .cr-fix {
  padding: 10px 0; border-bottom: 1px solid var(--line);
}
.cr-flaw:last-child, .cr-fix:last-child { border-bottom: 0; }
.cr-flaw strong { color: #f0a4a4; }
.cr-fix strong { color: var(--ok); }
/* The WHY is the half that makes it coaching rather than scolding. */
/* Indented under the heading line, not under a time chip — flaws and
   corrections no longer carry one. */
.cr-why {
  margin: 5px 0 0 2px; font-size: 12.5px; color: var(--muted);
  line-height: 1.55; border-left: 2px solid var(--line); padding-left: 10px;
}
.cr-opt {
  margin: 6px 0 0 2px; display: flex; gap: 7px; font-size: 12.5px;
  color: var(--text); line-height: 1.5;
}
.cr-bullet { color: var(--ok); }
.cr-one {
  margin-top: 18px; padding: 12px 14px; border-radius: 11px;
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
}
.cr-one p { margin: 0; font-size: 14px; font-weight: 600; line-height: 1.5; }
/* Shown, not hidden. A read with no gaps on a five-person mat is a read that
   was not paying attention. */
.cr-gaps { margin-top: 14px; }
.cr-gaps summary {
  cursor: pointer; color: var(--muted); font-size: 12px; list-style: none;
}
.cr-gaps summary::-webkit-details-marker { display: none; }
.cr-gaps summary::before { content: "＋ "; }
.cr-gaps[open] summary::before { content: "－ "; }
.cr-gaps ul { margin: 7px 0 0; padding-left: 18px; }
.cr-gaps li { color: var(--muted); font-size: 12px; line-height: 1.55; }

/* ---------- where this round went ----------
   Proportional, so a glance answers "was I underneath the whole time?"
   without reading nine rows. */
.track { margin: 0 0 16px; }
.track-head { display: flex; align-items: baseline; justify-content: space-between;
              gap: 12px; margin-bottom: 7px; }
.track-head .cr-kicker { margin: 0; }
.track-num { font-size: 12.5px; font-weight: 700; color: var(--text); }
.track-bar {
  display: flex; height: 16px; border-radius: 999px; overflow: hidden;
  border: 1px solid var(--line); background: var(--surface-2);
}
.track-seg { border: 0; padding: 0; cursor: pointer; min-width: 2px; }
.track-seg.is-bottom  { background: linear-gradient(180deg, #f59e0b, #ef4444); }
.track-seg.is-top     { background: linear-gradient(180deg, #34d399, #10b981); }
.track-seg.is-neutral { background: var(--line); }
.track-seg:hover { filter: brightness(1.25); }

/* The position rows collapse now: the curated library is one tap away
   rather than a wall under the coach's read. */
.tl-item > .tl-head { list-style: none; }
.tl-item > .tl-head::-webkit-details-marker { display: none; }
.tl-item .tl-label::after {
  content: "›"; margin-left: 7px; color: var(--muted);
  display: inline-block; transition: transform .15s ease;
}
.tl-item[open] .tl-label::after { transform: rotate(90deg); }
.tl-time { cursor: pointer; padding: 2px 7px; border-radius: 6px;
  background: var(--surface-2); border: 1px solid var(--line);
  color: var(--muted); font: inherit; font-size: 11.5px; }
.tl-time:hover { color: var(--text); border-color: var(--accent); }

/* ---------- round by round (Progress) ---------- */
.rounds { margin: 0 0 26px; }
.rounds h3 { margin: 0; font-size: 16px; }
.round-card {
  display: grid; grid-template-columns: auto 1fr auto; gap: 4px 14px;
  align-items: center; width: 100%; text-align: left; font: inherit;
  cursor: pointer; color: var(--text);
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 12px; padding: 12px 14px; margin-top: 10px;
}
.round-card:hover { border-color: var(--accent); }
.rc-score {
  font-variant-numeric: tabular-nums; font-weight: 800; font-size: 15px;
  padding: 6px 10px; border-radius: 9px; background: var(--surface-2);
  border: 1px solid var(--line); white-space: nowrap;
}
.rc-score.is-ahead { border-color: var(--accent); }
.rc-when { font-weight: 700; font-size: 13.5px; }
.rc-meta { grid-column: 2 / -1; color: var(--muted); font-size: 12px; }
.rc-bar { grid-column: 1 / -1; display: flex; height: 6px; border-radius: 999px;
  overflow: hidden; background: var(--surface-2); margin-top: 4px; }
.rc-fill { background: linear-gradient(90deg, #f59e0b, #ef4444); }

/* ---------- scoreboard: advantages, finishes, and why ---------- */
.sb-adv {
  display: block; margin-top: 2px; font-size: 11px; color: var(--muted);
  font-variant-numeric: tabular-nums;
}
/* A finish is the result of the match, so it sits above the point lines
   rather than among them — and never inside the tally, because a submission
   ends a match rather than scoring in it. */
.sb-finish {
  display: flex; gap: 10px; align-items: baseline; margin-top: 12px;
  padding: 10px 12px; border-radius: 11px;
  background: color-mix(in srgb, var(--ok) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--ok) 32%, transparent);
}
.sb-finish.bad {
  background: rgba(239,68,68,.10); border-color: rgba(239,68,68,.30);
}
.sb-finish strong { font-size: 14px; }
.sb-finish .sb-why { margin-top: 3px; }
.sb-row { display: flex; align-items: baseline; gap: 9px; flex: 1; }
.sb-explain > summary { list-style: none; cursor: pointer; }
.sb-explain > summary::-webkit-details-marker { display: none; }
.sb-explain > summary .sb-what::after {
  content: "›"; margin-left: 6px; color: var(--muted);
  display: inline-block; transition: transform .15s ease;
}
.sb-explain[open] > summary .sb-what::after { transform: rotate(90deg); }
.sb-why {
  margin: 5px 0 2px 46px; font-size: 12.5px; color: var(--muted);
  line-height: 1.55;
}
.sb-events li.good .sb-p, .sb-finish.good strong { color: var(--ok); }
.sb-events li.bad .sb-p, .sb-finish.bad strong { color: #f0a4a4; }

/* A match has a result; a class round is only being measured against a rule
   set it was never played under. */
.sb-ends {
  display: inline-block; margin-left: 8px; padding: 1px 8px;
  border-radius: 999px; font-size: 10.5px; font-weight: 800;
  letter-spacing: .5px; text-transform: uppercase;
  background: var(--ok); color: #04231a;
}
.sb-finish.bad .sb-ends { background: #f0a4a4; color: #2a0d0d; }

/* ---------- the coach's short list ----------
   A few names, never a table. The constraint is the feature: a coach with
   forty rows opens it once. */
.mat-report { margin: 0 0 30px; }
.mat-card {
  padding: 14px 16px; margin-top: 10px;
  background: var(--surface); border: 1px solid var(--line);
  border-left: 3px solid var(--line); border-radius: 12px;
  display: flex; flex-direction: column; gap: 6px; align-items: flex-start;
}
.mat-card.bad  { border-left-color: var(--warn, #f59e0b); }
.mat-card.good { border-left-color: var(--ok); }
.mat-head { display: flex; align-items: baseline; gap: 9px; flex-wrap: wrap; width: 100%; }
.mat-head strong { font-size: 15px; }
.mat-kind {
  font-size: 10.5px; font-weight: 800; letter-spacing: .5px;
  text-transform: uppercase; padding: 2px 8px; border-radius: 999px;
}
.mat-kind.bad  { background: rgba(245,158,11,.16); color: #f2b45a; }
.mat-kind.good { background: rgba(52,211,153,.16); color: var(--ok); }
.mat-rounds { margin-left: auto; font-size: 12px; color: var(--muted); }
.mat-headline { margin: 0; font-size: 14px; font-weight: 600; }
.mat-detail { margin: 0; font-size: 13px; color: var(--muted); line-height: 1.5; }
.mat-card .btn { margin-top: 4px; }

/* The curriculum line. Curated library text, not generated — it cannot be
   wrong about what an escape is. */
.pattern {
  margin-top: 16px; padding: 14px 16px; border-radius: 12px;
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 28%, transparent);
}
.pattern-head { margin: 0; font-size: 14.5px; font-weight: 600; line-height: 1.5; }
.pattern-fix { margin: 6px 0 0; font-size: 13px; color: var(--muted); line-height: 1.55; }

/* ---------- everyone (the page behind the short list) ---------- */
.back { margin-bottom: 14px; }
.cls-row {
  display: grid;
  grid-template-columns: minmax(0, 1.4fr) minmax(90px, 1fr) auto;
  gap: 4px 14px; align-items: center;
  padding: 12px 14px; margin-bottom: 8px;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 11px;
}
/* Under the noise floor: present, and visibly not a trend. */
.cls-row.is-thin { opacity: .72; }
.cls-who { display: flex; align-items: baseline; gap: 9px; min-width: 0; }
.cls-who strong {
  font-size: 14.5px; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; border-left: 3px solid transparent; padding-left: 7px;
}
.cls-who strong[data-belt="white"]  { border-left-color: #d9d2c5; }
.cls-who strong[data-belt="blue"]   { border-left-color: #2f6fd0; }
.cls-who strong[data-belt="purple"] { border-left-color: #8257e0; }
.cls-who strong[data-belt="brown"]  { border-left-color: #9a6a3c; }
.cls-who strong[data-belt="black"]  { border-left-color: #24262b; }
.cls-rounds { font-size: 12px; color: var(--muted); white-space: nowrap; }
.cls-bar {
  height: 7px; border-radius: 999px; background: var(--surface-2);
  border: 1px solid var(--line); overflow: hidden;
}
.cls-fill { height: 100%; background: linear-gradient(90deg, #f59e0b, #ef4444); }
.cls-num { display: flex; align-items: baseline; gap: 8px; white-space: nowrap; }
.cls-share { font-size: 13.5px; font-weight: 700; font-variant-numeric: tabular-nums; }
.cls-delta { font-size: 12px; font-weight: 700; font-variant-numeric: tabular-nums; }
.cls-delta.good { color: var(--ok); }
.cls-delta.bad  { color: #f0a4a4; }
.cls-meta { grid-column: 1 / -1; font-size: 12px; color: var(--muted); }

/* ---------- team stats ----------
   Its own tab now. A coach checking how the gym is doing is a thing they
   come here to do, not a detour from somebody else's question. */
.team-summary {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: 10px; margin: 18px 0 20px;
}
.tsum {
  padding: 12px 14px; border-radius: 12px;
  background: var(--surface); border: 1px solid var(--line);
}
.tsum span {
  display: block; font-size: 21px; font-weight: 800; line-height: 1.1;
  font-variant-numeric: tabular-nums;
}
.tsum label { display: block; margin-top: 3px; font-size: 11.5px; color: var(--muted); }

.cls-item {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 11px; margin-bottom: 8px; overflow: hidden;
}
.cls-item.is-thin { opacity: .72; }
.cls-item > .cls-row { list-style: none; cursor: pointer; }
.cls-item > .cls-row::-webkit-details-marker { display: none; }
.cls-item[open] { border-color: var(--accent); }
.cls-who strong::after {
  content: "›"; margin-left: 7px; color: var(--muted);
  display: inline-block; transition: transform .15s ease;
}
.cls-item[open] .cls-who strong::after { transform: rotate(90deg); }

/* the opened detail */
.cls-detail {
  padding: 2px 14px 14px; display: flex; flex-direction: column; gap: 14px;
  border-top: 1px solid var(--line); margin-top: 10px; padding-top: 14px;
}
.cls-block h4 {
  margin: 0 0 7px; font-size: 11px; font-weight: 800; letter-spacing: .8px;
  text-transform: uppercase; color: var(--muted);
}
.cls-pos {
  display: grid; grid-template-columns: minmax(0, 150px) 1fr auto;
  gap: 10px; align-items: center; padding: 3px 0; font-size: 13px;
}
.cls-pos-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cls-pos-bar {
  height: 6px; border-radius: 999px; background: var(--surface-2);
  border: 1px solid var(--line); overflow: hidden;
}
.cls-pos-fill { height: 100%; background: linear-gradient(90deg, #f59e0b, #ef4444); }
.cls-pos-n { font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }

.cls-facts {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(92px, 1fr));
  gap: 8px;
}
.cls-fact {
  padding: 9px 10px; border-radius: 9px;
  background: var(--surface-2); border: 1px solid var(--line);
}
.cls-fact span {
  display: block; font-size: 15px; font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.cls-fact label { display: block; margin-top: 2px; font-size: 10.5px; color: var(--muted); }

.cls-subs { list-style: none; margin: 0; padding: 0; }
.cls-subs li {
  display: flex; justify-content: space-between; gap: 10px;
  padding: 5px 0; font-size: 13px; border-bottom: 1px solid var(--line);
}
.cls-subs li:last-child { border-bottom: 0; }
.cls-subs strong { color: #f0a4a4; text-transform: capitalize; }
.cls-subs span { color: var(--muted); font-variant-numeric: tabular-nums; }

/* ---------- My stats: sub-tabs outside the player ----------
   The player's subtabs live inside a dialog and are padded and sticky for
   that. In a normal view they need neither — and the panel padding would
   indent the content away from the sections above it. */
.stats-subtabs {
  padding: 0; margin: 4px 0 0; position: static; background: none;
}
.stats-subtabs + .panel, .stats-subtabs ~ .panel { padding: 18px 0 0; }

/* ---------- the drill deck ----------
   Deliberately quieter than the rest of the app. This is a place to practise,
   not a place to be congratulated: no streaks, no confetti, no score anybody
   else can see. The one strong colour is reserved for the right answer. */
.game-body { padding-top: 4px; }

.game-empty { border: 1px dashed var(--line); border-radius: var(--radius);
  padding: 22px 18px; text-align: center; background: var(--surface); }
.game-empty-title { margin: 0 0 6px; font-weight: 600; font-size: 15px; }
.game-empty .hint { margin: 0 auto; max-width: 34em; }
.game-empty + .btn { margin-top: 12px; width: auto; background: var(--surface-2);
  color: var(--text); border-color: var(--line); }

.q-count { font-size: 12px; color: var(--muted); text-transform: uppercase;
  letter-spacing: .06em; margin: 0 0 10px; }

.q-card { border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface); padding: 16px; }
.q-prompt { margin: 0 0 14px; font-size: 16px; font-weight: 600;
  line-height: 1.35; }
.q-choices { display: grid; gap: 8px; }
.q-choice { font: inherit; text-align: left; padding: 13px 14px;
  border: 1px solid var(--line); border-radius: 10px; background: var(--surface-2);
  color: var(--text); cursor: pointer; }
.q-choice:hover:not([disabled]) { border-color: var(--accent); }
.q-choice[disabled] { cursor: default; opacity: .55; }
/* The right answer is always shown, whether or not it was picked — being told
   only that you were wrong teaches nothing. */
.q-choice.is-right { opacity: 1; border-color: var(--ok);
  background: rgba(52,211,153,.14); color: var(--text); }
.q-choice.is-wrong { opacity: 1; border-color: var(--bad);
  background: rgba(255,92,92,.12); }

.q-result { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--line);
  font-size: 14.5px; }
.q-result.is-right strong { color: var(--ok); }
.q-result.is-wrong strong { color: var(--warn); }
.q-result .hint { margin: 8px 0 0; }
.q-next { margin-top: 14px; width: auto; background: var(--accent);
  color: var(--accent-ink); }

/* ---------- cameras (coach only) ---------- */
.cam { border: 1px solid var(--line); border-radius: 11px; padding: 12px 13px;
  margin-bottom: 9px; background: var(--surface-2); }
.cam-head { display: flex; align-items: center; gap: 10px; }
.cam-name { font-size: 14.5px; flex: 1; min-width: 0; }
.cam-state { font-size: 10.5px; text-transform: uppercase; letter-spacing: .07em;
  padding: 3px 8px; border-radius: 20px; border: 1px solid; white-space: nowrap; }
/* "Not connected" is the state a coach must act on, so it is the loud one —
   a listener waiting in accept() looks healthy and is not. */
.cam-state.is-waiting { color: var(--warn); border-color: rgba(255,176,32,.4);
  background: rgba(255,176,32,.1); }
.cam-state.is-ready { color: var(--ok); border-color: rgba(52,211,153,.4);
  background: rgba(52,211,153,.1); }
.cam-state.is-off, .cam-state.is-wired, .cam-state.is-unknown {
  color: var(--muted); border-color: var(--line); }
.cam-detail { margin: 7px 0 0; font-size: 12.5px; color: var(--muted); line-height: 1.45; }
.cam-acts { display: flex; gap: 7px; flex-wrap: wrap; margin-top: 11px; }

.cam-setup { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--line); }
.cam-setup-head { margin: 0 0 9px; font-size: 11px; text-transform: uppercase;
  letter-spacing: .07em; color: var(--muted); font-weight: 700; }
.cam-copy { display: grid; grid-template-columns: 1fr auto; gap: 6px 10px;
  align-items: center; margin-bottom: 8px; }
.cam-copy-label { grid-column: 1 / -1; font-size: 11px; color: var(--muted);
  text-transform: uppercase; letter-spacing: .06em; }
/* Wraps rather than scrolls: a value you cannot see all of is one you cannot
   check against what you typed into the camera. */
.cam-copy-value { font-size: 12.5px; background: var(--bg); border: 1px solid var(--line);
  border-radius: 7px; padding: 8px 10px; word-break: break-all; line-height: 1.4;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--text); }
/* "Standing by" is the normal state for most of the day — recording is
   started by hand when the rolls start — so it must not read as a fault.
   Only "Trouble" is loud. */
.cam-state.is-live { color: var(--ok); border-color: rgba(52,211,153,.4);
  background: rgba(52,211,153,.1); }
.cam-state.is-waiting { color: var(--warn); border-color: rgba(255,176,32,.4);
  background: rgba(255,176,32,.1); }
.cam-state.is-trouble { color: var(--bad); border-color: rgba(255,92,92,.4);
  background: rgba(255,92,92,.1); }
.cam-state.is-standby, .cam-state.is-untested { color: var(--text-2);
  border-color: var(--line); background: var(--surface); }
/* A member who has not consented sees nothing from the gym's cameras, and the
   list alone gives no clue why. */
.cam-consent { border: 1px solid rgba(255,176,32,.4); background: rgba(255,176,32,.08);
  border-radius: 11px; padding: 12px 13px; margin-bottom: 11px; }
.cam-consent strong { display: block; font-size: 14px; margin-bottom: 4px; }
.cam-consent p { margin: 0; font-size: 12.5px; color: var(--text-2); line-height: 1.45; }
.cam-rules { margin: 0; padding-left: 18px; display: grid; gap: 5px; }
.cam-rules li { font-size: 12.5px; color: var(--muted); line-height: 1.45; }

/* ---------- the camera pitch on Upload ----------
   Quieter than the form above it. This is a suggestion for next time, not a
   thing to do now, so it must not compete with the Upload button.

   IT IS `.cam-pitch` AND NOT `.pitch`, AND THAT IS A BUG FIX.

   `.pitch` is the landing page's row of feature cards, declared hundreds of
   lines above as a three-column grid. This block was written later for a
   completely unrelated component, reused the name, and `display: flex` won
   everywhere — so the landing page's cards silently stopped being a grid.
   Measured at 1280px: four cards at 183/174/155/312px with their tops on
   three different lines, where the grid would have given four equal columns.

   Nothing on the landing page said anything was wrong. That is the whole
   hazard of a generic class name in one flat stylesheet, and it had already
   happened once in this file with `.steps`. */
.cam-pitch { display: flex; align-items: center; gap: 14px; margin-top: 16px;
  background: var(--surface); border: 1px solid var(--line); }
.cam-pitch > div { flex: 1; min-width: 0; }
.cam-pitch strong { display: block; font-size: 14.5px; margin-bottom: 3px; }
.cam-pitch p { margin: 0; font-size: 12.5px; color: var(--muted); line-height: 1.45; }
.cam-pitch .btn { flex: none; background: var(--surface-2); color: var(--text);
  border-color: var(--line); }
.cam-pitch .btn:hover { border-color: var(--accent); }
@media (max-width: 520px) {
  .cam-pitch { flex-direction: column; align-items: stretch; }
  .cam-pitch .btn { width: 100%; }
}
/* A button that has to read as a link, because it navigates rather than acts. */
.linkish { background: none; border: 0; padding: 0; font: inherit;
  color: var(--accent); cursor: pointer; text-decoration: underline; }
.linkish:hover { filter: brightness(1.15); }

/* ---------- the add-camera dialog ---------- */
#add-camera { max-width: 440px; }
#add-camera h2 { margin: 0 0 4px; font-size: 20px; }
#add-camera .sub { margin: 0 0 18px; font-size: 13.5px; color: var(--muted); }
#add-camera select {
  width: 100%; font: inherit; padding: 11px 12px; border-radius: 10px;
  background: var(--surface-2); color: var(--text); border: 1px solid var(--line);
  -webkit-appearance: none; appearance: none;
  /* The chevron has to be drawn: stripping the native appearance to match the
     inputs also strips the only affordance that says this opens. */
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238b97a6' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 11px center;
  background-size: 16px; padding-right: 36px;
}
#add-camera select:focus, #add-camera input:focus { outline: 2px solid var(--accent);
  outline-offset: 1px; }
#add-camera #ac-new-folder { margin-top: 8px; }
#add-camera .err { margin: 0 0 12px; }

/* ---------- the file-in-shelf dialog ---------- */
#file-dlg {
  border: 1px solid var(--line); border-radius: 16px; background: var(--surface);
  color: var(--text); max-width: 420px; width: calc(100% - 32px); padding: 24px;
}
/* Darker than the other backdrops on purpose: this one opens ON TOP of the
   player, which is itself a dialog over a dimmed page. At .72 the video
   showed through and the two modals read as one busy screen. */
#file-dlg::backdrop { background: rgba(0,0,0,.6); }
#file-dlg h2 { margin: 0 0 4px; font-size: 20px; letter-spacing: -0.4px; }
#file-dlg .sub { margin: 0 0 18px; font-size: 13.5px; color: var(--muted); }
#file-dlg .err { margin: 0 0 12px; }
#file-dlg .field > label { color: var(--muted); font-weight: 600; }

.shelf-list {
  display: flex; flex-direction: column; gap: 2px;
  /* Somebody with twenty shelves must still be able to reach Save. */
  max-height: 46vh; overflow-y: auto; margin-bottom: 16px;
}
.shelf {
  display: flex; align-items: center; gap: 11px; cursor: pointer;
  padding: 9px 10px; border-radius: 9px; border: 1px solid transparent;
}
.shelf:hover { background: var(--surface-2); }
.shelf:focus-within { border-color: var(--accent); }
.shelf input { width: 17px; height: 17px; flex: none; accent-color: var(--accent); }
.shelf-name {
  flex: 1; font-size: 14px; font-weight: 600; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.shelf-n {
  color: var(--muted); font-size: 12px; font-variant-numeric: tabular-nums;
  flex: none;
}
/* A ticked row is the answer to "where is this filed?", so it reads as set
   rather than merely hovered. */
.shelf:has(input:checked) { background: var(--surface-2); }
.shelf:has(input:checked) .shelf-name { color: var(--accent); }

/* ---------- attempt rates ---------- */
.rates { display: grid; gap: 9px; margin-bottom: 20px; }
.rate { display: grid; grid-template-columns: 8.5em 1fr auto; gap: 10px;
  align-items: center; font-size: 13.5px; }
.rate-name { color: var(--text-2); }
.rate-bar { height: 7px; border-radius: 4px; background: var(--surface-2); overflow: hidden; }
.rate-fill { height: 100%; background: var(--accent); border-radius: 4px; }
.rate-n { font-variant-numeric: tabular-nums; font-size: 12.5px; color: var(--muted);
  white-space: nowrap; }

/* ---------------------------------------------------------------- display
 *
 * HIERARCHY BY WEIGHT, NOT BY WIDTH. This block used to switch headings to a
 * condensed family and then raise every size to compensate for how narrow
 * that made them. With one normal-width family the compensation has to come
 * back out, or everything is simply too big.
 *
 * What a heading gets now is more WEIGHT and slightly tighter tracking —
 * which is how a normal-width face is supposed to be set. Large type at
 * default tracking reads loose; small uppercase type at default tracking
 * reads cramped. Hence negative letter-spacing above ~20px and positive
 * below ~12px, and nothing in between.
 */
h1, h2, h3,
.brand, .brand-word, .hero-title, .section h2, .sec-head,
.stat span, .life span, .sb-n {
  font-family: var(--font-display);
  font-weight: 700;
  letter-spacing: -0.015em;
}
.tab, .subtab { font-weight: 600; letter-spacing: 0; }
/* The small uppercase labels. Tracking OPENS here rather than closing,
   because caps at 11px with no tracking set solid. */
.eyebrow, .stat label, .life label, .sb-side label, .sb-title, .src-tag, .tier {
  font-weight: 600; letter-spacing: .07em;
}

/* Numbers that stack in a column line up, and a heading never leaves one
   word stranded on its own line. Both are free and both are the difference
   between "typeset" and "typed". */
.stat span, .life span, .sb-n, .week-num, .bar-val, .rate-n, .thumb .dur,
.clip-card .meta .line .when {
  font-variant-numeric: tabular-nums;
}
h1, h2, h3, .hero-title, .section h2, .sec-head, .empty p { text-wrap: balance; }
.hero-sub, .sub, .tagline, .card p, .empty p { text-wrap: pretty; }

/* Back down from the condensed-era sizes. These were raised to make a narrow
   face look the right size; at 100% width they are simply large. */
.section h2 { font-size: 19px; letter-spacing: -0.02em; }
.stat span { font-size: 24px; }
.life span { font-size: 23px; }
.sb-n { font-size: 30px; }
.hero-title { letter-spacing: -0.03em; }

/* READING SIZES, which is the other half of "hard to read".
 *
 * The app was built at 12-13px for anything that was not a heading, which is
 * a density decision made before there was a typeface worth reading. 14px
 * with a 1.55 line-height is the floor for body text here now; the muted
 * colour does enough to push it back without also making it small. */
body { font-size: 15px; line-height: 1.55; }
.sub, .hint, .footnote { font-size: 13.5px; line-height: 1.55; }
.meta span { font-size: 12.5px; }
.clip-card .meta .line { font-size: 12.5px; }

/* ------------------------------------------------------------------ icons
 *
 * One size rule for every drawn icon, because the thing that made the glyphs
 * they replace look wrong was never the shape — it was that each one was a
 * character in whatever font happened to catch it, at whatever size that
 * font drew it. `1em` ties every icon to the text it sits beside instead.
 */
.ico { width: 1em; height: 1em; flex: none; display: block; }
.icon-btn .ico { width: 17px; height: 17px; }
.thumb .play .ico { width: 20px; height: 20px; }

/* The icon buttons are 34px squares and a finger is not. Rather than growing
   them — which would push the player controls onto two rows on a phone — the
   TAP TARGET is extended past the visible button with a pseudo-element, so
   the thing you can hit is 44px while the thing you can see is unchanged.
   That is the whole 44x44 rule: it is about the hit area, not the paint. */
.icon-btn, .filed-chip .x, .close-x {
  display: inline-grid; place-items: center; position: relative;
}
.icon-btn::after, .filed-chip .x::after {
  content: ""; position: absolute; left: 50%; top: 50%;
  width: 44px; height: 44px; transform: translate(-50%, -50%);
}
.icon-btn { transition: background-color .15s ease, border-color .15s ease; }
.icon-btn:hover { background: var(--line); }
.filed-chip .x .ico { width: 10px; height: 10px; }

/* The trend marker beside a week-over-week change. Sized from the line it
   labels rather than set in px, so it stays put if the row's size changes. */
.change-dot { display: inline-flex; align-items: center; }
.change-dot .ico { width: 15px; height: 15px; }
.banner.kept { gap: 6px; justify-content: flex-start; }
.banner.kept .ico { width: 13px; height: 13px; }
.cr-bullet .ico { width: 14px; height: 14px; }

/* ------------------------------------------------------- loading skeletons
 *
 * The roll list used to be emptied and then awaited, so between opening a tab
 * and the response landing there was nothing on screen at all — on gym wifi
 * that is a blank page followed by a pop, which reads as broken rather than
 * as loading. A skeleton in the SHAPE of the thing that is coming also
 * reserves the space, so nothing under it jumps when the real cards arrive.
 *
 * The shimmer is a moving gradient rather than an opacity pulse, because a
 * pulse on a dark ground is nearly invisible — measured on this palette, the
 * whole cycle moved about six values per channel.
 */
@keyframes skel-sweep { to { background-position: 200% 0; } }
/* THESE TWO VALUES WERE MEASURED, NOT PICKED.
 *
 * The first attempt used var(--surface) and var(--surface-2), which are the
 * app's own card colours and therefore looked like the obvious answer. On
 * screen the placeholder came out at rgb(20,24,29) against a page ground of
 * rgb(15,19,25) — a contrast ratio of 1.04, which is not a dim shimmer, it
 * is nothing. The sweep moved 6-9 values per channel, the same magnitude as
 * a background change that was once shipped here and reported back as "it
 * still looks the same".
 *
 * So the placeholder is deliberately LIGHTER than any surface in the app.
 * It is the one element that is supposed to look like a hole where content
 * will be, and it is on screen for a second and a half. Re-measure if you
 * change them: rgb(38,46,56) to rgb(62,74,88) against rgb(15,19,25) of
 * the page — a contrast of 1.36 at its darkest and 2.06 at its brightest, with
 * the sweep moving 24-32 values per channel. */
.skel {
  background: linear-gradient(100deg,
      #262e38 30%, #3e4a58 50%, #262e38 70%);
  background-size: 200% 100%;
  animation: skel-sweep 1.4s linear infinite;
  border-radius: 6px;
}
.skel-card {
  border: 1px solid var(--line); border-radius: var(--radius);
  overflow: hidden; background: var(--surface);
}
.skel-card .skel-thumb { aspect-ratio: 16 / 9; border-radius: 0; }
.skel-card .skel-lines { padding: 12px; display: flex; flex-direction: column; gap: 8px; }
.skel-card .skel-lines i { display: block; height: 11px; }
.skel-card .skel-lines i:nth-child(1) { width: 62%; }
.skel-card .skel-lines i:nth-child(2) { width: 44%; }
.skel-card .skel-lines i:nth-child(3) { width: 52%; }
/* The class list is rows, not cards. Same height as a real one so the list
   does not change length when the answer lands. */
.skel-row { height: 46px; margin-bottom: 8px; }

/* A placeholder that never stops moving is a spinner with extra steps. It
   stays, it just stops animating — the shape is what does the work. */
@media (prefers-reduced-motion: reduce) {
  .skel { animation: none; background: #2c3540; }
}

/* The stat row holds NUMBERS and one piece of TEXT, and the text is the one
   that sets the height. "Bottom side control" in a 113px column wraps to
   three lines, and because the cards are a grid row they ALL become three
   lines tall — 176px of card on a phone to say "60 min" and "77.3%".
   Two across gives the text enough width to wrap to two. Measured at 390px:
   every card 176px tall before, 95px for the numbers and 135px for the text
   after. Below 480px the columns get narrower than the words. */
@media (max-width: 480px) {
  .stat-row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* ==========================================================================
   THE ROLL CARD
   ==========================================================================
   The card is the whole of My rolls, and it had grown to about 400px to
   carry a date, a time range that is the same on every whole-mat round, and
   a sentence about retention. Everything below is that being paid back.
   ========================================================================== */

/* THE FRAME IS STILL CROPPED, AND THAT WAS TESTED RATHER THAN ASSUMED.
 *
 * A phone records 1080x1920, so `object-fit: cover` on a 16:9 card keeps a
 * horizontal band through the middle and discards the rest. That looked
 * like a bug worth fixing, so it was fixed: `contain`, with a blurred copy
 * of the same frame filling the sides.
 *
 * It was worse. On a 205px card the contained picture is about 68px wide —
 * the whole frame, at a size where nobody can tell whose round it is, inside
 * a large blurred smear. The crop is better precisely because a phone
 * filming a mat puts the two people across the middle, which is the band
 * `cover` keeps. Reverted; do not re-derive it.
 */

/* How long is left, as a chip on the picture. The quiet state is two
   characters in the corner; the state worth acting on is not.

   IT IS `.life-chip` AND NOT `.life`, AND THAT IS A BUG FIX.

   `.life` was already taken — it is the lifetime stat card on My stats,
   declared about nine hundred lines above this. This block came later, so
   `position: absolute` won, and all three of those cards collapsed onto
   `left: 8px` with their row at zero height. My stats has been broken that
   way since the roll card was redesigned, and nothing said so: the cards
   were still in the DOM, still had their text, and simply painted in the
   wrong place.

   Third time in this file — after `.steps` and `.pitch`. A generic word in
   one flat stylesheet is a collision waiting for whoever writes the next
   component, and the failure is always silent. */
.life-chip {
  position: absolute; left: 8px; bottom: 8px; z-index: 2;
  font-family: var(--font-display); font-size: 11px; font-weight: 700;
  letter-spacing: .04em; font-variant-numeric: tabular-nums;
  background: rgba(0,0,0,.6); color: var(--text-2);
  padding: 2px 6px; border-radius: 5px;
  display: inline-flex; align-items: center; gap: 4px;
  backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
}
.life-chip.soon { background: rgba(255,176,32,.9); color: #241a00; }
.life-chip.keep { background: rgba(52,211,153,.9); color: #052e21; padding: 3px; }
.life-chip.keep .ico { width: 11px; height: 11px; }

/* The play affordance is smaller and quieter than it was.
 *
 * It sat at 46px and had to shrink with the card — but the bigger change is
 * that it no longer sits over the middle of the picture at full strength.
 * On a grid of thumbnails the frame IS the label: it is how somebody picks
 * their round out of nine, and a solid disc through the centre of every one
 * covers exactly the part that distinguishes them.
 *
 * It stays visible rather than appearing on hover, because half the people
 * using this are on a phone and there is no hover there — it just steps back
 * until the card is the one being looked at. */
.thumb .play {
  width: 34px; height: 34px; z-index: 2;
  background: rgba(255,255,255,.14);
  opacity: .78; transition: opacity .15s ease, background-color .15s ease;
}
.clip-card:hover .thumb .play { opacity: 1; background: rgba(255,255,255,.26); }
.thumb .play .ico { width: 14px; height: 14px; }
.thumb .dur, .thumb .src-tag { z-index: 2; }
@media (prefers-reduced-motion: reduce) { .thumb .play { transition: none; } }

/* The meta block. A title that is always a real name, then one line, then
   the shelves — where it used to be a blank strong, a date, a range and a
   full-width retention strip. */
.clip-card .meta { padding: 9px 10px 10px; gap: 3px; }
.clip-card .meta strong {
  font-family: var(--font-display); font-size: 15.5px; font-weight: 700;
  letter-spacing: .01em; line-height: 1.2;
  /* A long mat label must not push the card taller than its neighbours: the
     grid row takes the tallest, so one verbose title costs every card. */
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.clip-card .meta .line {
  display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap;
  font-size: 12px; color: var(--muted); min-width: 0;
}
.clip-card .meta .line .when { white-space: nowrap; }
/* "marked" is a state, so it reads as a state rather than as another clause
   of the sentence beside it. */
.clip-card .meta .line .mine {
  color: var(--accent); font-weight: 600; font-size: 11px;
  letter-spacing: .02em; background: rgba(59,168,255,.12);
  border-radius: 4px; padding: 0 5px; white-space: nowrap;
}
.clip-card .filed { margin-top: 4px; }

/* Hover lifts the card rather than only changing its border. On a grid of
   pictures a 1px border change is not a state anybody notices. */
.clip-card { transition: border-color .15s ease, transform .15s ease, box-shadow .15s ease; }
.clip-card:hover {
  border-color: var(--line-2); transform: translateY(-2px);
  box-shadow: 0 8px 22px rgba(0,0,0,.45);
}
@media (prefers-reduced-motion: reduce) {
  .clip-card:hover { transform: none; }
}

/* ==========================================================================
   ELEVATION
   ==========================================================================
   Colour alone was never going to do it. A lighter rectangle with no shadow
   reads as a hole cut in the page rather than something resting on it, which
   is why nudging the card colour on its own had no effect the first time.
   The shadow is what says which side of the page a thing is on.

   Applied only to the surfaces that are meant to be OBJECTS — cards,
   dialogs, dropdowns, the toast. Not to regions of the page: a stat panel, a
   bar track and a report section are part of the floor, and shadowing them
   too is the same mistake as shadowing nothing.
   ========================================================================== */
.clip-card, .card, .skel-card { box-shadow: var(--shadow); }
.clip-card:hover { box-shadow: var(--shadow-hi); }
dialog, .dropdown, .toast { box-shadow: var(--shadow-hi); }

/* The hairline along a card's top edge catches the light from the wash. It
   has to get brighter as the card does, or it vanishes into a surface that
   is now much closer to it. */
.clip-card::before, .card::before {
  background: linear-gradient(90deg, transparent, rgba(255,255,255,.14), transparent);
}

/* The skeleton was tuned against the OLD floor, where it had to be unusually
   light to be visible at all. On the new surfaces it would glare, so it goes
   back to being one step above a card. Re-measured: rgb(53,53,56) to
   rgb(78,78,84) against a page floor of rgb(12,12,14). */
.skel {
  background: linear-gradient(100deg,
      var(--surface-2) 30%, #4e4e54 50%, var(--surface-2) 70%);
}
@media (prefers-reduced-motion: reduce) { .skel { background: var(--surface-2); } }

/* The date escape hatch on the upload form. Indented under the select it
   belongs to, so it reads as part of that question rather than as a new one. */
.date-block {
  margin: 10px 0 0; padding: 12px 14px;
  background: var(--surface); border: 1px solid var(--line); border-radius: 11px;
}
.date-block label { display: block; font-size: 13px; color: var(--muted); margin-bottom: 7px; }
.date-block input[type="datetime-local"] {
  width: 100%; font: inherit; font-size: 15px;
  padding: 11px 12px; border-radius: 9px;
  background: var(--bg); color: var(--text); border: 1px solid var(--line);
  /* Safari renders the native picker's glyph in the UA's own dark-on-light,
     which on this ground is a black smudge. */
  color-scheme: dark;
}
.date-block .hint { margin: 8px 0 0; }

/* "Rolling started at 0:31" above the breakdown — a seek, so it looks tappable
   without shouting. */
.roll-start {
  display: inline-flex; align-items: center; gap: 6px; margin: 4px 0 14px;
  padding: 6px 12px; border-radius: 999px; border: 1px solid var(--line);
  background: var(--surface-2); color: var(--text-2); font: inherit; font-size: 13px;
  cursor: pointer;
}
.roll-start:hover { color: var(--text); border-color: var(--accent); }

/* ================================================================ the shell, v2
 *
 * THE SIGNED-IN APP NOW SPEAKS THE LANDING PAGE'S LANGUAGE. The landing page
 * was the screen people called modern; the app behind it was a 900px column
 * of 18px headings and identical grey tiles under a receding-lines backdrop,
 * and read as a different, older product the moment somebody signed in.
 *
 * What carries over is what made that page work, not its scenery:
 *   - a heavy two-tone headline (800, tight tracking, the second line grey),
 *   - small uppercase eyebrows with wide tracking to label sections,
 *   - a wide shell (1180px, the landing page's) so a 1440 screen is used,
 *   - one picture large instead of every picture small.
 * Still one family at a normal width — hierarchy is weight and size.
 */
.mat-bg { display: none; }

/* ---------- top bar: a wordmark, not just a monogram ---------- */
.topbar {
  padding: 14px max(20px, calc((100% - 1180px) / 2));
  padding-top: calc(14px + env(safe-area-inset-top));
  border-bottom: 0; background: transparent;
  backdrop-filter: none; -webkit-backdrop-filter: none;
}
.topbar-gym { display: flex; flex-direction: column; line-height: 1.15; gap: 1px; }
.topbar-gym .wordmark { color: var(--text); font-size: 15px; font-weight: 800; letter-spacing: -.02em; }
.topbar-gym span { font-size: 12px; color: var(--muted); }

/* ---------- nav: icon + label, accent underline, aligned to the shell ---------- */
.tabs {
  top: 0; gap: 6px;
  padding: 0 max(12px, calc((100% - 1180px) / 2 - 12px));
  border-bottom: 1px solid color-mix(in srgb, var(--line) 70%, transparent);
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  scrollbar-width: none;
}
.tabs::-webkit-scrollbar { display: none; }
.tab {
  position: relative; padding: 14px 12px 15px; font-size: 14px; color: var(--muted);
  border-bottom: 0; gap: 8px; transition: color .15s ease;
}
.tab-ico { width: 18px; height: 18px; flex: none; }
.tab:hover { color: var(--text); }
.tab.is-active { color: var(--text); }
.tab.is-active .tab-ico { color: var(--accent); }
.tab::after {
  content: ""; position: absolute; left: 10px; right: 10px; bottom: -1px; height: 2px;
  border-radius: 2px; background: var(--accent); transform: scaleX(0);
  transition: transform .2s ease;
}
.tab.is-active::after { transform: scaleX(1); }

/* ---------- the page ---------- */
.wrap { padding: 44px 24px 96px; }

.eyebrow {
  margin: 0 0 10px; font-size: 11px; font-weight: 600; color: var(--muted);
  text-transform: uppercase; letter-spacing: .16em;
}
/* The pill from the older pitch styles above is kept — it is the reference
   site's "AI-POWERED CONSTRUCTION" chip — but it hugs its text instead of
   stretching across a flex column. A list label stays plain. */
.eyebrow { width: fit-content; }
.grid-label { border: 0; background: none; padding: 0; }
.grid-label::before { display: none; }

/* Every view opens on its headline, the way a landing section does. */
.view > h2:first-child,
.view > .pending-banner + .page-hero h2,
.page-hero .headline {
  font-size: clamp(30px, 3.4vw, 46px); font-weight: 800; line-height: 1.04;
  letter-spacing: -.035em; margin: 0 0 12px;
}
.headline > span { display: block; color: color-mix(in srgb, var(--muted) 72%, var(--bg)); }
.headline > span:empty { display: none; }
.view > h2:first-child + .sub {
  font-size: 16.5px; line-height: 1.55; max-width: 62ch; margin: 0 0 32px; color: var(--text-2);
}
.page-hero { align-items: flex-end; margin-bottom: 32px; }
/* The wide shell is for PICTURES. A calendar is square cells, so at 1180px
   each day became a 160px tile; a form or a quiz answer stretched that far is
   a line the eye cannot track. These views keep a reading width and sit on
   the same left edge as everything else. */
.view[data-view="classes"], .view[data-view="upload"],
.view[data-view="games"], .view[data-view="settings"] { max-width: 820px; }
.page-hero .headline { margin: 0; }
.view h3 { font-size: 19px; font-weight: 750; letter-spacing: -.02em; }

/* Small buttons are pills, like the landing page's sign-in tab. */
.btn.ghost.sm {
  border-radius: 999px; padding: 9px 16px; color: var(--text-2);
  background: color-mix(in srgb, var(--surface) 60%, transparent);
}
.btn.ghost.sm:hover { color: var(--text); border-color: var(--line-2); }

/* ---------- the newest round, large ---------- */
.roll-feature { margin: 0 0 40px; }
.feature-card {
  display: grid; grid-template-columns: minmax(0, 1.75fr) minmax(280px, 1fr);
  border-radius: 22px; overflow: hidden; border: 1px solid var(--line);
  background: linear-gradient(180deg, var(--card-hi), var(--card-lo));
  box-shadow: var(--shadow-hi);
}
.feature-thumb { aspect-ratio: 16 / 9; }
.feature-thumb::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(90deg, transparent 70%, rgba(0,0,0,.35));
}
.feature-thumb .play {
  width: 76px; height: 76px; background: rgba(255,255,255,.16);
  border: 1px solid rgba(255,255,255,.22); z-index: 2;
  transition: transform .2s ease, background .2s ease;
}
.feature-thumb .play .ico { width: 28px; height: 28px; }
.feature-thumb:hover .play { transform: scale(1.08); background: var(--accent); color: var(--accent-ink); }
.feature-thumb .dur { font-size: 12.5px; padding: 3px 8px; right: 14px; bottom: 14px; }
.feature-body {
  padding: clamp(24px, 3vw, 40px); display: flex; flex-direction: column;
  justify-content: center; gap: 0;
}
.feature-body .eyebrow { color: var(--accent); }
.feature-body h3 { font-size: clamp(24px, 2.4vw, 32px); font-weight: 800; letter-spacing: -.03em; line-height: 1.1; margin: 0; }
.feature-when { margin: 8px 0 18px; color: var(--muted); font-size: 15px; }
.feature-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 26px; }
.chip {
  font-size: 12.5px; font-weight: 600; padding: 5px 11px; border-radius: 999px;
  background: var(--surface-2); color: var(--text-2);
}
.chip.accent { background: color-mix(in srgb, var(--accent) 18%, transparent); color: var(--accent); }
.chip.warn { background: color-mix(in srgb, var(--warn) 18%, transparent); color: var(--warn); }
.feature-cta {
  width: auto; align-self: flex-start; display: inline-flex; align-items: center; gap: 9px;
  padding: 13px 22px; border-radius: 999px; font-size: 15px; font-weight: 700;
  box-shadow: 0 8px 24px color-mix(in srgb, var(--accent) 30%, transparent);
}
.feature-cta .ico { width: 15px; height: 15px; }
.feature-cta:hover { filter: brightness(1.08); }
.grid-label { margin: 0 0 14px; }

/* ---------- the grid: fewer, bigger, and they move ---------- */
.grid { gap: 22px; }
.clip-card {
  border-radius: 18px; box-shadow: var(--shadow);
  transition: transform .2s ease, box-shadow .2s ease, border-color .2s ease;
}
.clip-card:hover { transform: translateY(-3px); box-shadow: var(--shadow-hi); border-color: var(--line-2); }
.clip-card .thumb { overflow: hidden; }
.clip-card .thumb .shot { transition: transform .4s ease; }
.clip-card:hover .thumb .shot { transform: scale(1.04); }
.clip-card .meta { padding: 14px 16px 16px; gap: 4px; }
.clip-card .meta strong { font-size: 16px; font-weight: 700; letter-spacing: -.015em; }
.clip-card .meta .line { font-size: 13px; }

/* ---------- stats: numbers you can read from across the room ---------- */
.lifetime { gap: 16px; margin: 0 0 40px; }
.life {
  flex: 1 1 200px; padding: 24px 24px 22px; border-radius: 18px; position: relative;
  overflow: hidden; background: linear-gradient(180deg, var(--card-hi), var(--card-lo));
  box-shadow: var(--shadow);
}
.life::before {
  content: ""; position: absolute; inset: 0 0 auto 0; height: 3px;
  background: linear-gradient(90deg, var(--accent), transparent);
}
.life span { font-size: clamp(34px, 3.4vw, 46px); letter-spacing: -.04em; line-height: 1; }
.life label {
  margin-top: 10px; font-size: 11px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .14em; color: var(--muted);
}
.attire-pick { margin-bottom: 26px; }
.attire { border-radius: 999px; padding: 7px 16px; }

@media (max-width: 760px) {
  .wrap { padding: 28px 16px 80px; }
  .tab { padding: 12px 10px 13px; font-size: 13.5px; }
  .feature-card { grid-template-columns: 1fr; border-radius: 18px; }
  .feature-thumb::after { display: none; }
  .feature-body { padding: 20px 20px 22px; }
  .page-hero { flex-wrap: wrap; align-items: flex-start; }
  .grid { grid-template-columns: repeat(auto-fill, minmax(min(100%, 220px), 1fr)); gap: 16px; }
}
@media (prefers-reduced-motion: reduce) {
  .clip-card:hover, .clip-card:hover .thumb .shot, .feature-thumb:hover .play { transform: none; }
}

/* What each camera has heard of the timer. Quiet: it is a status, not a
   control, and most of the time it simply says the timer was found. */
.timer-heard { list-style: none; margin: 4px 0 14px; padding: 0; display: grid; gap: 6px; }
.timer-heard li { display: flex; gap: 8px; align-items: baseline; font-size: 13px;
  color: var(--muted); line-height: 1.45; }
.timer-heard li strong { color: var(--text); font-weight: 600; white-space: nowrap; }
.timer-heard li::before { content: ""; flex: none; width: 7px; height: 7px;
  border-radius: 50%; background: var(--muted); transform: translateY(-1px); }
.timer-heard li.heard-found::before { background: var(--ok); }
.timer-heard li.heard-not_found::before { background: var(--bad); }

/* ---------- empty states that say what happens next ----------
 * Every member's first visit is empty, so this is a first impression rather
 * than an edge case: an icon, a headline in the page's voice, one sentence on
 * what fills it, and the button that goes there. */
.empty.empty-state {
  border: 1px solid var(--line); border-radius: 20px; padding: clamp(32px, 5vw, 56px) 24px;
  background: linear-gradient(180deg, var(--card-hi), var(--card-lo));
  display: flex; flex-direction: column; align-items: center; gap: 0;
}
.empty-state .empty-ico {
  width: 54px; height: 54px; border-radius: 16px; display: grid; place-items: center;
  color: var(--accent); background: color-mix(in srgb, var(--accent) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 28%, transparent); margin-bottom: 18px;
}
.empty-state .empty-ico svg { width: 26px; height: 26px; }
.empty-state h3 { margin: 0 0 8px; font-size: 22px; font-weight: 800; letter-spacing: -.025em; color: var(--text); }
.empty-state p { max-width: 46ch; font-size: 15px; line-height: 1.55; margin: 0 0 22px; color: var(--text-2); }
.empty-state .btn { border-radius: 999px; padding: 11px 22px; font-weight: 700; }

/* The join code in Settings: the same large code as the create-gym screen,
   because it is the one thing a coach reads aloud or copies. */
.settings-joincode { margin: 6px 0 14px; user-select: all; }
.settings-joincode.is-empty { color: var(--muted); font-size: 18px; letter-spacing: 0;
  font-weight: 600; user-select: none; }
.joincode-actions { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; }
.joincode-actions .btn { flex: 1 1 160px; }
.joincode-note { margin-top: 12px; text-align: center; }
