/* Default theme = Dark. Themes are selected via <html data-theme> (see
   js/theme.js); each non-default theme overrides these CSS variables below. */
:root {
  color-scheme: dark;
  --bg: #0f172a;
  --surface: #1e293b;
  --surface-2: #334155;
  --border: #475569;
  /* Default accent = charcoal (js/accent.js DEFAULT_ACCENT). The live accent is applied
     via [data-accent] (pre-paint + the rules near the foot of this file); these are the
     CSS-level FALLBACKS for when no accent attr is applied (degraded / stale-partial
     deploy) — kept charcoal so nothing ever flashes the old blue. */
  --primary: #374151;
  --primary-dark: #1f2937;
  /* Secondary brand accents — a second + third user-pickable accent (js/accent.js,
     data-secondary / data-tertiary). Defaults form a teal + amber trio with the
     blue primary; each has a darker hover/active shade. */
  --secondary: #14b8a6;
  --secondary-dark: #0d9488;
  --tertiary: #f59e0b;
  --tertiary-dark: #d97706;
  --text: #f8fafc;
  --text-secondary: #94a3b8;
  --danger: #ef4444;
  --success: #22c55e;
  --warning: #f59e0b;
  --radius: 12px;
  --radius-sm: 8px;
  /* Full-screen overlay scrim (rest timer / stopwatch) + the faint countdown
     ring track + the timer phase / pace colours. Themed so the timer and
     stopwatch follow the app's dark / light / high-contrast style. */
  --scrim: rgba(0, 0, 0, 0.78);
  --ring-track: rgba(255, 255, 255, 0.08);
  --phase-red: #ef4444;
  --phase-orange: #f97316;
  --phase-yellow: #eab308;
  --phase-green: #22c55e;
  /* Display face for condensed-caps micro-labels (feat_forge_display_type). --font comes FIRST so a
     white-label tenant font overrides the display axis too, exactly as it overrides the body; with no
     tenant font set --font is unset and this resolves to Barlow Semi Condensed. */
  --font-display: var(--font, 'Barlow Semi Condensed', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
}

/* Iron Forge — THE DEFAULT THEME (feat_forge_ember_default_theme, branding addendum §1).
   Warm forged steel rather than the old blue-slate dark: the forge metaphor was only ever
   in the copy. Paired with the ember accent, but it is a normal accent-driven theme — pick
   any accent and it still works (unlike noir/blanc/chroma/sepia, which pin --primary).
   The --primary fallback here is ember, so a degraded load without [data-accent] still
   reads as Iron Forge rather than flashing charcoal. */
[data-theme="forge"] {
  color-scheme: dark;
  /* Surface ramp. The first cut of this palette stepped by ~10/255 between levels, which made
     forge the FLATTEST of all three palettes on every boundary — page|card 1.09, card|raised 1.12,
     card|border 1.31, against dark's 1.22 / 1.41 / 1.93. A DRAWN border was fainter than dark's
     IMPLIED edge, so hairlines stopped reaching the eye and adjacent panels merged. Since forge is
     the DEFAULT theme, that was everyone's default experience. (owner 2026-07-21: "everything is the
     same colour, I can't really see the edges between things".)
     Now stepped to 1.16 / 1.25 / 1.58 — the midpoint between the original and dark, chosen so forge
     stays moodier than the other themes while its edges actually read.
     Any future theme: keep card|border at or above ~1.5, or the hairlines are decorative.
     (fix_forge_theme_flat_surfaces_and_wheel_edges) */
  --bg: #161009;
  --surface: #271f15;
  --surface-2: #3b2f1f;
  --border: #4e3e29;
  --primary: #e0602a;
  --primary-dark: #b8461c;
  --secondary: #14b8a6;
  --secondary-dark: #0d9488;
  --tertiary: #f59e0b;
  --tertiary-dark: #d97706;
  --text: #f6ede1;
  --text-secondary: #b6a892;
  --danger: #ef4444;
  --success: #22c55e;
  --warning: #f59e0b;
  --scrim: rgba(20, 14, 8, 0.86);
  /* Warm ring track so the timer's unfilled arc sits in the forge palette. */
  --ring-track: rgba(224, 96, 42, 0.14);
  /* Ring phases stay SEMANTIC — the colour encodes time remaining, not the accent
     (branding addendum §4). Same values as dark. */
  --phase-red: #ef4444;
  --phase-orange: #f97316;
  --phase-yellow: #eab308;
  --phase-green: #22c55e;
}
/* Surfaces sit close in tone on the warm palette — a hairline edge gives cards a
   definite boundary, as noir/blanc do. */
[data-theme="forge"] .card,
[data-theme="forge"] .section-card,
[data-theme="forge"] .lift-card,
[data-theme="forge"] .workout-card { border: 1px solid var(--border); }

/* ── THE FOCUS MARKER — "do this next" ────────────────────────────────────────
   The signature forge glow, applied to the ONE element per screen that is the
   primary action for the user's current state. See DECISIONS 2026-07-21.
   js/focus.js is the arbiter; this is only the paint.

   IT IS APPLIED BY CLASS, NOT SCOPED TO A COMPONENT. It used to be hardcoded onto
   .grace-banner (the upgrade card), which meant a PAYING member saw no focus at all
   and a free member saw the app pointing at the shop rather than at their workout.
   The upgrade card is now just one candidate among several, and the last one —
   training outranks selling.

   EVERY THEME GETS IT (owner 2026-07-20). It was briefly forge-only, which made the
   product's signature element vanish on a theme switch; deriving both layers from the
   theme's OWN tokens means each theme renders its own version instead of one theme
   owning the treatment. That is also why there are no --promo-* literals any more.

   --focus-pos is set inline by focus.js from a STABLE hash of the target, so each
   focus glows from its own corner and stays put across re-renders. The fallback
   keeps the rule sane if the class is ever applied without the JS.

   The z-index pair is load-bearing. An absolutely-positioned ::before paints ABOVE
   non-positioned in-flow siblings, so without it the highlight washes across the
   card's own headline and buttons instead of glowing behind them.
   pointer-events:none fixes clicks only, not the visual.

   NEVER promote this to a bare `.card` rule: overflow:hidden is required to contain
   the radial, and it CLIPS children that deliberately leave the card (e.g.
   .ps-fork-menu, position:absolute; top:100%). Opt in per element, after checking
   that element for escaping children. */
.is-focus {
  position: relative;
  overflow: hidden;
  background: linear-gradient(120deg,
    color-mix(in srgb, var(--surface) 88%, var(--primary) 12%),
    color-mix(in srgb, var(--surface-2) 82%, var(--primary) 18%));
}
.is-focus::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background: radial-gradient(circle at var(--focus-pos, top right),
    color-mix(in srgb, var(--primary) 35%, transparent), transparent 70%);
}
.is-focus > * { position: relative; z-index: 1; }

/* The INLINE form, for a focus that lands on a row rather than a card: a radial glow
   on a short wide row reads as a smudge and fights the row separators, so the same
   concept renders as an accent tint plus a left rail. (Same shape as .set-row-next,
   which is a finer-grained "where you are in the list" affordance and is NOT the
   screen focus — the two coexist.) */
.is-focus-inline {
  background: color-mix(in srgb, var(--primary) 12%, transparent);
  box-shadow: inset 3px 0 0 var(--primary);
}

/* high-contrast opts OUT of the glow. That theme exists to maximise text/background
   separation for users who need it, and a translucent wash over the card surface is
   the one thing that directly undoes it — the same reason the accent rules already
   exclude it. It keeps the flat card; it loses a flourish, which is the correct trade. */
[data-theme="high-contrast"] .is-focus { background: var(--surface); }
[data-theme="high-contrast"] .is-focus::before { content: none; }

/* Light theme — light surfaces, dark text, accessible contrast. The --primary
   fallback stays charcoal (see :root note) so an unstyled/stale load never flashes
   blue; the live accent still overrides via [data-accent]. */
[data-theme="light"] {
  color-scheme: light;
  --bg: #eaeef2;
  --surface: #ffffff;
  --surface-2: #dfe5ec;
  --border: #bdc6d1;
  --primary: #374151;
  --primary-dark: #1f2937;
  --secondary: #0d9488;
  --secondary-dark: #0f766e;
  --tertiary: #d97706;
  --tertiary-dark: #b45309;
  --text: #0f172a;
  --text-secondary: #475569;
  --danger: #dc2626;
  --success: #16a34a;
  --warning: #d97706;
  /* Light frosted scrim + darker ring track / phase colours that stay legible
     on a light backdrop. */
  --scrim: rgba(241, 245, 249, 0.9);
  --ring-track: rgba(15, 23, 42, 0.12);
  --phase-red: #dc2626;
  --phase-orange: #ea580c;
  --phase-yellow: #a16207;
  --phase-green: #16a34a;
}

/* High contrast — standalone accessibility theme: near-black text on white,
   black borders, and saturated dark accents that hold ≥7:1 contrast (WCAG AAA)
   against white, so white-on-accent buttons stay legible too. */
[data-theme="high-contrast"] {
  color-scheme: light;
  --bg: #ffffff;
  --surface: #ffffff;
  --surface-2: #e6e6e6;
  --border: #000000;
  --primary: #0033cc;
  --primary-dark: #002299;
  /* AAA-tuned secondary accents that hold ≥7:1 on white (white-on-accent stays
     legible). High contrast pins these — the named-accent overrides below are
     scoped to exclude it, exactly like --primary. */
  --secondary: #006b6b;
  --secondary-dark: #004d4d;
  --tertiary: #8a4b00;
  --tertiary-dark: #6b3a00;
  --text: #000000;
  --text-secondary: #1a1a1a;
  --danger: #b00000;
  --success: #006b2d;
  --warning: #6b4500;
  /* Near-opaque white scrim (blur/translucency erodes contrast) + bold black
     track + saturated dark phase colours that hold AAA contrast on white. */
  --scrim: rgba(255, 255, 255, 0.97);
  --ring-track: #000000;
  --phase-red: #b00000;
  --phase-orange: #8a4b00;
  --phase-yellow: #6b5800;
  --phase-green: #006b2d;
}
/* On white surfaces, cards/inputs lose their edges without a visible border,
   and focus needs to be unmistakable. */
[data-theme="high-contrast"] .card,
[data-theme="high-contrast"] .section-card,
[data-theme="high-contrast"] .lift-card,
[data-theme="high-contrast"] .workout-card {
  border: 1px solid var(--border);
}
[data-theme="high-contrast"] .tab-bar { border-top: 2px solid var(--border); }
[data-theme="high-contrast"] :focus-visible { outline: 3px solid var(--primary); outline-offset: 2px; }

/* Noir — true-black OLED: pure-black surfaces, near-white text, and a MONOCHROME
   white accent (pinned below so the accent picker can't tint it). Semantic colours
   (danger/success/warning) and phase colours stay as the dark theme so errors and
   the timer ramp still read. (feat_theme_noir) */
[data-theme="noir"] {
  color-scheme: dark;
  --bg: #000000;
  --surface: #0a0a0a;
  --surface-2: #222222;
  --border: #333333;
  --primary: #ffffff;
  --primary-dark: #e0e0e0;
  /* Secondary/tertiary kept as dark greys so the monochrome look holds AND
     white-on-accent (.btn-secondary etc.) stays legible without overrides. */
  --secondary: #404040;
  --secondary-dark: #525252;
  --tertiary: #525252;
  --tertiary-dark: #404040;
  --text: #f5f5f5;
  --text-secondary: #8a8a8a;
  --danger: #ef4444;
  --success: #22c55e;
  --warning: #f59e0b;
  --scrim: rgba(0, 0, 0, 0.92);
  --ring-track: rgba(255, 255, 255, 0.1);
  --phase-red: #ef4444;
  --phase-orange: #f97316;
  --phase-yellow: #eab308;
  --phase-green: #22c55e;
}
/* On pure black, surfaces sit a hair off the background — a hairline border gives
   cards/inputs a definite edge. This border is what CARRIES the card edge in noir: page|card is
   1.06, far under the 1.15 fill floor, and deliberately so — flat greys ARE the theme. Until
   fix_theme_surface_ramps_below_contrast_floor the border was --border #1c1c1c, only 1.16 against
   the card, so the mechanism was right and simply could not be seen. It is now #333333 (1.57). */
[data-theme="noir"] .card,
[data-theme="noir"] .section-card,
[data-theme="noir"] .lift-card,
[data-theme="noir"] .workout-card { border: 1px solid var(--border); }
/* A surface/outline button on pure black is nearly invisible (black-on-black) — give every button a
   slim border so its edge reads. This used to hardcode #333 BECAUSE the card --border (#1c1c1c) was
   too faint on a button's smaller footprint; --border is now #333333 itself, so the exception has
   dissolved and it goes back through the token. (fix_noir_theme_button_border, then
   fix_theme_surface_ramps_below_contrast_floor) */
[data-theme="noir"] .btn { border: 1px solid var(--border); }
/* The white accent means anything FILLED with --primary needs dark text (the app
   otherwise hardcodes white-on-primary). Scoped to the prominent primary-filled
   controls; everything else uses --primary only for borders/dots/rings. */
