/* Latched Climbing — the design system.
 *
 * Three layers, in this order:
 *   1. THEMES    every color the app can draw, five palettes deep.
 *   2. TOKENS    the scales — space, radius, type, elevation, motion.
 *   3. PARTS     components, built only from layers 1 and 2.
 *
 * The rule that keeps it coherent: a component never names a raw color or a
 * raw pixel value. If something needs a shade or a step that isn't here, the
 * scale is wrong and the scale gets fixed. That is the whole discipline.
 */

/* ── 1. themes ───────────────────────────────────────────────────────────── */

body[data-theme=midnight] {
  --bg: #0c0f13; --card: #151a21; --card-2: #1b212a; --line: #262e39;
  --text: #e8edf2; --muted: #8b97a5; --soft: #c7d0da;
  --accent: #2dd4a7; --accent-dim: rgba(45,212,167,0.12);
  --amber: #e8b44a; --amber-dim: rgba(232,180,74,0.12);
  --blue: #5aa9e6; --red: #e5646b; --teal: #4cc3d9; --plum: #b07ce8;
  --nav: #0a0d11; --on-accent: #06251c; --marker: #ffffff;
  --gold: #f0c14b; --gold-dim: rgba(240,193,75,0.13);
  --shadow: 0deg 0% 0%; --shadow-strength: 0.5;
  --scrim: rgba(4,6,9,0.66); --wash: #080a0d;
  --state: rgba(255,255,255,0.06);
}
body[data-theme=basecamp] {
  --bg: #f2ecdf; --card: #fffdf8; --card-2: #f6f0e4; --line: #e0d5bf;
  --text: #3f362b; --muted: #7a6b59; --soft: #5c5344;
  --accent: #6d7c39; --accent-dim: rgba(117,132,63,0.14);
  --amber: #a8781f; --amber-dim: rgba(201,146,46,0.14);
  --blue: #47788f; --red: #b54f38; --teal: #3e7d74; --plum: #74495c;
  --nav: #ece4d2; --on-accent: #fffdf6; --marker: #3f362b;
  --gold: #96701c; --gold-dim: rgba(180,133,34,0.15);
  --shadow: 30deg 24% 32%; --shadow-strength: 0.13;
  --scrim: rgba(38,31,22,0.42); --wash: #e6ddca;
  --state: rgba(63,54,43,0.05);
}
body[data-theme=chalk] {
  --bg: #f5f6f7; --card: #ffffff; --card-2: #edeff2; --line: #e1e5ea;
  --text: #182029; --muted: #5f6b78; --soft: #3f4a56;
  --accent: #0d9d76; --accent-dim: rgba(13,157,118,0.10);
  --amber: #a97812; --amber-dim: rgba(214,154,36,0.12);
  --blue: #3f8ede; --red: #d9515c; --teal: #1899a8; --plum: #8d5bbf;
  --nav: #ffffff; --on-accent: #ffffff; --marker: #182029;
  --gold: #94701a; --gold-dim: rgba(184,140,36,0.13);
  --shadow: 214deg 30% 40%; --shadow-strength: 0.12;
  --scrim: rgba(16,22,29,0.44); --wash: #e7eaee;
  --state: rgba(24,32,41,0.05);
}
body[data-theme=sandstone] {
  --bg: #171009; --card: #221910; --card-2: #2b2015; --line: #3d2f1f;
  --text: #f1e7d8; --muted: #a3907c; --soft: #d8c9b6;
  --accent: #9cb35e; --accent-dim: rgba(156,179,94,0.13);
  --amber: #e0a84e; --amber-dim: rgba(224,168,78,0.13);
  --blue: #7aa5bd; --red: #d96450; --teal: #5da89b; --plum: #b06a8c;
  --nav: #120c06; --on-accent: #1d2410; --marker: #ffffff;
  --gold: #e8bd63; --gold-dim: rgba(232,189,99,0.14);
  --shadow: 28deg 40% 4%; --shadow-strength: 0.5;
  --scrim: rgba(12,8,4,0.66); --wash: #100b05;
  --state: rgba(255,255,255,0.055);
}
body[data-theme=alpenglow] {
  --bg: #101122; --card: #191b31; --card-2: #20223c; --line: #2c2f4e;
  --text: #eceafb; --muted: #9195b5; --soft: #c9c9e2;
  --accent: #f06a9a; --accent-dim: rgba(240,106,154,0.13);
  --amber: #edb054; --amber-dim: rgba(237,176,84,0.13);
  --blue: #7d8df2; --red: #e8554e; --teal: #5ad0c8; --plum: #b48af5;
  --nav: #0c0d1c; --on-accent: #33101e; --marker: #ffffff;
  --gold: #f2c86a; --gold-dim: rgba(242,200,106,0.14);
  --shadow: 250deg 40% 4%; --shadow-strength: 0.55;
  --scrim: rgba(8,9,20,0.68); --wash: #0b0c19;
  --state: rgba(255,255,255,0.06);
}

/* The logo's own duotone. Fixed, not themed — a brand mark that changes color
   with the wallpaper isn't a brand mark. */