[data-theme="noir"] .btn-primary,
[data-theme="noir"] .accent-custom-wrap.is-active,
[data-theme="noir"] .log-pill,
[data-theme="noir"] .settings-tab.active,
[data-theme="noir"] .hub-seg-opt.active,
[data-theme="noir"] .data-migration-banner,
[data-theme="noir"] .section-card[open] > summary::after,
[data-theme="noir"] .accent-gradient { color: #000; }
/* Noir is fully black & white: desaturate EVERYTHING the palette can't reach —
   coloured emoji, SVG icons with hardcoded fills (e.g. the amber flame), photos,
   charts, semantic colours. A grayscale filter on the document ROOT does this in
   one stroke; per the CSS spec the root element is EXEMPT from filter creating a
   containing block, so position:fixed overlays (rest timer, banners) are NOT
   broken. (feat_theme_noir — full monochrome) */
html[data-theme="noir"] { filter: grayscale(1); }

/* Blanc — the inverse of Noir: paper-white surfaces, near-black text, and a
   MONOCHROME black accent (pinned below so the accent picker can't tint it).
   Like Noir it grayscales the whole document, so it's a true black-&-white light
   theme. --primary is black and the app hardcodes white-on-primary, so filled
   primary controls (white text on black) stay legible with no override — the
   mirror image of Noir's dark-text-on-white-accent fix. (feat_blanc_theme) */
[data-theme="blanc"] {
  color-scheme: light;
  --bg: #ffffff;
  --surface: #f7f7f7;
  --surface-2: #dfdfdf;
  --border: #c8c8c8;
  --primary: #000000;
  --primary-dark: #1f1f1f;
  /* Secondary/tertiary kept as light greys so the monochrome look holds AND
     white-on-accent (.btn-secondary etc.) stays legible without overrides. */
  --secondary: #bdbdbd;
  --secondary-dark: #ababab;
  --tertiary: #ababab;
  --tertiary-dark: #bdbdbd;
  --text: #0a0a0a;
  --text-secondary: #6b6b6b;
  --danger: #ef4444;
  --success: #22c55e;
  --warning: #f59e0b;
  --scrim: rgba(255, 255, 255, 0.92);
  --ring-track: rgba(0, 0, 0, 0.1);
  --phase-red: #ef4444;
  --phase-orange: #f97316;
  --phase-yellow: #eab308;
  --phase-green: #22c55e;
}
/* On near-white, surfaces sit a hair off the background — a hairline border gives
   cards/inputs a definite edge (mirrors Noir's on-black treatment) — and as in noir it is this
   border, not the fill step, that carries the edge. It was #e0e0e0 (1.23 against the card, i.e.
   invisible); now #c8c8c8 (1.56). (fix_theme_surface_ramps_below_contrast_floor) */
[data-theme="blanc"] .card,
[data-theme="blanc"] .section-card,
[data-theme="blanc"] .lift-card,
[data-theme="blanc"] .workout-card { border: 1px solid var(--border); }
/* A light .btn-surface on white loses its edge — give every button a slim border
   so they stay discernible (the light-theme counterpart of fix_noir_theme_button_border). */
[data-theme="blanc"] .btn { border: 1px solid var(--border); }
/* Fully black & white: desaturate everything the palette can't reach — coloured
   emoji, SVG icons, photos, charts, semantic colours. Grayscale on the document
   ROOT (exempt from creating a containing block, so fixed overlays aren't broken)
   does it in one stroke, exactly like Noir. (feat_blanc_theme) */
html[data-theme="blanc"] { filter: grayscale(1); }

/* Chroma — a saturated, jewel-toned dark theme: a deep indigo/violet base with an
   electric violet primary and cyan + magenta secondaries. Unlike Dark (which is a
   neutral slate that defers to the accent picker), Chroma is OPINIONATED like
   Noir/Blanc — it pins its own vivid accent below so the whole app stays colourful
   whatever accent is stored. --primary is a purple-600 so the app's hardcoded
   white-on-primary stays legible. (feat_pwa_chroma_theme) */
[data-theme="chroma"] {
  color-scheme: dark;
  --bg: #12092a;
  --surface: #211648;
  --surface-2: #34226d;
  --border: #4a3491;
  --primary: #9333ea;
  --primary-dark: #7e22ce;
  --secondary: #22d3ee;
  --secondary-dark: #06b6d4;
  --tertiary: #f472b6;
  --tertiary-dark: #ec4899;
  --text: #f5f3ff;
  --text-secondary: #b7a9e0;
  --danger: #fb7185;
  --success: #34d399;
  --warning: #fbbf24;
  /* Violet-tinted scrim + ring track and a vivid phase ramp, so the timer/stopwatch
     carry the theme's colour. */
  --scrim: rgba(10, 4, 26, 0.86);
  --ring-track: rgba(168, 85, 247, 0.16);
  --phase-red: #fb7185;
  --phase-orange: #fb923c;
  --phase-yellow: #facc15;
  --phase-green: #34d399;
}

/* Sepia — a warm, parchment-toned LIGHT theme: aged-paper surfaces, espresso-brown
   text, and an opinionated walnut-brown accent pinned below (like Noir/Blanc/Chroma)
   so the whole app stays warm whatever accent is stored. Deliberately NOT grayscaled
   — the warm tone IS the theme. --primary is a mid walnut, dark enough that the app's
   hardcoded white-on-primary stays legible. (feat_theme_sepia) */
[data-theme="sepia"] {
  color-scheme: light;
  --bg: #e5d9c2;
  --surface: #f7eddb;
  --surface-2: #e2d2b0;
  --border: #cbb994;
  --primary: #7c5a3a;
  --primary-dark: #634527;
  --secondary: #9c6b3f;
  --secondary-dark: #7c5228;
  --tertiary: #b08d57;
  --tertiary-dark: #8f6f3e;
  --text: #3b2f1e;
  --text-secondary: #6f5b40;
  --danger: #a3341e;
  --success: #4d7c2f;
  --warning: #b5791a;
  /* Warm parchment scrim + ring track and a phase ramp that stays legible on the
     light scrim, so the timer/stopwatch carry the theme's warm tone. */
  --scrim: rgba(237, 224, 200, 0.92);
  --ring-track: rgba(59, 47, 30, 0.14);
  --phase-red: #b91c1c;
  --phase-orange: #c2410c;
  --phase-yellow: #a16207;
  --phase-green: #4d7c2f;
}
/* On the low-contrast parchment, cards sit only a hair off the background — a hairline
   border gives them a definite edge (mirrors Blanc's on-white treatment). */
[data-theme="sepia"] .card,
[data-theme="sepia"] .section-card,
[data-theme="sepia"] .lift-card,
[data-theme="sepia"] .workout-card { border: 1px solid var(--border); }

/* "Flatten icons" (feat_theme_flatten_icons_and_accent_lock) — recolour every colourful
   EMOJI (flatten.js wraps them in .app-icon) into the ACCENT. Line/mask icons already
   follow --primary; this targets the emoji. Since feat_flatten_icons_tonal_duotone_chromatic_accents
   the recolour is a TONAL DUOTONE, not a flat silhouette: an SVG filter (#app-icon-duotone,
   defs in index.html) grayscales the glyph, clamps its shadow floor for legibility, tints it to
   the accent (feFlood flood-color: var(--primary) — so it tracks the live accent, no JS), and
   clips to the glyph's own alpha (transparent stays transparent). The accent's hue wears the
   emoji's internal light/dark; an achromatic accent (Noir #fff / Blanc #000) degenerates to
   shades of grey. */
/* When NOT flattened, display:contents so the wrapper generates NO box: the emoji rejoins the
   surrounding text and doesn't become a stray flex/grid item (which spread section <summary>
   rows edge-to-edge — the "justified" regression). A CSS `filter` needs a box, so flattened
   icons opt into inline-block below. (fix_flatten_app_icon_summary_layout) */
.app-icon { display: contents; }
/* Borderless edit affordance — a bare pencil, no button chrome (owner 2026-07-07). Used for the
   Account username / password edit pencils. */
.settings-edit-btn { border: none; background: transparent; color: var(--text-secondary); cursor: pointer; display: inline-flex; align-items: center; padding: 4px; border-radius: 4px; transition: color 0.15s; }
.settings-edit-btn:hover { color: var(--primary); }
html[data-flatten="1"] .app-icon {
  display: inline-block; /* a filter needs a box; the summary slot already opts in the same way */
  filter: url(#app-icon-duotone);
}
#app-icon-duotone feFlood { flood-color: var(--primary); }
/* "Invert" (feat_flatten_icons_invert_secondary_accent): while flattened, recolour the
   icon silhouettes into the accent's paired SECONDARY colour (--accent-danger — the
   scheme's live second colour shown next to it in the picker, amber for charcoal) instead
   of --primary. Covers the emoji silhouettes plus the accent-following line/mask/nav icons
   so the whole icon set stays one colour. Only active with data-flatten-invert="1" (default
   on for the charcoal accent). Higher specificity (html + 2 attrs + class) beats the per-nav
   `.tab .icon-mask { color: --primary }` rules; it targets icons only, so text labels keep
   the accent. NB: this tracks --accent-danger (the accent's paired swatch), NOT the theme's
   --danger — for charcoal the two differ (amber #f59e0b vs the theme red), which is exactly
   the case the checkbox is designed for. (fix_flatten_invert_accent_paired_colour) */
html[data-flatten="1"][data-flatten-invert="1"] .app-icon {
  filter: url(#app-icon-duotone-invert);
}
#app-icon-duotone-invert feFlood { flood-color: var(--accent-danger); }
/* The forge anvil mark (nav Home + Forge tool) is an <img>, so it opts into the SAME tonal-duotone
   flatten as the emoji glyphs — the accent hue wears the anvil's internal light/dark, restoring the
   detail the flat mask discarded. Unflattened it shows its own amber art. (fix_forge_icons_restore_lost_detail) */
html[data-flatten="1"] .forge-mark { filter: url(#app-icon-duotone); }
html[data-flatten="1"][data-flatten-invert="1"] .forge-mark { filter: url(#app-icon-duotone-invert); }
html[data-flatten="1"][data-flatten-invert="1"] .icon-mask,
html[data-flatten="1"][data-flatten-invert="1"] .icon-glyph,
html[data-flatten="1"][data-flatten-invert="1"] .tab svg,
html[data-flatten="1"][data-flatten-invert="1"] .tab-tool svg {
  color: var(--accent-danger);
}

/* Accent colours (user-pickable in Settings, see js/accent.js). A separate axis
   from the theme: each overrides --primary / --primary-dark. Scoped to exclude
   high-contrast, which keeps its own AAA-tuned blue. Default 'blue' matches the
   app's original accent, so an un-set user sees no change. (user 2026-06-09) */
/* EACH ACCENT APPLIES BOTH OF ITS SWATCH COLOURS. The picker draws two dots per accent —
   the primary (`hex`) and its paired danger (`danger`) — and states that selecting it
   "applies both" (js/pages/settings.js). Until 2026-07-20 only the primary was actually
   applied: --accent-danger fell back to the theme's --danger for every accent but charcoal,
   so the WARM pairings (violet/amber/rose/pink/ember, all #111827) were advertised in the
   swatch and never applied — the picker lied. The cool accents only looked correct by
   coincidence, their pairing being the same red as --danger.
   --accent-danger drives the flatten "Invert" recolour, .badge-rir, .badge-lead and the
   modal ✕, so this is what makes BOTH icon toggles honour the accent you picked.
   SYNC POINT: these must match `danger` in js/accent.js ACCENTS[] — the swatch reads from
   there, these rules are what actually apply. A spec asserts the two agree. (owner 2026-07-20) */
html:not([data-theme="high-contrast"])[data-accent="blue"]    { --primary: #3b82f6; --primary-dark: #2563eb; --accent-danger: #ef4444; }
html:not([data-theme="high-contrast"])[data-accent="indigo"]  { --primary: #6366f1; --primary-dark: #4f46e5; --accent-danger: #ef4444; }
html:not([data-theme="high-contrast"])[data-accent="violet"]  { --primary: #8b5cf6; --primary-dark: #7c3aed; --accent-danger: #111827; }
html:not([data-theme="high-contrast"])[data-accent="emerald"] { --primary: #10b981; --primary-dark: #059669; --accent-danger: #ef4444; }
html:not([data-theme="high-contrast"])[data-accent="teal"]    { --primary: #06b6d4; --primary-dark: #0891b2; --accent-danger: #ef4444; }
html:not([data-theme="high-contrast"])[data-accent="amber"]   { --primary: #f59e0b; --primary-dark: #d97706; --accent-danger: #111827; }
html:not([data-theme="high-contrast"])[data-accent="rose"]    { --primary: #f43f5e; --primary-dark: #e11d48; --accent-danger: #111827; }
html:not([data-theme="high-contrast"])[data-accent="pink"]    { --primary: #ec4899; --primary-dark: #db2777; --accent-danger: #111827; }
/* Ember — the Iron Forge accent and the PWA's default (branding addendum §1). The amber
   already used for the forge marks, promoted to THE accent. Bright enough to stand off the
   dark surfaces AND to read as link text, so unlike charcoal it needs no dark-theme reversal
   and no --link override. Its picker pairing is the near-black neutral, now actually
   applied (see the sync-point note above) rather than falling through to the theme red. */
html:not([data-theme="high-contrast"])[data-accent="ember"]   { --primary: #e0602a; --primary-dark: #b8461c; --accent-danger: #111827; }
/* Charcoal scheme (feat_accent_two_colour_schemes): a monochrome slate accent — dark
   enough for white text on light themes, light enough to stand off the dark themes'
   near-black surfaces (noir's button border covers pure black). Paired with an amber
   danger below. */
html:not([data-theme="high-contrast"])[data-accent="charcoal"] { --primary: #374151; --primary-dark: #1f2937; --accent-danger: #f59e0b; }

/* Accent-coloured TEXT links (help/hint copy) track the accent by default. But the
   CHARCOAL accent is a dark slate tuned for button BACKGROUNDS (white text on it);
   as TEXT on the dark theme's dark surface it's unreadable — so lift links to a
   readable light slate there. (owner 2026-07-04: assistance-hint link unreadable in
   dark mode.) */
:root { --link: var(--primary); }
/* The accent's paired SECONDARY colour (the swatch shown beside it in the picker), used by
   the flatten "Invert" recolour, .badge-rir, .badge-lead and the modal ✕. Every NAMED accent
   now sets this explicitly (above) so the applied colour matches the swatch. This :root value
   is the FALLBACK for the cases with no named accent: a CUSTOM scheme (accent.js sets --danger
   inline, so this follows it automatically) and an unset accent.
   (fix_flatten_invert_accent_paired_colour; per-accent wiring 2026-07-20) */
:root { --accent-danger: var(--danger); }
html[data-theme="dark"][data-accent="charcoal"],
html:not([data-theme])[data-accent="charcoal"] { --link: #94a3b8; }

/* LIGHT theme: darken link text to clear WCAG AA (4.5:1) against the light surfaces.
   --link defaults to the accent (--primary). On every DARK/coloured theme that clears
   4.5:1 against the page (measured: forge 5.28, dark 5.00, noir 21, high-contrast 8.95),
   but the LIGHT theme is the only one pairing a mid-saturation accent with a near-white
   page — the default ember measured just 3.07:1 (fix_light_theme_link_contrast_below_aa).
   Underlining satisfies the separate "don't rely on colour alone" rule but does NOT fix
   contrast, so links here take a HUE-PRESERVING darker shade of the accent (RGB scaled
   uniformly → same hue, lower value), each tuned to ≥4.6:1 against the DARKEST light
   surface a link sits on (--surface-2 #dfe5ec) and correspondingly higher on --bg/--surface.
   Charcoal is already 8.84:1 as text on light, so it keeps its accent unchanged.
   Guarded per-accent by theme-contrast.test.mjs; floor recorded in docs/branding-and-styling.md. */
html[data-theme="light"][data-accent="blue"]    { --link: #2d62ba; }
html[data-theme="light"][data-accent="indigo"]  { --link: #5456cc; }
html[data-theme="light"][data-accent="violet"]  { --link: #724bca; }
html[data-theme="light"][data-accent="emerald"] { --link: #0a7350; }
html[data-theme="light"][data-accent="teal"]    { --link: #046f81; }
html[data-theme="light"][data-accent="amber"]   { --link: #8c5a06; }
html[data-theme="light"][data-accent="rose"]    { --link: #b93047; }
html[data-theme="light"][data-accent="pink"]    { --link: #af3572; }
html[data-theme="light"][data-accent="ember"]   { --link: #a7481f; }
/* CUSTOM hex accents are set inline by accent.js and can be arbitrarily light, so AA
   cannot be GUARANTEED for a pathological pick (e.g. pale yellow). Best-effort: mix the
   chosen accent toward the near-black text colour so links darken automatically on the
   light page while keeping the user's hue. Not covered by the guard (the hex is runtime);
   the residual limitation is documented in docs/branding-and-styling.md. */
html[data-theme="light"][data-accent="custom"]  { --link: color-mix(in srgb, var(--primary) 60%, #0f172a); }

/* Charcoal is our DEFAULT scheme: a dark slate tuned for white-text buttons on the
   LIGHT themes. On the DARK themes that near-black fill vanishes into the dark
   surfaces — buttons AND icons don't stand out at all. So on dark + charcoal we
   REVERSE the accent to a LIGHT slate (with dark text on it), the way amber/noir
   pop against black. Scoped to charcoal only — the bright accents already stand off
   the dark surfaces and want their own hue. (owner 2026-07-04: dark+charcoal buttons
   don't stand out; reverse it like amber.) */
html[data-theme="dark"][data-accent="charcoal"],
html:not([data-theme])[data-accent="charcoal"] {
  --primary: #cbd5e1;       /* slate-300 — light, stands off the dark surfaces */
  --primary-dark: #e2e8f0;  /* slate-200 — hover lightens, like a light button */
}
/* The now-light accent means anything FILLED with --primary needs DARK text (the app
   hardcodes white-on-primary). Same control list as Noir's white-accent fix. */
html[data-theme="dark"][data-accent="charcoal"] .btn-primary,
html[data-theme="dark"][data-accent="charcoal"] .btn-secondary,
html[data-theme="dark"][data-accent="charcoal"] .accent-custom-wrap.is-active,
html[data-theme="dark"][data-accent="charcoal"] .log-pill,
html[data-theme="dark"][data-accent="charcoal"] .settings-tab.active,
html[data-theme="dark"][data-accent="charcoal"] .hub-seg-opt.active,
html[data-theme="dark"][data-accent="charcoal"] .data-migration-banner,
html[data-theme="dark"][data-accent="charcoal"] .section-card[open] > summary::after,
html[data-theme="dark"][data-accent="charcoal"] .accent-gradient,
html[data-theme="dark"][data-accent="charcoal"] .grace-cta,
html:not([data-theme])[data-accent="charcoal"] .btn-primary,
html:not([data-theme])[data-accent="charcoal"] .btn-secondary,
html:not([data-theme])[data-accent="charcoal"] .accent-custom-wrap.is-active,
html:not([data-theme])[data-accent="charcoal"] .log-pill,
html:not([data-theme])[data-accent="charcoal"] .settings-tab.active,
html:not([data-theme])[data-accent="charcoal"] .hub-seg-opt.active,
html:not([data-theme])[data-accent="charcoal"] .data-migration-banner,
html:not([data-theme])[data-accent="charcoal"] .section-card[open] > summary::after,
html:not([data-theme])[data-accent="charcoal"] .accent-gradient,
html:not([data-theme])[data-accent="charcoal"] .grace-cta { color: #111827; }

/* Secondary + tertiary accents — same hue swatches as the primary accent, picked
   independently per channel (data-secondary / data-tertiary, js/accent.js). Same
   high-contrast exemption. A 'custom' choice sets the var inline (no rule here),
   so the inline style wins. (feat_secondary_accent_colours) */
html:not([data-theme="high-contrast"])[data-secondary="blue"]    { --secondary: #3b82f6; --secondary-dark: #2563eb; }
html:not([data-theme="high-contrast"])[data-secondary="indigo"]  { --secondary: #6366f1; --secondary-dark: #4f46e5; }
html:not([data-theme="high-contrast"])[data-secondary="violet"]  { --secondary: #8b5cf6; --secondary-dark: #7c3aed; }
html:not([data-theme="high-contrast"])[data-secondary="emerald"] { --secondary: #10b981; --secondary-dark: #059669; }
html:not([data-theme="high-contrast"])[data-secondary="teal"]    { --secondary: #06b6d4; --secondary-dark: #0891b2; }
html:not([data-theme="high-contrast"])[data-secondary="amber"]   { --secondary: #f59e0b; --secondary-dark: #d97706; }
html:not([data-theme="high-contrast"])[data-secondary="rose"]    { --secondary: #f43f5e; --secondary-dark: #e11d48; }
html:not([data-theme="high-contrast"])[data-secondary="pink"]    { --secondary: #ec4899; --secondary-dark: #db2777; }
html:not([data-theme="high-contrast"])[data-tertiary="blue"]     { --tertiary: #3b82f6; --tertiary-dark: #2563eb; }
html:not([data-theme="high-contrast"])[data-tertiary="indigo"]   { --tertiary: #6366f1; --tertiary-dark: #4f46e5; }
html:not([data-theme="high-contrast"])[data-tertiary="violet"]   { --tertiary: #8b5cf6; --tertiary-dark: #7c3aed; }
html:not([data-theme="high-contrast"])[data-tertiary="emerald"]  { --tertiary: #10b981; --tertiary-dark: #059669; }
html:not([data-theme="high-contrast"])[data-tertiary="teal"]     { --tertiary: #06b6d4; --tertiary-dark: #0891b2; }
html:not([data-theme="high-contrast"])[data-tertiary="amber"]    { --tertiary: #f59e0b; --tertiary-dark: #d97706; }
html:not([data-theme="high-contrast"])[data-tertiary="rose"]     { --tertiary: #f43f5e; --tertiary-dark: #e11d48; }
html:not([data-theme="high-contrast"])[data-tertiary="pink"]     { --tertiary: #ec4899; --tertiary-dark: #db2777; }

/* Noir pins a monochrome white accent regardless of the accent picker (mirrors how
   high-contrast pins its blue). Placed AFTER the accent rules so it wins the
   equal-specificity tie when data-accent/-secondary/-tertiary are set. (feat_theme_noir) */
/* --accent-danger pinned to the theme's own danger here (and on Blanc below) to PRESERVE
   today's behaviour now that the accent rules apply their paired colour: without this pin
   a picked accent's pairing would leak a hue into these deliberately monochrome themes.
   Chroma + Sepia already pin their own. (owner 2026-07-20) */
html[data-theme="noir"][data-accent]    { --primary: #ffffff; --primary-dark: #e0e0e0; --accent-danger: var(--danger); }
html[data-theme="noir"][data-secondary] { --secondary: #404040; --secondary-dark: #525252; }
html[data-theme="noir"][data-tertiary]  { --tertiary: #525252; --tertiary-dark: #404040; }
/* Blanc pins a monochrome BLACK accent regardless of the accent picker — the
   inverse of Noir's pin above. (feat_blanc_theme) */
html[data-theme="blanc"][data-accent]    { --primary: #000000; --primary-dark: #1f1f1f; --accent-danger: var(--danger); }
html[data-theme="blanc"][data-secondary] { --secondary: #bdbdbd; --secondary-dark: #ababab; }
html[data-theme="blanc"][data-tertiary]  { --tertiary: #ababab; --tertiary-dark: #bdbdbd; }
/* Chroma pins its vivid violet/cyan/magenta accent regardless of the accent picker
   (same equal-specificity-then-source-order trick as Noir/Blanc), so its colour
   identity holds however the user's accent is set. (feat_pwa_chroma_theme) */
html[data-theme="chroma"][data-accent]    { --primary: #9333ea; --primary-dark: #7e22ce; --accent-danger: #22d3ee; }
html[data-theme="chroma"][data-secondary] { --secondary: #22d3ee; --secondary-dark: #06b6d4; }
html[data-theme="chroma"][data-tertiary]  { --tertiary: #f472b6; --tertiary-dark: #ec4899; }
/* Sepia pins its warm walnut accent regardless of the picker — opinionated like
   Chroma. --accent-danger pinned to a warm brick so the flatten-invert recolour
   stays in the sepia family. (feat_theme_sepia) */
/* --accent-danger drives the flattened icon silhouettes (flatten-invert, default on for
   charcoal) + the Primary pill. In Sepia the old brick red (#a3341e) read too vibrant/red
   against the parchment; a flatter, browner tone keeps the sepia warmth. (fix_sepia_icon_flatter_brown) */
html[data-theme="sepia"][data-accent]    { --primary: #7c5a3a; --primary-dark: #634527; --accent-danger: #8a5a3a; }
html[data-theme="sepia"][data-secondary] { --secondary: #9c6b3f; --secondary-dark: #7c5228; }
html[data-theme="sepia"][data-tertiary]  { --tertiary: #b08d57; --tertiary-dark: #8f6f3e; }
/* The flatten "Invert" recolour paints icons in --accent-danger (the accent's paired
   swatch). The default charcoal accent pairs to amber (#f59e0b), which clashes badly
   with Chroma's violet/indigo palette (orange icons). Chroma pins its own paired
   colour — its vivid cyan secondary — so flattened icons read as an on-theme
   violet+cyan pair whatever accent is stored. Two-attribute selector + placed after
   the charcoal rule so it wins. (fix_chroma_icon_colour_off_theme) */

/* On the red-family accents — rose, pink, amber, violet — the standard danger
   red blends into the accent and stops reading as the destructive action, so the
   danger button switches to a high-contrast neutral: WHITE on the dark theme
   (default) and BLACK on the light theme, so it stays legible + distinct on each
   surface. Every other accent + high-contrast keep red. (user 2026-06-12) */
html:not([data-theme="high-contrast"])[data-accent="rose"]   .btn-danger,
html:not([data-theme="high-contrast"])[data-accent="pink"]   .btn-danger,
html:not([data-theme="high-contrast"])[data-accent="amber"]  .btn-danger,
html:not([data-theme="high-contrast"])[data-accent="ember"]  .btn-danger,
html:not([data-theme="high-contrast"])[data-accent="violet"] .btn-danger {
  background: #f1f5f9;   /* dark theme (default): white button, dark text */
  color: #0f172a;
}
html[data-theme="light"][data-accent="rose"]   .btn-danger,
html[data-theme="light"][data-accent="pink"]   .btn-danger,
html[data-theme="light"][data-accent="amber"]  .btn-danger,
html[data-theme="light"][data-accent="ember"]  .btn-danger,
html[data-theme="light"][data-accent="violet"] .btn-danger {
  background: #111827;   /* light theme: black button, white text */
  color: #fff;
}
/* Charcoal scheme → amber danger (amber is visible on every theme; dark text stays
   legible, and it pops against the monochrome accent). (feat_accent_two_colour_schemes) */
html:not([data-theme="high-contrast"])[data-accent="charcoal"] .btn-danger {
  background: #f59e0b;
  color: #111827;
}

/* Accent picker swatches in Settings. */
/* White-label icon override (feat_tenant_branding_icons): a tenant-uploaded glyph
   rendered in place of a built-in. Sized to the surrounding text by default. */
.icon-glyph { display: inline-block; width: 1em; height: 1em; vertical-align: -0.125em; object-fit: contain; }
.accent-btn { display: inline-flex; align-items: center; gap: 7px; }
.accent-dot { width: 14px; height: 14px; border-radius: 50%; display: inline-block; flex: none; box-shadow: inset 0 0 0 1px rgba(0,0,0,0.15); }
/* The paired danger swatch in a two-colour scheme button — a smaller dot tucked
   against the accent dot. (feat_accent_two_colour_schemes) */
.accent-dot--danger { width: 10px; height: 10px; margin-left: -5px; }
/* Marks the default swatch in a channel so the baseline is identifiable. */
.accent-default-tag { display: inline-flex; align-items: center; font-size: 11px; text-transform: uppercase; letter-spacing: .04em; opacity: .75; margin-left: 2px; border: 1px solid currentColor; border-radius: 4px; padding: 1px 3px 0; line-height: 1; }

/* Clear-data / delete-account "type your username" copy affordance: the name and
   the copy icon share ONE bordered box (bordered on all sides), and the icon
   flips to a tick for 5s on copy. (fix_delete_user_copy_button_border_tick) */
.cd-copy-field { display: inline-flex; align-items: center; gap: 6px; max-width: 100%; }
.cd-copy-field #cd-username { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cd-copy-btn { flex: none; border: none; background: transparent; color: var(--text-secondary); cursor: pointer; display: inline-flex; align-items: center; padding: 2px; border-radius: 4px; transition: color 0.15s; }
.cd-copy-btn:hover { color: var(--text); }
.cd-copy-btn.copied { color: var(--success, var(--primary)); }
/* Per-channel accent picker rows (primary/secondary/tertiary) + the custom-colour
   swatch that opens the native all-colour selector. (feat_secondary_accent_colours) */
.accent-row { margin-top: 10px; }
.accent-row:first-of-type { margin-top: 0; }
.accent-row-label { font-weight: 600; margin: 0 0 6px 4px; }
.accent-custom-wrap {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 8px 14px; font-size: 13px; font-weight: 600;
  border-radius: var(--radius-sm); background: var(--surface-2); color: var(--text);
  cursor: pointer;
}
.accent-custom-wrap.is-active { background: var(--primary); color: #fff; }
.accent-custom {
  width: 14px; height: 14px; flex: none; padding: 0; border: none;
  border-radius: 50%; background: none; cursor: pointer;
  box-shadow: inset 0 0 0 1px rgba(0,0,0,0.15);
}
.accent-custom::-webkit-color-swatch-wrapper { padding: 0; }
.accent-custom::-webkit-color-swatch { border: none; border-radius: 50%; }
.accent-custom::-moz-color-swatch { border: none; border-radius: 50%; }

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

/* Anchors inherit the surrounding text colour by default. Much of the app uses
   <a> as cards/rows/buttons (e.g. .active-program-card, .btn, .badge), and with no
   base colour those fell to the UA link palette — most visibly the purple
   *visited*-link colour bleeding into headings like the home hub's "Up Next: <lift>"
   (user 2026-07-03). Genuine text-links opt back in explicitly (.help-content a,
   .switch-link a, .analytics-consent-text a → var(--primary)). */
a { color: inherit; }

/* Checkboxes / radios follow the theme accent instead of the browser default
   (which ignored dark mode + the accent picker). (user 2026-06-09) */
input[type="checkbox"], input[type="radio"] { accent-color: var(--primary); }

/* DEFAULT styling for text-like controls so a bare <input>/<select>/<textarea>
   is never an unstyled native control. Low specificity on purpose — explicit
   classes (.form-group input, .input-inline, .settings-select, …) and inline
   styles still win, so this only catches controls nobody styled. Width is left
   to context (flex rows etc.). (user 2026-06-15: "every single time you add a
   control you don't style it") */
/* The :not() chain is wrapped in :where() so it contributes ZERO specificity. Without it this arm
   scored (0,4,1) — four points from the four :not()s — while `select` and `textarea` in the SAME
   selector list scored (0,0,1). A component rule like `.assist-row .assist-sets` (0,2,0) therefore
   overrode the padding on a <select> but LOST on an <input>, so a text box rendered 6px taller than
   the dropdown beside it (owner 2026-07-22: "the text input for sets is taller than the dropdowns").
   :where() makes the three arms symmetrical, which is what the shared block always intended.
   (fix_assist_ex_reads_as_text_not_dropdown) */
input:where(:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"])),
select,
textarea {
  padding: 10px 12px;
  background: var(--surface);
  border: 1px solid var(--surface-2);
  border-radius: var(--radius-sm);
  color: var(--text);
  font-size: 15px;
  font-family: inherit;
  outline: none;
  transition: border-color 0.2s;
}
input:where(:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"])):focus,
select:focus,
textarea:focus { border-color: var(--primary); }
/* Placeholders are paler than a real value, so a placeholder (e.g. a suggested value) never reads
   as an already-filled field (owner 2026-07-07). --text-secondary at 0.6 sits clearly below the
   --text of a typed value. More-specific placeholder rules (rest-timer / plate cell) still win. */
::placeholder { color: var(--text-secondary); opacity: 0.6; }

/* Custom dropdown chevron, inset from the right edge (the native arrow sits flush to the
   box edge — owner 2026-06-30). `select:not([multiple])` at specificity (0,1,1) beats the
   single-class select rules, so every dropdown gets the inset arrow without touching each
   class. Neutral grey reads on every theme. */
select:not([multiple]) {
  -webkit-appearance: none; -moz-appearance: none; appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' fill='none' stroke='%23888' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M1 1.5 6 6.5 11 1.5'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 12px center;
  background-size: 12px;
  padding-right: 34px;
}

html, body {
  min-height: 100%;
  /* Always reserve the vertical scrollbar gutter so the layout-viewport width is
     identical whether or not a page overflows. Without this, a route that scrolls
     (e.g. Settings) is ~15px narrower than one that doesn't (e.g. Forge), and the
     fixed .tab-bar + centred #app — both anchored via left:50%/margin:auto — jump
     horizontally between the two. (fix_toolbar_static_forge_settings) */
  scrollbar-gutter: stable;
  /* --font is the white-label override (feat_tenant_branding_fonts); unset = the
     system stack. Tenant fonts: bundled @font-face below, a Google <link>, or an
     uploaded @font-face injected at runtime by tenant-theme.js. */
  font-family: var(--font, 'Barlow', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
  background: var(--bg);
  color: var(--text);
  -webkit-font-smoothing: antialiased;
}

#app {
  padding: 16px;
  /* Clear the device status bar / notch in the Android (Capacitor) shell.
     env(safe-area-inset-top) is 0 in a normal browser, so this is a no-op there. */
  padding-top: calc(16px + env(safe-area-inset-top));
  /* 80px clears the fixed tab bar; add the device bottom inset (home indicator /
     gesture bar) so the last element isn't clipped on devices where the tab bar
     grows by env(safe-area-inset-bottom). 0 in a normal browser. */
  padding-bottom: calc(80px + env(safe-area-inset-bottom));
  max-width: 480px;
  margin: 0 auto;
  width: 100%;
}

/* Tab bar — matches the 480px content column; on phones (viewport ≤ 480px)
   width:100% takes effect first so the bar still goes edge-to-edge. */
.tab-bar {
  position: fixed;
  bottom: 0;
  left: 50%;
  transform: translateX(-50%);
  width: 100%;
  max-width: 480px;
  display: flex;
  background: var(--surface);
  border-top: 1px solid var(--surface-2);
  padding: 8px 0;
  padding-bottom: max(8px, env(safe-area-inset-bottom));
  z-index: 100;
}

.tab-bar.hidden { display: none; }

/* App-shell notification bell — fixed top-right, above page content. */
.notif-bell {
  position: fixed;
  top: max(10px, env(safe-area-inset-top));
  right: 12px;
  width: 40px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 50%;
  background: var(--surface);
  color: var(--text-secondary);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
  cursor: pointer;
  z-index: 200;
}
.notif-bell.hidden { display: none; }
/* Unsaved-data alert — amber chip just left of the notification bell, shown
   whenever there are writes captured offline and not yet synced. (user 2026-06-11) */
.unsaved-alert {
  position: fixed;
  top: max(10px, env(safe-area-inset-top));
  right: 60px;
  height: 40px;
  padding: 0 12px;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  border: none;
  border-radius: 20px;
  background: var(--warning, #b45309);
  color: #fff;
  font-weight: 600;
  font-size: 14px;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
  cursor: pointer;
  z-index: 200;
}
.unsaved-alert.hidden { display: none; }
.unsaved-alert__count { font-variant-numeric: tabular-nums; }
.notif-bell__badge {
  position: absolute;
  top: -2px;
  right: -2px;
  min-width: 18px;
  height: 18px;
  padding: 0 4px;
  border-radius: 9px;
  background: #ef4444;
  color: #fff;
  font-size: 11px;
  font-weight: 700;
  line-height: 18px;
  text-align: center;
}
.notif-bell__badge.hidden { display: none; }

/* Unread reminder in the notifications feed — an accent rail until read on open. */
.notif-unread { border-left: 3px solid var(--accent, var(--primary, #c0392b)); }

.tab {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  text-decoration: none;
  color: var(--text-secondary);
  font-size: 11px;
  font-weight: 600;
  transition: color 0.2s;
}

/* One standard: every nav/toolbar icon follows the theme accent (--primary),
   active/inactive differentiated by opacity. The anvil/cog brand marks are
   rendered as CSS masks (.icon-mask) so a single background-color re-tints them
   to the accent; the inline Progress flame + middle-tab option glyphs use
   currentColor. (Tenant-override images stay as <img> and keep their own art.) */
.icon-mask {
  display: inline-block;
  width: 24px;
  height: 24px;
  background-color: currentColor;
  -webkit-mask: var(--icon-src) center / contain no-repeat;
  mask: var(--icon-src) center / contain no-repeat;
}
.tab svg { width: 24px; height: 24px; color: var(--primary); opacity: 0.55; transition: opacity 0.2s; }
.tab .icon-mask { color: var(--primary); opacity: 0.55; transition: opacity 0.2s; }
.tab img { width: 24px; height: 24px; object-fit: contain; opacity: 0.55; transition: opacity 0.2s; }
.tab.active svg,
.tab.active .icon-mask,
.tab.active img { opacity: 1; }
.tab.active { color: var(--primary); }

/* Training toolbar (feat_training_toolbar_tools): while a workout is open the
   bottom nav becomes the three workout tools. The tool buttons reuse .tab for
   layout; reset the native <button> chrome and tint the (currentColor) glyphs to
   the accent so they read as active controls, not inactive nav. */
.tab-tool {
  background: none;
  border: none;
  margin: 0;
  padding: 0;
  font: inherit;
  cursor: pointer;
  color: var(--primary);
  -webkit-tap-highlight-color: transparent;
}
.tab-tool svg { opacity: 0.9; }
/* The Forge (home) tool uses the anvil mask (.icon-mask), not an inline <svg> —
   .tab-tool already sets color:var(--primary), so it renders as a bright accent
   silhouette matching its Timer/Stopwatch siblings. (.tab-tool img is kept for
   tenant-override art, which stays at its own colour.) */
.tab-tool .icon-mask { opacity: 0.9; }
.tab-tool img { opacity: 0.9; }
/* Blanked tool (e.g. Timer while a rest runs / Stopwatch while cardio is open):
   keep the flex slot so the row doesn't reflow, but hide + disable it — the
   minimised countdown / cardio chip owns its corner instead. */
.tab-tool--blank { visibility: hidden; pointer-events: none; }
/* Timer tool while a closed rest is still counting: a gentle pulse hints that a
   tap RESUMES the countdown rather than starting a fresh rest. */
.tab-tool--resuming svg { opacity: 1; animation: tab-tool-pulse 1.2s ease-in-out infinite; }
@keyframes tab-tool-pulse { 50% { opacity: 0.4; } }

/* Mini rest timer (feat_toolbar_mini_timer_button): the centre Timer button is a
   live minimised countdown while a rest runs — a draining ring with the mm:ss (or
   idle GO!) centred INSIDE it. The ring grows to ~the full height of an icon+label
   tab so it fills the slot without making the bar taller / reflowing its siblings.
   Colour ramps like the full timer. */
.tab-tool--mini { justify-content: center; overflow: visible; }
.tab-mini-ring {
  /* 58px so the timer dominates — but a negative top margin keeps its LAYOUT height
     equal to the icon+label tabs (~43px) so the bottom bar does NOT grow taller; the
     ring pokes above the bar as a raised focal button. The disc carries the SAME
     surface fill + 1px border + top shadow as the toolbar, so the part sticking out
     reads as the bar bulging up (owner 2026-07-04). The uniform 15px countdown (even
     10:00 / 60:00) fits inside — measured against a stroke-8 ring. */
  position: relative;
  width: 58px;
  height: 58px;
  margin-top: -15px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--surface);
  border: 1px solid var(--surface-2);
  border-radius: 50%;
  box-shadow: 0 -2px 6px rgba(0, 0, 0, 0.07);
}
.tab-mini-ring svg { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
/* stroke-width 6 (was 8): a slightly thinner mini ring. (fix_mini_timer_border_reset_and_thinner) */
.tab-mini-track { fill: none; stroke: var(--surface-2); stroke-width: 6; }
.tab-mini-prog {
  fill: none;
  stroke-width: 6;
  stroke-linecap: round;
  /* Drain smoothly between the once-a-second updates. */
  transition: stroke-dashoffset 0.9s linear;
}
/* Ramp mirrors the full timer's colours. */
.tab-mini-ring.red .tab-mini-prog { stroke: var(--phase-red); }
.tab-mini-ring.orange .tab-mini-prog { stroke: var(--phase-orange); }
.tab-mini-ring.yellow .tab-mini-prog { stroke: var(--phase-yellow); }
.tab-mini-ring.green .tab-mini-prog { stroke: var(--phase-green); }
.tab-mini-time {
  position: relative;
  font-size: 15px;            /* uniform for every countdown value, incl. 10:00 */
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--primary);
}
.tab-mini-time.is-go { font-size: 18px; letter-spacing: 0; }

/* Typography
   Headings take the CONDENSED DISPLAY face, uppercase, with tracking that opens up as
   the size comes down (smaller caps need more air to stay readable). Body copy,
   numerals, stat values and input labels stay on var(--font) / Barlow and are NOT
   uppercased — the contrast between shouted headings and quiet body copy IS the
   treatment. (feat_forge_display_headings_and_promo_glow, design 1c) */
h1 { font-size: 24px; font-family: var(--font-display); font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 16px; }
h2 { font-size: 18px; font-family: var(--font-display); font-weight: 700; text-transform: uppercase; letter-spacing: 0.8px; margin-bottom: 12px; }
h3 { font-size: 15px; font-family: var(--font-display); font-weight: 600; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px; color: var(--text-secondary); }

/* The hub wordmark — "IRON FORGE" — is this same <h1>, with wider tracking because a
   short two-word mark carries it where a sentence-length heading would not. */
h1.hub-title-app { letter-spacing: 1.5px; }

/* A WHITE-LABEL TENANT'S OWN NAME goes through that same <h1> (renderHubTitle), and a
   customer's brand is not ours to restyle: force-uppercasing would render "iForge" as
   "IFORGE", and imposing a condensed face would override a font they may have paid to
   set. So the tenant variant opts OUT of the display treatment and keeps var(--font) —
   the token their branding already overrides. */
h1.hub-title-brand {
  font-family: var(--font, 'Barlow', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
  text-transform: none;
  letter-spacing: normal;
}

/* Cards */
.card {
  background: var(--surface);
  border-radius: var(--radius);
  padding: 16px;
  margin-bottom: 12px;
}

/* Collapsible accordion section — used by the Settings Account tab so the
   long list of preferences collapses to a tidy stack, expanding one row at a
   time. */
.section-card {
  padding: 0;
}
.section-card > summary {
  padding: 16px;
  cursor: pointer;
  list-style: none;
  font-weight: 600;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  position: relative;
}
.section-card > summary::-webkit-details-marker { display: none; }
/* Prominent expand/collapse affordance: a filled circular badge (::after) with a
   border-drawn chevron (::before) centred on it. The chevron is drawn from two
   borders rather than a text glyph because a glyph's optical centre ≠ its box
   centre, so rotating it drifted (collapsed read "too low", open "too far left").
   A border chevron rotates cleanly around its true centre in both states.
   (feat_settings_equipment_polish; was user 2026-06-11) */
/* The badge (::after) is kept as a transparent 30px spacer that reserves the chevron's slot in the
   flex row — but with NO fill, so there's no circular disc behind the chevron. The owner found the
   filled disc (light --surface-2 collapsed / accent --primary open) an unwanted extra background.
   (fix_settings_expander_and_info_icon_circle_backgrounds) */
.section-card > summary::after {
  content: '';
  flex-shrink: 0;
  width: 30px;
  height: 30px;
  background: transparent;
}
.section-card[open] > summary::after { background: transparent; }
/* The chevron (::before) is a box that EXACTLY overlaps the badge (same right
   edge + width), with the arrow drawn by a mask centred in that box — so it is
   always dead-centre on the badge regardless of layout, and rotating it can't
   drift it out of the circle. Colour comes from `background` (themeable). */
.section-card > summary::before {
  content: '';
  position: absolute;
  top: 50%;
  right: 15px;       /* chevron nudged 1px right of the badge centre (chore_settings_header_polish) */
  width: 30px;       /* identical box to the badge */
  height: 30px;
  /* Chevron colour is a muted foreground token (not the accent), so with the disc gone it stays legible
     directly on the summary surface in every theme — including noir, where the old open-state #fff sat
     white-on-white. (fix_settings_expander_and_info_icon_circle_backgrounds) */
  background: var(--text-secondary);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M9 6l6 6-6 6' fill='none' stroke='%23000' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 11px 11px no-repeat;
          mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M9 6l6 6-6 6' fill='none' stroke='%23000' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 11px 11px no-repeat;
  transform: translateY(-50%) rotate(0deg); /* point right (collapsed) */
  transition: transform 0.18s ease, background 0.18s;
  pointer-events: none;
}
.section-card[open] > summary::before {
  transform: translateY(-50%) rotate(90deg); /* point down (open) */
  /* keep the same --text-secondary chevron colour when open (was #fff, which relied on the accent disc) */
}
.section-card .section-body { padding: 0 16px 16px; }
.section-card .section-summary-value {
  color: var(--text-secondary);
  font-weight: 400;
  font-size: 13px;
  margin-left: auto;
  margin-right: 8px;
  text-align: right; /* subtitle sits flush right beside the expand button (owner 2026-07-08) */
}
/* Give each section summary's leading icon a fixed-width, centred slot so every
   TITLE starts at the same x — the icons are emoji with differing advance widths,
   so without a slot the titles drift down the page. The global `.app-icon` is
   `display:contents` (so flatten doesn't turn the emoji into a stray flex item that
   space-between would fling to the row's edge); scoped to summaries we opt back into
   a box. To keep every row's value + chevron pinned right (and never spread the
   icon/title apart), the chevron badge also takes `margin-left:auto` — so rows
   WITHOUT a `.section-summary-value` (which carries the auto-margin) still push the
   badge to the edge instead of justifying. (fix_chroma_settings_polish) */
.section-card > summary .app-icon {
  display: inline-block;
  width: 1.4em;
  flex-shrink: 0;
  text-align: center;
  font-style: normal;
  /* Cancel most of the summary's 12px flex gap on the icon→title side so the slot
     doesn't widen the row (which wrapped the longest title, "Accent Colours"); the
     gap still spaces title↔badge↔value normally. */
  margin-right: -6px;
}
/* Only rows WITHOUT a `.section-summary-value` need the badge to absorb the free
   space (the value carries `margin-left:auto` when present). A SECOND auto margin
   on the badge would split the free space with the value's auto and strand it
   mid-row instead of flush against the badge. (fix_settings_subtitle_flush_right) */
.section-card > summary:not(:has(.section-summary-value))::after { margin-left: auto; }

.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 12px;
}

/* Buttons */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 12px 20px;
  border: none;
  border-radius: var(--radius-sm);
  font-size: 15px;
  /* CTAs take the display face too — a button is a call to action, so it shouts with
     the headings rather than whispering with body copy. text-transform is presentation
     only: the DOM text and the accessible name keep their original case, so specs and
     screen readers are unaffected. */
  font-family: var(--font-display);
  text-transform: uppercase;
  letter-spacing: 0.6px;
  font-weight: 700;
  cursor: pointer;
  transition: background 0.2s, opacity 0.2s;
  width: 100%;
  color: var(--text);
  /* .btn is also used on <a> elements (e.g. the workout-page "← Back" link).
     Reset the browser default anchor underline so anchor-as-button controls
     match real <button>s. */
  text-decoration: none;
}

.btn:disabled { opacity: 0.5; cursor: not-allowed; }
/* Solid colour-filled buttons keep light text on both themes (the body text
   colour flips to dark in light mode, which would be illegible on these). */
.btn-primary { background: var(--primary); color: #fff; }
.btn-primary:hover:not(:disabled) { background: var(--primary-dark); }
/* --danger-fg lets a CUSTOM scheme's danger carry a legible text colour (a light
   custom danger auto-switches to dark text). Named schemes set text in the accent
   rules above; the default is white. (feat_accent_two_colour_schemes) */
.btn-danger { background: var(--danger); color: var(--danger-fg, #fff); }
/* .btn-success is RETIRED — there is exactly ONE action colour per screen and it is the active
   accent (branding-and-styling.md, addendum §2). A green-filled button is always an action, so the
   class could only ever break that rule; --success now means done/logged STATE only (ticks, done
   badges, approved pills). Use .btn-primary for the action.
   (feat_single_action_colour_primary retired the CTAs, feat_selection_chips_use_primary the
   include/exclude chips — this was the last reference.) */
.btn-surface { background: var(--surface-2); }
/* Secondary/tertiary accents were dropped (feat_accent_two_colour_schemes) — these
   legacy classes now follow the single --primary accent. */
.btn-secondary { background: var(--primary); color: #fff; }
.btn-secondary:hover:not(:disabled) { background: var(--primary-dark); }
/* Reusable primary→secondary accent gradient (decorative surfaces only — not the
   rest-timer ring, whose colour encodes time remaining). */
.accent-gradient { background: linear-gradient(135deg, var(--primary), var(--primary-dark)); color: #fff; }

/* Forge-your-program wizard step indicator: a segmented progress bar. Uses --link
   (the readable accent — a legible slate on the low-contrast charcoal scheme) so the
   done/current segments stand out on every theme. (owner 2026-07-04: step markers
   poorly indicated.) */
.cb-wiz-steps { display: flex; gap: 6px; margin-bottom: 8px; }
.cb-wiz-dot { flex: 1; height: 5px; border-radius: 3px; background: var(--surface-2); }
.cb-wiz-dot.done { background: var(--link); opacity: 0.5; }
.cb-wiz-dot.active { background: var(--link); }

/* Goal/priority toggle-pair selections (SELECTED = btn-primary) now rely on the plain
   filled-accent look — a solid --primary button reads clearly against the unselected
   btn-surface on every theme, because the dark+charcoal scheme now REVERSES to a light
   accent in dark mode (see the reversal block above). No ring/checkmark needed — the
   fill alone marks the selection, matching the other Forge-your-program wizard steps.
   (owner 2026-07-04: drop the border, make the selected button stand out by fill.) */
.btn-sm { padding: 8px 14px; font-size: 13px; width: auto; }

.btn-outline {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text);
}

.btn-outline:hover:not(:disabled) { background: var(--surface); }

/* Pill toggle (used for Inverted option) */
.pill-toggle {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 6px 14px 6px 6px;
  border: 1px solid var(--border);
  border-radius: 999px;
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
  background: transparent;
  color: var(--text-secondary);
  align-self: flex-start;
  transition: background 0.2s, color 0.2s, border-color 0.2s;
}
.pill-toggle:disabled { opacity: 0.5; cursor: default; }
.pill-toggle .pill-dot {
  display: inline-block;
  width: 16px;
  height: 16px;
  border-radius: 999px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  transition: background 0.2s, border-color 0.2s, box-shadow 0.2s;
  flex-shrink: 0;
}
.pill-toggle.active {
  /* Use the user's accent (not a hardcoded indigo) so selected day pills + all
     toggles follow the accent picker. (user 2026-06-14) */
  background: color-mix(in srgb, var(--primary) 12%, transparent);
  border-color: var(--primary);
  color: var(--text);
}
.pill-toggle.active .pill-dot {
  background: var(--primary);
  border-color: var(--primary);
  box-shadow: inset 0 0 0 3px var(--surface);
}
.pill-toggle:hover:not(:disabled) { border-color: var(--primary); }

/* Forms */
.form-group { margin-bottom: 16px; }

.form-group label {
  display: block;
  font-size: 13px;
  font-weight: 600;
  color: var(--text-secondary);
  margin-bottom: 6px;
}

.form-group input, .form-group select {
  width: 100%;
  padding: 12px;
  background: var(--surface);
  border: 1px solid var(--surface-2);
  border-radius: var(--radius-sm);
  color: var(--text);
  font-size: 15px;
  outline: none;
  transition: border-color 0.2s;
}

.form-group input:focus, .form-group select:focus {
  border-color: var(--primary);
}

.form-group input::placeholder { color: var(--text-secondary); }

/* Standalone select control (outside a .form-group) — matches the app's inputs
   so dropdowns like the biometric "lock after" picker don't render bare/native. */
.settings-select {
  padding: 10px 12px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text);
  font-size: 15px;
  min-width: 180px;
  max-width: 100%;
  outline: none;
  transition: border-color 0.2s;
}
.settings-select:focus { border-color: var(--primary); }

/* Rest-timer duration buckets (Settings → Rest timer durations). Previously each
   input carried repeated inline surface-2/border styling and the reps→rest fields
   sat on one wrapping flex line that broke alignment on narrow screens. These
   classes lean on the themed base inputs and lay the fields out as two tidy
   label-left / control-right rows. (fix_rest_timer_durations_styling) */
.rt-bucket { padding: 12px; margin-top: 8px; }
.rt-bucket-head { display: flex; gap: 8px; align-items: center; }
.rt-bucket-head .rt-bucket-label { flex: 1; min-width: 0; padding: 8px 10px; }
.rt-bucket-fields { margin-top: 8px; display: flex; flex-direction: column; gap: 8px; }
.rt-field-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.rt-field-row > .rt-field-label { color: var(--text-secondary); font-size: 13px; }
.rt-range { display: flex; align-items: center; gap: 6px; }
.rt-range > span { color: var(--text-secondary); font-size: 13px; }
.rt-range input { text-align: right; padding: 8px 10px; }
.rt-range .rt-bucket-min, .rt-range .rt-bucket-max { width: 68px; }
.rt-range .rt-bucket-rest { width: 80px; }
/* Compact rest-timer bucket table (fix_settings_section_summary_wrap): read-only
   rows (label + reps, rest time, edit/delete) that expand inline to an editor. */
.rt-bucket-table { width: 100%; border-collapse: collapse; }
.rt-brow > td { padding: 8px 2px; border-bottom: 1px solid var(--border); vertical-align: middle; }
.rt-brow-main { width: 100%; }
.rt-brow-label { font-weight: 600; }
.rt-brow-rest { text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; padding-right: 8px; }
.rt-brow-actions { text-align: right; white-space: nowrap; }
.rt-brow-actions button { background: none; border: none; cursor: pointer; font-size: 15px; line-height: 1; padding: 4px 6px; color: var(--text); }
.rt-brow-editing .rt-e-label { width: 100%; padding: 8px 10px; }
.rt-e-fields { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-top: 8px; }
.rt-e-fields .rt-e-min, .rt-e-fields .rt-e-max { width: 68px; text-align: right; padding: 8px 10px; }
.rt-e-fields .rt-e-rest { width: 80px; text-align: right; padding: 8px 10px; }
.rt-e-fields .rt-field-label { color: var(--text-secondary); font-size: 13px; }
/* Labelled single-control rows in the same section (iso cap, heavy override). */
.rt-labeled-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 8px; }
.rt-labeled-row label { color: var(--text-secondary); font-size: 13px; }
.rt-labeled-row input { width: 96px; text-align: right; padding: 8px 10px; }

/* Top-left back affordance shared across every routed page. Touch-friendly
   target (min-height 40px, generous padding) since this is the primary back
   action on phones — Android hardware-back works but the explicit arrow is
   what the user expects to tap. Borderless (arrow + label only); the hover/
   active background tint is the sole affordance — the old 1px outline read as
   a boxed "line" around the control. */
.btn-back-arrow {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: transparent;
  border: none;
  border-radius: var(--radius-sm);
  color: var(--text);
  padding: 8px 8px 8px 0;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  min-height: 40px;
}
.btn-back-arrow:hover { background: var(--surface); }
.btn-back-arrow:active { background: var(--surface-2); }
.btn-back-arrow svg { display: block; }

/* Password input with show/hide eye toggle. The wrapper is `position: relative`
   so the toggle button can sit inside the input's right edge; the input grows
   its right padding so typed characters don't slide under the icon. */
.password-wrap { position: relative; }
.password-wrap input { padding-right: 44px; }
.password-toggle {
  position: absolute;
  top: 0;
  right: 0;
  height: 100%;
  width: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: 0;
  padding: 0;
  cursor: pointer;
  color: var(--text-secondary);
}
.password-toggle:hover { color: var(--text); }
.password-toggle:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; border-radius: var(--radius-sm); }
.password-toggle svg { display: block; }

/* The training-days picker rows put a label and a day-of-week select on
   one flex line. .form-group select otherwise stretches to width:100%,
   which steals horizontal space from labels like "Upper Body A" and
   forces a 3-line wrap. Cap the day-select so the label gets the room. */
.lift-order-row .lift-day-select {
  width: auto;
  min-width: 96px;
  flex: 0 0 auto;
}

/* Inline input (for logging sets) */
.input-inline {
  width: 70px;
  padding: 8px;
  background: var(--surface);
  border: 1px solid var(--surface-2);
  border-radius: var(--radius-sm);
  color: var(--text);
  font-size: 14px;
  text-align: center;
  outline: none;
}

.input-inline:focus { border-color: var(--primary); }

/* Stacked single-set editor (.set-editor): a small field label above each
   input, and a read-only value rendered to match an input's height/weight so
   prescribed (non-editable) reps/weight read as values rather than plain text. */
.set-editor-label {
  font-size: 12px;
  color: var(--text-secondary);
  margin-bottom: 4px;
}
.set-editor-value {
  display: inline-block;
  font-size: 16px;
  font-weight: 600;
  padding: 8px 0;
}
.set-editor .input-inline { width: 100%; box-sizing: border-box; }

/* Picker-wheel input: a scroll-snap column whose CENTRED row is the value. The neighbour
   rows are transformed in 3D by JS (rotateX back + scale + fade) so the column reads like
   a physical drum; touching the wheel FLOATS it taller as an ABSOLUTE overlay inside a
   fixed-size .rt-wheel slot, so nothing on the page reflows. Row height + pad + band MUST
   stay in step with WHEEL_ROW_PX (=30) in components/wheel-picker.js.
   (fix_rest_timer_wheel_float_and_fit) */
.rt-wheel {                          /* the reserved fixed footprint — never reflows */
  position: relative;
  display: inline-block;
  width: 84px;
  height: 70px;                      /* 30 centre + ~2/3 (20px) of each neighbour */
  vertical-align: middle;
}
/* Wheel slot. Timer-only since chore_wheels_timer_only — the generic hydrator that put these on
   numeric inputs app-wide is gone, so the only consumer is the rest timer's log panel.
   fixed-footprint slot next to it (mirrors .rt-wheel) so hydrating never reflows the form; the
   real input is hidden but kept for value reads. (feat_wheel_input_app_wide_all_numeric) */
.wheel-input {
  position: relative;
  display: inline-block;
  width: 84px;
  height: 70px;
  vertical-align: middle;
}
.wheel-hidden-input { display: none !important; }
.wheel-picker {
  position: absolute;
  top: 0; left: 0;
  width: 84px;
  height: 70px;                      /* fills the slot at rest */
  overflow: hidden;
  /* Spinning the wheel must never select/highlight the numbers, or fire the iOS long-press
     text callout. (fix_rest_timer_wheel_userselect_and_plate_align) */
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
  /* A wheel IS an input, so it takes the canonical input treatment (.settings-select above):
     --surface-2 fill, --border hairline. It used to be one level OFF on both — --surface fill
     inside a .card that is also --surface (zero step, so no edge at all) with a --surface-2
     border (1.12 against its own fill on forge, i.e. nothing). That is why a row of wheels in the
     assistance form read as one undifferentiated block.
     (fix_forge_theme_flat_surfaces_and_wheel_edges) */
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  /* This duration applies to the COLLAPSE (the state transitioned TO when .wheel-floating
     is removed) — slower, gentler fade-out. (owner, fix_rest_timer_wheel_float_collapse_timing) */
  transition: height .42s ease, top .42s ease, box-shadow .42s ease;
}
/* Mouse + keyboard affordances (feat_wheel_mouse_and_keyboard_aware): the wheel is the control now,
   so it takes focus and shows a pointer. The ring is on :focus, NOT :focus-visible — a mouse click is
   the gesture that engages the scroll-wheel, so the member has to be able to see that it took. */
.wheel-picker { cursor: pointer; }
.wheel-picker:focus {
  outline: 2px solid var(--primary);
  outline-offset: 1px;
}
.wheel-picker.wheel-floating {       /* touched: floats taller, centred on the slot */
  height: 150px;                     /* 5 rows × 30 */
  top: -40px;                        /* (70 − 150) / 2 — grows equally up + down */
  z-index: 30;
  box-shadow: 0 8px 24px rgba(0, 0, 0, .35);
  transition: height .14s ease, top .14s ease, box-shadow .14s ease;  /* snappy EXPAND */
}
.wheel-picker-scroll {
  height: 100%;
  overflow-y: scroll;
  scroll-snap-type: y mandatory;
  scrollbar-width: none;             /* Firefox */
  -ms-overflow-style: none;          /* old Edge */
  overscroll-behavior: contain;
  perspective: 480px;                /* the drum's depth (rows rotateX within it) */
}
.wheel-picker-scroll::-webkit-scrollbar { display: none; }
/* Wheels used to have a GATED mode — inert until tapped, so a drag begun on one scrolled the page
   instead. It existed because a form full of wheels left almost nowhere to grab. Both the mode and
   the forms are gone: wheels are timer-only, on a fixed panel with no page scroll to protect, so
   every wheel is live on first touch. (chore_wheels_timer_only, retiring
   fix_wheel_page_scroll_trapped_by_wheels) */
.wheel-pad { height: 20px; }         /* (70 − 30) / 2 — lets the first/last value centre */
.wheel-picker.wheel-floating .wheel-pad { height: 60px; }   /* (150 − 30) / 2 */
.wheel-row {
  box-sizing: border-box;            /* padding nudges the glyph WITHOUT resizing the 30px row */
  height: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
  scroll-snap-align: center;
  font-size: 15px;
  line-height: 1;
  padding-top: 2px;                  /* optical centre between the band lines (font sits high) */
  font-weight: 400;
  color: var(--text-secondary);
  transform-origin: center center;
  backface-visibility: hidden;
  transition: color .12s ease, font-weight .12s ease;  /* NOT transform — drum is per-frame */
}
.wheel-row-selected { color: var(--text); font-weight: 700; }
.wheel-row-dim { opacity: .4; }      /* sub-target AMRAP reps — still selectable */
/* The selection band marks WHICH VALUE YOU ARE ON — the wheel's whole point. It used to be two
   --border hairlines, i.e. the same weight as a decorative divider, which disappeared entirely on
   the warm forge palette. A selection marker is semantically ACCENT (branding doc), so it now uses
   --primary plus a faint accent wash across the row: readable at a glance on every theme, and it
   fixes the rest timer's wheels at the same time (same class).
   (fix_forge_theme_flat_surfaces_and_wheel_edges) */
.wheel-picker-band {
  position: absolute;
  left: 0; right: 0;
  top: 20px; height: 30px;           /* the centred row */
  pointer-events: none;
  border-top: 1px solid var(--primary);
  border-bottom: 1px solid var(--primary);
  background: color-mix(in srgb, var(--primary) 10%, transparent);
}
.wheel-picker.wheel-floating .wheel-picker-band { top: 60px; }

/* Double-tap / double-click text entry: an input overlaid on the centre row so you can TYPE a value
   (numeric keyboard) instead of scrolling; the wheel dims behind it. (feat_wheel_double_tap_text_entry) */
.wheel-text-entry {
  position: absolute; left: 50%; top: 35px; transform: translate(-50%, -50%);
  width: 88%; height: 30px; text-align: center;
  font-weight: 700; font-size: 16px; color: var(--text);
  background: var(--surface); border: 1px solid var(--primary); border-radius: 6px;
  z-index: 4;
}
.wheel-picker.wheel-floating .wheel-text-entry { top: 75px; }
.wheel-picker.wheel-typing .wheel-picker-scroll { opacity: 0.12; }

/* When the editor opens we scrollIntoView({block:'start'}) the edited row. On
   the Capacitor app (viewport-fit=cover) y=0 sits UNDER the device status bar
   (camera/clock), so leave a scroll-margin equal to the safe-area inset (plus a
   little breathing room) — the row then lands just below the status bar. In a
   normal browser env(safe-area-inset-top) is 0, so this is just the 16px gap. */
.set-edit-anchor { scroll-margin-top: calc(env(safe-area-inset-top) + 16px); }

/* Hide the native spinner arrows on number inputs everywhere — they look
   noisy at our compact input sizes, and on touch the text input plus the
   numeric keypad is enough. Standard cross-browser pattern. */
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
input[type="number"] {
  -moz-appearance: textfield;
  appearance: textfield;
}

/* Error & info */
.error-msg {
  background: rgba(239, 68, 68, 0.15);
  color: var(--danger);
  padding: 12px;
  border-radius: var(--radius-sm);
  margin-bottom: 16px;
  font-size: 14px;
}

.info-msg {
  color: var(--text-secondary);
  font-size: 14px;
  text-align: center;
  padding: 32px 16px;
}

/* Badge */
.badge {
  display: inline-block;
  padding: 3px 8px;
  border-radius: 99px;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
}

/* Tint tracks the ACTIVE accent, like .badge-rir below does for --accent-danger. It was a fixed
   blue rgba() under --primary-coloured text, so under any non-blue accent the tint and the label
   disagreed — invisible while the default was blue-ish, obvious the moment ember landed.
   color-mix also follows a CUSTOM hex accent (set inline by accent.js), which an rgba() literal
   never could. (fix_badge_primary_hardcoded_blue) */
.badge-primary { background: color-mix(in srgb, var(--primary) 18%, transparent); color: var(--primary); }

/* "No kit" — an exercise the member's active gym profile can't support. The owner's call was
   "filter out, show with badges and grey out", so the row is DEMOTED and MARKED, never removed:
   dimmed, badged, and sorted below the ones they can do — and still fully selectable, because a
   member may be about to buy the kit or be training somewhere else. Muted rather than warning-
   coloured: this is information, not an error, and half a catalogue in red would read as breakage.
   (feat_exercise_lists_mark_what_your_kit_supports) */
.badge-no-kit {
  background: color-mix(in srgb, var(--text-secondary) 16%, transparent);
  color: var(--text-secondary);
  cursor: help;
}
/* The row itself. Opacity only — no pointer-events:none, no display:none. It must stay tappable. */
.ex-unavailable { opacity: 0.55; }
/* ...but the CONTROL on the row keeps full contrast, so "greyed out" never reads as "disabled"
   on the one element the member is meant to be able to press. */
.ex-unavailable .btn,
.ex-unavailable input,
.ex-unavailable .badge-no-kit { opacity: 1; }
/* RIR badge — the owner wants it read as a WARNING-grade cue (accent-danger, not the
   informational primary tint): leave reps in the tank. One rule for every surface
   (log header, per-set targets, timer, today lists). (fix_rir_badge_danger_colour_coverage_and_muscle_exclusions) */
.badge-rir { background: color-mix(in srgb, var(--accent-danger) 18%, transparent); color: var(--accent-danger); }
/* The "Primary" (primary-lift) badge — a SOLID accent-danger pill (white on the
   scheme's paired danger colour) so the day's primary lift stands out sharply from
   the accent-tinted badges around it. Uses --accent-danger (the accent's paired
   danger, e.g. amber for charcoal) rather than the raw --danger red.
   (fix_primary_lift_badge_restyle_scope; fix_primary_badge_accent_danger) */
.badge-lead { background: var(--accent-danger); color: #fff; }
/* Home subscription-card call-to-action — a solid accent pill (matches .btn-primary),
   not the washed-out translucent badge it used to be. (owner 2026-07-03) */
.grace-cta { flex: none; white-space: nowrap; background: var(--primary); color: #fff; font-size: 13px; font-weight: 700; padding: 9px 15px; border-radius: 999px; }
.grace-banner:hover .grace-cta { background: var(--primary-dark); }
.badge-success { background: rgba(34, 197, 94, 0.2); color: var(--success); }
.badge-warning { background: rgba(245, 158, 11, 0.2); color: var(--warning); }
/* Secondary/tertiary accents were dropped (feat_accent_two_colour_schemes) — these
   legacy badges now tint from the single --primary accent. */
.badge-secondary { background: color-mix(in srgb, var(--primary) 20%, transparent); color: var(--primary); }
.badge-tertiary { background: color-mix(in srgb, var(--primary) 22%, transparent); color: var(--primary); }

/* Live banner while a background tenant data-migration runs (feat_data_migration_live_ux). */
.data-migration-banner {
  position: fixed;
  left: 50%;
  top: 12px;
  transform: translateX(-50%);
  z-index: 1000;
  max-width: calc(100% - 24px);
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 16px;
  border-radius: 99px;
  background: var(--primary);
  color: #fff;
  font-size: 14px;
  font-weight: 600;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
}
.data-migration-banner.done { background: var(--success, #22c55e); }
.data-migration-banner .dm-spinner {
  width: 14px;
  height: 14px;
  border: 2px solid rgba(255, 255, 255, 0.4);
  border-top-color: #fff;
  border-radius: 50%;
  animation: dm-spin 0.7s linear infinite;
}
@keyframes dm-spin { to { transform: rotate(360deg); } }

/* Inline "beta" tag for not-yet-fully-reviewed languages (feat_beta_language_badge). */
.beta-badge {
  display: inline-block;
  margin-left: 6px;
  padding: 1px 6px;
  border-radius: 99px;
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  background: rgba(245, 158, 11, 0.2);
  color: var(--warning);
  vertical-align: middle;
}
a.badge {
  text-decoration: none;
  vertical-align: middle;
  margin-left: 8px;
}

/* Link-as-badge: convention for any "link" affordance across the app — short
   inline action that isn't true back-navigation and doesn't deserve a full
   button. Apply with `.badge .badge-link` on a <button> or <a>. Examples:
   "Pick a program" on the log-only home, in-line cross-page jumps that
   used to be blue-underlined text. The .btn-back-arrow style stays
   reserved for actual back/history navigation (Settings / Progress / etc). */
.badge-link {
  background: var(--surface-2);
  color: var(--text-secondary);
  text-transform: none;
  letter-spacing: normal;
  font-size: 12px;
  padding: 6px 10px;
  cursor: pointer;
  border: 0;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  text-decoration: none;
}
.badge-link:hover { background: var(--surface); color: var(--text); }
.badge-link svg { display: block; width: 12px; height: 12px; }

/* Sets table */
.sets-table { width: 100%; border-collapse: collapse; }

.sets-table th {
  font-size: 11px;
  font-weight: 600;
  color: var(--text-secondary);
  text-transform: uppercase;
  text-align: left;
  padding: 8px 4px;
  border-bottom: 1px solid var(--surface-2);
}

.sets-table td {
  padding: 10px 4px;
  font-size: 14px;
  border-bottom: 1px solid rgba(51, 65, 85, 0.5);
  vertical-align: middle;
}

.sets-table .amrap { color: var(--warning); font-weight: 600; }

/* Help content rendered from Markdown (help->markdown epic). Tables no longer carry the sets-table class,
   so table styling is scoped to the help containers; also the callout + paragraph/list/code rhythm. */
/* The overview needs room beneath it — its last <p> zeroes its own margin, so without this the lead
   paragraph butts straight into whatever follows (owner 2026-07-22: "with some padding under it -
   there's currently none"). On the program page that is now the split picker.
   (fix_program_page_choose_without_scrolling) */
.help-intro { margin-bottom: 16px; }

.section-body p, .help-intro p { margin: 0 0 10px; }
.section-body p:last-child, .help-intro p:last-child { margin-bottom: 0; }
.section-body ul, .help-intro ul { margin: 8px 0; padding-left: 1.1rem; }
.section-body li, .help-intro li { margin: 4px 0; }
.section-body code, .help-intro code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.9em; background: var(--surface-2); padding: 1px 5px; border-radius: 4px;
}
.section-body table, .help-intro table { width: 100%; border-collapse: collapse; margin: 8px 0; }
.section-body th, .help-intro th {
  font-size: 11px; font-weight: 600; color: var(--text-secondary); text-transform: uppercase;
  text-align: left; padding: 8px 4px; border-bottom: 1px solid var(--surface-2);
}
.section-body td, .help-intro td {
  padding: 10px 4px; font-size: 14px; border-bottom: 1px solid rgba(51, 65, 85, 0.5); vertical-align: middle;
}
/* The one callout (juggernaut TM formula) — replaces the old inline-styled <p class="card">. */
.help-callout { margin: 10px 0; padding: 12px 14px; background: var(--surface-2); border-radius: 8px; }
.help-callout p { margin: 0; }
.help-callout-note { text-align: center; font-size: 16px; }

/* Drop-set cascade tag (feat_set_technique_drop_sets) — a subtle pill marking the
   anchor + its lighter back-to-back drops. */
.drop-badge {
  display: inline-block;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--accent, var(--primary));
  background: color-mix(in srgb, var(--accent, var(--primary)) 14%, transparent);
  border-radius: 4px;
  padding: 1px 5px;
  vertical-align: middle;
}

/* Myo-reps cluster badge — a distinct hue from the drop-set badge (they share the .drop-badge markup
   because myo-reps reuse the DropGroupId rail). (feat_myo_reps_adhoc) */
.drop-badge.myo-badge {
  color: var(--tertiary, var(--secondary));
  background: color-mix(in srgb, var(--tertiary, var(--secondary)) 14%, transparent);
}

/* Aligned variant for the standard workout's Working Sets + assistance cards.
   Separate <table>s size their own columns under the default auto layout, so
   the Reps/Weight inputs bumped to different x-positions card-to-card. A fixed
   layout with the same per-column widths (all these tables share the 5-column
   Set / Target / Reps / Weight / Actions shape) lines every card up. Inputs
   fill their column instead of a fixed 70px so they never overflow a column. */
.sets-table-aligned { table-layout: fixed; }
.sets-table-aligned th:nth-child(1), .sets-table-aligned td:nth-child(1) { width: 11%; }
.sets-table-aligned th:nth-child(2), .sets-table-aligned td:nth-child(2) { width: 27%; }
.sets-table-aligned th:nth-child(3), .sets-table-aligned td:nth-child(3) { width: 18%; }
.sets-table-aligned th:nth-child(4), .sets-table-aligned td:nth-child(4) { width: 18%; }
.sets-table-aligned th:nth-child(5), .sets-table-aligned td:nth-child(5) { width: 26%; }
.sets-table-aligned .input-inline { width: 100%; min-width: 0; box-sizing: border-box; padding-left: 4px; padding-right: 4px; }

/* Workout card */
.workout-card {
  background: var(--surface);
  border-radius: var(--radius);
  padding: 14px 16px;
  margin-bottom: 8px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  cursor: pointer;
  transition: background 0.2s;
  text-decoration: none;
  color: var(--text);
}

.workout-card:hover { background: var(--surface-2); }

.workout-card-locked {
  opacity: 0.5;
  cursor: not-allowed;
}
.workout-card-locked:hover { background: var(--surface); }

.workout-card .lift-name { font-weight: 600; font-size: 15px; }
.workout-card .week-label { font-size: 13px; color: var(--text-secondary); }

/* Unified session list (home) — one continuous, tap-to-expand list of every
   session in the program. The row "card" is a <button> so it's fully
   keyboard-accessible; the expanded detail is its sibling. */
.session-cycle-header { margin: 18px 0 8px; font-size: 16px; }
.session-week-header {
  margin: 12px 0 6px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-secondary);
}
/* Minimisable grouping headers (cycle/wave/phase + week). Tap to collapse the
   section; the chevron rotates to ▸ when collapsed. */
.section-collapse { display: flex; align-items: center; gap: 6px; cursor: pointer; user-select: none; }
.section-collapse:focus-visible { outline: 2px solid var(--accent, var(--primary)); outline-offset: 2px; border-radius: 4px; }
.section-chevron { display: inline-block; color: var(--text-secondary); transition: transform 0.2s; font-size: 0.8em; }
.session-cycle.collapsed > .session-cycle-header .section-chevron,
.session-week.collapsed > .session-week-header .section-chevron { transform: rotate(-90deg); }
.session-cycle.collapsed > .session-week { display: none; }
.session-week.collapsed > .session-item { display: none; }
.session-week.collapsed > .session-feedback-entry { display: none; }

/* Up-next affordance pinned above the session list — shown only while the cycle/week
   holding the next session is collapsed (toggled by wireSessionList), so minimising the
   active block never hides the way back into training. Sticky so it stays reachable while
   scrolling; sits below the fixed notifications bell. (fix_upnext_visible_when_block_collapsed) */
.session-upnext-pinned {
  position: sticky;
  top: calc(env(safe-area-inset-top, 0px) + 60px);
  z-index: 2;
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 12px;
  padding: 10px 14px;
  border-radius: var(--radius, 12px);
  background: var(--surface, #fff);
  border: 1px solid var(--warning, var(--border, #ccc));
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
}
.session-upnext-pinned[hidden] { display: none; }
.session-pinned-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.session-pinned-eyebrow {
  font-size: 12px; font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.04em; color: var(--warning, var(--text-secondary));
}
.session-pinned-title { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.session-pinned-cta { flex: 0 0 auto; white-space: nowrap; }

.session-item { margin-bottom: 8px; }

/* Premium badge — flags a Premium-only page/section (experiment 2026-06-14). */
.premium-badge {
  display: inline-block;
  vertical-align: middle;
  font-size: 11px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0.02em;
  padding: 4px 8px;
  border-radius: 999px;
  color: var(--primary);
  background: color-mix(in srgb, var(--primary) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--primary) 35%, transparent);
  white-space: nowrap;
}

/* Inline per-week feedback entry — sits at the end of a completed week's
   sessions. Muted/dashed by default (optional on most programs); accent + solid
   when emphasised for RP. (user 2026-06-14) */
.session-feedback-entry {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 8px;
  padding: 10px 12px;
  border: 1px dashed var(--border);
  border-radius: 10px;
  background: var(--surface);
  color: inherit;
  text-decoration: none;
}
.session-feedback-entry:focus-visible { outline: 2px solid var(--accent, var(--primary)); outline-offset: 2px; }
.session-feedback-entry.is-rp { border-style: solid; border-color: var(--primary); }
.session-feedback-entry .sfe-icon {
  font-size: 16px;
  width: 22px;
  text-align: center;
  color: var(--text-secondary);
  flex: none;
}
.session-feedback-entry.is-rp .sfe-icon { color: var(--primary); }
.session-feedback-entry.is-done .sfe-icon { color: var(--success); }
.session-feedback-entry .sfe-text { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.session-feedback-entry .sfe-label { font-weight: 600; }
.session-feedback-entry .sfe-chevron { color: var(--text-secondary); font-size: 18px; flex: none; }
/* When a session OR a block header is scrolled to the top on load, leave room
   for the fixed notifications bell (~40px) + the device status bar so it lands
   just below them, never tucked underneath. */
.session-item, .session-cycle, .session-week {
  scroll-margin-top: calc(env(safe-area-inset-top, 0px) + 60px);
}
.session-toggle {
  /* inherits .workout-card flex layout; a clickable div, not a <button> */
  width: 100%;
  margin-bottom: 0;
  text-align: left;
  align-items: center;
  gap: 10px;
}
.session-toggle:focus-visible { outline: 2px solid var(--accent, var(--primary)); outline-offset: 2px; }
.session-main { flex: 1; min-width: 0; }
.session-status { display: flex; flex-direction: column; align-items: flex-end; gap: 2px; }
.session-chevron {
  display: inline-block;
  margin-top: 2px;
  color: var(--text-secondary);
  transition: transform 0.2s;
}
.session-item.expanded .session-chevron { transform: rotate(180deg); }
.session-item.expanded .session-toggle { background: var(--surface-2); }
.session-detail {
  background: var(--surface);
  border-radius: 0 0 var(--radius) var(--radius);
  margin-top: -4px;
}
.session-detail-inner { padding: 8px 16px 14px; }
.session-detail-msg { padding: 8px 0; margin: 0; }
.session-lift {
  /* Stacked: name + info tags on the first line, the reps/scheme on the second.
     The detail line wraps freely rather than being pinned to one nowrap row. */
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 7px 0;
  border-top: 1px solid var(--surface-2);
}
.session-detail-inner .session-lift:first-child { border-top: none; }
.session-lift-name { font-weight: 600; }
/* The "Main" badge sits inline after the lift name in the listing (collapsed title
   + expanded main-lift row) — nudge it onto the text's optical centre so it doesn't
   ride high against the heavier title. (feat_main_badge_in_listing) */
.session-main-badge { vertical-align: middle; margin-left: 4px; position: relative; top: -1px; }
.session-lift-detail { text-align: left; }
.session-locked-note { margin: 10px 0 0; }
/* Expanded-view action rows: each is [button] [efficacy warning]. The buttons (Start,
   ⛔ Skip, Continue, View) live here, not on the collapsed row. The first action row
   gets a hairline separator from the preview above. (feat_session_card_expand_actions) */
.session-action-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 10px; }
.session-preview:not(:empty) + .session-action-row { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--surface-2); }
.session-action { width: auto; flex: none; white-space: nowrap; }
/* Start + Skip laid out side by side, splitting the width equally. Warnings are
   deferred to confirm dialogs, so no inline note sits between them.
   (fix_session_start_skip_side_by_side_confirm) */
.session-action-row--split { flex-wrap: nowrap; }
.session-action-row--split .session-action { flex: 1 1 0; text-align: center; }
/* Reschedule-a-session control in the expanded listing (feat_reschedule_in_listing). */
/* The native <input type="date"> is kept (OS picker + change event) but visually hidden — the
   pencil beside the title date triggers it via showPicker(). (feat_reschedule_relay_and_pencil,
   feat_reschedule_pencil_single_date) */
.session-reschedule-input {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); border: 0; opacity: 0; pointer-events: none;
}
/* The reschedule pencil sits inline with the title date. */
.week-label .session-reschedule-pencil { vertical-align: middle; padding: 2px; min-height: 0; }
/* Only show the reschedule pencil on the expanded (up-next / in-progress) row — collapsed rows stay a
   clean scannable column. (owner 2026-07-10) */
.session-item:not(.expanded) .session-reschedule-pencil { display: none; }
/* Whole-program date-shift control at the top of the session list (feat_program_date_editing_whole_and_phases). */
.program-shift { display: flex; justify-content: flex-end; margin: 0 0 10px; }
.program-shift-pencil { width: auto; gap: 6px; font-size: 12px; }
.program-shift-input {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); border: 0; opacity: 0; pointer-events: none;
}
/* The non-blocking efficacy note beside a Start/Skip button. */
.session-action-warn { font-size: 12px; line-height: 1.3; color: var(--warning); opacity: 0.9; flex: 1; min-width: 140px; }
.session-earlier-hint { text-align: center; margin: 2px 0 10px; }
.session-history-banner { margin-bottom: 12px; text-decoration: none; color: var(--text); }

/* Lift card */
.lift-card {
  background: var(--surface);
  border-radius: var(--radius);
  padding: 14px 16px;
  margin-bottom: 8px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.lift-card .lift-info { flex: 1; }
.lift-card .lift-name { font-weight: 600; font-size: 15px; }
.lift-card .lift-tm { font-size: 13px; color: var(--text-secondary); margin-top: 2px; }
.lift-card .lift-actions { display: flex; gap: 8px; }


/* Auth page */
.auth-page {
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  padding-top: 24px;
  position: relative;
  z-index: 1;
}

/* Sparks splash backdrop for the login / register routes.
   The image is fixed-positioned behind #app, covering the whole viewport.
   On narrow viewports we pin background-position to the right so the spark
   detail in the right half of the photo is what the user sees while the
   form column sits over the quieter left side. A dark gradient overlay
   keeps text legible. */
.auth-splash {
  position: fixed;
  inset: 0;
  background-image:
    linear-gradient(180deg, rgba(15,23,42,0.45) 0%, rgba(15,23,42,0.75) 60%, rgba(15,23,42,0.92) 100%),
    url('../assets/sparks.jpg');
  background-size: cover;
  background-position: right center;
  z-index: 0;
  pointer-events: none;
}

@media (min-width: 720px) {
  .auth-splash {
    background-position: center center;
  }
}

.auth-logo {
  display: block;
  margin: 0 auto 12px;
  width: auto;
  height: 96px;
  max-width: 100%;
  background: #fff;
  border-radius: 16px;
  padding: 8px;
  box-sizing: border-box;
}

.auth-page h1 {
  text-align: center;
  font-size: 32px;
  margin-bottom: 8px;
}

.auth-page .subtitle {
  text-align: center;
  color: var(--text-secondary);
  margin-bottom: 32px;
  font-size: 15px;
}
/* Onboarding default-training-view choice — two screenshot cards; the unselected one greys
   out. (feat_onboarding_default_training_view_choice) */
.dv-choices { display: flex; gap: 12px; justify-content: center; margin-top: 8px; }
.dv-choice {
  flex: 1 1 0; max-width: 190px; min-width: 0;
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  padding: 8px; background: var(--surface);
  border: 2px solid var(--surface-2); border-radius: var(--radius); cursor: pointer;
}
.dv-choice img {
  width: 100%; height: auto; border-radius: var(--radius-sm); display: block;
  transition: filter .15s ease, opacity .15s ease;
}
.dv-choice:not(.selected) img { filter: grayscale(1); opacity: .55; }
.dv-choice.selected { border-color: var(--primary); }
.dv-caption { font-size: 13px; font-weight: 600; color: var(--text-secondary); }
.dv-choice.selected .dv-caption { color: var(--text); }

.auth-page .switch-link {
  text-align: center;
  margin-top: 16px;
  font-size: 14px;
  color: var(--text-secondary);
}

.auth-page .switch-link a {
  color: var(--primary);
  text-decoration: none;
}

/* The .auth-splash backdrop is ALWAYS a dark slate photo+gradient, independent of the
   active theme. So the loose text sitting directly on it (headings, subtitle, labels,
   switch/forgot links, dev-link copy) must stay light regardless of theme/accent — on a
   light theme the dark --text/--text-secondary and, worst of all, the default charcoal
   accent's dark-slate --primary link (#374151) render near-invisible on it. Inputs are
   deliberately excluded: they sit on their own --surface fill and keep the theme colours.
   (owner 2026-07-06: 'Forgot your password?' + other copy unreadable on charcoal.)
   SCOPED to `.auth-splash + .auth-page` (login/register/forgot/reset/verify) — the
   onboarding wizard reuses `.auth-page` WITHOUT a splash and sits on the theme bg, so it
   must keep the normal theme colours (owner 2026-07-06: wizard subtitle unreadable). */
.auth-splash + .auth-page h1 { color: #f8fafc; }
.auth-splash + .auth-page .subtitle,
.auth-splash + .auth-page .switch-link,
.auth-splash + .auth-page .form-group label,
.auth-splash + .auth-page .text-secondary { color: #cbd5e1; }
.auth-splash + .auth-page .switch-link a,
.auth-splash + .auth-page .text-secondary a { color: #f8fafc; text-decoration: underline; }

/* Setup wizard progress dots */
.setup-progress {
  display: flex;
  justify-content: center;
  gap: 8px;
  margin-bottom: 32px;
}

.progress-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--surface-2);
  transition: background 0.3s;
}

.progress-dot.done { background: var(--success); }
.progress-dot.active { background: var(--primary); }

/* Loading spinner */
.spinner {
  display: inline-block;
  width: 20px;
  height: 20px;
  border: 2px solid var(--border);
  border-top-color: var(--primary);
  border-radius: 50%;
  animation: spin 0.6s linear infinite;
}

@keyframes spin { to { transform: rotate(360deg); } }

/* Modal overlay — scroll lives on the overlay itself so the scrollbar
   sits at the viewport's right edge (visually identical to the browser
   scrollbar) instead of as a mid-window strip inside the modal. The
   overlay is centred horizontally and starts from the top with padding,
   so a short modal still appears near the top of the viewport with a
   little breathing room and a tall modal scrolls with the page. */
.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.6);
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 24px 0;
  overflow-y: auto;
  z-index: 200;
}

.modal {
  background: var(--surface);
  border-radius: var(--radius);
  padding: 24px 16px;
  width: 100%;
  max-width: 480px;
}

.modal h2 { margin-bottom: 16px; }
/* Optional leading icon (emoji or SVG) above the title on a confirm dialog. */
.modal-icon { font-size: 32px; line-height: 1; text-align: center; margin-bottom: 8px; }
.modal-icon + h2 { margin-top: 0; }

.modal-actions {
  display: flex;
  gap: 12px;
  margin-top: 20px;
}

.modal-actions .btn { flex: 1; }

/* ── Tap-to-expand read-only training cards ───────────────────────────────── */
/* Pinned ("Focus") editor mode: while logging, the active card sticks to the top
   of the screen and the rest of the page fades out, so the user's focus stays on
   the card they're entering — and the in-flow editor means no scroll-reposition
   listeners fighting the rest timer on close. (feat_training_day_pin_active_card_fade) */
.training-card-pinned {
  position: sticky;
  top: 8px;
  z-index: 6;
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.4);
  border-color: var(--primary);
}
.logging-focus .training-card:not(.training-card-pinned) {
  opacity: 0.3;
  transition: opacity 0.2s ease;
}

/* Read-only summary cards on the workout day. .training-card-open marks a card
   whose body opens the input overlay on tap. */
.training-card-open { cursor: pointer; }
.training-card-open:hover { border-color: var(--primary); }
.training-card-open:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

/* Read-only summary table: fixed column layout so Set / Target / Logged line
   up cleanly across rows (and across cards). */
.sets-table-summary { table-layout: fixed; }
.sets-table-summary td, .sets-table-summary th { text-align: left; }
.sets-table-summary th:nth-child(1), .sets-table-summary td:nth-child(1) { width: 14%; }
.sets-table-summary th:nth-child(2), .sets-table-summary td:nth-child(2) { width: 48%; }
.sets-table-summary th:nth-child(3), .sets-table-summary td:nth-child(3) { width: 38%; }

/* Bigger, more readable text on the read-only training cards. */
.training-card h3 { font-size: 17px; }
.training-card .sets-table-summary td { font-size: 16px; padding-top: 11px; padding-bottom: 11px; }
.training-card .sets-table-summary th { font-size: 12px; }

/* The next loggable set's row is the tap target that reveals its editor. */
.set-row-open { cursor: pointer; }
.set-row-open > td:first-child { position: relative; }
.set-row-open:hover > td { background: var(--surface-2); }
.set-row-open:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }
.set-row-selected > td { background: var(--surface-2); }

/* The CURRENT set to log — persistently highlighted (tint + left accent) so it's
   obvious where you are in the workout, replacing the old "Tap to log ›" hint. */
.set-row-next > td { background: color-mix(in srgb, var(--primary) 12%, transparent); }
.set-row-next > td:first-child { box-shadow: inset 3px 0 0 var(--primary); }
/* Explicit "Log" affordance shown in the current row's logged-value column. */
.log-pill {
  display: inline-block;
  background: var(--primary);
  color: #fff;
  font-size: 13px;
  font-weight: 600;
  padding: 3px 12px;
  border-radius: 999px;
  white-space: nowrap;
}
/* The injected accordion edit row spans the table; strip the cell chrome so the
   editor card sits flush under the selected row. */
.set-edit-tr > td { padding: 4px 0 8px; border: none; }

.log-progress { font-size: 13px; color: var(--text-secondary); }
.log-progress-done { color: var(--success); font-weight: 600; }

/* Editor layout toggle (Floating / Inline) — temporary comparison control. */
.log-editor-mode { display: flex; align-items: center; gap: 8px; margin: 0 0 12px 2px; }

/* Input overlay (reuses .modal-overlay / .modal). Scrollable, generous touch
   targets, full editable table inside. */
.log-overlay { align-items: flex-start; z-index: 250; }
.log-overlay-modal { max-width: 520px; max-height: calc(100vh - 48px); display: flex; flex-direction: column; }
.log-overlay-body { overflow-y: auto; }
.log-overlay-body .input-inline { min-height: 40px; }
.log-overlay-body .sets-table td, .log-overlay-body .sets-table th { padding-top: 10px; padding-bottom: 10px; }
.log-overlay-close {
  background: none;
  border: none;
  color: var(--text-secondary);
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
  padding: 4px 8px;
  width: auto;
}
.log-overlay-close:hover { color: var(--text); }

/* Per-set editor (editorShell) — drops under the SELECTED set ROW. Two layout
   modes: .log-editor-overlay floats over the rows below (positionLogEditor
   anchors it to the row — no page jump); .log-editor-inline sits in flow inside
   the injected accordion edit-row. */
/* The inline + floating set-entry form needs a visible edge in EVERY theme.
   Previously only high-contrast drew one (via its [data-theme] .card border);
   in dark/light the editor blended into the rows behind it (user 2026-06-03). */
.log-editor-card { margin: 0; border: 1px solid var(--border); }
.log-editor-body .input-inline { min-height: 40px; }
.log-editor-body .sets-table td, .log-editor-body .sets-table th { padding-top: 10px; padding-bottom: 10px; }
.log-editor-inline { margin: 0; }
.log-editor-overlay {
  position: absolute;
  z-index: 50;
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
}

/* Page-style wizard — same DOM shape as a .modal-overlay + .modal pair, but
   renders inline as a regular full-width section instead of a fixed dark
   overlay. Used by 1800 to convert the Start-a-Program "modal" into a
   page-style wizard while keeping its internal markup untouched. The class
   is applied on the .modal-overlay element by home.js render code.
*/
.modal-overlay.as-page {
  position: static;
  inset: auto;
  background: transparent;
  padding: 0;
  overflow: visible;
  z-index: auto;
}
.modal-overlay.as-page > .modal {
  background: transparent;
  border-radius: 0;
  padding: 0;
  max-width: none;
}

/* Start-block wizard */
.wizard-progress {
  display: flex;
  gap: 6px;
  margin-bottom: 8px;
}
.wizard-progress .wizard-dot {
  flex: 1;
  height: 4px;
  border-radius: 2px;
  background: var(--border);
}
.wizard-progress .wizard-dot.active { background: var(--primary); }
.wizard-step-label {
  font-size: 12px;
  color: var(--text-secondary);
  margin-bottom: 14px;
}
.wizard-step { display: none; }
.wizard-step.active { display: block; }

/* Section spacing */
.section { margin-bottom: 24px; }
.section-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 12px;
}

/* Settings: small heading above a group of accordion .section-card rows. */
.settings-group-heading {
  font-size: 0.85rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-secondary);
  margin: 24px 0 8px 4px;
}
.settings-group-heading:first-child { margin-top: 0; }

/* Divider */
.divider {
  height: 1px;
  background: var(--surface-2);
  margin: 16px 0;
}

/* Flex utils */
.flex { display: flex; }
.flex-between { display: flex; justify-content: space-between; align-items: center; }
.gap-8 { gap: 8px; }
.gap-12 { gap: 12px; }
.mt-8 { margin-top: 8px; }
.mt-16 { margin-top: 16px; }
.mb-16 { margin-bottom: 16px; }
.text-center { text-align: center; }
.text-secondary { color: var(--text-secondary); }
.text-sm { font-size: 13px; }
.text-xs { font-size: 11px; }
.text-lg { font-size: 17px; }

/* Mobile readability bump (1780).
   Most font-sizes in this file are in px, so the desktop browser values
   (which the user explicitly described as "fine") are preserved and a
   targeted media query bumps the smallest sizes up by 1-2px when the
   viewport is phone-class. Anything ≥ 16px is left alone — those are
   already comfortable on mobile. We DON'T scale the html font-size
   because there are no rem/em-based rules to inherit from it; cascading
   via @media is the actual lever.
*/
@media (max-width: 480px) {
  body { font-size: 16px; }
  .text-xs { font-size: 13px; }
  .text-sm, .week-label, .lift-tm,
  .workout-card .week-label, .lift-card .lift-tm { font-size: 14px; }
  .btn-sm { font-size: 14px; }
  .tab { font-size: 13px; }
  h3 { font-size: 16px; }

  /* Second pass (owner 2026-07-22: "ensure the text is large enough throughout now rather than me pick
     each item up one by one"). The 1780 bump above covered the generic utilities; these are the named
     rules an audit of every font-size in this file found still sitting at 11-12px on a phone. Grouped
     by ROLE, because the right floor depends on what the text is for:

     TAPPABLE — you have to hit it, often mid-set with one hand. 13px minimum. */
  .pill-toggle,
  .rt-skip-btn,
  .superset-toggle,
  .badge-link,
  .program-shift-pencil,
  .rem-trigger .rem-trig,
  .superset-connector:not(.is-joined) .superset-toggle,
  .superset-connector .round-extend,
  .session-preview .superset-connector .superset-toggle,
  .session-preview .superset-connector .round-extend { font-size: 13px; }

  /* READ WHILE TRAINING — set tables and the timer's own labels. You are reading these tired, at
     arm's length, sometimes through sweat. 13px minimum. */
  .sets-table th,
  .section-body th, .help-intro th,
  .training-card .sets-table-summary th,
  .set-editor-label,
  .rest-timer-log-field > span,
  .rest-timer-log-hint,
  .rest-timer-log-warmup-label,
  .rest-timer-day-list-label,
  .rest-timer-next-label,
  .rest-timer-eta-unit,
  .rt-today-count,
  .rt-history-unit,
  .rt-howto-links-label,
  .rt-plates-bar { font-size: 13px; }

  /* SUPPORTING LABELS — headers, hints and eyebrows. Not primary, but still meant to be read. 12px. */
  .recap-stat-label,
  .session-week-header,
  .session-pinned-eyebrow,
  .session-action-warn,
  .wizard-step-label,
  .ex-picker-heading,
  .cardio-ghost-label,
  .dumbbell-hint,
  .rem-interval-label,
  .cb-mini { font-size: 12px; }

  /* Badges and tags stay small ON PURPOSE — they are glyph-adjacent markers, not prose, and inflating
     them wrecks the layouts they sit in (.badge, .premium-badge, .drop-badge, .superset-tag,
     .superset-badge, .notif-bell__badge, .beta-badge). 11px is the floor, enforced by
     frontend/pwa/js/type-floor.test.mjs — nothing in this file may go below it at any viewport. */
}

/* Settings tabs */
.settings-tabs {
  display: flex;
  background: var(--surface);
  border-radius: var(--radius-sm);
  padding: 3px;
  margin-bottom: 16px;
  gap: 3px;
}

.settings-tab {
  flex: 1;
  padding: 10px;
  border: none;
  background: none;
  color: var(--text-secondary);
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  border-radius: 6px;
  transition: all 0.2s;
}

.settings-tab.active {
  background: var(--primary);
  color: #fff;
}

/* Help icon */
.help-icon {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* The glyph IS the circled-i (ⓘ U+24D8) — no CSS circle, just colour it. */
  color: var(--primary);
  font-size: 17px;
  line-height: 1;
  text-decoration: none;
  margin-left: 4px;
  padding: 0 0.5rem;
  vertical-align: middle;
  flex-shrink: 0;
}

/* Tap target, WITHOUT inflating the row the icon sits in.
   (fix_help_icon_no_longer_inflates_its_label_row)

   helpIcon() used to set min-width/min-height:44px inline. An inline-flex box is an ATOMIC INLINE, so
   the line box takes its height from the icon's margin box: every label carrying an 'i' rendered
   ~44px tall against ~16px for one without — what the owner saw on Forge step 1 ("items with an i are
   padded more than those without"). The icon's own box is back to its natural size and the enlarged
   hit area lives on this pseudo-element, which is out of flow and so costs no layout height.

   Bounded to ±6px vertically ON PURPOSE. 6px is the tightest gap the icon ever has to a neighbouring
   control (.form-group label has margin-bottom: 6px), so the target can never grow far enough to
   swallow a tap meant for the control underneath — the very failure mode the 44px box was added to
   fix (a47fe359), pointed the other way. That gives ~44 x 29 CSS px: well past the WCAG 2.2 AA
   target-size minimum of 24x24, ~2.5x the glyph's own ink, and full width in the axis a thumb
   actually travels along a line of text. */
.help-icon::after {
  content: '';
  position: absolute;
  top: -6px;
  bottom: -6px;
  left: 50%;
  width: 44px;
  transform: translateX(-50%);
}

.help-icon:hover { color: var(--primary-dark); }

/* Help page content */
.help-content p { margin-bottom: 12px; line-height: 1.6; font-size: 15px; }
.help-content h3 { margin-top: 20px; margin-bottom: 8px; }
.help-content ul { margin-bottom: 12px; padding-left: 20px; }
.help-content li { margin-bottom: 6px; line-height: 1.5; font-size: 15px; }
.help-content a { color: var(--link); }
.help-content table { margin-bottom: 16px; }

/* Rest timer overlay */
/* While a full rest-timer overlay is up, lock the page behind it so scrolling the
   overlay (or a stray touch) can't scroll the log page underneath. Released when the
   overlay is minimised to the floating chip. (fix_timer_overlay_scroll_lock) */
html.rest-timer-scroll-lock, html.rest-timer-scroll-lock body { overflow: hidden; }

.rest-timer-overlay {
  position: fixed;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  /* `safe center` keeps the content top-aligned + SCROLLABLE when the keyboard
     shrinks the viewport (plain `center` traps the overflow), and overflow-y lets
     the focused input scroll above the keyboard — so the ring shifts up instead
     of being squeezed. (bug: keyboard shrinks the timer ring) */
  justify-content: safe center;
  overflow-y: auto;
  gap: 24px;
  padding: 16px 0;
  background: var(--scrim);
  color: var(--text);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  z-index: 300;
  pointer-events: auto;
}
/* The ring keeps its size when the viewport shrinks (keyboard) — never squeezed. */
.rest-timer-ring { flex-shrink: 0; }
/* Train surface (has-log): top-anchor instead of vertically centring, so the
   ring keeps a FIXED position when the area below it swaps between the up-next
   line (while counting) and the taller log form (at GO!). Centring made the
   whole stack re-centre on that height change → the circle "jumped". The
   non-has-log timers (manual rest, session-end) stay centred. (user 2026-06-10) */
.rest-timer-overlay.has-log {
  justify-content: flex-start;
  /* Top-anchor the ring at a FIXED position (so it doesn't jump when the area below
     swaps up-next ↔ log form). The top-pad used to land the ring centre ~40% down, which
     pushed the tab panel off the bottom on a phone. Tightened so the whole stack fits a
     standard phone (390×844) with zero scroll, floored low for short screens.
     (fix_rest_timer_overlay_phone_fit) */
  gap: 16px;
  padding: max(16px, calc(40vh - 280px)) 16px 20px;
}
/* The below-ring wrapper is TRANSPARENT in portrait — its children (next / log /
   tabs) behave as direct overlay flex items exactly as before. (Landscape rule below
   turns it into a real side column.) (fix_timer_landscape_rotation) */
.rest-timer-below { display: contents; }

/* Landscape on a short viewport (phones turned sideways): lay the ring and the
   area-below-it SIDE BY SIDE instead of stacked, using the wider aspect ratio.
   Header spans the top; ring bottom-left; the below-wrapper becomes a scrollable
   column bottom-right. Portrait + tall screens (tablets/desktop) are untouched.
   (user 2026-06-24 — fix_timer_landscape_rotation) */
@media (orientation: landscape) and (max-height: 600px) {
  .rest-timer-overlay.has-log {
    display: grid;
    grid-template-columns: auto minmax(240px, 380px);
    grid-template-areas: "head head" "ring below";
    align-content: center;
    justify-content: center;
    column-gap: 32px;
    row-gap: 10px;
    padding: 12px 24px;
    overflow-y: auto;
  }
  .rest-timer-overlay.has-log > .rest-timer-header { grid-area: head; justify-self: center; }
  /* Nudge the ring up slightly off dead-centre so it sits a touch higher in the landscape
     side-by-side layout (owner 2026-07-06). translateY keeps the grid cell size unchanged. */
  .rest-timer-overlay.has-log > .rest-timer-ring { grid-area: ring; align-self: center; transform: translateY(-12px); }
  .rest-timer-overlay.has-log > .rest-timer-below {
    grid-area: below;
    align-self: center;
    display: flex;
    flex-direction: column;
    /* Centre the (max-width-capped) next/log/tabs within the column — without this
       they pin to the column's left edge, so the skip/done buttons sit off-centre
       above the tabs. Matches the portrait overlay's align-items:center.
       (fix_timer_landscape_button_alignment) */
    align-items: center;
    gap: 12px;
    min-width: 0;
    max-height: 92vh;
    overflow-y: auto;
  }
  /* The eta chip stays pinned top-left; the dismiss stays top-right. */
  .rest-timer-overlay.has-log > .rest-timer-dismiss { position: fixed; top: 10px; right: 14px; }
}
.rest-timer-next {
  text-align: center;
  background: var(--surface);
  border: 1px solid var(--surface-2);
  border-radius: var(--radius);
  padding: 14px 22px;
  max-width: 320px;
}
.rest-timer-next-label {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 1px;
  color: var(--text-secondary);
  margin-bottom: 4px;
}
/* "Today's session" line-up on the workout start overlay — names only, no
   reps/weight, so the lifter sees what's coming. (user 2026-06-10) */
.rest-timer-day-list {
  width: 100%;
  max-width: 320px;
  background: var(--surface);
  border: 1px solid var(--surface-2);
  border-radius: var(--radius);
  padding: 14px 18px;
}
.rest-timer-day-list-label {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 1px;
  color: var(--text-secondary);
  margin-bottom: 8px;
}
.rest-timer-day-list-items {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.rest-timer-day-list-items li { font-size: 15px; font-weight: 600; color: var(--text); }
/* Manual numbering (connector rows must not shift the count). */
.rest-timer-day-list-items .day-item { display: flex; align-items: baseline; gap: 8px; }
.rest-timer-day-list-items .day-item-num {
  min-width: 16px; text-align: right; color: var(--text-secondary);
  font-variant-numeric: tabular-nums; font-weight: 600;
}
/* The splash line-up is DISPLAY-ONLY (owner 2026-07-15, fix_today_list_groups_display_only): the
   offer pills (.day-ss / .day-ss-btn / .day-ss-offer) were removed with their buttons.
   A JOINED run reuses the canonical .superset-group / .superset-rail — one continuous accent bar
   with the round LABEL running VERTICALLY along it — so the Today list matches the log view and the
   program listing (owner 2026-07-14: "rotate the grouping title … like the other 2 views"). Tightened
   for the compact line-up: small margins, a little vertical breathing room so the vertical label has
   height, and a slightly smaller rail label. */
.rest-timer-day-list-items .day-group { padding: 0; }
.rest-timer-day-list-items .day-group .superset-group { margin: 2px 0; }
.rest-timer-day-list-items .day-group .superset-group-cards { gap: 4px; padding: 4px 0; }
.rest-timer-day-list-items .day-group .superset-rail { font-size: 11px; letter-spacing: 0.06em; }
.rest-timer-day-list-items .day-item-grp { display: flex; align-items: baseline; gap: 8px; }
/* Long names truncate rather than colliding with the rail or forcing the row wide. */
.rest-timer-day-list-items .day-item-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rest-timer-next-detail {
  font-size: 17px;
  font-weight: 700;
  color: var(--text);
}
.rest-timer-ring {
  position: relative;
  width: 240px;
  height: 240px;
  display: flex;
  align-items: center;
  justify-content: center;
  filter: drop-shadow(0 0 24px rgba(0, 0, 0, 0.4));
}
.rest-timer-svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
.rest-timer-track {
  fill: none;
  stroke: var(--ring-track);
  stroke-width: 1.5;
}
.rest-timer-progress {
  fill: none;
  stroke-width: 2;
  stroke-linecap: round;
  transition: stroke-dashoffset 1s linear, stroke 0.3s;
}
.rest-timer-ring.red    .rest-timer-progress { stroke: var(--phase-red); }
.rest-timer-ring.orange .rest-timer-progress { stroke: var(--phase-orange); }
.rest-timer-ring.yellow .rest-timer-progress { stroke: var(--phase-yellow); }
.rest-timer-ring.green  .rest-timer-progress { stroke: var(--phase-green); }
.rest-timer-ring.red    .rest-timer-text { color: var(--phase-red); }
.rest-timer-ring.orange .rest-timer-text { color: var(--phase-orange); }
.rest-timer-ring.yellow .rest-timer-text { color: var(--phase-yellow); }
.rest-timer-ring.green  .rest-timer-text { color: var(--phase-green); }
.rest-timer-inner {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
}
.rest-timer-stage {
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 3px;
  text-transform: uppercase;
  min-height: 16px;
  color: inherit;
  opacity: 0.85;
}
/* Skip/Restore control inside the ring — a small subtle pill under the clock text. Skip jumps to the
   log view during a working-set countdown (the clock keeps running); Restore returns to the ticking
   view. (feat_timer_skip_to_log_with_restore) */
.rt-skip-btn {
  position: relative;
  z-index: 1;
  margin-top: 10px;
  padding: 3px 16px;
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--text-secondary);
  background: none;
  border: 1px solid var(--border);
  border-radius: 999px;
  cursor: pointer;
}
.rt-skip-btn[hidden] { display: none; }
.rest-timer-text {
  position: relative;
  font-size: 56px;
  font-weight: 400;
  font-variant-numeric: tabular-nums;
  letter-spacing: -1px;
  transition: color 0.3s;
  line-height: 1;
}
/* PAUSED ring (showing the lift, not a countdown): one line "10 × 25" — the ×
   sits between reps and weight, all the same colour. The reps-only/no-weight case
   shows a small "REPS" label over the rep count. Scoped to .paused so the live
   countdown time keeps its big 56px. (user 2026-06-09) */
.rest-timer-overlay.paused .rest-timer-stage {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 2px;
  opacity: 0.85;
  min-height: 0;
}
/* Paused "lift this" ring: weight stacked ABOVE reps, both the SAME font, at the
   countdown's weight (400) so every timer-circle display matches. (user 2026-06-10) */
.rest-timer-overlay.paused .rest-timer-text {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  font-size: 30px;
  font-weight: 400;
  letter-spacing: 0;
  line-height: 1.1;
}
.rest-timer-overlay.paused .rest-timer-text .rt-ring-weight,
.rest-timer-overlay.paused .rest-timer-text .rt-ring-reps { font-size: inherit; font-weight: inherit; line-height: 1.1; }
/* The unit (kg/lb) rides small alongside the weight inside the ring. */
.rest-timer-overlay.paused .rt-ring-unit { font-size: 0.5em; font-weight: 400; opacity: 0.75; }
/* Hold stopwatch (feat_hold_stopwatch_replaces_timer_ring): the count-up time is the focal number,
   so keep it at the countdown's big size — the paused "lift this" 30px is too small here. */
.rest-timer-overlay.hold.paused .rest-timer-text {
  font-size: 56px;
  font-weight: 400;
  letter-spacing: -1px;
}
/* The stopwatch controls are injected up-front but stay hidden through the rest countdown; they appear
   only once the ring flips to the stopwatch at GO! (the overlay gains .hold). Start/Stop sits below the
   ring; the delay/sound management lives in the Controls tab. */
.hold-stopwatch-toggle, .hold-stopwatch-manage { display: none; }
.rest-timer-overlay.hold .hold-stopwatch-toggle { display: flex; justify-content: center; margin: 4px 0 2px; }
.rest-timer-overlay.hold .hold-stopwatch-manage { display: flex; align-items: center; justify-content: center; gap: 16px; flex-wrap: wrap; padding: 6px 0; }
.rest-timer-dismiss {
  position: absolute;
  top: calc(16px + env(safe-area-inset-top));
  right: calc(16px + env(safe-area-inset-right));
  width: 40px;
  height: 40px;
  border-radius: 50%;
  /* Danger-coloured so the one control that CLOSES the timer is unmissable (a ring tap no longer
     closes — only this ✕ does). --accent-danger is the theme's danger-pair (pinned per theme, so a
     pinned/high-contrast theme keeps it); --danger-fg is the legible paired glyph colour, matching
     .btn-danger. (fix_rest_timer_close_only_cross_danger_style) */
  background: var(--accent-danger);
  border: 1px solid var(--accent-danger);
  color: var(--danger-fg, #fff);
  font-size: 18px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* "Time left" chip — top-left of the timer surface, IN PLACE (not the floating
   minimised chip). A small ring whose border slowly drains through the session +
   the estimated minutes remaining. (user 2026-06-10) */
.rest-timer-eta {
  position: absolute;
  top: calc(14px + env(safe-area-inset-top));
  left: calc(14px + env(safe-area-inset-left));
  width: 56px;
  height: 56px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.rest-timer-eta-svg { position: absolute; inset: 0; width: 100%; height: 100%; }
.rest-timer-eta-track { fill: none; stroke: var(--ring-track); stroke-width: 2.5; }
.rest-timer-eta-prog { fill: none; stroke: var(--text-secondary); stroke-width: 2.5; stroke-linecap: round; opacity: 0.7; }
.rest-timer-eta-label { display: flex; align-items: baseline; gap: 1px; color: var(--text-secondary); line-height: 1; }
.rest-timer-eta-min { font-size: 17px; font-weight: 700; font-variant-numeric: tabular-nums; }
.rest-timer-eta-unit { font-size: 11px; font-weight: 600; }

/* Maximised-timer controls: ±30s nudge row + a 1/2/3/5/8/10-minute preset grid.
   Icon-based, generously spaced. Hidden in the minimized state (rule below). */
.rest-timer-quick-picks {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 18px;
  margin: 0 auto;
  width: 100%;
  max-width: 320px;
}
.rest-timer-adjust { display: flex; gap: 14px; justify-content: center; }
.rest-timer-adjust-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 10px 20px;
  border-radius: 999px;
  background: var(--surface);
  border: 1px solid var(--surface-2);
  color: var(--text);
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
}
.rest-timer-adjust-btn svg { width: 18px; height: 18px; }
.rest-timer-adjust-btn:active { background: var(--surface-2); }
.rest-timer-presets {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
  width: 100%;
}
.rest-timer-preset {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 12px 0;
  border-radius: var(--radius);
  background: var(--surface);
  border: 1px solid var(--surface-2);
  color: var(--text);
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
}
.rest-timer-preset svg { width: 20px; height: 20px; opacity: 0.8; }
.rest-timer-preset:active { background: var(--surface-2); }

/* Minimised rest timer: the floating corner chip is RETIRED — the centre toolbar button IS
   the minimised affordance (feat_toolbar_mini_timer_button). A minimised overlay simply
   hides; the countdown carries on against the wall clock and the toolbar's mini-timer paints
   it, and tapping that maximises the overlay again.
   The `.minimized .rest-timer-ring` chip styling (56px circle, drop-shadow, touch-action for
   the drag) was inert under `display: none` and is gone, along with the drag handlers it
   existed for. (feat_rest_timer_minimise_to_toolbar_button) */
.rest-timer-overlay.minimized {
  display: none;
}
/* Floating workout tools — draggable chips (look like the minimised timer): the
   rest-timer start button + the stopwatch. z-index below the timer overlay (300).
   Default corners are bottom-left, clear of the tab bar + the minimised timer
   chip (bottom-right); the stopwatch sits directly above the start button. */

/* Both default to the bottom-right, ABOVE the tab bar: the rest-start (hourglass)
   button shares the slot with the minimised rest timer (only one shows at a
   time); the stopwatch sits directly above it, close (user 2026-06-06). */
#rest-start-btn { right: 16px; bottom: 96px; }
#rest-start-btn svg { width: 26px; height: 26px; opacity: 1; }
#stopwatch-btn { right: 16px; bottom: 158px; }
#stopwatch-btn svg { width: 24px; height: 24px; opacity: 1; }
#plate-calc-btn { right: 16px; bottom: 220px; }
#plate-calc-btn svg { width: 24px; height: 24px; opacity: 1; }
/* Add-supplement "+" chip on the program page — same bottom-right slot the rest
   timer uses on the workout page (the two pages never show both). */
#supplement-chip { right: 16px; bottom: 96px; }
#supplement-chip svg { width: 26px; height: 26px; opacity: 1; }

/* ── Supplements page forms ──────────────────────────────────────────────────
   .form-row / .form-actions were used by supplements.js but never styled, so the
   add form's spacing read poorly. Define them + the repeatable reminder rows. */
.form-row { display: flex; gap: 12px; flex-wrap: wrap; }
.form-row > .form-group { flex: 1; min-width: 140px; margin-bottom: 0; }
.form-actions { margin-top: 16px; display: flex; gap: 8px; }
.reminders-section { margin-top: 16px; }
.reminders-list { display: flex; flex-direction: column; gap: 10px; margin: 8px 0; }
.reminder-row {
  border: 1px solid var(--surface-2);
  border-radius: var(--radius-sm);
  padding: 10px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  background: var(--surface);
}
.reminder-row-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
/* Trigger pills (Time of day / Every n days / After a workout) prefer one line:
   no wrap, share the row width, shrink to fit. (user 2026-06-15) */
.rem-trigger { display: flex; gap: 6px; flex: 1; min-width: 0; }
.rem-trigger .rem-trig {
  font-size: 11px; padding: 4px 8px;
  flex: 1 1 auto; white-space: nowrap; text-align: center;
}
.rem-time-fields { display: flex; flex-direction: column; gap: 8px; }
/* Theme the reminder inputs — they live outside .form-group, so without this the
   time picker + message box render as unstyled white controls on the dark theme
   (the "drop-downs are wrong" report). (user 2026-06-14) */
.reminder-row input[type="time"],
.reminder-row input[type="date"],
.reminder-row input[type="number"],
.reminder-row input[type="text"] {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text);
  padding: 8px 10px;
  font-size: 14px;
  color-scheme: dark light; /* native time/date picker chrome follows the theme */
}
.reminder-row input:focus { border-color: var(--primary); outline: none; }
.rem-time-fields .rem-time { width: 140px; }
.reminder-row .rem-msg { width: 100%; }
.reminder-row .rem-remove {
  background: none; border: none; color: var(--text-secondary);
  font-size: 16px; line-height: 1; cursor: pointer; padding: 4px 8px;
}
.reminder-days { display: flex; gap: 6px; flex-wrap: wrap; }
.reminder-days .rem-day { width: 38px; }
/* "Every N days" fields — N + start date sit on one wrapping row, each label
   stacked above its input to match .form-group. (user 2026-06-15) */
.rem-interval-fields { display: flex; gap: 12px; flex-wrap: wrap; }
.rem-interval-label {
  display: flex; flex-direction: column; gap: 4px;
  font-size: 12px; color: var(--text-secondary);
}
.rem-interval-fields .rem-interval { width: 90px; }
.rem-interval-fields .rem-start { width: 170px; }
.reminders-hint { margin-top: 4px; }

/* Per-supplement edit / history panel — give it room when populated so the
   form doesn't sit flush against the card edge (the "camped" edit view).
   Empty (closed) → no chrome. (user 2026-06-15) */
.sup-detail:not(:empty) {
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--surface-2);
}

/* Full-screen cardio timing screen (opened from the stopwatch chip) — modelled on
   the rest-timer screen but the ring counts UP. Scrollable (it carries the cardio
   config form). */
.cardio-screen-overlay {
  position: fixed;
  inset: 0;
  z-index: 300;
  background: var(--scrim);
  color: var(--text);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  overflow-y: auto;
  display: flex;
  justify-content: center;
  padding: calc(16px + env(safe-area-inset-top)) 16px 96px;
}
/* Content follows the app theme (dark / light / high-contrast) — same as the
   rest timer. */
.cardio-screen-overlay h2 { color: var(--text); }
.cardio-screen-overlay label { color: var(--text-secondary); }
.cardio-screen-overlay input,
.cardio-screen-overlay select {
  width: 100%;
  padding: 10px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text);
  font-size: 15px;
}
.cardio-screen-overlay .log-overlay-close { color: var(--text-secondary); }
.cardio-screen-inner { width: 100%; max-width: 420px; display: flex; flex-direction: column; align-items: center; gap: 16px; }
.cardio-screen-head { display: flex; align-items: center; justify-content: space-between; width: 100%; }
.cardio-screen-ring { position: relative; width: 220px; height: 220px; cursor: pointer; }
.cardio-screen-ring svg { width: 220px; height: 220px; }
/* Match the rest-timer ring: faint track, thin bright fill. */
.cardio-screen-ring .ring-track { stroke: var(--ring-track); }
.cardio-screen-ring .ring-fill { stroke: var(--phase-green); transition: stroke-dashoffset 0.25s linear, stroke 0.3s ease; }
/* Pace colour vs the ghost targets (feat_cardio_timer_colour_by_pace). */
.cardio-screen-ring.pace-red .ring-fill { stroke: var(--phase-red); }
.cardio-screen-ring.pace-amber .ring-fill { stroke: var(--phase-orange); }
.cardio-screen-ring.pace-green .ring-fill { stroke: var(--phase-green); }
.cardio-screen-time {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  font-size: 44px; font-weight: 400; font-variant-numeric: tabular-nums; letter-spacing: -1px; color: var(--text);
}
.cardio-screen-controls { display: flex; gap: 10px; }
/* Ghost pace targets (previous + best) shown under the count-up ring. */
.cardio-ghosts { display: flex; gap: 18px; justify-content: center; flex-wrap: wrap; min-height: 18px; }
.cardio-ghost { font-size: 14px; font-variant-numeric: tabular-nums; color: var(--text-secondary); }
.cardio-ghost-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-secondary); margin-right: 2px; }
.cardio-screen-form { width: 100%; }
.cardio-stay-awake { display: flex; align-items: center; gap: 8px; font-size: 15px; }
.cardio-stay-awake input { width: 18px; height: 18px; flex: none; }
.cardio-advanced { width: 100%; margin-top: 4px; }
.cardio-advanced > summary { cursor: pointer; padding: 8px 0; color: var(--text); font-weight: 600; }

/* Minimised cardio screen — only the ring shows, parked bottom-right above the
   tab bar (shares the corner with the now-hidden stopwatch chip). */
.cardio-screen-overlay.minimized {
  inset: auto; bottom: 76px; right: 16px; padding: 0;
  background: transparent; backdrop-filter: none; -webkit-backdrop-filter: none;
  overflow: visible; display: block;
}
.cardio-screen-overlay.minimized .cardio-screen-inner { width: auto; gap: 0; }
.cardio-screen-overlay.minimized .cardio-screen-head,
.cardio-screen-overlay.minimized .cardio-screen-controls,
.cardio-screen-overlay.minimized .cardio-ghosts,
.cardio-screen-overlay.minimized .cardio-screen-form { display: none; }
.cardio-screen-overlay.minimized .cardio-screen-ring {
  /* Match the floating chip exactly (56px) — see rest-timer note above. */
  width: 56px; height: 56px; background: var(--surface); border-radius: 50%;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.35);
}
.cardio-screen-overlay.minimized .cardio-screen-ring svg { width: 56px; height: 56px; }
.cardio-screen-overlay.minimized .cardio-screen-time { font-size: 15px; }

.rest-timer-overlay.minimized .rest-timer-text { font-size: 15px; }
/* The minimised chip's countdown reads as a clear slim DECREASING border hugging
   the chip edge. The base ring (stroke 2, r=44 in a 100-unit viewBox) renders as a
   faint hairline INSET from the 56px edge — easy to miss, and in dark mode the
   faint track vanishes so only a stub arc shows. Fix: scale the SVG out so the ring
   sits at the edge (scale preserves the dash ratios, so no offset math breaks),
   thicken the colored arc, and give the track enough contrast to always show the
   full border outline that the colour drains around. (user 2026-06-13) */
.rest-timer-overlay.minimized .rest-timer-svg { transform: scale(1.07); }
.rest-timer-overlay.minimized .rest-timer-progress { stroke-width: 6; }
.rest-timer-overlay.minimized .rest-timer-track { stroke-width: 6; stroke: var(--text-secondary); opacity: 0.35; }
.rest-timer-overlay.minimized .rest-timer-header,
.rest-timer-overlay.minimized .rest-timer-stage,
.rest-timer-overlay.minimized .rest-timer-next,
.rest-timer-overlay.minimized .rest-timer-log,
.rest-timer-overlay.minimized .rest-timer-quick-picks,
/* The whole tab strip (Controls / Plates / Today / History bar + panels) collapses
   too — minimised shows ONLY the clock circle, nothing else (user 2026-06-24: "the
   menu and plate calculator is showing"). */
.rest-timer-overlay.minimized .rest-timer-tabs,
.rest-timer-overlay.minimized .rest-timer-eta,
.rest-timer-overlay.minimized .rest-timer-dismiss { display: none; }

/* Lift name (large) above the ring throughout the workout, with the warm-up
   step count ("Warm-up 1 of 4") as a subtitle. (user 2026-06-09) */
.rest-timer-header { text-align: center; }
.rest-timer-lift-name { font-size: 30px; font-weight: 700; line-height: 1.1; }
.rest-timer-sub { font-size: 13px; font-weight: 600; letter-spacing: 1.5px; text-transform: uppercase; color: var(--text-secondary); margin-top: 4px; }

/* ── Inline logging panel on the rest timer ────────────────────────────────
   Log the next set straight from the timer page — reps/weight + tick/cross and,
   for accessories, a "don't increase weight" hold (user 2026-06-08). */
.rest-timer-log {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  margin-top: 18px;
  width: 100%;
  max-width: 320px;
}
.rest-timer-log-inputs { display: flex; gap: 14px; justify-content: center; }
.rest-timer-log-field { display: flex; flex-direction: column; gap: 4px; align-items: center; }
.rest-timer-log-field > span { font-size: 12px; color: var(--text-secondary); font-weight: 600; }
/* Lift-mode reps/weight read at the same size as the warm-up step's big target
   (.rest-timer-log-warmup-target, 26px) — they used to be smaller. (user 2026-06-09) */
.rest-timer-log-field input { width: 100px; text-align: center; font-size: 26px; font-weight: 700; }
.rest-timer-log-field .set-editor-value { font-size: 26px; font-weight: 700; padding: 0; }
/* A PRESCRIBED (read-only) value sharing a log row with an editable WHEEL used to render
   as this 26px static span — stranded at the TOP of the flex row (the row is stretch-
   aligned and each field top-packs), so it read ~1.7x the wheel's 15px selected row AND
   ~35px above the wheel's centreline (owner report 2026-07-19: "77.5 so massive, not
   aligned"). When a wheel is present, match it: the wheel's selected-row type (15px/700)
   in a 70px slot, vertically centred, so a locked value reads as a locked wheel row on the
   SAME centreline. The no-wheel case (both axes read-only) keeps the 26px treatment above.
   (fix_timer_prescribed_value_wheel_alignment) */
.rest-timer-log-inputs:has(.rt-wheel) .rest-timer-log-field .set-editor-value {
  font-size: 15px;
  font-weight: 700;
  min-height: 70px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
/* The weight value's wrap gets the same 70px height so the absolutely-centred kg/RIR
   side group (top:50%) re-centres on the value. */
.rest-timer-log-inputs:has(.rt-wheel) .rest-timer-log-weight-wrap { min-height: 70px; }
/* Placeholders (the rep range / "?") sit paler than typed values. (user 2026-06-10) */
.rest-timer-log-field input::placeholder { color: var(--text-secondary); opacity: 0.5; font-weight: 600; }
/* The unit (kg/lb) + RIR badge ride to the right of the weight VALUE in one side group,
   positioned ABSOLUTELY and vertically centred on the value — so the 'Weight' label stays
   centred over the value alone, and the RIR badge lines up with the unit at any value
   height (wheel or input). (fix_rest_timer_wheel_polish) */
.rest-timer-log-weight-wrap { position: relative; display: inline-flex; align-items: center; justify-content: center; }
.rest-timer-log-side {
  position: absolute;
  left: 100%;
  margin-left: 5px;
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
}
.rest-timer-log-unit { font-size: 14px; font-weight: 600; color: var(--text-secondary); }
.rest-timer-log-hold {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 14px;
  color: var(--text);
}
.rest-timer-log-hold input { width: 18px; height: 18px; }
.rest-timer-log-actions { display: flex; gap: 12px; align-items: center; justify-content: center; width: 100%; }
.rest-timer-log-actions .rt-log-confirm { flex: 1; padding: 12px 0; font-size: 16px; font-weight: 700; letter-spacing: 1px; }
.rest-timer-log-actions .rt-log-skip { padding: 12px 16px; }
/* Persistent train surface (has-log). While a WORKING (non-warm-up) rest is
   COUNTING, show the full timer controls (±30s adjust AND the duration presets)
   and HIDE the log form — the log buttons only become available once the rest
   hits GO! and the ring flips to "lift this" (paused). Warm-up rests are fixed at
   60s, so they never show the timer controls. (user 2026-06-09) */
.rest-timer-overlay.has-log .rest-timer-quick-picks { display: none; }
.rest-timer-overlay.has-log .rest-timer-presets { display: none; }
/* The timer controls (±30s adjust + duration presets) show ONLY while the rest is
   COUNTING — for working sets AND warm-up rests (fix_warmup_timer_today_and_controls:
   warm-up rests get their controls back so a warm-up rest can be extended/shortened).
   Once the ring flips to "lift this" (paused) and the Log form appears, the controls
   hide so the entry form owns the surface — adjusting rest mid-entry makes no sense.
   (user 2026-06-11, reversing the 2026-06-10 "always show" while-paused rule.) */
.rest-timer-overlay.has-log:not(.paused) .rest-timer-quick-picks { display: flex; }
.rest-timer-overlay.has-log:not(.paused) .rest-timer-presets { display: grid; }
/* The log form is hidden while the rest COUNTS; it returns at GO! (paused). The counting
   (.rest-timer-log-next) and log (.rest-timer-log-form) groups are STACKED in one grid cell
   and swapped with visibility — the region always reserves the taller (log) height, so the
   tab bar below never bounces between the two states; it sits where the log state puts it.
   (owner 2026-07-16 — fix_timer_tabs_stable_position_and_pill_nudge) */
.rest-timer-overlay.has-log:not(.warmup):not(.minimized) .rest-timer-log { display: grid; align-items: center; justify-items: center; }
.rest-timer-overlay.has-log:not(.warmup):not(.minimized) .rest-timer-log > .rest-timer-log-next,
.rest-timer-overlay.has-log:not(.warmup):not(.minimized) .rest-timer-log > .rest-timer-log-form { grid-area: 1 / 1; }
.rest-timer-log-form { display: flex; flex-direction: column; align-items: center; gap: 12px; width: 100%; }
.rest-timer-overlay.has-log:not(.warmup):not(.paused) .rest-timer-log-form { visibility: hidden; }
.rest-timer-log-hint { font-size: 12px; color: var(--text-secondary); text-align: center; max-width: 300px; }
/* The lift/exercise name on the timer log form, so you always know which lift. */
.rest-timer-log-lift { font-size: 16px; font-weight: 700; text-align: center; }
/* Warm-up step card (guide only): big read-only weight × reps + a label. */
.rest-timer-log-warmup-label { font-size: 12px; text-transform: uppercase; letter-spacing: 1px; color: var(--text-secondary); text-align: center; }
.rest-timer-log-warmup-target { font-size: 26px; font-weight: 700; text-align: center; }
/* Warm-up timer (user 2026-06-09): a single focal lift, no small "up next" line.
   PAUSED → show the current step + Done. COUNTING → show the NEXT lift (big) and
   hide Done (you're resting; it auto-advances at GO!). Skip stays available. */
.rest-timer-overlay.warmup.paused .rest-timer-log-next { display: none; }
.rest-timer-overlay.warmup:not(.paused) .rest-timer-log-current,
.rest-timer-overlay.warmup:not(.paused) .rt-log-confirm { display: none; }
/* Working set: the "up next" reps × weight shows while the rest COUNTS; it's
   replaced by the log form once you're doing the set (paused). (user 2026-06-09)
   visibility (not display) — the groups share a grid cell above, so the swap can't
   move the tab bar. (fix_timer_tabs_stable_position_and_pill_nudge) */
.rest-timer-overlay.has-log:not(.warmup).paused .rest-timer-log-next { visibility: hidden; }

/* Info-style "use dumbbells" note for sub-bar prescribed weights — neutral
   (not a warning colour), just a helpful aside. */
.dumbbell-hint {
  font-size: 12px;
  color: var(--text-secondary);
  background: var(--surface-2);
  border-radius: var(--radius);
  padding: 6px 10px;
}

/* ── Workout date field ────────────────────────────────────────────────────
   The native <input type="date"> renders like a select dropdown on mobile
   WebKit, which reads as a plain dropdown rather than a date field (user
   feedback 2026-06-01). On touch devices, strip that dropdown chrome and
   surface a clear, tinted calendar glyph so it reads as a tappable date field
   that opens the OS picker — matching the desktop look the user already likes.
   Desktop (pointer: fine) keeps its native appearance untouched. */
@media (pointer: coarse) {
  #workout-date-input {
    -webkit-appearance: none;
    appearance: none;
    min-width: 150px;
    text-align: left;
  }
  #workout-date-input::-webkit-date-and-time-value { text-align: left; }
  #workout-date-input::-webkit-calendar-picker-indicator {
    opacity: 1;
    cursor: pointer;
    margin-left: 6px;
    filter: invert(1); /* dark glyph → light, visible on the dark field */
  }
  #workout-date-input::-webkit-inner-spin-button { display: none; }
}

/* Biometric lock overlay — covers the whole app until the user unlocks. Highest
   z-index so nothing shows through. (user 2026-06-13) */
.biometric-lock-overlay {
  position: fixed;
  inset: 0;
  z-index: 9000;
  background: var(--bg);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
}
.biometric-lock-card { text-align: center; max-width: 320px; }
.biometric-lock-icon { font-size: 44px; margin-bottom: 8px; }
.biometric-lock-card h2 { margin-bottom: 6px; }
.biometric-lock-card .btn { margin-top: 16px; width: 100%; }

/* Plate calculator (feat_plate_calculator) */
.plate-chip {
  display: inline-block;
  padding: 6px 12px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 999px;
  font-weight: 700;
  font-size: 15px;
  color: var(--text);
}
.plate-toggle {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 10px;
  background: var(--surface);
  border: 1px solid var(--surface-2);
  border-radius: var(--radius-sm);
  font-size: 14px;
  cursor: pointer;
}

/* Body tab — bodyweight + measurements (feat_bodyweight_measurements) */
.body-field {
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: 14px;
  margin-bottom: 10px;
}
.body-field > span { color: var(--text-secondary); }
.body-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0 12px;
}
/* Each field is a stacked label+input that fills its grid column. Without min-width:0
   the number inputs keep their intrinsic width and the 2-col grid can't shrink → the
   tape-measurements form overflows narrow screens. (fix_body_tape_measurements_width) */
.body-field {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
  margin-bottom: 12px;
}
.body-field input { width: 100%; min-width: 0; }

/* Home relevant quick-links (feat_home_hub_relevant_cards) */
.hub-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
  gap: 8px;
  margin-bottom: 16px;
}
/* Card columns: 1 (single, full-width, stacked) | 2 (default) | 3. (feat_home_columns) */
.hub-cards.columns-1 { grid-template-columns: 1fr; }
.hub-cards.columns-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.hub-cards.columns-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
/* Legacy two-way layout class (superseded by columns-N) — kept harmless. */
.hub-cards.layout-rows { grid-template-columns: 1fr; }

/* Emoji glyph used by a customised centre toolbar tab — sized to match the .tab svg/img. */
.tab-emoji { font-size: 20px; line-height: 24px; height: 24px; opacity: 0.55; transition: opacity 0.2s; }
.tab.active .tab-emoji { opacity: 1; }

/* Home-customiser extra sections (card layout + centre-tab picker). */
.hub-edit-section { margin-top: 16px; }
.hub-edit-section-label { font-size: 13px; font-weight: 700; color: var(--text-secondary); margin-bottom: 8px; }
.hub-seg { display: flex; gap: 0; border: 1px solid var(--border); border-radius: var(--radius-sm); overflow: hidden; }
.hub-seg-opt { flex: 1; padding: 10px 12px; background: var(--surface); color: var(--text-secondary); border: 0; font-weight: 600; font-size: 14px; cursor: pointer; }
.hub-seg-opt + .hub-seg-opt { border-left: 1px solid var(--border); }
.hub-seg-opt.active { background: var(--primary); color: #fff; }
.hub-middle-select { width: 100%; padding: 10px 12px; background: var(--surface); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius-sm); font-size: 14px; }
.hub-card {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  padding: 12px 14px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text);
  text-decoration: none;
  font-weight: 600;
  font-size: 14px;
}
.hub-card span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hub-card:active { background: var(--surface-2); }
/* Customise affordance under the hub cards (feat_home_customisable_cards). Subtle
   text button so it doesn't compete with the cards themselves. The cards grid
   keeps its own margin-bottom; pull the button up to sit just beneath it. */
.hub-customise {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  margin: -8px 0 16px;
  padding: 4px 2px;
  background: none;
  border: none;
  color: var(--text-secondary);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
}
.hub-customise:active { color: var(--text); }
/* The Customise + View-all affordances sit on one row under the hub cards. */
.hub-actions { display: flex; align-items: center; gap: 16px; }
.hub-actions .hub-customise { text-decoration: none; }
/* Country (+ US state) picker — stacked fields, label above each select. */
.location-picker { display: flex; flex-direction: column; gap: 10px; }
.location-field { display: flex; flex-direction: column; gap: 4px; }
.location-label { font-size: 13px; color: var(--text-secondary); }
.location-picker select { width: 100%; }
/* Hub customiser modal: one reorderable toggle row per card. */
.hub-edit-list { display: flex; flex-direction: column; gap: 6px; }
.hub-edit-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}
.hub-edit-arrows { display: flex; flex-direction: column; gap: 2px; }
.hub-edit-arrows button {
  background: none;
  border: none;
  color: var(--text-secondary);
  cursor: pointer;
  padding: 0;
  font-size: 14px;
  line-height: 1;
}
.hub-edit-arrows button:disabled { visibility: hidden; }
.hub-edit-label { flex: 1; min-width: 0; font-weight: 600; font-size: 14px; }
.hub-edit-toggle { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; color: var(--text-secondary); white-space: nowrap; }
/* Active-program "next session" card on the home hub — accented so it reads as the
   day's primary action. (feat_home_hub_program_route_and_customise) */
.active-program-card {
  border-left: 3px solid var(--primary);
  padding: 14px 16px;
}
.active-program-card:active { background: var(--surface-2); }

/* Weekly recap + streak home card (feat_streaks_weekly_recap) */
.recap-card { padding: 14px 16px; }
.streak-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px 10px;
  /* primary→secondary accent gradient (feat_secondary_accent_colours) */
  background: linear-gradient(135deg, var(--primary), var(--primary-dark));
  color: #fff;
  border-radius: 999px;
  font-size: 13px;
  font-weight: 700;
  white-space: nowrap;
}
.recap-stats {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 8px;
}
.recap-stat { text-align: center; }
.recap-stat-value { font-size: 20px; font-weight: 700; color: var(--text); }
.recap-stat-label { font-size: 11px; color: var(--text-secondary); margin-top: 2px; }
.recap-up { color: var(--success, #10b981); }
.recap-down { color: var(--text-secondary); }

/* Tabbed control area on the rest timer (feat_session_timer_tabs) */
.rest-timer-tabs { width: 100%; max-width: 360px; margin: 4px auto 0; }
.rt-tabbar {
  display: flex;
  gap: 4px;
  background: var(--surface-2);
  border-radius: 999px;
  padding: 3px;
}
.rt-tab {
  flex: 1;
  border: none;
  background: transparent;
  color: var(--text-secondary);
  font-size: 13px;
  font-weight: 600;
  padding: 7px 4px;
  border-radius: 999px;
  cursor: pointer;
}
.rt-tab.active { background: var(--surface); color: var(--text); }
/* Fixed-height panel area + centered content so the tab bar above never shifts
   when you switch tabs (the overlay is vertically centred). (feat_session_timer_tabs) */
.rt-tabpanel {
  padding-top: 10px;
  min-height: 210px;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.rt-tabpanel[hidden] { display: none; }
/* Short screens (small phones): the fixed 210px panel + the full-size ring won't all fit,
   so let the tab area take the remaining space and the ACTIVE PANEL scroll INSIDE it — the
   ring, log form and ✓/✗ stay put; only the panel (plates / history) scrolls.
   (fix_rest_timer_overlay_phone_fit) */
@media (max-height: 820px) {
  .rest-timer-overlay.has-log #rest-timer-tabs {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
  }
  .rest-timer-overlay.has-log .rt-tabpanel:not([hidden]) {
    flex: 1 1 auto;
    min-height: 0;              /* was a fixed 210px — allow it to shrink + scroll */
    overflow-y: auto;
    justify-content: flex-start;
  }
}
.rt-plates-head { text-align: center; font-size: 13px; color: var(--text-secondary); margin-bottom: 8px; }
.rt-plate-chips { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; }
.rt-plate-chip {
  display: inline-block;
  padding: 6px 12px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 999px;
  font-weight: 700;
  font-size: 15px;
}
/* Timer Plates tab: each per-side plate as a coloured DISC with the weight inside — the colours match
   the settings picker (both go through plateHex). The disc carries its own text colour inline (YIQ), and
   a translucent ring so a pale/white disc still reads its edge on any theme. A ×N multiplier sits beside
   the disc. (feat_timer_plate_calc_coloured_discs) */
.rt-plate-discs { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; align-items: center; padding: 14px 0; }
.rt-plate-disc-wrap { display: inline-flex; align-items: center; gap: 3px; }
.rt-plate-disc-lg {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 40px; height: 40px; padding: 0 6px; border-radius: 50%;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.25);
  font-weight: 700; font-size: 15px; line-height: 1;
}
.rt-plate-x { font-weight: 700; font-size: 13px; color: var(--text-secondary); }
.rt-panel-empty, .rt-panel-warn { text-align: center; font-size: 13px; color: var(--text-secondary); margin: 4px 0 0; }
/* How-to tab: exercise name, a brief form cue, and the Info/Coaching demo links. */
.rt-howto-name { font-weight: 700; font-size: 14px; text-align: center; margin-bottom: 6px; }
.rt-howto-cue { font-size: 13px; line-height: 1.5; color: var(--text); margin: 0; text-align: center; }
.rt-howto-links { margin-top: 10px; display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 6px; }
.rt-howto-links-label { font-size: 12px; color: var(--text-secondary); }
.rt-panel-warn { color: var(--warning, #f59e0b); }
.rt-panel-loading { text-align: center; padding: 12px 0; }
.rt-history-row {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  padding: 7px 4px;
  border-bottom: 1px solid var(--border);
  font-size: 14px;
}
.rt-history-row:last-child { border-bottom: none; }
.rt-history-date { color: var(--text-secondary); white-space: nowrap; }
.rt-history-sets { text-align: right; font-weight: 600; }
.rt-history-unit { color: var(--text-secondary); font-weight: 400; font-size: 12px; }
/* "Today" tab — the day's exercises with progress (feat_rest_timer_today_tab). */
.rt-today-list { list-style: none; margin: 0; padding: 0; }
.rt-today-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 4px;
  border-bottom: 1px solid var(--border);
  font-size: 14px;
}
.rt-today-item:last-child { border-bottom: none; }
.rt-today-mark { width: 16px; text-align: center; color: var(--text-secondary); flex: none; }
.rt-today-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rt-today-count { color: var(--text-secondary); font-size: 12px; font-weight: 600; white-space: nowrap; }
.rt-today-item.done .rt-today-mark { color: var(--success, #22c55e); }
.rt-today-item.done .rt-today-name { color: var(--text-secondary); }
.rt-today-item.current { font-weight: 700; }
.rt-today-item.current .rt-today-mark { color: var(--primary); }
.rt-today-item.skipped .rt-today-name { text-decoration: line-through; color: var(--text-secondary); }
/* Read-only superset pairing in the Today list: a shared accent rail down the left
   edge of the two joined rows + a small 'superset' tag on the first. (owner 2026-07-04) */
.rt-today-item.ss { border-left: 3px solid var(--primary); padding-left: 8px; }
.rt-today-item.ss-first { border-top-left-radius: var(--radius-sm, 6px); }
.rt-today-item.ss-last { border-bottom-left-radius: var(--radius-sm, 6px); }
.rt-today-ss-tag {
  margin-left: 8px; font-size: 11px; font-weight: 700; letter-spacing: 0.06em;
  color: var(--primary); vertical-align: middle;
}
.rt-plates-bar { text-align: center; font-size: 12px; color: var(--text-secondary); margin-bottom: 6px; }
.rt-plates-entry { display: flex; flex-direction: column; align-items: center; gap: 6px; margin-bottom: 6px; }
/* Per-exercise loading editor — plate toggle chips. (feat_per_exercise_loading_editor) */
.le-plates { display: flex; flex-wrap: wrap; gap: 6px; }
.le-plate { border: 1px solid var(--border); background: var(--surface-2); color: var(--text); border-radius: 999px; padding: 6px 12px; font-size: 13px; cursor: pointer; }
.le-plate.on { background: var(--primary); color: #fff; border-color: var(--primary); }
.rt-plates-entry label { font-size: 13px; color: var(--text-secondary); }
.rt-plates-entry input { max-width: 130px; text-align: center; }

/* Coloured-disc plate picker — a 2-column grid (coloured disc · weight · "have it"
   checkbox · count) shared by Settings → Equipment and the timer's plate editor.
   A blank count = unlimited (∞ placeholder). (feat_plate_picker_colours) */
/* Two explicit columns: heavy plates (5kg+/10lb+) left, light change plates right. */
.plate-grid { display: flex; gap: 16px; align-items: flex-start; }
.plate-col { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6px; }
/* position: relative anchors the floating .plate-palette popover to this cell. */
.plate-cell { position: relative; display: flex; align-items: center; gap: 8px; padding: 3px 0; }
.plate-pick { display: flex; align-items: center; gap: 8px; cursor: pointer; flex: 1; min-width: 0; }
/* Push the ownership checkbox to the right edge of the (flex:1) label so the
   checkboxes line up in a column adjacent to the count text box, rather than
   floating just after variable-width weight labels. (fix_plate_picker_checkbox_align) */
.plate-pick .plate-own { margin-left: auto; }
/* The competition-colour disc, drawn as a ring with a transparent centre hole (like
   a real plate). White/pale plates keep a border so their edge reads on any theme.
   The hole is punched with a radial mask so it's genuinely see-through. */
.plate-disc {
  width: 20px; height: 20px; flex: none; border-radius: 50%; border: 1px solid var(--border);
  /* Smaller centre hole — about half the previous radius (owner 2026-07-01). */
  -webkit-mask: radial-gradient(circle at 50% 50%, transparent 0 16%, #000 18%);
          mask: radial-gradient(circle at 50% 50%, transparent 0 16%, #000 18%);
  padding: 0; cursor: pointer; /* it's a <button> now — tap to recolour (feat_plate_colours_user_customisable) */
}
/* The per-plate colour picker: preset swatches + a free colour input + reset + cancel. A FLOATING
   popover anchored to the disc — shown only when the disc is tapped, dismissed on click-away/Escape/
   Cancel (wirePlateColours), so it overlays rather than pushing the layout. (fix_plate_colour_picker_floating_popover) */
.plate-palette { position: absolute; top: 100%; left: 0; z-index: 30; margin-top: 4px;
  min-width: min(240px, 86vw); padding: 8px; background: var(--surface-2); border: 1px solid var(--border);
  border-radius: var(--radius-sm); box-shadow: 0 6px 20px rgba(0,0,0,.28); }
.plate-swatches { display: flex; flex-wrap: wrap; gap: 6px; }
.plate-swatch { width: 24px; height: 24px; padding: 0; border-radius: 50%; border: 1px solid var(--border); cursor: pointer; }
.plate-swatch:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.plate-palette-actions { display: flex; align-items: center; gap: 12px; margin-top: 8px; }
.plate-custom-label { display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--text-secondary); }
.plate-custom { width: 34px; height: 26px; padding: 0; border: 1px solid var(--border); border-radius: var(--radius-sm); background: none; cursor: pointer; }
.plate-colour-reset { margin-left: auto; }
/* Max loadable weight line under the plate picker — reserve height so it never shifts the
   modal as it flips between a total and ∞. (owner 2026-07-01) */
.le-max { min-height: 1.4em; margin-top: 8px; font-weight: 600; font-size: 14px; text-align: center; }
/* The ∞ glyph reads clearer a size up — in the max line + as the unlimited-count placeholder. (owner 2026-07-01) */
.rt-inf { font-size: 1.6em; line-height: 1; vertical-align: -0.12em; }
.plate-wt { font-weight: 600; }
.plate-cell .plate-count { width: 64px; flex: none; text-align: right; padding: 6px 8px; }
.plate-cell .plate-count::placeholder { font-size: 1.5em; opacity: 0.7; }
.plate-cell:not(.on) .plate-pick { opacity: 0.55; }

/* Bands inventory rows (Settings → Equipment): the label takes its own line and
   the count/tension fields wrap below, so the three number inputs never overflow
   the card on a narrow phone. (feat_settings_equipment_polish) */
.resist-band-row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px; padding: 6px 0; }
.resist-band-row .rb-label { flex: 1 1 100%; font-weight: 600; }
.resist-band-row .rb-fields { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.resist-band-row .rb-fields input { width: 56px; text-align: right; }
/* The compact numeric inputs in the inline bands/chains panel must escape the
   `.settings-select { min-width: 180px }` default, or they blow out the card
   (the cause of the bands overflow). Their own width (56px / inline) then wins. */
.resist-config .settings-select { min-width: 0; }

/* Custom program builder (feat_custom_program_builder) */
.cb-mini { display: flex; flex-direction: column; gap: 2px; font-size: 12px; }
.cb-mini > span { color: var(--text-secondary); }
.cb-mini input { width: 100%; }

/* Analytics consent banner (feat_session_replay_rrweb_or_posthog). A bottom
   sheet shown once until the user opts in/out of PostHog product analytics.
   Sits above the tab bar (z 200 > tab-bar's 100). */
#analytics-consent {
  position: fixed;
  left: 50%;
  bottom: 0;
  transform: translateX(-50%);
  width: 100%;
  max-width: 480px;
  z-index: 200;
  background: var(--surface);
  border-top: 1px solid var(--border);
  box-shadow: 0 -6px 20px rgba(0, 0, 0, 0.25);
  padding-bottom: env(safe-area-inset-bottom);
  animation: analytics-consent-in 0.18s ease-out;
}
@keyframes analytics-consent-in { from { transform: translate(-50%, 100%); } to { transform: translate(-50%, 0); } }
.analytics-consent-inner { padding: 14px 16px; display: flex; flex-direction: column; gap: 12px; }
.analytics-consent-text { margin: 0; font-size: 13px; line-height: 1.4; color: var(--text-secondary); }
.analytics-consent-text a { color: var(--link); white-space: nowrap; }
.analytics-consent-actions { display: flex; gap: 8px; justify-content: flex-end; }
.analytics-consent-actions .btn { flex: 0 0 auto; min-width: 96px; }

/* ── Progression scripting authoring (feat_progression_scripting_authoring_ui) ── */
.ps-intro, .ps-library .ps-intro { color: var(--text-secondary); margin: 4px 0 16px; }
.ps-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 8px; }
.ps-actions .btn { flex: 1 1 auto; }
.ps-section { font-size: 15px; margin: 22px 0 8px; }
.ps-script-card { display: block; text-decoration: none; color: inherit; margin-top: 8px; }
.ps-script-head { display: flex; align-items: center; gap: 8px; justify-content: space-between; }
.ps-script-sub { color: var(--text-secondary); font-size: 13px; margin-top: 4px; }
.ps-hook-tag { display: inline-block; font-family: ui-monospace, monospace; font-size: 11px; background: var(--surface-2); border-radius: var(--radius-sm); padding: 1px 6px; }
.ps-origin { font-size: 11px; color: var(--text-secondary); }
.ps-status { font-size: 11px; border-radius: var(--radius-sm); padding: 1px 7px; text-transform: capitalize; }
.ps-status-pending { background: var(--warning); color: #2a1d00; }
.ps-status-approved { background: var(--success); color: #032a12; }
.ps-status-rejected { background: var(--danger); color: #fff; }
.ps-refs { display: grid; gap: 8px; }
.ps-ref-card { display: flex; flex-direction: column; gap: 3px; text-decoration: none; color: inherit; }
.ps-ref-card span { color: var(--text-secondary); font-size: 13px; }
.ps-ref-card .ps-hook-tag { align-self: flex-start; }

/* editor widget */
.ps-editor { display: flex; flex-direction: column; gap: 10px; }
.ps-toolbar { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.ps-toolbar .btn { flex: 0 0 auto; }
.ps-fork-wrap { position: relative; }
.ps-fork-menu { position: absolute; z-index: 30; top: 100%; left: 0; margin-top: 4px; width: min(320px, 86vw);
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: 0 8px 28px rgba(0,0,0,.28); overflow: hidden; }
.ps-fork-item { display: flex; flex-direction: column; gap: 2px; width: 100%; text-align: left; padding: 10px 12px; background: none; border: none; border-bottom: 1px solid var(--border); cursor: pointer; color: inherit; }
.ps-fork-item:last-child { border-bottom: none; }
.ps-fork-item:hover { background: var(--surface-2); }
.ps-fork-item span { font-size: 12px; color: var(--text-secondary); }

.ps-meta { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.ps-name-input { flex: 2 1 180px; }
.ps-desc-input { flex: 3 1 220px; }
.ps-hook-label { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-secondary); }
.ps-hook-select { padding: 6px; }
.ps-hooks-detected { flex-basis: 100%; font-size: 12px; color: var(--primary); }

.ps-code-wrap { position: relative; display: flex; border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; background: var(--bg); }
.ps-gutter { margin: 0; padding: 10px 6px 10px 10px; text-align: right; color: var(--text-secondary); opacity: .6;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; line-height: 1.5; user-select: none;
  overflow: hidden; min-width: 2.4em; background: var(--surface-2); white-space: pre; }
.ps-code { flex: 1; border: none; resize: vertical; min-height: 320px; padding: 10px; margin: 0; background: var(--bg); color: var(--text);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; line-height: 1.5; tab-size: 2; white-space: pre; overflow: auto; }
.ps-code:focus { outline: none; }

.ps-tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--border); margin-top: 2px; }
.ps-tab { background: none; border: none; padding: 8px 12px; cursor: pointer; color: var(--text-secondary); font-weight: 600; font-size: 13px; border-bottom: 2px solid transparent; }
.ps-tab.active { color: var(--text); border-bottom-color: var(--primary); }
.ps-badge { display: inline-block; min-width: 18px; text-align: center; background: var(--danger); color: #fff; border-radius: 9px; font-size: 11px; padding: 0 5px; }
.ps-panels { min-height: 120px; }
.ps-panel { padding: 10px 2px; }
.ps-contract, .ps-sample, .ps-json { margin: 0; padding: 10px; background: var(--surface-2); border-radius: var(--radius);
  font-family: ui-monospace, monospace; font-size: 12px; line-height: 1.45; overflow: auto; max-height: 360px; white-space: pre; }

.ps-result { display: flex; flex-direction: column; gap: 6px; }
.ps-result-scope { font-size: 11px; color: var(--text-secondary); font-family: ui-monospace, monospace; }
.ps-result-row { display: flex; flex-wrap: wrap; gap: 8px; align-items: baseline; padding: 8px 10px; background: var(--surface-2); border-radius: var(--radius); }
.ps-result-key { font-weight: 700; }
.ps-slot { font-size: 13px; }
.ps-slot strong { color: var(--success); }
.ps-hold { color: var(--text-secondary); }
.ps-ok { color: var(--success); font-weight: 600; }
.ps-warn { color: var(--warning); font-weight: 600; }
.ps-err { color: var(--danger); }
.ps-err code { background: var(--surface-2); padding: 1px 4px; border-radius: 4px; }

.ps-problem { display: flex; flex-direction: column; gap: 2px; width: 100%; text-align: left; padding: 8px 10px; margin-bottom: 6px;
  background: var(--surface-2); border: 1px solid var(--border); border-left: 3px solid var(--danger); border-radius: var(--radius); cursor: pointer; color: inherit; }
.ps-problem-loc { font-size: 11px; color: var(--text-secondary); }
.ps-problem-tok { font-family: ui-monospace, monospace; font-weight: 700; color: var(--danger); }
.ps-problem-msg { font-size: 13px; }
.ps-import-input { width: 100%; margin: 8px 0; }

/* lessons */
.ps-lesson-card { border: 1px solid var(--border); border-radius: var(--radius); padding: 14px; margin-bottom: 12px; background: var(--surface); }
.ps-lesson-top { display: flex; gap: 8px; align-items: center; margin-bottom: 6px; }
.ps-lesson-step { font-size: 12px; color: var(--text-secondary); font-weight: 700; }
.ps-lesson-feature { font-size: 11px; background: var(--surface-2); border-radius: var(--radius-sm); padding: 2px 8px; }
.ps-lesson-body { color: var(--text); line-height: 1.55; }
.ps-lesson-body code { background: var(--surface-2); padding: 1px 5px; border-radius: 4px; font-family: ui-monospace, monospace; font-size: 12px; }
.ps-lesson-body ul { padding-left: 18px; }
.ps-lesson-try { margin-top: 10px; padding: 10px 12px; background: var(--surface-2); border-radius: var(--radius); font-size: 14px; }
.ps-lesson-nav { display: flex; gap: 8px; margin-top: 12px; flex-wrap: wrap; }
.ps-lesson-nav .btn { flex: 1 1 auto; }

/* roomier editor on wider screens */
@media (min-width: 760px) {
  .ps-code { min-height: 420px; }
}

/* ── Curated white-label fonts (feat_tenant_branding_fonts) ───────────────────
   Self-hosted latin-subset woff2 (no CDN). Lazy: only fetched when a tenant
   selects one (sets --font to the family); non-tenant users never load them. */
/* ── Condensed-caps micro-labels (feat_forge_display_type, branding addendum §3) ──
   Section eyebrows, table headers and stage labels ("THIS WEEK", "SET 2 OF 3") wear the display
   face. Applied as ONE grouped rule over the EXISTING uppercase label classes rather than new
   ad-hoc sub-headers — the addendum's point is to retire those, not add more.
   Badges (.badge / .beta-badge / .drop-badge) are deliberately EXCLUDED: they are 9-11px pills, a
   different role from a section label, and condensing them costs legibility for no branding gain. */
.sets-table th,
.section-body th,
.help-intro th,
.session-week-header,
.session-pinned-eyebrow,
.settings-group-heading,
.rest-timer-next-label,
.rest-timer-day-list-label,
.cardio-ghost-label,
.rest-timer-log-warmup-label,
.ex-picker-heading {
  font-family: var(--font-display);
  font-weight: 600;
  letter-spacing: 1.2px;
}
/* These two carry deliberately WIDER tracking from earlier tickets (3px stage, 1.5px sub) — take
   the display face but keep their tuned spacing rather than flattening it to the 1.2px default. */
.rest-timer-stage,
.rest-timer-sub {
  font-family: var(--font-display);
}

/* ── Iron Forge app faces (feat_forge_display_type, branding addendum §3) ─────
   Barlow for body/numerals, Barlow Semi Condensed for the condensed-caps micro-labels.
   UNLIKE the curated white-label fonts below these are the app's OWN faces, so they are
   fetched for every member — self-hosted latin-subset woff2 (no CDN), font-display:swap so
   text paints immediately in the system stack and swaps when they arrive. Weights are
   400/600/700 only; 500 and 800 are banned and ratchet-enforced. */
@font-face { font-family: 'Barlow'; font-style: normal; font-weight: 400; font-display: swap; src: url('../assets/fonts/barlow-400.woff2') format('woff2'); }
@font-face { font-family: 'Barlow'; font-style: normal; font-weight: 600; font-display: swap; src: url('../assets/fonts/barlow-600.woff2') format('woff2'); }
@font-face { font-family: 'Barlow'; font-style: normal; font-weight: 700; font-display: swap; src: url('../assets/fonts/barlow-700.woff2') format('woff2'); }
@font-face { font-family: 'Barlow Semi Condensed'; font-style: normal; font-weight: 600; font-display: swap; src: url('../assets/fonts/barlow-semi-condensed-600.woff2') format('woff2'); }
@font-face { font-family: 'Barlow Semi Condensed'; font-style: normal; font-weight: 700; font-display: swap; src: url('../assets/fonts/barlow-semi-condensed-700.woff2') format('woff2'); }

@font-face { font-family: 'Inter'; font-style: normal; font-weight: 400; font-display: swap; src: url('../assets/fonts/inter-400.woff2') format('woff2'); }
/* Forge assistance chooser grouping: a joined run reuses the canonical .superset-group / .superset-rail
   (one accent bar with the round label running vertically) + a .superset-connector offer between
   ungrouped neighbours — matching the log view and program listing. Sized for the chooser's input rows.
   (fix_forge_assist_grouping_canonical_design) */
.assist-rows .superset-group { margin: 8px 0; }
.assist-rows .superset-group-cards > .assist-row { margin-top: 8px; }
.assist-rows .superset-connector { margin: 4px 0; }

/* Assistance-row fields: themed here instead of via inline styles on each <select>/<input>, so they
   read as part of the app. The compact size (13px, tighter padding) suits the dense row. The exercise
   <select> was the worst offender — its inline `padding:6px` clobbered the base rule's `padding-right`,
   so the dropdown chevron overlapped the exercise name. (ui_assist_swap_dropdown_restyle) */
.assist-row .assist-ex,
.assist-row .assist-reps,
.assist-row .assist-sets {
  padding: 7px 10px;
  /* background-COLOR, not the `background` shorthand: the shorthand resets background-image, which would
     wipe the base select rule's dropdown chevron. */
  background-color: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text);
  /* 15px = the documented size for a button/input (docs/branding-and-styling.md § Type). These were at
     13px, which the scale reserves for .btn-sm — two steps down from what a form control should be, on
     the screen where you choose your programme. The owner asked for one line (exercise · sets · reps ·
     bin); measured at a 360px viewport the row is 313px and the other three controls plus gaps and the
     chevron's padding leave ~111px for the name, which truncates "Incline Dumbbell Bench Press" even at
     13px — so the name keeps its own line and gets the readable size instead.
     (fix_assist_ex_reads_as_text_not_dropdown) */
  font-size: 15px;
}
.assist-row .assist-ex { flex: 1; min-width: 0; text-overflow: ellipsis; }
.assist-row .assist-sets { width: 46px; text-align: center; }
.assist-row .assist-reps { width: 68px; }
/* Keep room for the base rule's chevron (it sits at right 12px); a narrower field wants it tucked in a
   touch so the value doesn't run under it. */
.assist-row .assist-ex,
.assist-row .assist-reps { padding-right: 30px; background-position: right 8px center; }
/* The exercise control is a BUTTON opening the searchable picker (feat_assist_swap_searchable_picker),
   styled to keep reading as the field it replaced — incl. the select chevron, which buttons don't
   inherit from the select:not([multiple]) base rule. */
.assist-row button.assist-ex {
  text-align: left;
  white-space: nowrap;
  overflow: hidden;
  font-family: inherit;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' fill='none' stroke='%23888' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M1 1.5 6 6.5 11 1.5'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-size: 12px;
}

/* Searchable exercise picker overlay (components/exercise-picker.js). */
/* Generic field-look trigger button for the picker — reads as the select it replaced,
   incl. the base select chevron buttons don't inherit. (fix_logonly_picker_searchable) */
button.ex-picker-btn {
  width: 100%;
  text-align: left;
  padding: 10px 34px 10px 10px;
  background-color: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text);
  font-size: 15px;
  font-family: inherit;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' fill='none' stroke='%23888' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M1 1.5 6 6.5 11 1.5'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 12px center;
  background-size: 12px;
}
.ex-picker-list {
  max-height: 55vh;
  overflow-y: auto;
  border: 1px solid var(--surface-2);
  border-radius: var(--radius);
  padding: 4px 6px;
}
.ex-picker-heading {
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-secondary);
  padding: 8px 6px 2px;
}
.ex-picker-row {
  display: block;
  width: 100%;
  text-align: left;
  padding: 8px 6px;
  border: none;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--text);
  font-size: 14px;
  font-family: inherit;
}
.ex-picker-row.is-selected { color: var(--primary); font-weight: 600; }
.ex-picker-row:active { background: var(--surface-2); }
.ex-picker-custom { color: var(--text-secondary); }

/* Sub-label inside a settings section that groups more than one control (e.g. the Visuals section:
   Logging editor layout · Wheel inputs · Wheels for lists). (feat_wheels_lists_inline_all_dropdowns) */
.settings-subheading { font-weight: 600; font-size: 14px; margin-bottom: 4px; }

/* Assistance row — INLINE controls (chore_wheels_timer_only). The exercise name sits on its own
   line; sets, rep range and the bin share the meta row beneath it. This was briefly a summary row +
   a pencil-opened form, because three WHEELS could not share one horizontal row on a phone; with
   wheels timer-only, native controls fit and the second view was deleted. */
.assist-row { margin-top: 10px; }
/* LAYOUT ONLY — the field look (surface-2 fill, border, padding, and the select chevron at
   `right 8px center`) comes from the .assist-ex/.assist-reps/.assist-sets rule further up, and this
   rule must not fight it. It previously carried `background: none; border: none; padding: 0`, left
   over from the summary-row design where the name was a bare line of text. Two things went wrong once
   the row went inline: the control stopped reading as a control at all, and — because `background` is
   the SHORTHAND — it reset `background-position` to top-left while the chevron `background-image`
   survived on the more specific `button.assist-ex` selector. Result: a down arrow sitting on top of
   the first letter of the exercise name (owner, 2026-07-22: "it's just text with a down arrow over
   the start of the text"). Set geometry here; never the paint. */
.assist-row .assist-ex {
  display: block;
  width: 100%;
  cursor: pointer;
}
.assist-row-meta {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 2px;
}
/* Sets + reps sit LEFT, under the start of the exercise name (owner 2026-07-22, revising an earlier
   "pull them right"); the bin keeps the right edge on its own. `margin-left: auto` on the bin does
   that without justify-content, so only the two controls the owner named actually move. */
.assist-row-meta .assist-remove { margin-left: auto; }
.assist-row-meta .assist-sets { width: 72px; flex: 0 0 auto; }
/* Reps held to its content ("15-20" plus the chevron's 30px) instead of flex:1 — flexing it filled
   the whole row at 198px for five characters. */
.assist-row-meta .assist-reps { width: 96px; flex: 0 0 auto; }
.assist-row .assist-remove {
  background: none;
  border: none;
  padding: 2px 4px;
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
  color: var(--text-secondary);
}
.assist-row .assist-remove { color: var(--accent-danger); }

/* Ad-hoc superset connector between two adjacent assistance cards
   (feat_superset_adhoc_assistance): OFFER (not joined) = a centred "⮂ Superset" pill;
   JOINED = a small 'superset' tag (+ ✕ to unjoin on user pairs). No brace. */
/* Bottom margin matches the card's 12px margin-bottom (:812) so the connector button sits centred
   between the two cards. Without it the top gap collapses to the card's 12px while the bottom collapses
   to the next card's 0, pushing the button hard against the card below. (fix_compound_set_button_below_padding) */
.superset-connector { display: flex; align-items: center; justify-content: center; gap: 8px; margin: 1px 0 12px; }
.superset-toggle { font-size: 12px; padding: 4px 10px; }
.superset-badge { font-size: 12px; font-weight: 600; }
/* The un-joined OFFER (log view) — Superset AND Compound/Tri/Giant (.round-extend) — is ONE consistent
   rounded accent-outlined pill, transparent interior, no faint line/fill behind. (fix_grouped_set_offer_pill_style;
   was: superset = borderless faint text, extend = grey btn-outline — inconsistent.) */
.superset-connector:not(.is-joined) .superset-toggle,
.superset-connector .round-extend {
  opacity: 1;
  color: var(--primary);
  border: 1px solid var(--primary);
  border-radius: 999px;
  background: transparent;
  padding: 3px 12px; /* symmetric → label centred in the fixed line box (fix_offer_pill_vcenter) */
  font-size: 12px;
  width: auto;
  /* Fixed line box so ⮂ (fallback font) and '+' pills are identical height. (fix_offer_pill_lineheight) */
  line-height: 15px;
}
.superset-tag { font-size: 11px; font-weight: 700; letter-spacing: 0.06em; color: var(--primary); }
.superset-tag-btn { background: none; border: none; padding: 2px 4px; cursor: pointer; color: var(--primary); display: inline-flex; align-items: center; gap: 5px; }
/* All superset controls are bare <button>s — force the app font so they never fall back
   to the UA button font (the "two different fonts" the pill showed). (owner 2026-07-03) */
.superset-rail, .superset-tag-btn, .day-ss-btn, .superset-toggle, .superset-tag { font-family: inherit; }

/* JOINED pair, LOG VIEW (#assistance-list): the two cards are wrapped in a
   .superset-group so ONE accent rail spans both — a single 3px left border with the
   word 'superset' set vertically along it. No per-card edge, no brace. The rail is the
   unjoin control for an ad-hoc pair; a program-authored pair (.is-server) is read-only.
   (owner 2026-07-03: drop the { and the per-card edge — one bar spanning both, label
   vertical along it.) */
.superset-group {
  position: relative;
  border-left: 3px solid var(--primary);
  /* Curve the top & bottom ends inward, matching the rounded card edge the accent
     border used to sit on. (owner 2026-07-03) */
  border-top-left-radius: var(--radius);
  border-bottom-left-radius: var(--radius);
  padding-left: 26px;
  margin: 10px 0;
}
.superset-group-cards { display: flex; flex-direction: column; }
/* The last card's 12px bottom margin sits INSIDE the group, so the accent rail /
   border-left overshoots past the card. Drop it so the rail ends at the card edge.
   (owner 2026-07-04) */
.superset-group-cards > :last-child { margin-bottom: 0; }
.superset-rail {
  position: absolute; left: 0; top: 0; bottom: 0; width: 26px;
  display: flex; align-items: center; justify-content: center;
  writing-mode: vertical-rl; transform: rotate(180deg);
  background: none; border: none; padding: 0; cursor: pointer;
  font-size: 11px; font-weight: 700; letter-spacing: 0.08em;
  color: var(--primary);
}
.superset-group.is-server .superset-rail { cursor: default; }

/* PROGRAM LISTING (.session-preview): a JOINED pair renders as a vertical rail beside
   the two combined rows (via .superset-group, shared with the log view); the OFFER
   (not joined) sits on the divider line, pulled all the way to the RIGHT edge of the row.
   (owner 2026-07-04: vertical line beside the combined; the un-joined offer pill sits far right.)
   The connector is zero-height; its button is absolutely placed onto the boundary. */
.session-preview .superset-connector { position: relative; height: 0; margin: 0; overflow: visible; display: block; }
.session-preview .superset-connector > * {
  position: absolute; right: 0; top: 0; transform: translateY(-50%);
  background: var(--surface); padding-left: 8px;
}
/* The offer pill sits ON the row-boundary divider, pulled far right. Keep the divider line between
   exercises, but give the pill an OPAQUE background so it MASKS the line just behind itself — the line
   runs across the row and is cleanly interrupted where the pill sits, rather than removed or running
   through it. (owner 2026-07-08: "I want the line, just not where it runs behind the new buttons".) */
.session-preview .superset-connector .superset-toggle,
.session-preview .superset-connector .round-extend {
  font-size: 11px; padding: 2px 8px; background: var(--surface); line-height: 14px; /* symmetric → centred in fixed line box (fix_offer_pill_vcenter) */
}
/* Joined pair rail, sized down for the compact listing rows. */
.session-preview .superset-group { margin: 4px 0; padding-left: 22px; }
.session-preview .superset-rail { width: 22px; font-size: 11px; letter-spacing: 0.06em; }

@font-face { font-family: 'Inter'; font-style: normal; font-weight: 600; font-display: swap; src: url('../assets/fonts/inter-600.woff2') format('woff2'); }
@font-face { font-family: 'Roboto'; font-style: normal; font-weight: 400; font-display: swap; src: url('../assets/fonts/roboto-400.woff2') format('woff2'); }
@font-face { font-family: 'Roboto'; font-style: normal; font-weight: 600; font-display: swap; src: url('../assets/fonts/roboto-600.woff2') format('woff2'); }
@font-face { font-family: 'Poppins'; font-style: normal; font-weight: 400; font-display: swap; src: url('../assets/fonts/poppins-400.woff2') format('woff2'); }
@font-face { font-family: 'Poppins'; font-style: normal; font-weight: 600; font-display: swap; src: url('../assets/fonts/poppins-600.woff2') format('woff2'); }
@font-face { font-family: 'Lato'; font-style: normal; font-weight: 400; font-display: swap; src: url('../assets/fonts/lato-400.woff2') format('woff2'); }
@font-face { font-family: 'Lato'; font-style: normal; font-weight: 600; font-display: swap; src: url('../assets/fonts/lato-600.woff2') format('woff2'); }
@font-face { font-family: 'Montserrat'; font-style: normal; font-weight: 400; font-display: swap; src: url('../assets/fonts/montserrat-400.woff2') format('woff2'); }
@font-face { font-family: 'Montserrat'; font-style: normal; font-weight: 600; font-display: swap; src: url('../assets/fonts/montserrat-600.woff2') format('woff2'); }

/* Full-screen boot blocker (feat_client_bootstrap_handshake): shown when the API reports
   this app version is unsupported (update_required) or it's in maintenance. */
.app-blocker { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; padding: 24px; background: var(--bg); z-index: 9999; }
.app-blocker-card { max-width: 360px; text-align: center; }
.app-blocker-mark { font-size: 44px; margin-bottom: 12px; }
.app-blocker-card h1 { font-size: 22px; margin: 0 0 10px; color: var(--text); }
.app-blocker-card p { color: var(--text-secondary); line-height: 1.5; margin: 0; }

/* Training notes — borderless notepad affordance on the session header, each lift
   card and each set editor; .has-note marks a surface that carries a note, and the
   .note-dot flags a set row at a glance. (feat_training_notes_session_lift_set) */
.note-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; padding: 0; flex: none;
  background: transparent; border: none; border-radius: var(--radius-sm);
  /* Same colour as the app's other line/mask icons — the accent (--primary), which the flatten
     pipeline recolours along with them; not the muted --text-secondary it used to be, which read as
     the odd one out. (owner 2026-07-18: 'same colour as all the other icons if flat, otherwise its
     standard colour'. fix_workout_note_button_placement_and_expandable_lift_header) */
  color: var(--primary); cursor: pointer;
}
.note-btn:hover { color: var(--text); background: var(--surface-2); }
/* A note already exists: keep the accent glyph but sit it on a subtle chip so the "has content" state
   is still distinguishable now that the empty state is also accent-coloured. */
.note-btn.has-note { background: var(--surface-2); }

/* Expandable exercise-name header on the workout log cards (fix_workout_note_button_placement_and_expandable_lift_header).
   Collapsed summary row = name + note icon + status badges; the caret + a tap reveal the Info/Coaching
   links. Non-expandable (no caret, default cursor) when the exercise has no coaching links to show. */
.lift-header { margin-bottom: 8px; }
.lift-header > summary { list-style: none; display: flex; align-items: center; gap: 8px; cursor: pointer; }
.lift-header > summary::-webkit-details-marker { display: none; }
.lift-header[data-no-links] > summary { cursor: default; }
.lift-header-right { margin-left: auto; display: flex; align-items: center; gap: 6px; flex: none; }
.lift-header-caret { flex: none; color: var(--text-secondary); transition: transform 0.18s ease; }
.lift-header[open] .lift-header-caret { transform: rotate(90deg); }
.lift-header-links { display: flex; gap: 6px; padding: 8px 0 2px; flex-wrap: wrap; }
/* Assistance cards carry an absolute skip/reorder control in the top-right corner — reserve room so
   the caret + badges don't sit under it. */
.lift-header-assist > summary { padding-right: 34px; }
.note-dot {
  display: inline-block; width: 7px; height: 7px; margin-left: 5px;
  border-radius: 50%; background: var(--primary); vertical-align: middle;
}
.note-textarea {
  width: 100%; box-sizing: border-box; margin-top: 4px;
  padding: 10px 12px; background: var(--surface-2);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  color: var(--text); font: inherit; resize: vertical; min-height: 96px;
}
.note-modal .modal-actions { margin-top: 12px; }