:root { --brand-ink: #171310; --brand-paper: #f8f3ea; --brand-rust: #b4522f; }

/* Text that sits ON a heat-ramp color. The ramp is built from each theme's own
   hues, so on a light theme those hues are mid-dark and want light text, and
   on a dark theme they are bright and want dark text. One variable, flipped
   once, instead of a contrast calculation at every call site. */
:root { --on-heat: #fffdf8; }

/* ── ink: the semantic colours, as TEXT ───────────────────────────────────────
   --accent, --red and --gold are FILL colours. They were also being used as
   text, and measured against the surfaces they actually sit on, that failed
   WCAG AA in nineteen places — every one of them on a light theme, and none of
   them visible to anybody developing in the dark. The worst were an open
   disclosure title at 3.12:1 (chalk), DELETE MY ACCOUNT at 3.60, and EDIT
   PROFILE at 3.36 — a control whose only interactive cue IS its colour.

   So text gets its own token, in the same shape as --on-heat above: one
   variable, decided once per theme on measurement, instead of a contrast
   calculation at every call site.

   Derived with color-mix rather than written as hex, so re-palettes carry. Each
   percentage is the measured minimum that clears 4.6:1 (a margin over 4.5, so
   rounding never lands under) on EVERY surface the token is used on — the page
   ground, --card, --card-2, and the matching *-dim tint for the pill cases.
   Light themes darken toward black; dark themes lighten toward white; a theme
   that already passes everywhere passes the colour straight through, spelled
   out rather than inherited, because a :root default here would reference a
   theme-scoped var and compute to garbage.

   Where a colour keeps its own meaning it keeps its own hue: red stays red and
   gold stays gold. This changes lightness, never meaning. */
body[data-theme=basecamp] {
  --accent-ink: color-mix(in oklab, var(--accent) 82%, #000);
  --red-ink:    color-mix(in oklab, var(--red) 92%, #000);
  --gold-ink:   color-mix(in oklab, var(--gold) 82%, #000);
  --amber-ink:  color-mix(in oklab, var(--amber) 88%, #000);
}
body[data-theme=chalk] {
  --accent-ink: color-mix(in oklab, var(--accent) 78%, #000);
  --red-ink:    color-mix(in oklab, var(--red) 84%, #000);
  --gold-ink:   color-mix(in oklab, var(--gold) 87%, #000);
  --amber-ink:  color-mix(in oklab, var(--amber) 80%, #000);
}
body[data-theme=midnight] {
  --amber-ink:  var(--amber);
  --accent-ink: var(--accent);
  --red-ink:    color-mix(in oklab, var(--red) 96%, #fff);
  --gold-ink:   var(--gold);
}
body[data-theme=sandstone] {
  --amber-ink:  var(--amber);
  --accent-ink: var(--accent);
  --red-ink:    color-mix(in oklab, var(--red) 92%, #fff);
  --gold-ink:   var(--gold);
}
body[data-theme=alpenglow] {
  --amber-ink:  var(--amber);
  --accent-ink: var(--accent);
  --red-ink:    color-mix(in oklab, var(--red) 91%, #fff);
  --gold-ink:   var(--gold);
}
body, body[data-theme=titanium] {
  --accent-ink: var(--accent);
  --red-ink:    var(--red);
  --gold-ink:   var(--gold);
  --amber-ink:  var(--amber);
}
body[data-theme=midnight], body[data-theme=sandstone],
/* Chalk joins them on measurement, not on taste. Its ramp stops are light
   enough that the near-white default read 3.37-3.91 against all four — the
   selected effort chip sat at 3.88, under AA — while dark ink clears 4.92 on
   every one of them. Nothing else in the app pairs ink with a heat fill, so
   this is the whole blast radius. */
body[data-theme=alpenglow], body[data-theme=titanium],
body[data-theme=chalk] { --on-heat: #0a0c11; }

/* ── MATERIAL ────────────────────────────────────────────────────────────────
   A theme is not only a palette — it is what the surfaces are made of. Six
   tokens describe that, and every component reads them without knowing which
   theme is on, so the whole set below is palette data rather than new rules.

     --surface-blur   what the frosting is
     --hairline       the specular top edge that makes glass read as glass
     --aur-1..3       the three lights the glass is refracting
     --grad-a/-b      the accent's gradient, for the few places that earn one

   --hairline must be a valid shadow rather than `none`: `none` cannot appear
   inside a comma-separated box-shadow list, and would invalidate the whole
   declaration including the elevation sitting next to it. */
/* --grad-a/-b are deliberately NOT defaulted here. They would have to default
   to var(--accent), and --accent is theme-scoped on body — a :root token
   referencing one computes to the guaranteed-invalid value. The fallback
   lives at the use site instead, where --accent is in scope. */
:root {
  --surface-blur: none; --hairline: 0 0 0 0 transparent;
  --aur-1: transparent; --aur-2: transparent; --aur-3: transparent;
}

/* Every theme gets the glass, tuned to its own light. Light palettes want a
   near-opaque white pane and a bright bevel; dark ones want a thinner, cooler
   pane and a faint one. The numbers differ per theme because "translucent"
   over paper and "translucent" over ink are not the same material. */
body[data-theme=basecamp] {
  --card: rgba(255,253,248,0.74); --card-2: rgba(246,240,228,0.78);
  /* was #7a6b59: 4.25 on the page ground, under AA for every muted label
     that sits on it rather than on a card */
  --muted: #6f6151;
  --surface-blur: blur(18px) saturate(150%);
  --hairline: inset 0 1px 0 rgba(255,255,255,0.85);
  --aur-1: rgba(109,124,57,0.20); --aur-2: rgba(168,120,31,0.16);
  --aur-3: rgba(181,79,56,0.13);
  --grad-a: #5e6b31; --grad-b: #46603d;
}
body[data-theme=chalk] {
  --card: rgba(255,255,255,0.76); --card-2: rgba(237,239,242,0.80);
  --surface-blur: blur(18px) saturate(150%);
  --hairline: inset 0 1px 0 rgba(255,255,255,0.9);
  --aur-1: rgba(13,157,118,0.14); --aur-2: rgba(63,142,222,0.14);
  --aur-3: rgba(141,91,191,0.11);
  --grad-a: #0a6e51; --grad-b: #075a66;
}
body[data-theme=midnight] {
  --card: rgba(30,38,49,0.66); --card-2: rgba(40,49,62,0.72);
  --surface-blur: blur(20px) saturate(165%);
  --hairline: inset 0 1px 0 rgba(255,255,255,0.07);
  --aur-1: rgba(45,212,167,0.17); --aur-2: rgba(90,169,230,0.15);
  --aur-3: rgba(176,124,232,0.12);
  --grad-a: #2dd4a7; --grad-b: #35b9c9;
}
body[data-theme=sandstone] {
  --card: rgba(45,34,22,0.68); --card-2: rgba(56,43,29,0.74);
  --surface-blur: blur(20px) saturate(160%);
  --hairline: inset 0 1px 0 rgba(255,236,208,0.08);
  --aur-1: rgba(156,179,94,0.17); --aur-2: rgba(224,168,78,0.16);
  --aur-3: rgba(176,106,140,0.12);
  --grad-a: #9cb35e; --grad-b: #c79a4a;
}
body[data-theme=alpenglow] {
  --card: rgba(32,35,60,0.66); --card-2: rgba(42,45,74,0.72);
  --surface-blur: blur(20px) saturate(170%);
  --hairline: inset 0 1px 0 rgba(255,255,255,0.08);
  --aur-1: rgba(240,106,154,0.18); --aur-2: rgba(125,141,242,0.16);
  --aur-3: rgba(90,208,200,0.12);
  --grad-a: #f06a9a; --grad-b: #a878f0;
}

/* The ambient light itself: one rule, six palettes. Fixed so it does not
   scroll — the app floats over it rather than sitting on it. */
body::before {
  content: ""; position: fixed; inset: -25%; z-index: -1; pointer-events: none;
  background:
    radial-gradient(42% 30% at 16% 6%, var(--aur-1), transparent 70%),
    radial-gradient(38% 28% at 88% 20%, var(--aur-2), transparent 70%),
    radial-gradient(46% 34% at 46% 102%, var(--aur-3), transparent 70%);
  filter: blur(26px);
  animation: drift 30s var(--ease) infinite alternate;
}
@keyframes drift {
  from { transform: translate3d(-2%, -1%, 0) scale(1); }
  to { transform: translate3d(2%, 2%, 0) scale(1.08); }
}

/* The few places an accent earns a gradient. Everywhere else stays flat —
   a gradient on every surface is not richness, it is noise. */
.primary, .sent, .editor-done, .subtabs-ink {
  background-image: linear-gradient(135deg, var(--grad-a, var(--accent)),
                                            var(--grad-b, var(--accent)));
}
.hero .streak-n {
  background-image: linear-gradient(135deg, var(--sg-a, var(--grad-a, var(--accent))),
                                            var(--sg-b, var(--grad-b, var(--accent))));
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
.hero.open { --sg-a: var(--amber); --sg-b: var(--gold); }
.hero.cold { --sg-a: var(--muted); --sg-b: var(--soft); }

/* ── Titanium ────────────────────────────────────────────────────────────────
   The deliberate outlier. Where the other five are flat painted surfaces on a
   solid ground, this one is frosted glass floating over a slow-drifting
   aurora: translucent fills, a specular hairline along every top edge, wider
   radii, tighter type, and the system palette you would expect on a very new
   phone. It exists to prove the design system can describe a different
   material, not just a different set of colours. */
/* The bare `body` rides with Titanium because Titanium is the default: the
   served markup carries data-theme and boot.js only ever changes it, so this
   grouping should never actually be reached — but if it ever is, the CSS
   fallback and boot.js's fallback have to name the same theme. */
body, body[data-theme=titanium] {
  --bg: #06070a; --card: rgba(255,255,255,0.055); --card-2: rgba(255,255,255,0.085);
  --line: rgba(255,255,255,0.11);
  --text: #f5f5f7; --muted: #8e8e93; --soft: #d1d1d6;
  --accent: #30d158; --accent-dim: rgba(48,209,88,0.16);
  --amber: #ff9f0a; --amber-dim: rgba(255,159,10,0.16);
  --blue: #0a84ff; --red: #ff453a; --teal: #40c8e0; --plum: #bf5af2;
  --nav: rgba(12,13,17,0.66); --on-accent: #04220e; --marker: #ffffff;
  --gold: #ffd60a; --gold-dim: rgba(255,214,10,0.15);
  --shadow: 240deg 60% 2%; --shadow-strength: 0.75;
  --scrim: rgba(3,4,7,0.62); --wash: #030407;
  --state: rgba(255,255,255,0.08);
  --surface-blur: blur(22px) saturate(180%);
  --hairline: inset 0 1px 0 rgba(255,255,255,0.10);
  --aur-1: rgba(48,209,88,0.20); --aur-2: rgba(10,132,255,0.20);
  --aur-3: rgba(191,90,242,0.16);
  --grad-a: #34d75c; --grad-b: #1fc2a4;
  --r1: 10px; --r2: 14px; --r3: 18px; --r4: 20px; --r5: 28px;
  font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display",
               "Segoe UI Variable Text", "Segoe UI", Roboto, sans-serif;
  letter-spacing: -0.008em;
}
/* the one thing still exclusive to Titanium: tighter display type, and the
   coloured glow under a primary action. Everything else it used to own is
   now a token every theme sets. */
body[data-theme=titanium] h2 { letter-spacing: -0.03em; font-weight: 700; }
body[data-theme=titanium] .big { letter-spacing: 0.04em; }
body[data-theme=titanium] .primary,
body[data-theme=titanium] .sent {
  border-color: transparent; color: #04220e;
  box-shadow: 0 8px 22px -8px rgba(48,209,88,0.6), var(--hairline);
}
body[data-theme=titanium] .subtabs-ink { box-shadow: 0 4px 14px -4px rgba(48,209,88,0.55); }
/* A tile nested inside a section card steps to --card-2 so it does not draw the
   same fill as the surface under it. On the other five themes --card-2 is a
   fixed shade whatever it lands on; here every fill is translucent, so it
   COMPOUNDS: --card-2 over --card composites two steps lighter than a flat
   theme's --card-2 ever does, and --muted on it measured 4.47:1 — under AA, and
   introduced by the nesting rather than by the palette. On glass the second
   layer of the SAME fill is already the step (0.055 over 0.055 reads as a
   surface), which measures 4.91 and leaves the token alone. */
body[data-theme=titanium] .device,
body[data-theme=titanium] .group .rec { background: var(--card); }

body { color-scheme: dark; }   /* native widgets (date picker) follow the theme */
body[data-theme=basecamp], body[data-theme=chalk] { color-scheme: light; }

/* ── 2. tokens ───────────────────────────────────────────────────────────── */

:root {
  /* space: a 4px base, because every gap in the app is a multiple of it */
  --s1: 4px; --s2: 8px; --s3: 12px; --s4: 16px; --s5: 20px;
  --s6: 24px; --s7: 32px; --s8: 40px;

  /* radius */
  --r1: 8px; --r2: 10px; --r3: 12px; --r4: 16px; --r5: 22px; --rf: 999px;

  /* type: five sizes and two label sizes, and that is the whole scale */
  --f-hero: 34px; --f-title: 22px; --f-stat: 18px; --f-body: 14px;
  --f-small: 12.5px; --f-micro: 11px;
  --f-label: 10.5px;   /* the uppercase section labels */
  --f-tiny: 9.5px;     /* axis ticks and bar values */

  /* motion: short for feedback, medium for movement, long for arrival */
  --d1: 110ms; --d2: 190ms; --d3: 300ms; --d4: 480ms;
  --ease: cubic-bezier(0.2, 0, 0, 1);          /* decelerate into place */
  --ease-in: cubic-bezier(0.4, 0, 1, 1);
  --ease-spring: cubic-bezier(0.2, 0.9, 0.3, 1.2);

  --shell: 460px;
  /* The sticky topbar's own height: 30px of content, its 16px padding top and
     bottom, and the 1px divider. Anything that sticks below it offsets by this
     rather than by a guess; measured, not estimated. */
  --topbar-h: 63px;
}

/* Elevation: one shadow hue per theme, three heights. Layered shadows read as
   light falling on a surface; a single blurred box never does.
 *
 * These MUST be declared on `body`, not on `:root`, and the reason is subtle
 * enough to be worth writing down. A custom property resolves its var()
 * references against the element it is DECLARED on — not the element that
 * later uses it. --shadow is a theme value, and the themes live on
 * body[data-theme]. Declared up on :root, var(--shadow) would find nothing,
 * the whole token would compute to the guaranteed-invalid value, and every
 * `box-shadow: var(--e2)` in the file would silently resolve to `none`.
 * Silently: no console warning, no broken layout — just a flat app. */
body {
  --e1: 0 1px 2px hsl(var(--shadow) / calc(var(--shadow-strength) * 0.5));
  --e2: 0 1px 2px hsl(var(--shadow) / calc(var(--shadow-strength) * 0.45)),
        0 3px 8px -2px hsl(var(--shadow) / calc(var(--shadow-strength) * 0.4));
  --e3: 0 2px 4px hsl(var(--shadow) / calc(var(--shadow-strength) * 0.4)),
        0 10px 24px -6px hsl(var(--shadow) / calc(var(--shadow-strength) * 0.5));
}

/* ── 3. base ─────────────────────────────────────────────────────────────── */

* { box-sizing: border-box; min-width: 0; -webkit-tap-highlight-color: transparent; }
html, body { height: 100%; }
body {
  margin: 0; background: var(--bg); color: var(--text);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  font-size: var(--f-body); line-height: 1.5; overflow-wrap: break-word;
  -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility;
}
/* Numbers are the product. Tabular figures stop a stat from shivering as it
   counts up, and keep columns of them in line. */
.num, .tile .v, .ring-num, .stat-v, .try .meta, .pyr .n, .wallrow .meta,
.wk-val, .tally-count, .streak-n, .prog-now .v, .prog-meta, .prog-dose,
input[type=number] {
  font-variant-numeric: tabular-nums; font-feature-settings: "tnum" 1;
}
:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--r1);
}
::placeholder { color: var(--muted); opacity: 1; }
::selection { background: var(--accent-dim); }

/* transparent, so the ambient light behind the page is not painted over —
   the app floats on it rather than sitting on it */
.shell {
  max-width: var(--shell); margin: 0 auto; min-height: 100%;
  display: flex; flex-direction: column; position: relative;
  background: transparent;
}
.content { flex: 1 1 auto; padding: var(--s4) var(--s4) 96px; }

/* On a desktop the phone-width column shouldn't float in a void: wash the
   page behind it and give the column its own faint pane, so the aurora still
   shows through but the reading area stays distinct from the surround. */
@media (min-width: 620px) {
  body { background: var(--wash); }
  .shell {
    box-shadow: var(--e3); min-height: 100vh;
    background: color-mix(in srgb, var(--bg) 82%, transparent);
    backdrop-filter: var(--surface-blur);
  }
}

/* ── topbar + the logo lockup ───────────────────────────────────────────── */

/* Sticky, so the account button and the logo stay reachable down a long
   scroll — the same reason the nav is fixed.
   The negative margins pull it out to the content's own padding and the
   matching padding puts the contents back, so the bar's background spans the
   full column instead of stopping short at the text margin.
   `sticky` rather than `fixed` on purpose: it is unaffected by the ancestor
   backdrop-filter that broke the nav, because it resolves against the nearest
   scrollport rather than the nearest containing block. */
.topbar {
  position: sticky; top: 0; z-index: 30;
  display: flex; justify-content: space-between; align-items: center;
  gap: var(--s3); min-height: 30px;
  margin: calc(var(--s4) * -1) calc(var(--s4) * -1) var(--s4);
  padding: var(--s4);
  background: var(--nav);
  backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
  /* transparent until something scrolls under it, so the bar only announces
     itself when it is actually covering something */
  border-bottom: 1px solid transparent;
  transition: border-color var(--d2) var(--ease);
}
.topbar.stuck { border-bottom-color: var(--line); }
.lockup { display: flex; align-items: center; gap: var(--s2); color: var(--text); }
.lockup .mark { width: 34px; height: 25px; display: block; flex: none; }
.lockup .words { display: flex; flex-direction: column; line-height: 1; }
.wordmark {
  font-family: Rockwell, "Roboto Slab", "Bookman Old Style", Georgia, serif;
  font-weight: 700; font-size: 15px; letter-spacing: 0.04em; color: var(--text);
}
.subwordmark {
  font-size: 7.5px; letter-spacing: 0.30em; font-weight: 700; margin-top: 2px;
  color: var(--brand-rust);
}
body[data-theme=midnight] .subwordmark, body[data-theme=alpenglow] .subwordmark,
body[data-theme=sandstone] .subwordmark { color: var(--amber-ink); }
.top-right { display: flex; gap: var(--s2); align-items: center; color: var(--muted); font-size: var(--f-small); }
.timer { font-variant-numeric: tabular-nums; }
.timer.live::before {
  content: ""; display: inline-block; width: 6px; height: 6px; border-radius: 50%;
  background: var(--red); margin-right: 6px; vertical-align: middle;
  animation: pulse 2s var(--ease) infinite;
}
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.25; } }

h2 { font-size: var(--f-title); margin: 0 0 var(--s3); letter-spacing: -0.015em; font-weight: 800; }
.h2-sub { color: var(--muted); font-size: var(--f-small); margin: -10px 0 var(--s4); }
.s-label {
  font-size: var(--f-label); letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--muted); font-weight: 800; margin: var(--s5) 0 var(--s2);
}
.s-label .opt { font-weight: 400; text-transform: none; letter-spacing: 0; }
.muted { color: var(--muted); }
.hidden { display: none !important; }

/* ── tab bar ─────────────────────────────────────────────────────────────── */

.nav {
  position: fixed; bottom: 0; left: 0; right: 0; display: flex;
  border-top: 1px solid var(--line); background: var(--nav);
  padding: var(--s2) var(--s2) calc(var(--s3) + env(safe-area-inset-bottom));
  max-width: var(--shell); margin: 0 auto; z-index: 40;
  backdrop-filter: blur(12px);
}
.nav button {
  flex: 1; display: flex; flex-direction: column; align-items: center; gap: 3px;
  background: none; border: 0; padding: 6px 0 2px; cursor: pointer;
  font: inherit; font-size: var(--f-tiny); letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--muted); font-weight: 700;
  transition: color var(--d1) var(--ease);
}
.nav svg { width: 22px; height: 22px; stroke-width: 1.8; }
.nav .pip {
  width: 4px; height: 4px; border-radius: 50%; background: transparent;
  margin-top: 1px; transition: background var(--d2) var(--ease);
}
.nav button[aria-selected=true] { color: var(--accent-ink); }
.nav button[aria-selected=true] .pip { background: var(--accent); }
.nav button[aria-selected=true] svg { stroke-width: 2.3; }

/* ── surfaces ────────────────────────────────────────────────────────────── */

.card, .sum, .tile, .try, .wk-bars, .week, .insight, .felt-row, .editor,
.lift-form, .prog, .hero, .composer {
  background: var(--card); border: 1px solid var(--line); border-radius: var(--r3);
}
.card, .sum, .tile, .try, .wk-bars, .week, .insight, .felt-row, .prog, .hero,
.composer {
  box-shadow: var(--e1), var(--hairline);
}
/* Frosting is opt-in per theme via --surface-blur, and only on the larger
   surfaces: backdrop-filter is expensive, and a list of forty blurred rows
   costs far more than it buys. */
.card, .sum, .hero, .wk-bars, .week, .tile, .rec, .subtabs, .sheet, .toast,
.composer, .disc {
  backdrop-filter: var(--surface-blur);
}
.sum { padding: var(--s3) var(--s4); font-size: var(--f-body); color: var(--soft); }
.sum b { color: var(--text); }
.card { padding: var(--s4); }

/* ── chips ───────────────────────────────────────────────────────────────── */

.chips { display: flex; flex-wrap: wrap; gap: 7px; }
.chip {
  border: 1px solid var(--line); background: var(--card); color: var(--text);
  border-radius: var(--r2); padding: 10px 6px; text-align: center;
  font-weight: 800; font-size: var(--f-body); font-family: inherit;
  /* A chip's label is a single token — "Sandstone" broken across two lines on
     a narrow phone reads as two options. The row wraps instead: a long chip
     claims the width it needs and pushes its siblings down rather than folding
     its own text.
     THE BASIS IS `auto`, AND THAT IS THE WHOLE RULE. It used to be a fixed
     56px, which read as the same thing and is not: flex-shrink 0 protects the
     BASIS, so every chip was pinned at 56px, grew to an equal share of the row,
     and clipped whatever did not fit. Invisible while every label was two
     characters — and then "Movement RiNo" arrived beside five other gyms and
     six chips read "Movemen", "Lincoln La", "Movemen". With `auto` the basis is
     the label, min-width keeps the short ones from becoming stamps, and a row
     that cannot fit wraps. */
  flex: 1 0 auto; min-width: 56px; cursor: pointer; user-select: none;
  white-space: nowrap;
  transition: background var(--d1) var(--ease), border-color var(--d1) var(--ease),
              color var(--d1) var(--ease), transform var(--d1) var(--ease);
}
.chip.small { font-weight: 700; font-size: var(--f-small); padding: 8px 7px;
              flex: 1 0 auto; min-width: 58px; }
.chip:active { transform: scale(0.95); }
@media (hover: hover) { .chip:hover { background: var(--card-2); } }
.chip.sel { border-color: var(--accent); background: var(--accent-dim); color: var(--text); }
.chip.warn.sel { border-color: var(--amber); background: var(--amber-dim); color: var(--text); }

/* Effort chips carry the heat ramp, so choosing a 9 and reading the effort
   histogram are the same visual language. --hc is set per chip from the same
   ramp() the charts use. */
.chip.heat {
  --hc: var(--accent);
  border-color: color-mix(in srgb, var(--hc) 45%, transparent);
  color: var(--hc);
  background: color-mix(in srgb, var(--hc) 12%, var(--card));
}
.chip.heat.sel {
  background: var(--hc); border-color: var(--hc); color: var(--on-heat);
  box-shadow: var(--e2);
}
/* The app's assumption, not the athlete's answer. It has to be visibly a
   third state: solid like .sel would claim they chose it, plain like an
   unselected chip would hide that a value is going to be stored. Dashed and
   half-tinted says "this is what we've put, change it if it's wrong". */
.chip.heat.assumed {
  background: color-mix(in srgb, var(--hc) 20%, var(--card));
  border-color: color-mix(in srgb, var(--hc) 70%, transparent);
  border-style: dashed; color: var(--hc); box-shadow: none;
}
@media (hover: hover) {
  .chip.heat:hover { background: color-mix(in srgb, var(--hc) 22%, var(--card)); }
  .chip.heat.sel:hover { background: var(--hc); }
}

/* ── buttons ─────────────────────────────────────────────────────────────── */

.big {
  flex: 1; border: none; border-radius: var(--r4); padding: 17px 0; text-align: center;
  font-weight: 900; font-size: 15px; letter-spacing: 0.1em; cursor: pointer;
  font-family: inherit; position: relative; overflow: hidden;
  transition: transform var(--d1) var(--ease), filter var(--d1) var(--ease),
              opacity var(--d1) var(--ease);
}
.btns { display: flex; gap: var(--s3); margin-top: var(--s4); }
/* background-COLOR, not the background shorthand: the shorthand resets
   background-image, which would wipe out the accent gradient the material
   section sets on exactly these three controls. */
.fell { background: var(--card); border: 1.5px solid var(--amber); color: var(--amber-ink); box-shadow: var(--e1); }
.sent { background-color: var(--accent); border: 1.5px solid var(--accent); color: var(--on-accent); box-shadow: var(--e2); }
/* Colour only, all four of them. `width: 100%` and a margin used to live on
   `.primary` and on `.danger`, and that is layout wearing a colour class's
   name — it leaked onto the one `.mini.danger` in the app, the SIGN OUT button
   in the device list. `.mini` is `flex: none`, so the button demanded the whole
   row and refused to shrink, squeezing "iPhone · Safari" into a column one
   character wide. `.primary` had the identical rule and had simply not met a
   small button yet. Layout belongs to the size class. */
.primary { background-color: var(--accent); border: 1.5px solid var(--accent);
           color: var(--on-accent); box-shadow: var(--e2); }
.danger { background: transparent; border: 1.5px solid var(--red); color: var(--red-ink); }
.ghost { background: var(--card); border: 1px solid var(--line); color: var(--text); box-shadow: var(--e1); }
.big.primary { width: 100%; margin-top: var(--s4); }
.big.danger { width: 100%; margin-top: var(--s3); }
.big:active { transform: scale(0.975); filter: brightness(0.97); }
/* `disabled` does not only grey a button out — it takes it out of the tab order
   and hides it from the accessibility tree. That is how the refuse-to-sell gate
   came to be a button a keyboard user could not reach, sitting above a
   paragraph explaining a refusal they would therefore never hear. aria-disabled
   says the same thing to assistive technology and keeps the tab stop, so the
   button can carry an aria-describedby pointing at its own reason. */
.big[disabled], .big[aria-busy=true], .big[aria-disabled=true] {
  opacity: 0.55; pointer-events: none;
}

/* the press ripple: a scaled radial wash from where the finger landed */
.big::after {
  content: ""; position: absolute; left: var(--rx, 50%); top: var(--ry, 50%);
  width: 12px; height: 12px; margin: -6px 0 0 -6px; border-radius: 50%;
  background: currentColor; opacity: 0; transform: scale(1); pointer-events: none;
}
.big.rippling::after { animation: ripple var(--d4) var(--ease); }
@keyframes ripple {
  from { opacity: 0.24; transform: scale(1); }
  to   { opacity: 0; transform: scale(26); }
}

.end-pill {
  font-size: var(--f-micro); font-weight: 800; letter-spacing: 0.12em; color: var(--red-ink);
  border: 1px solid var(--red); border-radius: var(--rf); padding: 4px 12px;
  cursor: pointer; background: transparent; font-family: inherit;
}
.end-pill:active { transform: scale(0.95); }
.skip { text-align: center; color: var(--muted); font-size: var(--f-small);
        margin-top: var(--s3); cursor: pointer; background: none; border: 0;
        font-family: inherit; width: 100%; padding: var(--s2); }
.hint { color: var(--muted); font-size: 11.5px; text-align: center; margin-top: 6px; }
.back-link { color: var(--muted); font-size: var(--f-small); cursor: pointer;
             margin-bottom: 6px; background: none; border: 0; font-family: inherit;
             padding: 4px 0; display: inline-block; }

/* ── inputs ──────────────────────────────────────────────────────────────── */

input[type=text], input[type=date], input[type=number], input[type=email],
textarea, select {
  width: 100%; background: var(--card); border: 1px solid var(--line);
  border-radius: var(--r2); color: var(--text); font-family: inherit;
  font-size: var(--f-body); padding: 11px 12px;
  transition: border-color var(--d1) var(--ease), box-shadow var(--d1) var(--ease);
}
input:focus, textarea:focus, select:focus {
  outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-dim);
}
textarea { resize: none; height: 68px; font-size: 13px; }
/* No native <select> ships in this app any more — the exercise picker was the
   last one, and it was unreadable on a dark theme because the OS paints the
   popup's background and only the TEXT color is inherited from here. If one
   ever comes back, these two lines are what stop it happening again; they are
   honoured unevenly across platforms, which is exactly why the picker is a
   sheet instead. */
option, optgroup { background: var(--card); color: var(--text); }

/* ── the segmented control ───────────────────────────────────────────────── */

/* A heading and its control, competing for one narrow row. On a 375px screen
   "Send pyramid · 12 weeks" beside a three-button control does not fit, and
   without wrap the control is crushed until its own words break — "HIDE"
   across two lines. Wrapping lets the control drop to its own row at full
   width instead, which costs a few pixels of height and nothing else. */
.sec-head {
  display: flex; justify-content: space-between; align-items: center;
  flex-wrap: wrap; row-gap: var(--s2); column-gap: var(--s3);
  margin: var(--s5) 0 var(--s2);
}
.sec-head .s-label { margin: 0; flex: 1 1 auto; min-width: 0; }
.seg { display: inline-flex; border: 1px solid var(--line); border-radius: var(--r1);
       overflow: hidden; background: var(--card); flex: 0 0 auto; }
/* `> span` because the history month's chip lives inside a <button> that wraps
   the whole strip, and a chip cannot be a button in there. Direct child only,
   so it never catches a span someone nests inside a real .seg button. */
.seg div, .seg > span, .seg button {
  padding: 5px 11px; font-size: var(--f-tiny); font-weight: 800; letter-spacing: 0.1em;
  color: var(--muted); cursor: pointer; user-select: none; background: none;
  border: 0; font-family: inherit; white-space: nowrap;
  transition: background var(--d1) var(--ease), color var(--d1) var(--ease);
}
.seg div.sel, .seg > span.sel, .seg button.sel {
  background: var(--accent-dim); color: var(--text);
}

/* ── the start screen: streak hero + week ring ───────────────────────────── */

.hero { padding: var(--s4); display: flex; align-items: center; gap: var(--s4); }
.hero-ring { flex: none; position: relative; width: 76px; height: 76px; }
.hero-ring svg { width: 76px; height: 76px; transform: rotate(-90deg); }
.hero-ring .track { fill: none; stroke: var(--card-2); stroke-width: 8; }
.hero-ring .fill {
  fill: none; stroke: var(--accent); stroke-width: 8; stroke-linecap: round;
  transition: stroke-dashoffset var(--d4) var(--ease);
}
.hero-ring .cap {
  position: absolute; inset: 0; display: flex; flex-direction: column;
  align-items: center; justify-content: center; line-height: 1;
}
.hero-ring .cap b { font-size: var(--f-stat); font-variant-numeric: tabular-nums; }
.hero-ring .cap span { font-size: var(--f-tiny); color: var(--muted); margin-top: 3px;
                       text-transform: uppercase; letter-spacing: 0.08em; font-weight: 700; }
.hero-body { flex: 1 1 auto; min-width: 0; }
.streak-n { font-size: var(--f-hero); font-weight: 800; letter-spacing: -0.03em; line-height: 1; }
.streak-unit { font-size: var(--f-small); color: var(--muted); font-weight: 700;
               text-transform: uppercase; letter-spacing: 0.1em; margin-left: 6px; }
/* what the number counts, in the same register as .streak-unit — the headline
   is a ratio now and a ratio with no unit under it is two numbers and a word */
.hero-sub { font-size: var(--f-tiny); color: var(--muted); font-weight: 700;
            text-transform: uppercase; letter-spacing: 0.08em; margin-top: 4px; }
.hero-note { color: var(--soft); font-size: var(--f-small); margin-top: 6px; }
/* the streak's state drives its gradient stops, up in the material section —
   a flat `color` here would win over background-clip: text and kill it */

.dayline { display: flex; gap: 5px; margin-top: var(--s3); }
.dayline .d { flex: 1; text-align: center; font-size: var(--f-tiny); color: var(--muted); }
.dayline .dot {
  height: 5px; border-radius: var(--rf); background: var(--card-2);
  border: 1px solid var(--line); margin-bottom: 4px;
}
.dayline .dot.on { background: var(--accent); border-color: var(--accent); }
.dayline .dot.today { border-color: var(--blue); border-width: 1.5px; }
/* A program's opinion about a day, on the same strip as the sessions it
   actually got. `plan` is asked and still ahead; `miss` is asked and the day
   has gone. A day with NO dot class is a day the program says nothing about —
   which is what keeps "rest day" and "no opinion" visibly different rather than
   collapsing both into an empty slot. */
.dayline .dot.plan { background: var(--accent-dim); border-color: var(--accent); }
.dayline .dot.miss { background: var(--card-2); border-color: var(--amber); }

/* ── tiles ───────────────────────────────────────────────────────────────── */

.tiles { display: flex; gap: var(--s2); margin-top: var(--s3); }
.tiles.wrap { flex-wrap: wrap; }
.tile { flex: 1; padding: var(--s3) var(--s2); text-align: center; }
.tile .v { font-size: 17px; font-weight: 800; letter-spacing: -0.01em; }
.tile .l { font-size: var(--f-tiny); color: var(--muted); text-transform: uppercase;
           letter-spacing: 0.08em; margin-top: 3px; line-height: 1.25; }

/* ── highlights: what a session broke ───────────────────────────────────── */

.hl { display: flex; flex-direction: column; gap: var(--s2); margin-top: var(--s3); }
.hl-item {
  display: flex; align-items: center; gap: var(--s3); padding: var(--s3) var(--s4);
  border-radius: var(--r3); border: 1px solid var(--line); background: var(--card);
  box-shadow: var(--e1); animation: rise var(--d4) var(--ease) both;
}
.hl-item.gold { border-color: color-mix(in srgb, var(--gold) 55%, transparent);
                background: var(--gold-dim); }
.hl-item.accent { border-color: color-mix(in srgb, var(--accent) 45%, transparent);
                  background: var(--accent-dim); }
.hl-item .badge {
  flex: none; width: 32px; height: 32px; border-radius: 50%; display: grid;
  place-items: center; background: var(--card); border: 1px solid var(--line);
}
.hl-item.gold .badge { color: var(--gold-ink); border-color: color-mix(in srgb, var(--gold) 45%, transparent); }
.hl-item.accent .badge { color: var(--accent-ink); border-color: color-mix(in srgb, var(--accent) 40%, transparent); }
.hl-item svg { width: 17px; height: 17px; }
.hl-item .t { font-weight: 800; font-size: 13.5px; }
.hl-item .d { color: var(--soft); font-size: var(--f-small); margin-top: 1px; }
@keyframes rise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
.hl-item:nth-child(2) { animation-delay: 70ms; }
.hl-item:nth-child(3) { animation-delay: 140ms; }
.hl-item:nth-child(4) { animation-delay: 210ms; }

.wrap-hero { font-size: 19px; font-weight: 800; margin: 2px 0; }

/* ── records shelf ───────────────────────────────────────────────────────── */

.rec-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s2); }
.rec {
  background: var(--card); border: 1px solid var(--line); border-radius: var(--r3);
  padding: var(--s3); box-shadow: var(--e1);
}
.rec .k { font-size: var(--f-tiny); text-transform: uppercase; letter-spacing: 0.08em;
          color: var(--muted); font-weight: 800; }
.rec .v { font-size: var(--f-stat); font-weight: 800; margin-top: 3px;
          font-variant-numeric: tabular-nums; }
.rec .w { font-size: var(--f-micro); color: var(--muted); margin-top: 1px; }
.track-row { margin-bottom: var(--s3); }
.track-head { display: flex; justify-content: space-between; align-items: baseline;
              font-size: var(--f-small); margin-bottom: 5px; }
.track-head .n { color: var(--muted); font-variant-numeric: tabular-nums; }
.meter { height: 7px; border-radius: var(--rf); background: var(--card-2);
         border: 1px solid var(--line); overflow: hidden; }
.meter i { display: block; height: 100%; background: var(--accent-ink);
           transition: width var(--d4) var(--ease); }
.ladder { display: flex; flex-wrap: wrap; gap: 6px; }
.rung {
  border: 1px solid var(--line); background: var(--card); border-radius: var(--r2);
  padding: 7px 10px; font-size: var(--f-small); box-shadow: var(--e1);
}
.rung b { font-weight: 800; }
.rung span { color: var(--muted); margin-left: 6px; font-size: var(--f-micro); }

/* ── the live logger ─────────────────────────────────────────────────────── */

/* The composer is a card because it is one act. This screen was nine
   label/content pairs in a row — every one an .s-label over a .chips, with the
   same margins and the same weight — so there was no visual answer to "where
   does making a climb stop and reviewing them start". The inputs get a
   surface; the record below gets a rule. */
.composer { padding: var(--s3) var(--s4) var(--s4); }
.composer > *:first-child { margin-top: 0; }
/* --s5 between sections is right on a bare column and too airy inside a card */
.composer .s-label { margin-top: var(--s4); }
.composer .hint { margin-bottom: 0; }

.tally-head {
  display: flex; justify-content: space-between; align-items: center;
  margin: var(--s5) 0 var(--s2); padding-top: var(--s4);
  border-top: 1px solid var(--line);
}
.tally-head .s-label { margin: 0; }
.tally-count { font-size: var(--f-small); color: var(--muted); font-weight: 400; }
.log-end { width: 100%; margin-top: var(--s5); }

/* A .sec-head inside the composer: both `.composer .s-label` and
   `.sec-head .s-label` are two-class selectors, so the later one in this file
   wins on source order alone — and that is .composer's 16px margin-top, which
   would knock the label out of line with its segment. Named explicitly rather
   than left to whichever rule happens to move. */
.composer .sec-head { margin: var(--s4) 0 var(--s2); }
.composer .sec-head .s-label { margin: 0; }

.try {
  display: flex; justify-content: space-between; align-items: center; gap: var(--s2);
  padding: 10px var(--s3); font-size: 13px; margin-bottom: 6px; cursor: pointer;
  /* opacity is in here for the offline queue: a .pending row sits at 0.55 and
     going solid is the app saying the climb actually landed. Without the
     transition it just snapped, which read as a repaint rather than a save. */
  transition: background var(--d1) var(--ease), transform var(--d1) var(--ease),
              opacity var(--d2) var(--ease);
}
/* The grade wears the heat ramp and the wall wears its exposure-chart hue —
   the app's two existing colour languages, on the row's two real facts.

   The colour is in the FILL and the BORDER; the text is --text. Two other
   arrangements were measured first and both failed AA. Ramp-coloured text on a
   faint tint read 2.9:1 on Chalk and 3.3:1 on Basecamp — a mid-ramp green on a
   near-white card has nowhere to go. A solid ramp fill with --on-heat over it
   fixed the four dark themes (6.7-10.4) but not the light pair: on Basecamp,
   white clears AA on blue/accent/red and fails on amber (3.85) while black does
   the exact reverse (red 3.86), so NO single ink works there.

   --text is high-contrast on --card in every theme by construction, and the
   ramp is continuous — eighteen grades interpolated between four stops — so an
   arrangement that only had to hold at the stops would be checked in the wrong
   places anyway. The grade is this row's primary content and does not get to be
   the least legible thing in it. */
.try .gpill {
  flex: none; font-weight: 800; font-size: 12px; padding: 3px 8px;
  border-radius: var(--rf); font-variant-numeric: tabular-nums; color: var(--text);
  background: color-mix(in srgb, var(--gc, var(--line)) 24%, var(--card));
  border: 1px solid color-mix(in srgb, var(--gc, var(--line)) 60%, transparent);
}
/* an ungraded climb has no place on the ramp, so it stays off it entirely */
.try .gpill.plain { background: var(--card-2); color: var(--soft); }
.try .wdot {
  display: inline-block; width: 7px; height: 7px; border-radius: 50%;
  background: var(--wc); margin-right: 5px; vertical-align: 1px;
}
.try:active { transform: scale(0.99); }
@media (hover: hover) { .try:hover { background: var(--card-2); } }

/* A ROW YOU CAN ACTUALLY OPERATE. Half the .try rows in this app are records —
   a climb that happened, a week that is coming — and half are controls. The
   controls were divs carrying an onclick, which is a control for the pointer
   and for nothing else: in the coached store `openSheet` puts focus on the
   cancel button, and with no second tab stop the whole catalogue could be read
   and not one product could be opened without a mouse.

   So the controls are buttons, with the UA styling taken back off. Everything
   the row wears still comes from .try; this only undoes what the tag brings. */
button.try {
  width: 100%; font: inherit; font-size: 13px; color: var(--text);
  text-align: left; -webkit-appearance: none; appearance: none;
}
/* The global :focus-visible rounds its ring at --r1, which is right for the
   many things that have no radius of their own. A row has one, and inheriting
   the smaller value drew a ring a different shape from the card inside it. */
button.try:focus-visible { border-radius: var(--r3); }
/* .try .grow stacks a title over a subtitle, and the subtitle was a <div> —
   which is not phrasing content and so cannot live inside a <button>. As a
   <span> it has to be told what being a div already made it. */
.try .grow .meta { display: block; }

/* A row that is a RECORD, not a control. cursor:pointer and the hover tint
   have been on .try since the logger, where every row does open something; the
   coached preview then reused the class for Week one and The rest of it, which
   answer to nothing. A row that lights up under the cursor and does nothing
   when pressed is the interface lying about itself — and on a phone, where
   there is no hover, the lie is only ever discovered by pressing. */
.try.static { cursor: default; }
.try.static:active { transform: none; }
@media (hover: hover) { .try.static:hover { background: var(--card); } }
/* Today's prescription, as one line under the greeting. No surface of its own:
   it is a fact about today in the same voice as the greeting above it, and a
   card here would be a third panel competing with the hero and the last-session
   tiles for the top of the screen the app opens on. */
.plan-row {
  display: flex; align-items: center; gap: var(--s2); width: 100%;
  background: none; border: 0; padding: 0 0 var(--s3); margin: 0;
  font: inherit; color: var(--muted); text-align: left; cursor: pointer;
}
.plan-row .pl-k {
  flex: none; font-size: var(--f-label); font-weight: 800; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--accent-ink);
}
.plan-row .pl-v {
  flex: 1 1 auto; min-width: 0; font-size: var(--f-small); color: var(--soft);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.plan-row .row-chev { flex: none; }
.plan-row:active { opacity: 0.6; }

/* A row the program has asked for and the log has not got yet. Dashed, because
   it is a prescription rather than a record — everything else in a .try list is
   something that happened, and the two must not read the same. */
.try.planned { border-style: dashed; background: none; box-shadow: none; }
.try.planned .res { color: var(--accent-ink); }
.try .res { font-weight: 800; }
.try .res.s { color: var(--accent-ink); }
.try .res.f { color: var(--amber-ink); }
/* The store's recommendation. Gold rather than accent, because accent already
   means "yours" two lines up and a shop where owned and suggested are the same
   colour is a shop nobody can skim. */
.try .res.w { color: var(--gold-ink); }
.try .meta { color: var(--muted); }
.try .grow { flex: 1 1 auto; }
.try.pending { opacity: 0.55; }
.try.fresh { animation: pop var(--d3) var(--ease-spring); }
@keyframes pop {
  from { opacity: 0; transform: translateY(-6px) scale(0.98); }
  to { opacity: 1; transform: none; }
}

.editor { padding: var(--s3); margin: -2px 0 var(--s2); display: flex;
          flex-direction: column; gap: var(--s3); background: var(--card-2);
          animation: rise var(--d2) var(--ease); }
.stepper { display: flex; align-items: center; justify-content: center; gap: var(--s4); }
.stepper .step {
  width: 40px; height: 40px; border-radius: var(--r2); border: 1px solid var(--line);
  background: var(--card); color: var(--text); font-size: 18px; font-weight: 800;
  display: flex; align-items: center; justify-content: center; cursor: pointer;
  user-select: none; font-family: inherit;
}
.stepper .step:active { transform: scale(0.92); }
.stepper b { font-size: 16px; min-width: 68px; text-align: center; }
/* the loggable-grade range: two steppers, because eighteen grades twice over
   would be thirty-six buttons to express one interval */
.range-one { display: flex; align-items: center; justify-content: space-between;
             gap: var(--s3); margin-top: var(--s2); }
.range-one .rl { font-size: var(--f-label); letter-spacing: 0.12em;
                 text-transform: uppercase; color: var(--muted); font-weight: 800; }
.range-one .stepper { justify-content: flex-end; gap: var(--s3); }
.range-one .stepper b { min-width: 52px; font-variant-numeric: tabular-nums; }
.ed-label { font-size: var(--f-label); letter-spacing: 0.14em; text-transform: uppercase;
            color: var(--muted); font-weight: 800; margin-top: 2px; }
.ed-label span { font-weight: 400; text-transform: none; letter-spacing: 0; }
.editor input[type=text] { font-size: 13px; padding: 9px 11px; }
.editor-actions { display: flex; align-items: center; gap: var(--s3); }
.editor-done {
  flex: 1; border: none; border-radius: var(--r2); padding: 12px 0; font-weight: 900;
  letter-spacing: 0.08em; font-size: var(--f-small); background-color: var(--accent);
  color: var(--on-accent); cursor: pointer; font-family: inherit;
}
.editor-remove {
  color: var(--red-ink); border: 1px solid var(--red); background: transparent;
  border-radius: var(--r2); padding: 11px 14px; font-size: var(--f-small);
  font-weight: 800; cursor: pointer; font-family: inherit;
}
.editor-done:active, .editor-remove:active { transform: scale(0.98); }
.tally-toggle { cursor: pointer; user-select: none; background: none; border: 0;
                font: inherit; padding: 0; text-align: left; }

.lift-form { padding: var(--s3); margin-bottom: var(--s2); display: flex;
             flex-direction: column; gap: var(--s2); background: var(--card-2); }
.lift-form .cols { display: flex; gap: var(--s2); }
.lift-form .cols > div { flex: 1; min-width: 0; }
.lift-form input { font-size: 13px; padding: 9px 11px; }
/* the picker's trigger, wearing the same clothes the <select> did so the form
   reads as one control stack */
.lf-pick {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s2);
  width: 100%; background: var(--card); border: 1px solid var(--line);
  border-radius: var(--r2); color: var(--text); font-family: inherit;
  font-size: 13px; font-weight: 600; padding: 9px 11px; text-align: left;
  cursor: pointer; transition: border-color var(--d1) var(--ease);
}
.lf-pick .lf-pick-v { flex: 1 1 auto; min-width: 0; overflow: hidden;
                      text-overflow: ellipsis; white-space: nowrap; }
/* --soft, not --muted: until something is picked this placeholder IS the
   control's label, and muted lands just under 4.5:1 on the dark themes. The
   lighter weight is what marks it unset. */
.lf-pick .lf-pick-v.ph { color: var(--soft); font-weight: 400; }
.lf-pick .lf-pick-c { flex: none; width: 8px; height: 8px; margin-right: 2px;
                      border-right: 2px solid var(--muted);
                      border-bottom: 2px solid var(--muted);
                      transform: rotate(45deg) translate(-2px, -2px); }
@media (hover: hover) { .lf-pick:hover { border-color: var(--accent); } }
.lift-form .cols input { margin-top: 4px; }

/* ── check-in + pain ─────────────────────────────────────────────────────── */

.ck-hint { text-align: left; margin: 0 0 var(--s2); }
.ck-row { display: flex; align-items: center; gap: var(--s2); margin-bottom: 6px; }
/* One line, always. The labels are short enough now that they fit, but a
   longer one wrapping would grow the row and knock its chips out of line with
   the rows above and below it — the alignment is what makes the three rows
   read as one scale. */
.ck-row .lb { flex: 0 0 80px; font-size: var(--f-tiny); letter-spacing: 0.08em;
              font-weight: 800; color: var(--muted); text-transform: uppercase;
              line-height: 1.2; white-space: nowrap; }
.ck-row .chips { flex: 1; }
.ck-dots { display: flex; gap: 4px; flex-wrap: wrap; align-items: center; }
.ck-dot { width: 11px; height: 11px; border-radius: 50%; background: var(--card-2);
          border: 1px solid var(--line); }
.pain-none {
  width: 100%; text-align: center; border: 1px solid var(--line); background: var(--card);
  color: var(--text); border-radius: var(--r3); padding: 15px 0; font-weight: 800;
  font-size: 15px; cursor: pointer; user-select: none; font-family: inherit;
  box-shadow: var(--e1); transition: background var(--d1) var(--ease),
                                     border-color var(--d1) var(--ease), color var(--d1) var(--ease);
}
.pain-none.sel { border-color: var(--accent); background: var(--accent-dim); color: var(--text); }
.pain-none:active { transform: scale(0.98); }
.undo-pill {
  font-size: var(--f-micro); font-weight: 800; letter-spacing: 0.1em; color: var(--muted);
  border: 1px solid var(--line); border-radius: var(--rf); padding: 4px 12px;
  cursor: pointer; background: var(--card); font-family: inherit;
}
.undo-pill:active { transform: scale(0.95); }

/* ── insight sub-tabs ────────────────────────────────────────────────────── */
/* The dashboard used to be one scroll of ten panels. Three groups answer three
   different questions — what shape am I in, what am I climbing, how am I
   arriving — and splitting them is what makes any of it navigable. */

/* below the topbar, not under it: two sticky bars both at top:0 would overlap,
   and the one with the higher z-index would simply hide the other */
.subtabs-bar {
  position: sticky; top: var(--topbar-h); z-index: 20; background: var(--bg);
  padding: 2px 0 10px; margin-bottom: var(--s2);
}
.subtabs {
  position: relative; display: flex; gap: 2px; padding: 3px;
  background: var(--card-2); border: 1px solid var(--line);
  border-radius: var(--rf);
}
/* nowrap is load-bearing at four tabs: on a 320px screen the widest label is
   within a pixel or two of its share of the bar, and left to wrap it breaks
   mid-word into a two-line tab. Tightening the tracking buys the margin the
   fourth tab spent. */
.subtabs button {
  flex: 1; position: relative; z-index: 1; background: none; border: 0;
  font: inherit; font-size: var(--f-small); font-weight: 800;
  letter-spacing: 0.04em; text-transform: uppercase; color: var(--muted);
  padding: 9px 2px; border-radius: var(--rf); cursor: pointer;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  transition: color var(--d2) var(--ease);
}
/* below ~360px the four labels no longer fit at the standard size, and a tab
   reading "WORKOUT…" is worse than a tab reading small */
@media (max-width: 359px) {
  .subtabs button { font-size: var(--f-micro); letter-spacing: 0.01em; padding: 9px 1px; }
}
.subtabs button[aria-selected=true] { color: var(--on-accent); }
.subtabs-ink {
  position: absolute; top: 3px; bottom: 3px; left: 0; z-index: 0;
  border-radius: var(--rf); background-color: var(--accent); box-shadow: var(--e1);
  transition: transform var(--d3) var(--ease-spring), width var(--d3) var(--ease);
}
.win-chips { margin: 0 0 var(--s3); }
.panel { animation: panel-in var(--d3) var(--ease); }
@keyframes panel-in {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: none; }
}

/* ── 3D stages ───────────────────────────────────────────────────────────── */
/* Real perspective, built from CSS transforms rather than a WebGL dependency:
   the app has a strict same-origin CSP and no build step, and a megabyte of
   3D library to draw eight boxes would be a bad trade. Both stages are
   decoration ON TOP of a numeric list that carries the same data, so nothing
   is lost if a browser declines to composite them. */

.stage {
  position: relative; perspective: 900px; perspective-origin: 50% 42%;
  margin: var(--s3) 0 var(--s2); border-radius: var(--r3);
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--card) 92%, transparent),
    color-mix(in srgb, var(--card-2) 92%, transparent));
  border: 1px solid var(--line); box-shadow: var(--e1); overflow: hidden;
  /* horizontal drags spin the scene, vertical ones still scroll the page */
  touch-action: pan-y; cursor: grab; user-select: none;
}
.stage.dragging { cursor: grabbing; }
/* no backdrop-filter on a stage, ever: it is a grouping property and would
   flatten the 3D underneath exactly like an opacity animation does */
/* The fade lives on .stage, NOT on .stage-inner, and the rule behind that is
   the single most important line in this section: opacity is a GROUPING
   property, so an element that animates it is forced to transform-style:
   flat. Fading .stage-inner would quietly collapse the entire 3D scene
   underneath it — the perspective stops applying and every child face
   projects edge-on. .stage sits outside the 3D chain (it only provides the
   perspective), so it can fade freely. */
.stage { animation: stage-fade var(--d4) var(--ease) both; }
@keyframes stage-fade { from { opacity: 0; transform: translateY(10px); } }
.stage-inner { position: absolute; inset: 0; transform-style: preserve-3d; }
.stage-cap {
  position: absolute; left: 0; right: 0; bottom: 6px; text-align: center;
  font-size: var(--f-tiny); letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--muted); font-weight: 700; z-index: 5;
}
.stage-empty {
  position: absolute; inset: 0; display: grid; place-items: center;
  color: var(--muted); font-size: 13px; padding: 0 var(--s5); text-align: center;
}

/* the send pyramid, as a pyramid */
.pyr3d { height: 196px; }
.pyr3d .stage-inner { transform-origin: 50% 55%; }
.pyr3d .ground {
  position: absolute; inset: 0; transform-style: preserve-3d;
  transform: rotateX(58deg) rotateZ(var(--spin, 0deg));
}
.tier {
  position: absolute; left: 50%; top: 58%;
  width: var(--w); height: var(--d);
  margin-left: calc(var(--w) / -2); margin-top: calc(var(--d) / -2);
  transform: translateZ(var(--z)); transform-style: preserve-3d;
  animation: tier-in var(--d4) var(--ease-spring) both;
}
/* transform only — see the note on .stage above; an opacity keyframe here
   would flatten the tier and lay its skirt down edge-on */
@keyframes tier-in { from { transform: translateZ(0) scale(0.55); } }
.tier i {
  position: absolute; display: block; background: var(--c);
  border-radius: 3px;
}
/* FIVE FACES AND ONE LIGHT. A box needs all four walls or the solid falls apart
   the moment you drag it: with only a near skirt, rotating past 90deg showed the
   hole where the far wall should be, and the darkened face was ALWAYS the same
   one, so the light appeared to rotate with the object instead of staying put.
   Four fixed darkness steps is how a low-poly renderer fakes a single light —
   crucially it is STABLE under rotation, which is the whole point.
   Darkened by mixing, not by filter(): a filter forces its element out of the
   parent's 3D rendering context in some engines. Same reason there is no opacity
   anywhere in this subtree.
   The light sits high, front-left: top brightest, then left, right, and the far
   wall darkest. */
.tier .top { inset: 0; transform: translateZ(var(--h)); background: var(--c); }
/* The near skirt. It starts at top:100% (the +Y edge, which the ground's 58deg
   tilt brings toward the viewer) and must stand up along +Z. rotateX maps the
   element's own +Y to (0, cos, sin), so +90deg — and only +90 — sends it to +Z;
   -90 would bury the skirt underneath the floor where nothing can see it. */
.tier .front {
  left: 0; top: 100%; width: var(--w); height: var(--h);
  transform-origin: top center; transform: rotateX(90deg);
  background: color-mix(in srgb, var(--c) 72%, #000);
  border-radius: 0 0 3px 3px;
}
/* The far wall: the same construction as .front, at the other edge. top:0 is the
   -Y edge, which the tilt sends away from the viewer, and rotateX(90deg) stands
   its height up along +Z from there. It fills the hole you used to see at spin
   180. No translateZ — the edge it hangs from is already the edge it belongs on. */
.tier .back {
  left: 0; top: 0; width: var(--w); height: var(--h);
  transform-origin: top center; transform: rotateX(90deg);
  background: color-mix(in srgb, var(--c) 52%, #000);
  border-radius: 0 0 3px 3px;
}
/* The two side walls, and the transform order here is the whole trick.
   A side wall needs its WIDTH to run along depth (world +Y) and its HEIGHT to
   stand up (world +Z). Reading right-to-left, rotateX(90deg) first sends local +Y
   to +Z, then rotateZ(90deg) sends local +X to +Y — so `rotateZ(90deg)
   rotateX(90deg)` is the composition that does it.
   Getting this backwards is not subtle: rotateY(-90deg) rotateX(90deg) maps the
   element's WIDTH to vertical instead, which stood each wall --d tall — 170px on
   the base tier — and threw 92px of geometry off the top of the stage.
   Both walls share the transform; only `left` differs, which is what puts one in
   the x=0 plane and the other in x=w. */
.tier .left, .tier .right {
  top: 0; width: var(--d); height: var(--h);
  transform-origin: left top; transform: rotateZ(90deg) rotateX(90deg);
  border-radius: 0 0 3px 3px;
}
.tier .left { left: 0; background: color-mix(in srgb, var(--c) 84%, #000); }
.tier .right { left: var(--w); background: color-mix(in srgb, var(--c) 62%, #000); }
/* The ghost's rim for one tier: the square an even pyramid would have here,
   outlined at the same z as the real tier's own top face so the two are read
   against each other. NO fill, so it adds nothing that can occlude — the whole
   reason the ideal is a wireframe rather than a translucent solid, since a
   translucent solid would need opacity and opacity flattens the scene.
   --muted rather than a semantic colour: this is structure, not a reading, and it
   must not compete with the grade heat it sits on top of. Mixed toward
   transparent from an OPAQUE token, so the alpha is the alpha. */
.pyr3d .pyr-ideal {
  position: absolute; left: 50%; top: 58%;
  width: var(--gw); height: var(--gw);
  margin-left: calc(var(--gw) / -2); margin-top: calc(var(--gw) / -2);
  transform: translateZ(calc(var(--z) + var(--h)));
  border: 1px dashed color-mix(in srgb, var(--muted) 70%, transparent);
  border-radius: 3px; background: none; pointer-events: none;
}
/* Ground contact. Without it the stack floats: a pyramid reads as massive partly
   from where it meets the floor. A radial fade INSIDE the preserve-3d chain, so
   it cannot use opacity or filter — the gradient's own alpha does the work. It
   sits at z:0, the ground plane, scaled off the base tier's own side. */
.pyr3d .pyr-shadow {
  position: absolute; left: 50%; top: 58%; width: var(--bw); height: var(--bw);
  margin-left: calc(var(--bw) / -2); margin-top: calc(var(--bw) / -2);
  border-radius: 50%; pointer-events: none;
  background: radial-gradient(closest-side,
    color-mix(in srgb, var(--shadow-ink, #000) 34%, transparent),
    transparent 72%);
}
/* The tier labels. Deliberately NOT inside .stage-inner: no preserve-3d, no
   transform, so they are flat, upright and painted after the scene — which is
   what makes them immune to both the occlusion and the mirroring that kept text
   out of here before. Their left/top come from JS, measured off each tier's
   projected rect.

   pointer-events: none is load-bearing, not tidiness: this layer covers the
   whole stage, and the stage is the drag-to-rotate surface. */
.pyr-labels { position: absolute; inset: 0; pointer-events: none; z-index: 4; }
/* Zero-size measurement targets: the left and right ends of a tier's top-face
   centre line, at the same z as that face. Siblings of .tier rather than
   children, because .tier is animated and an animated element reports its
   from-frame rect (translateZ(0) scale(0.55)) for the first 400ms — long enough
   for a label pass to place every label in one heap. These never animate. */
.pyr3d .anch {
  position: absolute; left: 50%; top: 58%; width: 0; height: 0;
  transform: translateZ(calc(var(--z) + var(--h)));
}
.pyr3d .anch.l { margin-left: calc(var(--w) / -2); }
.pyr3d .anch.r { margin-left: calc(var(--w) / 2); }
.pyr-lbl {
  position: absolute; left: 0; top: 0; opacity: 0;
  font-size: 10.5px; font-weight: 800; letter-spacing: 0.04em;
  font-variant-numeric: tabular-nums; white-space: nowrap; color: var(--text);
  /* A hairline plate, for the rotated angles where a label can end up over a
     tier rather than over the stage background.
     Mixed from --bg, NOT --card-2, and that is a correctness fix rather than a
     taste one. color-mix(..., X 88%, transparent) MULTIPLIES alphas, so it only
     yields a real 88% when X is opaque. On the five painted themes --card-2 is a
     hex and this worked; on Titanium — the DEFAULT — it is
     rgba(255,255,255,0.085), so the plate landed at 0.075 alpha, which is no
     plate at all, and a near-white label over a bright heat face measured
     1.77:1. --bg is opaque in all six, so 88% means 88% and the worst
     label-over-tier reading across every theme is 8.62 instead of 1.77. It also
     reads truer: the plate is a chip of the page ground laid over the scene. */
  padding: 1px 4px; border-radius: 4px;
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  transition: opacity var(--d1) var(--ease);
}
.pyr-lbl.on { opacity: 1; }
/* While the scene is being dragged the labels chase it a frame behind, so they
   step back rather than smear across it.
   Scoped to .on, or the rule would raise every CULLED label from 0 to 0.45 and
   the overlaps this whole pass exists to prevent would appear on drag. */
.pyr3d.dragging .pyr-lbl.on { opacity: 0.45; transition: none; }

/* .pyr-legend lived here: the key naming the depth axis's wall colours. Depth is
   geometry now, not a data channel, so there is no axis left to name — and the
   walls kept their own scene below, at the angles they actually are. */

/* wall exposure, at the angles the walls actually are */
.wall3d { height: 216px; perspective: 900px; perspective-origin: 50% 50%; }
.wall3d .stage-inner {
  transform: rotateY(var(--spin, -14deg)) rotateX(4deg);
  transform-style: preserve-3d;
}
.plane {
  position: absolute; left: var(--x); bottom: 30%;
  width: var(--w); height: var(--hh);
  margin-left: calc(var(--w) / -2);
  transform-origin: bottom center;
  transform: rotateX(var(--lean));
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--c) 60%, transparent),
    color-mix(in srgb, var(--c) 26%, transparent));
  border: 1px solid color-mix(in srgb, var(--c) 72%, transparent);
  border-radius: 4px;
  animation: plane-in var(--d4) var(--ease) both;
}
@keyframes plane-in { from { transform: rotateX(var(--lean)) scaleY(0.15); } }
.plane.ghost { background: none; border-style: dashed;
               border-color: color-mix(in srgb, var(--muted) 45%, transparent); }
/* ── the climber ─────────────────────────────────────────────────────────────
   An articulated figure built from 3D bones, not a flat drawing pasted on the
   wall. Every joint is a point in the wall's own local space, and the third
   coordinate is what makes it read: hands and feet sit ON the wall (z ~ 0)
   while the torso stands off it, so a slab climber is stacked over their feet
   and a board climber hangs out into the room. JS solves the yaw and pitch
   that aim each bone from one joint to the next.

   The plane must preserve-3d for any of that to survive, and the figure's
   translucency comes from an alpha in `color` rather than `opacity` —
   opacity would group the element and flatten every bone into the wall. */
.plane { transform-style: preserve-3d; }
.climber3d {
  position: absolute; left: 50%; top: 50%;
  width: 44px; height: 78px; margin: -39px 0 0 -22px;
  transform-style: preserve-3d;
  color: color-mix(in srgb, var(--text) 84%, transparent);
}
.bone {
  position: absolute; left: 0; top: 0;
  width: var(--l); height: var(--t); margin-top: calc(var(--t) / -2);
  border-radius: var(--rf); background: currentColor;
  transform-origin: left center;
  transform: translate3d(var(--x), var(--y), var(--z))
             rotateY(var(--yaw)) rotateZ(var(--pitch));
}
.climber3d .head {
  position: absolute; left: 0; top: 0; width: 11px; height: 11px;
  margin: -5.5px 0 0 -5.5px; border-radius: 50%; background: currentColor;
  transform: translate3d(var(--x), var(--y), var(--z));
}
/* the contact patches: where this body is actually touching the wall */
.climber3d .grip {
  position: absolute; left: 0; top: 0; width: 7px; height: 7px;
  margin: -3.5px 0 0 -3.5px; border-radius: 50%;
  border: 1.5px solid currentColor;
  transform: translate3d(var(--x), var(--y), var(--z));
}
.plane .tag {
  position: absolute; left: 50%; top: calc(100% + 7px); transform: translateX(-50%);
  font-size: 9.5px; font-weight: 800; letter-spacing: 0.05em;
  color: var(--soft); white-space: nowrap;
}
.plane .pct {
  position: absolute; left: 50%; top: -16px; transform: translateX(-50%);
  font-size: 10px; font-weight: 800; color: var(--soft); white-space: nowrap;
}
.plane.ghost .pct { opacity: 0.5; }
.wall3d .floor {
  position: absolute; left: 5%; right: 5%; bottom: 30%; height: 1px;
  background: linear-gradient(90deg, transparent, var(--line) 18%, var(--line) 82%, transparent);
}

/* ── cross-screen transitions ────────────────────────────────────────────── */
/* The View Transitions API where it exists: the browser snapshots the old
   screen and cross-fades it to the new one, which is smoother than anything
   achievable by animating both at once. Everywhere else the per-view
   animation above still runs, so nothing regresses. */
@media (prefers-reduced-motion: no-preference) {
  ::view-transition-old(root) {
    animation: vt-out 170ms var(--ease-in) both;
  }
  ::view-transition-new(root) {
    animation: vt-in 280ms var(--ease) both;
  }
}
@keyframes vt-out { to { opacity: 0; transform: translateY(-10px) scale(0.985); } }
@keyframes vt-in { from { opacity: 0; transform: translateY(14px) scale(0.99); } }

/* ── the load gauge ──────────────────────────────────────────────────────── */

.ring-box { display: flex; flex-direction: column; align-items: center; margin: var(--s3) 0 6px; }
.ring-num { font-size: 44px; font-weight: 800; letter-spacing: -0.03em; line-height: 1; }
.ring-tag { font-size: var(--f-micro); letter-spacing: 0.16em; font-weight: 800; margin-top: 6px; }
.gauge { position: relative; width: 100%; max-width: 320px; margin-top: var(--s4); }
.gauge-track { position: relative; display: flex; height: 14px; }
.gz { height: 100%; transition: opacity var(--d3) var(--ease); }
.gz:first-child { border-radius: 7px 0 0 7px; }
.gz:last-child { border-radius: 0 7px 7px 0; }
.gauge-marker {
  position: absolute; top: -4px; width: 3px; height: 22px; background: var(--marker);
  border-radius: 2px; box-shadow: 0 0 4px hsl(var(--shadow) / 0.6);
  transition: left var(--d4) var(--ease);
}
.gauge-scale { position: relative; height: 13px; margin-top: 4px; font-size: var(--f-tiny); color: var(--muted); }
.gauge-scale span { position: absolute; transform: translateX(-50%); }
.ring-sub { color: var(--muted); font-size: 11.5px; margin-top: var(--s2); text-align: center; }

/* ── charts ──────────────────────────────────────────────────────────────── */

.wk-bars { display: flex; gap: 6px; padding: var(--s3) var(--s3) var(--s2); }
.wk-col { flex: 1; display: flex; flex-direction: column; justify-content: flex-end;
          align-items: center; gap: 4px; height: 78px; }
.wk-bar { width: 100%; max-width: 26px; border-radius: 4px 4px 0 0; background: var(--card-2);
          border: 1px solid var(--line); border-bottom: none;
          transform-origin: bottom; animation: grow var(--d4) var(--ease) both; }
@keyframes grow { from { transform: scaleY(0); } to { transform: scaleY(1); } }
.wk-bar.cur { background: var(--accent-dim); border-color: var(--accent); }
.wk-lbl { font-size: 9px; color: var(--muted); white-space: nowrap; }
.wk-val { font-size: 9px; font-weight: 700; color: var(--accent-ink); white-space: nowrap; }
.wk-val.dim { color: var(--muted); font-weight: 600; }
.ax-label { text-align: center; font-size: 9px; font-weight: 700; letter-spacing: 0.12em;
            text-transform: uppercase; color: var(--muted); margin-top: 3px; }
.wk-meta { color: var(--muted); font-size: 11.5px; margin-top: 6px; }

/* The "Something's wrong" row. Deliberately quiet, and beside the version
   footnote rather than among the account controls: it is the same kind of fact
   about this build, and a loud support button collects taps that are really
   questions. Underlined so it still reads as tappable at this weight — the
   global :focus-visible ring covers the keyboard case. */
/* padding matches .back-link, the app's other small text link at this size —
   19px of glyph is a hard tap, and this is not the place to invent a second
   convention for the same thing. */
.report-row { display: block; background: none; border: 0; padding: 4px 0;
              margin-top: var(--s3); font: inherit; font-size: var(--f-small);
              color: var(--muted); text-decoration: underline;
              text-underline-offset: 3px; cursor: pointer; }
.report-row:hover { color: var(--text); }
.wk-bars.tl { gap: 2px; padding: var(--s3) var(--s2) var(--s2); }
.tl .wk-bar { max-width: none; border-radius: 2px 2px 0 0; }

.felt-row { display: flex; justify-content: space-between; align-items: baseline; gap: var(--s2);
            padding: 10px var(--s3); font-size: 13px; margin-bottom: 6px; }
.felt-row .g { font-weight: 800; }
.felt-row .grow { flex: 1 1 auto; }

.insight { padding: var(--s3) var(--s3); font-size: 13px; color: var(--soft);
           margin-bottom: var(--s2); display: flex; gap: var(--s3); }
.insight .dot { flex: 0 0 8px; width: 8px; height: 8px; border-radius: 50%;
                margin-top: 6px; background: var(--accent); }
.insight.flag { border-color: color-mix(in srgb, var(--amber) 45%, transparent);
                background: var(--amber-dim); }
.insight .dot.a { background: var(--amber); }

/* A finding that carries its own argument. The card stops being a row and
   becomes a stack the moment it has a head — .insight is a flex ROW everywhere
   else in the app and inheriting that here would have put the paragraph beside
   the claim in a 40px column. */
.insight:has(.ins-head) { flex-direction: column; gap: 0; padding: 0; }
.ins-head {
  display: flex; align-items: flex-start; gap: var(--s2); width: 100%;
  background: none; border: 0; padding: var(--s3); margin: 0;
  font: inherit; color: var(--text); text-align: left; cursor: pointer;
  border-radius: inherit;
}
.ins-head .t, .insight > .t { flex: 1 1 auto; min-width: 0; }
/* Chevron only, and it is not a strong signal on purpose: the line above it is
   the product, and an affordance that competed with it would turn a list of
   findings back into a list of controls. */
.ins-head .disc-chev {
  flex: none; width: 16px; height: 16px; margin-top: 1px; color: var(--muted);
  transition: transform var(--d2) var(--ease);
}
.ins-head[aria-expanded=true] .disc-chev { transform: rotate(180deg); }
@media (hover: hover) { .ins-head:hover .disc-chev { color: var(--soft); } }
.insight .d { padding: 0 var(--s3) var(--s3); color: var(--soft); }
.insight .ins-basis {
  margin-top: 6px; font-size: var(--f-label); font-weight: 800;
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted);
}

/* THE PROGRAM MARK. A rounded-square tile with a monoline glyph in it — the
   store's rows and its one header wear the same drawing at two sizes, which is
   the whole of why a shop reads as a shop.

   Neutral by default and coloured only by STATE, in the two hues the badge at
   the other end of the row already uses: accent is "yours", gold is "for you".
   Eight products in eight hues was tried first and it read as a toybox — the
   app spends colour on grade and on wall angle, and a third meaning would have
   made all three quieter. */
.pico {
  flex: none; display: grid; place-items: center;
  width: 34px; height: 34px; border-radius: 10px;
  background: var(--card-2); color: var(--soft);
  border: 1px solid var(--line);
}
.pico svg { width: 19px; height: 19px; }
.pico.s { background: var(--accent-dim); color: var(--accent-ink);
          border-color: color-mix(in srgb, var(--accent) 40%, transparent); }
.pico.w { background: var(--gold-dim); color: var(--gold-ink);
          border-color: color-mix(in srgb, var(--gold) 40%, transparent); }
.pico.lg { width: 44px; height: 44px; border-radius: 13px; }
.pico.lg svg { width: 25px; height: 25px; }
.co-head { display: flex; align-items: center; gap: var(--s3);
           margin-bottom: var(--s4); }
.co-head .grow { flex: 1 1 auto; min-width: 0; }
/* Both margins go, and the second one is the trap: .h2-sub carries a -10px top
   margin to tuck under an h2's own bottom margin, so zeroing only the h2 lifts
   the tagline onto the title. */
.co-head h2, .co-head .h2-sub { margin: 0; }

.pyr { display: flex; flex-direction: column; gap: 5px; }
.pyr .r { display: flex; align-items: center; gap: var(--s2); font-size: 11.5px; }
.pyr .g { width: 30px; color: var(--muted); font-weight: 700; }
/* The bar's own track, and it is load-bearing. The bar used to be a direct flex
   item with an inline percentage, so the percentage resolved against the WHOLE
   row — grade label and count included — making every bar 1.157x too long, and
   flex-shrink then clamped the longest one: 88% rendered at 100% of the space
   actually available, so the top tier looked identical to a full-width bar. The
   track is the item that absorbs the slack; the bar measures against the track. */
.pyr .track { flex: 1 1 auto; min-width: 0; display: block; height: 14px; }
.pyr .bar { display: block; height: 14px; border-radius: 4px;
            background: var(--accent); opacity: 0.85;
            min-width: 2px; transform-origin: left;
            animation: growx var(--d4) var(--ease) both; }
@keyframes growx { from { transform: scaleX(0); } to { transform: scaleX(1); } }
.pyr .bar.tr { background: var(--amber); }
.pyr .bar.ra { background: var(--blue); }
.pyr .n { color: var(--muted); white-space: nowrap; }

/* ── workout progressions ────────────────────────────────────────────────── */
/* One card per exercise: the shape of the series, the current number, and the
   arithmetic behind it. The sparkline is decoration ON TOP of numbers that say
   the same thing — same rule as the 3D stages — so a browser that draws no SVG
   loses nothing but the shape. */

.prog { padding: var(--s3) var(--s3) 10px; margin-bottom: var(--s2); }
/* the role groups reuse the Profile screen's disclosure wholesale — same
   component, same behaviour, one idiom in the app for "a section that opens" */
/* The one .disc that is NOT a group on a field. Profile's sections took the
   card surface, but a progression group holds .prog charts and .prog is itself
   on the surface list — a card inside a card, at the same fill, reads as a
   rendering mistake. Here the disclosure is a row in a list, so it opts back
   out and keeps the hairline it always had. */
.wgroup {
  padding: 0; background: none; border: 0; border-radius: 0; box-shadow: none;
  border-top: 1px solid var(--line);
}
.wgroup:last-of-type { border-bottom: 1px solid var(--line); }
.disc + .wgroup { margin-top: 0; }
.wgroup .disc-body { padding-bottom: var(--s2); }
.wgroup .disc-body > * + * { margin-top: 0; }   /* .prog carries its own margin */
.prog-head { display: flex; align-items: baseline; gap: var(--s2); }
.prog-name { flex: 1 1 auto; min-width: 0; font-weight: 800; font-size: 13.5px;
             overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.prog-pat { flex: 0 0 auto; font-size: var(--f-tiny); font-weight: 800;
            letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted); }
/* the reading sits ABOVE the chart, not beside it — the chart needs the full
   card width for its axis labels to fit */
.prog-top { display: flex; align-items: baseline; justify-content: space-between;
            gap: var(--s2); margin-top: 8px; }
.prog-read b { font-size: 20px; font-weight: 800; letter-spacing: -0.01em; }
.prog-read i { font-style: normal; font-size: var(--f-tiny); font-weight: 700;
               color: var(--muted); text-transform: uppercase; letter-spacing: 0.08em; }

/* the top margin is where the point labels sit — they hang above their point,
   and the highest point is at the top of the plot */
.prog-chart { position: relative; margin: 15px 0 0; height: 46px;
              touch-action: pan-y; cursor: crosshair; }
/* the y-axis readings, hung on the chart itself. The peak rides the dashed
   line; the floor states the plot's baseline, which is the series MINIMUM and
   not zero — read as zero, a sparkline exaggerates every change on it. */
.prog-peak, .prog-floor {
  position: absolute; right: 0; font-size: var(--f-tiny); font-weight: 700;
  letter-spacing: 0.04em; padding: 1px 4px; border-radius: 4px;
  background: var(--card); white-space: nowrap;
}
.prog-peak { transform: translateY(-50%); color: var(--soft); }
.prog-floor { bottom: -2px; color: var(--muted); }
/* a value on every logged day that has room for one. dressChart() measures
   these after layout and adds .hide to whichever collide — see the note there
   on why nothing estimates label widths. */
.prog-pt-lbl {
  position: absolute; transform: translate(-50%, -150%); pointer-events: none;
  font-size: 9px; font-weight: 700; color: var(--soft); white-space: nowrap;
  padding: 0 2px; border-radius: 3px; background: var(--card);
  font-variant-numeric: tabular-nums;
}
.prog-pt-lbl.hide { display: none; }

/* the scrubber: the dates too dense to label statically, on demand */
.prog-cursor {
  position: absolute; top: 0; bottom: 0; width: 1px; pointer-events: none;
  background: var(--soft); opacity: 0.45; transform: translateX(-0.5px);
}
.prog-tip {
  position: absolute; top: -4px; pointer-events: none; white-space: nowrap;
  font-size: var(--f-tiny); font-weight: 700; letter-spacing: 0.03em;
  color: var(--text); background: var(--card-2); border: 1px solid var(--line);
  border-radius: 4px; padding: 2px 5px; box-shadow: var(--e1);
  font-variant-numeric: tabular-nums;
}
.prog-xaxis { display: flex; justify-content: space-between; align-items: baseline;
              margin-top: 3px; font-size: var(--f-tiny); color: var(--muted);
              letter-spacing: 0.04em; }
/* weight, not opacity, marks this as the secondary label — opacity would drag
   an already-muted 4.9:1 under AA */
.prog-xaxis .mid { font-weight: 700; }
.prog-meta { display: flex; flex-wrap: wrap; gap: 2px 6px; margin-top: 8px;
             font-size: var(--f-micro); color: var(--muted); }
/* the dots live between the facts, not inside them, so a wrapped row never
   starts with a stranded separator */
.prog-meta span + span::before { content: "· "; opacity: 0.55; }
.prog-delta { font-weight: 800; color: var(--muted); }
.prog-delta.up { color: var(--accent-ink); }
.prog-delta.down { color: var(--blue); }
/* A plateau is neither good nor bad, so it takes --muted like the base rule and
   says nothing with colour. It deliberately does NOT read as .down: a series
   sitting on one number for four sessions was put there. */
.prog-delta.held { color: var(--muted); }
.prog-dose { margin-top: 4px; font-size: var(--f-micro); color: var(--muted); opacity: 0.8; }

/* preserveAspectRatio=none stretches the x axis to whatever width the row got,
   which is what makes the line fill the card at any screen size. Every stroke
   is non-scaling so that stretch never squashes it, and the round-capped
   zero-length "now" path is a dot for the same reason — a <circle> here would
   render as an ellipse. */
.spark { display: block; width: 100%; height: 100%; overflow: visible; }
/* one tick per logged day, so a two-point line is visibly not an eleven-point
   one. Same round-cap trick as the end dot, for the same reason. */
.spark .pt { stroke: var(--accent); stroke-width: 4; stroke-linecap: round;
             opacity: 0.55; vector-effect: non-scaling-stroke; }
.spark.down .pt { stroke: var(--blue); }
.spark .area { fill: var(--accent-dim); stroke: none; }
.spark .line { fill: none; stroke: var(--accent); stroke-width: 2;
               stroke-linejoin: round; stroke-linecap: round;
               vector-effect: non-scaling-stroke; }
.spark .now { stroke: var(--accent); stroke-width: 7; stroke-linecap: round;
              vector-effect: non-scaling-stroke; }
.spark .best { stroke: var(--muted); stroke-width: 1; stroke-dasharray: 3 4;
               opacity: 0.65; vector-effect: non-scaling-stroke; }
.spark.down .area { fill: color-mix(in srgb, var(--blue) 14%, transparent); }
.spark.down .line, .spark.down .now { stroke: var(--blue); }

.wallrow { display: flex; align-items: center; gap: var(--s2); font-size: var(--f-small); margin-bottom: 6px; }
.wallrow .name { width: 74px; color: var(--muted); font-weight: 700; }
.wallrow .track { flex: 1; height: 14px; background: var(--card); border: 1px solid var(--line);
                  border-radius: 4px; overflow: hidden; }
.wallrow .fill { height: 100%; background: var(--accent); opacity: 0.85;
                 transform-origin: left; animation: growx var(--d4) var(--ease) both; }
.wallrow .meta { color: var(--muted); white-space: nowrap; flex: 0 0 106px; text-align: right; }
/* movement patterns are named longer than walls are — "Shoulders + cuff" must
   not shove the bar off the row */
.wallrow.mix .name { width: 96px; font-size: var(--f-micro);
                     overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.wallrow.mix .meta { flex: 0 0 84px; font-size: var(--f-micro); }
.zero-note { color: var(--muted); font-size: var(--f-small); margin-top: 4px; }

.week { display: flex; justify-content: space-between; padding: var(--s3) var(--s4); }
.week .d { text-align: center; font-size: var(--f-micro); color: var(--muted); }
.week .c { width: 22px; height: 22px; border-radius: 50%; margin: 0 auto 4px;
           background: var(--card-2); border: 1px solid var(--line); }
.week .on { background: var(--accent-dim); border-color: var(--accent); }
.week .today { border-color: var(--blue); border-width: 2px; }

/* ── history ─────────────────────────────────────────────────────────────── */

/* History nests two deep: year, then month. The year carries the weight — it
   is the thing you scan for — and its months sit indented under it so the
   hierarchy is visible without a rule or a box around anything. */
.yr + .yr { border-top: 1px solid var(--line); }
.yr-head {
  width: 100%; display: flex; align-items: center; gap: var(--s3);
  padding: var(--s3) 0; background: none; border: 0; cursor: pointer;
  font-family: inherit; text-align: left; color: var(--text);
}
.yr-n {
  font-weight: 800; font-size: 19px; letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums; flex: none;
}
.yr-meta {
  flex: 1 1 auto; min-width: 0; color: var(--muted); font-size: 12px;
  text-align: right; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.yr-head[aria-expanded=true] .yr-n { color: var(--accent-ink); }
.yr-head[aria-expanded=true] .disc-chev { transform: rotate(180deg); }
.yr-body:not(:empty) { padding-left: var(--s3); margin-bottom: var(--s3); }
.yr-body .sec-head { margin-top: var(--s3); }
/* THE MONTH HEADER: a heading wrapping a button, which is the shape an
   accordion header wants. It was a div with a delegated click and a SHOW/HIDE
   chip that only looked like a control — so a month could not be opened from
   the keyboard, and the month itself was invisible to anything navigating by
   heading even though it is the one thing dividing the archive.

   The button is the whole strip rather than just the chip: making only the chip
   a control would have shrunk a full-width row to a 50px target on a phone. */
.hist-h { margin: 0; font-size: inherit; font-weight: inherit; }
button.sec-head {
  width: 100%; background: none; border: 0; font: inherit; color: inherit;
  padding: 0; text-align: left; cursor: pointer;
}
.hist-head { cursor: pointer; }

/* ── feedback: toasts, sheets, banners, skeletons ────────────────────────── */

.toasts {
  position: fixed; left: 0; right: 0; bottom: calc(78px + env(safe-area-inset-bottom));
  z-index: 60; max-width: var(--shell); margin: 0 auto;
  padding: 0 var(--s4); display: flex; flex-direction: column; gap: var(--s2);
  pointer-events: none;
}
.toast {
  display: flex; align-items: center; gap: var(--s3); pointer-events: auto;
  background: var(--card); color: var(--text); border: 1px solid var(--line);
  border-radius: var(--r3); box-shadow: var(--e3); padding: 12px var(--s4);
  font-size: 13px; animation: toast-in var(--d3) var(--ease);
}
.toast.leaving { animation: toast-out var(--d2) var(--ease-in) both; }
.toast .msg { flex: 1 1 auto; }
.toast .act {
  background: none; border: 0; color: var(--accent-ink); font-family: inherit;
  font-weight: 800; font-size: var(--f-small); letter-spacing: 0.08em;
  text-transform: uppercase; cursor: pointer; padding: 4px 2px; flex: none;
}
.toast.bad { border-color: color-mix(in srgb, var(--red) 50%, transparent); }
.toast.bad .dotm { color: var(--red-ink); }
@keyframes toast-in { from { opacity: 0; transform: translateY(14px) scale(0.98); } to { opacity: 1; transform: none; } }
@keyframes toast-out { to { opacity: 0; transform: translateY(8px); } }

.scrim {
  position: fixed; inset: 0; background: var(--scrim); z-index: 70;
  animation: fade var(--d2) var(--ease); backdrop-filter: blur(2px);
}
.scrim.leaving { animation: fade var(--d2) var(--ease-in) reverse both; }
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }
.sheet {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 71;
  max-width: var(--shell); margin: 0 auto;
  background: var(--card); border: 1px solid var(--line); border-bottom: none;
  border-radius: var(--r5) var(--r5) 0 0; box-shadow: var(--e3);
  padding: var(--s5) var(--s5) calc(var(--s5) + env(safe-area-inset-bottom));
  animation: sheet-in var(--d3) var(--ease);
}
.sheet.leaving { animation: sheet-out var(--d2) var(--ease-in) both; }
@keyframes sheet-in { from { transform: translateY(100%); } to { transform: none; } }
@keyframes sheet-out { to { transform: translateY(100%); } }
.sheet .grab { width: 38px; height: 4px; border-radius: var(--rf); background: var(--line);
               margin: -8px auto var(--s4); }
.sheet h3 { margin: 0 0 6px; font-size: 17px; font-weight: 800; }
/* `pre-line` so a sheet body can be a short numbered list — the install
   instructions are three steps and a reason, and three steps run together into
   one paragraph are three steps nobody follows. Collapses runs of spaces, so
   every single-line body reads exactly as it did. */
.sheet p { margin: 0 0 var(--s5); color: var(--soft); font-size: 13.5px;
           white-space: pre-line; }
.sheet-body:not(:empty) { margin-bottom: var(--s5); }
/* A .big is `flex: 1` for the rows it normally lives in, which makes it an
   inline-block anywhere else — so two of them in a sheet body sat side by side
   and ran off the edge ("+ Add something" / "Make this a rest day"). Inside a
   sheet the buttons stack, the same as .sheet-actions one level down. */
.sheet-body > .big { display: block; width: 100%; margin-top: var(--s2); }

/* A sheet that edits something rises; a sheet that asks a question does not.
   The day editor opened as a 230px strip along the bottom edge of a 660px
   screen — a control you reach for with a thumb, showing a list you have to
   squint at, with two thirds of the screen spent on the scrim above it. This
   gives it a floor, so it opens at a consistent height whatever the day
   happens to hold, and lets its own body scroll rather than growing past the
   top of the screen. A confirm sheet keeps its natural height: making "are you
   sure?" half a screen tall would be the same mistake in the other direction. */
.sheet.tall {
  min-height: min(62vh, 560px); max-height: 88vh;
  display: flex; flex-direction: column;
}
.sheet.tall .sheet-body { flex: 1 1 auto; overflow-y: auto; overscroll-behavior: contain; }

/* ── the exercise picker ─────────────────────────────────────────────────── */
/* The library, browsable. It replaced a native <select> whose popup the OS
   painted white under this app's near-white text — see the note by `option`. */

.picker-search { margin-bottom: var(--s3); }
/* the LIST scrolls, not the sheet: the title and the way out stay put while
   sixty exercises go past */
.picker-list {
  max-height: min(46vh, 340px); overflow-y: auto; overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch; margin: 0 -4px; padding: 0 4px;
}
.picker-group {
  font-size: var(--f-tiny); font-weight: 800; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--muted); margin: var(--s3) 0 6px;
}
.picker-group:first-child { margin-top: 0; }
.picker-opt {
  display: block; width: 100%; text-align: left; font-family: inherit;
  font-size: 14px; font-weight: 600; color: var(--text);
  background: var(--card-2); border: 1px solid var(--line);
  border-radius: var(--r2); padding: 11px var(--s3); margin-bottom: 6px;
  cursor: pointer; transition: border-color var(--d1) var(--ease);
}
.picker-opt.sel { border-color: var(--accent); background: var(--accent-dim);
                  color: var(--text); }
.picker-opt.other { color: var(--soft); font-weight: 700; }
.picker-empty { color: var(--muted); font-size: 13px; padding: var(--s2) 0 var(--s3); }
@media (hover: hover) { .picker-opt:hover { border-color: var(--accent); } }
.sheet-actions { display: flex; flex-direction: column; gap: var(--s2); }
.sheet-actions .big { width: 100%; margin-top: 0; }

/* A ROW WITH TWO CONTROLS IN IT. Every other .try in the app is itself the
   button; this one cannot be, because it contains the ⋮ menu and a <button>
   inside a <button> is not a thing HTML has. So the row keeps the surface and
   splits: its own half opens the session, the ⋮ opens the menu.

   The padding moves off the row and onto the two of them, or each tap target
   would stop short of the edge the row looks like it reaches — and on a phone
   the 12px it stopped short of is the part people actually hit. */
.try.has-more { padding: 0; gap: 0; }
.try.has-more .try-main {
  flex: 1 1 auto; min-width: 0; display: flex; align-items: center;
  justify-content: space-between; gap: var(--s2);
  padding: 10px 0 10px var(--s3);
  background: none; border: 0; font: inherit; font-size: 13px;
  color: var(--text); text-align: left; cursor: pointer;
}
/* Inset, because the row's own edge is 1px away and a ring drawn outside it
   would sit on the card behind rather than on the thing that has focus. */
.try.has-more .try-main:focus-visible {
  border-radius: var(--r3); outline-offset: -3px;
}

/* the quick-actions affordance on a history row */
.row-more {
  flex: none; background: none; border: 0; color: var(--muted);
  font-size: 17px; line-height: 1; padding: 6px 0 6px 12px; cursor: pointer;
  font-family: inherit; border-radius: var(--r1);
}
/* It inherits the row's padding now that the row has none of its own, and it
   STRETCHES: at its natural height it was a 37px control centred in a 61px row,
   which left a dead strip above and below it that fell through to the row and
   opened the session instead. Tapping just over the menu and getting the thing
   the menu was about is the kind of miss nobody reports and everybody makes. */
.try.has-more .row-more {
  align-self: stretch; display: grid; place-items: center;
  padding: 0 var(--s3) 0 var(--s2);
}
.row-more:active { transform: scale(0.88); }
@media (hover: hover) { .row-more:hover { color: var(--text); } }

.banner {
  display: flex; align-items: center; gap: var(--s2); font-size: var(--f-small);
  padding: 9px var(--s3); border-radius: var(--r2); margin-bottom: var(--s3);
  border: 1px solid color-mix(in srgb, var(--amber) 45%, transparent);
  background: var(--amber-dim); color: var(--text);
  animation: rise var(--d2) var(--ease);
}
.banner .grow { flex: 1 1 auto; }
.banner svg { width: 15px; height: 15px; flex: none; color: var(--amber-ink); }
.banner button {
  background: none; border: 0; color: var(--accent-ink); font-family: inherit;
  font-weight: 800; font-size: var(--f-micro); letter-spacing: 0.08em;
  text-transform: uppercase; cursor: pointer; flex: none;
}
.banner.info { border-color: color-mix(in srgb, var(--accent) 40%, transparent);
               background: var(--accent-dim); }
.banner.info svg { color: var(--accent-ink); }

.sk { border-radius: var(--r3); background: var(--card-2); position: relative;
      overflow: hidden; }
.sk::after {
  content: ""; position: absolute; inset: 0; transform: translateX(-100%);
  background: linear-gradient(90deg, transparent, var(--state), transparent);
  animation: shimmer 1.4s infinite;
}
@keyframes shimmer { to { transform: translateX(100%); } }
.sk-line { height: 13px; margin-bottom: var(--s2); }
.sk-tile { height: 62px; flex: 1; }
.sk-block { height: 104px; margin-bottom: var(--s3); }

.empty { text-align: center; padding: var(--s7) var(--s4); color: var(--muted); }
.empty svg { width: 40px; height: 40px; opacity: 0.45; margin-bottom: var(--s3); }
.empty .t { font-weight: 800; color: var(--text); font-size: 15px; margin-bottom: 4px; }
.empty .d { font-size: 13px; max-width: 30ch; margin: 0 auto; }

/* ── the gate ────────────────────────────────────────────────────────────── */
/* Signed out there is no app to be a tab of, so this covers the whole viewport
   rather than living inside the shell. `.gated` on the body hides everything
   behind it — including the nav, which would otherwise offer three tabs that
   all 401. */

body.gated .shell { display: none; }

.gate {
  position: fixed; inset: 0; z-index: 40; overflow-y: auto;
  display: flex; align-items: center; justify-content: center;
  padding: var(--s5); background: var(--bg);
}
.gate-card {
  width: 100%; max-width: 380px;
  animation: view-in var(--d4) var(--ease);
}
.gate-lockup { justify-content: center; margin-bottom: var(--s6); }
.gate-lockup .mark { width: 48px; height: 36px; }
.gate-lockup .wordmark { font-size: 26px; }
.gate-title {
  font-family: var(--font-display, inherit);
  font-size: 26px; font-weight: 800; letter-spacing: -0.01em;
  color: var(--text); margin: 0 0 var(--s2);
}
.gate-sub { color: var(--soft); font-size: var(--f-body); line-height: 1.5; margin: 0 0 var(--s5); }
.gate-sub b { color: var(--text); overflow-wrap: anywhere; }
.gate .ed-label { margin-bottom: 6px; }
/* Both gate inputs, not just the email one: the paste field is type=text so
   that a bare token is not rejected by the browser's own URL validation. */
.gate input[type=email], .gate input[type=text] {
  width: 100%; background: var(--card); border: 1px solid var(--line);
  border-radius: var(--r2); color: var(--text); font-family: inherit;
  font-size: 16px;   /* 16px or iOS zooms the whole page on focus */
  padding: 13px 12px; box-shadow: var(--hairline);
  transition: border-color var(--d1) var(--ease), box-shadow var(--d1) var(--ease);
}
.gate input[type=email]:focus, .gate input[type=text]:focus {
  outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-dim);
}
/* Every control on this card is full width. `.big` is `flex: 1`, which does
   nothing outside a flex row, so a gate button not carrying `.big.primary`
   shrank to its own text — USE A DIFFERENT EMAIL sat at 188px beside a 335px
   button and read as a mistake. */
.gate .big { width: 100%; margin-top: var(--s4); }
.gate-fine { color: var(--muted); font-size: 12px; text-align: center; margin-top: var(--s5); }
/* The recovery path, set below a rule so it reads as an alternative to what
   the screen just told you rather than a second instruction. */
.gate-alt { border-top: 1px solid var(--line); padding-top: var(--s5); margin-top: var(--s5); }
.gate-alt-lead {
  color: var(--soft); font-size: 13px; line-height: 1.5; margin: 0 0 var(--s4);
}

/* ── the face ────────────────────────────────────────────────────────────── */

.avatar {
  border-radius: 50%; flex: none; display: block;
  object-fit: cover;   /* a non-square photo crops rather than distorting */
  background: var(--line);
  box-shadow: var(--hairline);
}
.avatar.mono {
  display: flex; align-items: center; justify-content: center;
  color: #fff; font-weight: 800; letter-spacing: 0.02em;
  /* the gradient carries the identity; a text shadow keeps the initials
     legible over the light end of every hue */
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.28);
}

.me-btn {
  background: none; border: 0; padding: 0; cursor: pointer;
  display: flex; align-items: center; border-radius: 50%;
  transition: transform var(--d1) var(--ease);
}
.me-btn:active { transform: scale(0.92); }
.me-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ── the greeting ────────────────────────────────────────────────────────── */

#greeting { margin-bottom: 2px; }
.greet-sub {
  color: var(--muted); font-size: 13px; margin-bottom: var(--s4);
}

/* ── the identity card ───────────────────────────────────────────────────── */

.me-card {
  display: flex; align-items: center; gap: var(--s3);
  padding: var(--s4); margin-bottom: var(--s3);
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--r3); box-shadow: var(--e2);
  backdrop-filter: var(--surface-blur); -webkit-backdrop-filter: var(--surface-blur);
}
/* The whole text block, not a button beside it. A 52px "Edit" label pinned right
   left the text column 102px wide at 320px, which is what broke the email over
   two lines — the tap target got its width back by becoming the text. */
.me-open {
  flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: var(--s2);
  background: none; border: 0; padding: var(--s1) 0; cursor: pointer;
  font-family: inherit; text-align: left; color: var(--text);
  transition: transform var(--d1) var(--ease);
}
.me-open:active { transform: scale(0.99); }
.me-open:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.me-head { display: flex; align-items: baseline; gap: var(--s2); flex-wrap: wrap; }
.me-photo {
  background: none; border: 0; padding: 0; cursor: pointer;
  border-radius: 50%; flex: none; position: relative;
  transition: transform var(--d1) var(--ease);
}
.me-photo:active { transform: scale(0.96); }
.me-photo:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
/* a small camera glyph, so the picture reads as something you can change */
.me-photo::after {
  content: ""; position: absolute; right: -1px; bottom: -1px;
  width: 22px; height: 22px; border-radius: 50%;
  background: var(--accent); border: 2px solid var(--bg);
  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='white' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 8.5h3.2l1.4-2h7.8l1.4 2H20a1 1 0 0 1 1 1V19a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V9.5a1 1 0 0 1 1-1z'/%3E%3Ccircle cx='12' cy='13.5' r='3.4'/%3E%3C/svg%3E");
  background-size: 13px 13px; background-repeat: no-repeat; background-position: center;
}
/* flex-grow, or the block sits at its content width and leaves a third of the
   card empty — and a long name wraps early while there is space beside it.
   min-width:0 is what lets it actually shrink below that content width. */
.me-text { flex: 1 1 auto; min-width: 0; display: block; }
/* The largest type on the screen, and the only thing on it that is a proper
   name. It used to be 17px against a 15px section title — two weights apart
   from identical, so the page had no first read. */
.me-name {
  font-weight: 800; font-size: var(--f-title); color: var(--text);
  letter-spacing: -0.02em; overflow-wrap: anywhere; line-height: 1.15;
}
.me-name.unset { color: var(--muted); font-weight: 600; }
.me-name { display: block; }
.me-email {
  display: block; color: var(--muted); font-size: var(--f-small); margin-top: 2px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Now a line in Account & privacy rather than a fragment in the identity card */
.me-since { color: var(--muted); font-size: var(--f-small); }
/* Free is a statement of fact, not a nudge — quiet. Premium is the one worth
   a glance, so it takes the gold the records shelf already uses. */
.tier-badge {
  font-size: 9.5px; letter-spacing: 0.12em; font-weight: 800;
  border-radius: 999px; padding: 3px 8px; flex: none;
  color: var(--muted); background: var(--state); border: 1px solid var(--line);
}
.tier-badge.premium {
  color: var(--gold-ink); background: var(--gold-dim);
  border-color: color-mix(in srgb, var(--gold) 45%, transparent);
}

/* ── disclosure sections ─────────────────────────────────────────────────── */
/* One component, used for every section of the Profile screen. The point is
   that the whole screen reads as one list of things you can open — with three
   different toggle idioms nothing announced itself as expandable, and the two
   headings that never collapsed looked the same as the ones that did.
 *
 * A GROUP ON A FIELD, not hairlines on a void. Profile was the only screen in
 * the app that never touched --card: Load, Progress, Workouts, Signals and the
 * composer all put their content on an elevated surface, while this screen drew
 * four rules directly on the page ground and let the content float between
 * them. That is what read as unfinished — the tokens for a designed screen were
 * already in the system, the screen just wasn't spending them. Same surface
 * recipe as .card, so a section here and a panel there are the same object. */

.disc {
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--r3); box-shadow: var(--e1), var(--hairline);
  padding: 0 var(--s4);
}
.disc + .disc { margin-top: var(--s3); }

/* ── a stack of disclosures as ONE inset list ────────────────────────────── */
/* Four disclosures, each its own bordered and shadowed card with a gap under
 * it, is four floating boxes down a screen whose actual content is four
 * headings. It reads as four separate things to decide between; it is one list
 * of places to look.
 *
 * So the STACK becomes the surface and the members give theirs up — the same
 * grouped-inset list .nav-group already builds one level down, and the same
 * device .hstat uses: a hairline between siblings rather than a gap and a
 * shadow around each. Each .disc keeps its own markup, its own aria wiring and
 * its own open state; all it loses is the box.
 *
 * The rules are on the STACK rather than in .disc itself so a lone disclosure
 * elsewhere in the app is untouched. */
.disc-stack {
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--r3); box-shadow: var(--e1), var(--hairline);
  backdrop-filter: var(--surface-blur); overflow: hidden;
}
.disc-stack > .disc {
  background: none; border: 0; border-radius: 0; box-shadow: none;
  backdrop-filter: none;
}
.disc-stack > .disc + .disc { margin-top: 0; border-top: 1px solid var(--line); }
/* An open section wants air under its body; a closed one is just a row, and a
   run of closed rows should sit at one pitch like any other list. */
.disc-stack > .disc[data-open=true] { background: var(--card-2); }
.disc-head {
  width: 100%; display: flex; align-items: center; gap: var(--s3);
  padding: var(--s4) 0; background: none; border: 0; cursor: pointer;
  font-family: inherit; text-align: left; color: var(--text);
}
.disc-title {
  font-weight: 800; font-size: 15px; letter-spacing: -0.01em; flex: none;
}
/* the row's quiet half: a count, or what is inside. It takes the slack so the
   chevron stays pinned right whatever the title's length. */
.disc-meta {
  flex: 1 1 auto; min-width: 0; color: var(--muted); font-size: 12px;
  text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.disc-chev {
  width: 18px; height: 18px; flex: none; color: var(--muted);
  transition: transform var(--d2) var(--ease);
}
.disc[data-open=true] .disc-chev { transform: rotate(180deg); }
.disc[data-open=true] .disc-title { color: var(--accent-ink); }
.disc-body { display: none; padding-bottom: var(--s5); }
.disc[data-open=true] .disc-body { display: block; animation: disc-in var(--d3) var(--ease); }
@keyframes disc-in { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }

/* Vertical rhythm inside a section, set once here rather than by a hand-tuned
   inline margin on each control — which is what the old markup did, and why
   the spacing drifted between sections. */
.disc-body > * + * { margin-top: var(--s3); }
.disc-body .ed-label { margin-top: var(--s5); }
.disc-body > .ed-label:first-child { margin-top: 0; }
.disc-body .hint { margin-top: 6px; }

/* ── the settings row ────────────────────────────────────────────────────── */
/* Label left, CURRENT VALUE right, chevron, expanding in place.
 *
 * The value in the head is the whole point. Preferences was four
 * label/prose/control stacks: to learn what your ladder was set to you had to
 * open the control that changes it, and the section's own summary described its
 * contents rather than its state. A settings row answers the question without
 * being touched — which is also what gives the screen a right-hand column to
 * align to, and a column is most of what "designed" means here.
 *
 * A .row is a row IN a group, so it draws a hairline between siblings and no
 * surface of its own: a card per setting inside a card per section is the
 * nesting that made the old dashboard read as boxes all the way down. */

.row + .row { border-top: 1px solid var(--line); }
.disc-body > .row + .row { margin-top: 0; }
.row-head {
  width: 100%; display: flex; align-items: center; gap: var(--s3);
  padding: var(--s3) 0; background: none; border: 0; cursor: pointer;
  font-family: inherit; text-align: left; color: var(--text);
}
.row-k { font-weight: 700; font-size: var(--f-body); flex: none; }
/* takes the slack so the chevron stays pinned right whatever the label's
   length, and ellipsises rather than wrapping — a gym name can be anything */
.row-v {
  flex: 1 1 auto; min-width: 0; text-align: right; color: var(--soft);
  font-size: var(--f-body); overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap;
}
.row-chev {
  width: 16px; height: 16px; flex: none; color: var(--muted);
  transition: transform var(--d2) var(--ease);
}
.row[data-open=true] .row-chev { transform: rotate(180deg); }
.row[data-open=true] .row-k { color: var(--accent-ink); }
.row-body { display: none; padding-bottom: var(--s4); }
.row[data-open=true] .row-body { display: block; animation: disc-in var(--d3) var(--ease); }
.row-body > * + * { margin-top: var(--s3); }

/* ── a group's own header, and the highlights strip ─────────────────────── */

.group { padding: var(--s3) var(--s4) 0; margin-bottom: var(--s3); }
.group-head { display: flex; align-items: baseline; gap: var(--s3); }
.group-title {
  font-size: 15px; font-weight: 800; letter-spacing: -0.01em; margin: 0;
  color: var(--text); flex: none;
}
.group-meta {
  flex: 1 1 auto; min-width: 0; color: var(--muted); font-size: var(--f-small);
  text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Three numbers, equal thirds, no surface — they sit ON the group's card, and a
   tile per number would be the card-in-card again. The rules between them are
   what separate them, the same device the .row uses one level down. */
.hstats { display: flex; margin: var(--s3) 0; }
.hstats + .row { border-top: 1px solid var(--line); }
.hstat { flex: 1 1 0; min-width: 0; padding: 0 var(--s2); }
.hstat + .hstat { border-left: 1px solid var(--line); }
.hstat:first-child { padding-left: 0; }
.hstat:last-child { padding-right: 0; }
.hstat .v {
  font-size: var(--f-title); font-weight: 800; letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums; line-height: 1.1;
}
.hstat .k {
  font-size: var(--f-tiny); text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--muted); font-weight: 800; margin-top: 4px;
}
.hstat .w { font-size: var(--f-micro); color: var(--muted); margin-top: 2px; }
/* the shelf's own tiles are a level deeper again, so they step to --card-2 */
.group .rec { background: var(--card-2); }

/* A card's ONE SENTENCE — what it concluded, or what it is offering. Body size
 * and body colour, not the footnote's: this is the card's content, and it was
 * .foot, which is the voice for things printed under content rather than for
 * content itself. Everything that used to follow it in the same paragraph now
 * lives behind a row. */
.lede { font-size: var(--f-body); color: var(--soft); line-height: 1.45;
        margin: var(--s3) 0; }
.lede + .row { border-top: 1px solid var(--line); }
/* .group leaves its bottom padding to the .row that normally ends it. A card
   that ends in an ACTION has to bring its own — and the width, since .ghost is
   the one .big variant that does not claim the line (it was written for a flex
   row of two). A card's single action spans the card. */
.group > .big:last-child { width: 100%; margin-bottom: var(--s4); }

/* THE TRAIN TAB'S OWN RHYTHM. Its three stacked blocks — the doors, the finger
 * test, the live plan — are independent claims, and two of them arrived flush
 * against each other: .nav-group carries no bottom margin and .group carries no
 * top one, so the finger card sat directly on the card offering the coached
 * programs and the two read as one card with a rule through it. Here rather
 * than on .card, because how much air a card wants is otherwise the business of
 * the screen it is on. The margins collapse against .group's own 12px, so the
 * gap is 16 either way round. */
#train-body > * + * { margin-top: var(--s4); }

/* A list of RECORDS inside a disclosure — one line each, tappable, hairline
 * between. It is not a .row: a .row is a disclosure and these go somewhere, and
 * nesting one .row list inside another's body would put two chevron
 * vocabularies in the same eight lines. Its own chevron class for the same
 * reason — see TLOG_CHEV. */
.tlog { display: flex; flex-direction: column; }
.tlog-item {
  width: 100%; display: flex; align-items: center; gap: var(--s3);
  /* 13, not the --s3 a .row-head uses: its label is body-sized and this one is
     13px, so the same padding left a 39px target for a control whose whole job
     is being tapped. */
  padding: 13px 0; background: none; border: 0; cursor: pointer;
  font-family: inherit; text-align: left; color: var(--text);
}
.tlog-item + .tlog-item { border-top: 1px solid var(--line); }
.tlog-item:active { opacity: 0.6; }
.tlog-k { font-weight: 700; font-size: 13px; flex: none; }
.tlog-v {
  flex: 1 1 auto; min-width: 0; text-align: right; color: var(--muted);
  font-size: 12px; font-variant-numeric: tabular-nums;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tlog-chev { width: 15px; height: 15px; flex: none; color: var(--muted); }
.tlog + .foot { margin-top: var(--s3); }

/* A card whose heading sits ABOVE a list of destinations rather than beside a
   value — the grouped-inset list both platforms use for a choice of places to
   go. The only thing it adds to .card is the gap under the heading and the
   bottom padding a .row supplies for itself, so the rows can reach the card's
   edges and their dividers can run its full width. */
.nav-group { padding: var(--s4) var(--s4) 0; }
.nav-group .group-title { margin-bottom: var(--s2); }

/* ── beta ────────────────────────────────────────────────────────────────── */
/* A small, quiet, EARNED badge. It is not a promotion and it is not a warning,
 * so it borrows the amber the app already uses for "this needs your attention
 * but nothing is wrong" rather than inventing a colour. Uppercase and tracked
 * like every other label in this app; sized to sit on a title's baseline
 * without pushing the line height around. */
.beta {
  display: inline-block; vertical-align: middle; flex: none;
  padding: 3px 7px; border-radius: var(--r1);
  background: var(--amber-dim); color: var(--amber-ink);
  font-size: var(--f-micro); font-weight: 800;
  text-transform: uppercase; letter-spacing: 0.1em; line-height: 1;
  /* Its own gap, so a third context does not mean a third rule here — which
     is what the first version did, and the badge landed flush against the
     word before it the moment it appeared somewhere new.
     UNCONDITIONAL, because the condition is not expressible: it always follows
     a TEXT node, and `:first-child` counts elements, so it matched every use
     and zeroed the gap it was supposed to protect. A badge that ever leads a
     line can have a modifier then; none does. */
  margin-left: var(--s2);
}

/* ── a shelf in the store ────────────────────────────────────────────────── */
/* Nine products in one flat list is a list nobody reads to the bottom of. The
 * heading is a real h3 rather than a styled div for the same reason the coached
 * page's section labels are: this is the sheet's structure, and structure that
 * exists only visually does not exist to anyone navigating by heading. */
.shelf + .shelf { margin-top: var(--s5); }
.shelf-head { margin-bottom: var(--s2); }
.shelf-title {
  margin: 0; font-size: var(--f-tiny); font-weight: 800;
  text-transform: uppercase; letter-spacing: 0.1em; color: var(--muted);
}
.shelf-blurb { font-size: var(--f-small); color: var(--muted); margin-top: 3px; }

/* ── the footnote ────────────────────────────────────────────────────────── */
/* Explanation, in a footer's voice, UNDER the thing it explains.
 *
 * These were .sum panels — bordered, shadowed, blur-backed, at body size —
 * sitting BETWEEN each label and its control, four times on one screen. That
 * is prose styled as heavily as content, which is what "a lot of noise with the
 * notes and tips" was pointing at: dismissing them was treating the symptom.
 * A grouped list puts this below the group, small and grey, or nowhere. */
.foot { color: var(--muted); font-size: var(--f-small); line-height: 1.5; }
.foot b { color: var(--soft); }
/* A footnote long enough to have paragraphs gets them. Four sentences of
   statistics set as one block is a wall — no rest for the eye, and no visible
   seam between the claim and the turn against it. */
.foot > p { margin: 0; }
.foot > p + p { margin-top: var(--s2); }

/* ── account ─────────────────────────────────────────────────────────────── */

.acct-id { padding: var(--s3) 0; }
.acct-email {
  font-weight: 700; color: var(--text); font-size: var(--f-body);
  overflow-wrap: anywhere;
}
.acct-name-row { display: flex; gap: var(--s2); align-items: stretch; margin-top: 6px; }
.acct-name-row input { flex: 1 1 auto; }
.mini {
  flex: none; background: var(--card); border: 1px solid var(--line);
  border-radius: var(--r1); color: var(--text); font-family: inherit;
  font-size: var(--f-label); font-weight: 800; letter-spacing: 0.1em;
  padding: 0 var(--s3); cursor: pointer; box-shadow: var(--e1);
}
.mini.danger { border-color: var(--red); color: var(--red-ink); background: transparent; }
/* SAVE reads as the commit action of its own row. It was byte-for-byte .ghost —
   and byte-for-byte the input it stands beside — so the only control on the
   Profile screen that WRITES something looked exactly like the field it writes
   from. Accent-dim and an accent border, not a solid fill: it commits one text
   input, and the screen's real primary actions are further down. */
.mini.commit {
  border-color: color-mix(in srgb, var(--accent) 55%, transparent);
  background: var(--accent-dim); color: var(--text);
}
@media (hover: hover) {
  .mini.commit:hover { background: color-mix(in srgb, var(--accent) 26%, var(--card)); }
}
.mini.commit:active { transform: scale(0.96); }
.mini:active { transform: scale(0.97); }

/* --card-2, not --card: the Account section is itself a card now, and a device
   row at the same fill as the surface under it has a border and nothing else. */
.device {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s3);
  padding: var(--s3); margin-top: 6px;
  background: var(--card-2); border: 1px solid var(--line); border-radius: var(--r2);
}
.device.here { border-color: var(--accent); }
/* min-width: 0 because a flex item will not shrink below its own min-content
   width without it — which for a device name means wrapping one character per
   line rather than ellipsising. Belt to the braces above. */
.device .d-main { min-width: 0; }
.device .d-name { font-weight: 700; font-size: 13.5px; color: var(--text); }
.device .d-when { color: var(--muted); font-size: 11.5px; margin-top: 2px; }
.d-tag {
  font-size: 9.5px; letter-spacing: 0.12em; font-weight: 800;
  color: var(--text); background: var(--accent-dim);
  border-radius: 999px; padding: 2px 6px; vertical-align: 1px;
}

/* Three paragraphs of policy, in the footnote's voice rather than in a bordered
   panel of its own. The claims are a contract the code keeps — a test asserts
   them — but a promise does not get louder by being boxed, and on a section that
   is now itself a card the box was a card inside a card. */
.privacy p { color: var(--muted); font-size: var(--f-small); line-height: 1.55; margin: 0 0 var(--s2); }
.privacy p:last-child { margin-bottom: 0; }

/* ── dismissible tips ─────────────────────────────────────────────────────────
   Only the copy that TEACHES A CONTROL is in here. The chart footnotes, axis
   labels, unit definitions, the pyramid legend, the pain findings and the
   privacy statement all wear similar classes and none of them is a tip: remove
   a definition and a number stops being readable. So the dismissible set is
   named by an explicit data-tip slug, never by .hint/.sum/.wk-meta as a
   selector — .wk-meta alone carries chart units, a summary stat AND the app
   version string.

   display: none on the element that OWNS the slug, never on its text: .sum sits
   on the shared surface list, so blanking its contents would leave a bordered,
   shadowed, blur-backed empty box sitting under the heading. */
body[data-tips-off~="start-button"] [data-tip="start-button"],
body[data-tips-off~="backlog"]      [data-tip="backlog"],
body[data-tips-off~="home-gym"]     [data-tip="home-gym"],
body[data-tips-off~="weekly-goal"]  [data-tip="weekly-goal"],
body[data-tips-off~="grade-range"]  [data-tip="grade-range"],
body[data-tips-off~="benchmark-profile"] [data-tip="benchmark-profile"] { display: none; }

/* absolute, so the .hint's centred text stays centred; the gutter is padding on
   the tip itself rather than a margin on the button, which is what keeps prose
   from running underneath the × at 320px */
[data-tip] { position: relative; }
.sum[data-tip] { padding-right: 38px; }
.foot[data-tip] { padding-right: 30px; }
.hint[data-tip] { padding-left: 26px; padding-right: 26px; }
.tip-x {
  position: absolute; right: 5px; top: 50%; transform: translateY(-50%);
  width: 24px; height: 24px; border-radius: 50%; border: 0; padding: 0;
  background: none; color: var(--muted); font-family: inherit; font-size: 15px;
  line-height: 1; cursor: pointer; display: grid; place-items: center;
  transition: background var(--d1) var(--ease), color var(--d1) var(--ease);
}
@media (hover: hover) { .tip-x:hover { background: var(--state); color: var(--text); } }
.tip-x:active { transform: translateY(-50%) scale(0.9); }

/* ── view transitions ────────────────────────────────────────────────────── */

.view { animation: view-in var(--d3) var(--ease); }
@keyframes view-in {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: none; }
}

/* ── the one accessibility escape hatch ──────────────────────────────────── */

.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;
}

/* Motion is decoration here — every animation above conveys the same thing
   its static state does. So when the platform says no, it all stops. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 1ms !important; animation-iteration-count: 1 !important;
    transition-duration: 1ms !important; scroll-behavior: auto !important;
  }
}
