/* ═══════════════════════════════════════════════════════
   ShotLab TOUR — "Range" modern-athletic instrument skin
   Graphite + signal orange · Archivo display & body
   See docs/superpowers/specs/2026-09-08-athletic-redesign-design.md
   ═══════════════════════════════════════════════════════ */

/* ── Fonts — self-hosted, deliberately ─────────────────────────────────
   Archivo was loaded from Google Fonts. Every visitor's IP address was
   therefore sent to Google on first paint, with no consent and no way to
   refuse — the exact arrangement a German court found unlawful in LG
   München I, 20 January 2022 (Az. 3 O 17493/20), and a live risk for an
   EU-established operator. Self-hosting removes the transfer entirely
   rather than papering over it with a consent banner.

   It also fixes two things that were not legal problems: a PWA that
   promises offline use could not render its own typeface offline, and the
   CSP no longer needs to allow fonts.googleapis.com or fonts.gstatic.com.

   One variable file per subset, carrying weight 400-800 and width
   100-125%. The Expanded family is the same file pinned to width 125%,
   which is what "Archivo Expanded" is. latin-ext is not optional: it
   carries the Czech diacritics (c/d/e/n/r/s/t/u/z with hacek and ring). */
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 400 800;
  font-stretch: 100% 125%;
  font-display: swap;
  src: url('fonts/archivo-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 400 800;
  font-stretch: 100% 125%;
  font-display: swap;
  src: url('fonts/archivo-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Archivo Expanded';
  font-style: normal;
  font-weight: 400 800;
  font-stretch: 125%;
  font-display: swap;
  src: url('fonts/archivo-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Archivo Expanded';
  font-style: normal;
  font-weight: 400 800;
  font-stretch: 125%;
  font-display: swap;
  src: url('fonts/archivo-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* ── Reset ─────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
/* `hidden` MEANS hidden, whatever display an author rule sets. The browser's
   own [hidden]{display:none} has the lowest priority there is, so ANY class
   that sets display — `.settings-row{display:flex}` here — silently beats it.
   That is how a guest came to see "Sign out" and "Delete my account" (QC V25):
   the rows were correctly marked hidden and rendered anyway, and a guest who
   tapped Sign out reloaded the page and lost every unsaved session. A code
   path that sets `el.hidden = true` has to be able to rely on it.
   test/browser/render-scan.js fails if any [hidden] element renders. */
[hidden] { display: none !important; }

/* ── Design tokens — "Range" (modern-athletic instrument) ──────────────
   Near-monochrome chrome. Saturated colour is reserved for DATA: the club
   scale (CLUB_COLORS in app.js), the A–F grade stripes, and the
   real/withheld/caution/loss verdicts. :root = light; html.dark overrides.
   Names in the C1 set (see docs/superpowers/specs/2026-09-08-…) are frozen
   because app.js reads them inline in runtime-injected modals. */
:root {
  color-scheme: light;

  /* neutrals (light) */
  --bg:        #FBFBFC;
  --surface:   #FFFFFF;
  --surface2:  #F3F4F6;
  --surface3:  #ECEEF1;
  --line:      #E4E6EA;   /* the card-defining element — a 1px hairline, not a shadow */
  --border:    var(--line);
  --line-strong: #CDD0D6;
  --border-hi: var(--line-strong);
  --text:       #0E1116;
  --text-muted: #3F4653;
  --text-dim:   #5A616D;

  /* accent — signal orange */
  --accent:      #B03600;
  /* ink on a SATURATED FILL — accent, red or green. It flips with the theme
     because the fills do: a light theme darkens them so white reads, a dark
     theme lightens them so near-black reads. One value cannot serve both,
     which is why three rules used to hard-code #fff and fail in dark. */
  --on-fill:     #FFFFFF;
  --accent-ink:  var(--on-fill);   /* frozen alias — app.js reads this name inline */
  --accent-weak: rgba(176,54,0,.10);
  /* The select chevron. An SVG in a data URI is a separate document: it
     cannot read a var() and does not inherit currentColor, so the accent has
     to be written into it literally — which is why it is a TOKEN and not a
     literal buried in a rule. It was exactly that for the whole redesign,
     percent-encoded as %23 so the colour sweep never saw it, and it still
     carried the retired red-orange in both themes. colours-are-tokens.js now
     decodes %23 and pins this value against --accent. The media type carries
     no `;utf8` on purpose: a semicolon inside a token value truncates it for
     every scanner that splits declarations on one. */
  --chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%23B03600' stroke-width='1.6' stroke-linecap='square' stroke-linejoin='miter'%3E%3Cpolyline points='5 9 12 16 19 9'/%3E%3C/svg%3E");
  --accent-glow: var(--accent-weak);
  /* activity ramp — the heatmap's three middle steps. Level 0 is a day with
     NO shots, so it is a neutral surface, not the faintest accent: tinting it
     paints every idle day in the brand colour and the grid reads as activity
     everywhere. Level 4 is --accent at full strength. */
  --hm1: rgba(176,54,0,.22);
  --hm2: rgba(176,54,0,.45);
  --hm3: rgba(176,54,0,.70);
  --pine:    var(--accent); /* deprecated alias, still read by app.js */
  --forest:  #8F2A00;       /* accent :hover / pressed */

  /* data — verdict semantics (temperature-matched, one notch desaturated) */
  --green:       #006F49;   /* real / gain */
  --green-light: #025C3C;
  --green-glow:  rgba(0,111,73,.45);   /* the status dot's expanding ring */
  --withheld:    #6B7480;   /* gated / no answer — intentionally dull */
  --yellow:      #855705;   /* caution / tentative */
  --red:         #B52D2A;   /* loss / regression / danger */
  --turf:        var(--red);
  --blue:        #0162B2;   /* info, "modelled" tags, secondary links */

  /* geometry — hard edges; buttons are cut, not rounded */
  --radius:    0;
  --radius-md: 0;   /* frozen name (app.js modals); square */
  --radius-sm: 2px; /* chips / tiny badges only */
  --cut:       10px;
  --nav-h:     60px;
  --bottom-nav-h: 68px;
  --tap:       44px;

  /* tracking — uppercase is the whole reason these exist. 73 uppercase rules
     carried 20 different hand-typed values between them, and at the sizes they
     render, `.04em` on a .8rem label is 0.51px: a third of what the reference
     systems call the floor for machined-looking caps, and the reason the button
     labels read cramped beside the kickers.

     THREE STEPS, NOT TWO. The plan specified `--track-caps` and a negative
     `--track-display`, which is right for display type and wrong for display
     CAPS: tracking scales inversely with size, so the value that opens a .6rem
     label blows a 1.7rem heading apart. Using one caps value for both is the
     same error the plan names when it refuses one value for caps and display.
     So caps split by size, and --track-display stays for the non-uppercase
     display tier where Linear's negative pull actually applies. The same
     argument gives the scale a bottom rung: the .58rem mono tags carried
     .22em and .14em, and those were not arbitrary — at that size .09em is
     0.83px and reads as cramped as .04em does at .8rem. A two-rung scale
     would have flattened the one tier that was already right, which is the
     mistake this plan opened by accusing the labels of. */
  --track-caps-xs: .14em;   /* uppercase below .7rem — the mono micro-tags */
  --track-caps:    .09em;   /* uppercase labels .7rem to .95rem */
  --track-caps-lg: .03em;   /* uppercase at heading and display sizes */
  --track-display: -.02em;  /* the display tier, which is NOT uppercase */

  /* space + motion */
  --s1: 4px; --s2: 8px; --s3: 12px; --s4: 16px; --s5: 24px; --s6: 40px; --s7: 64px;
  --dur: .14s;

  --overlay: rgba(0,0,0,.6);   /* modal scrim — the six app.js-injected modals read this */

  /* elevation — overlays only; chrome is flat */
  --shadow-sm: none;
  --shadow-md: 0 8px 30px rgba(0,0,0,.12);
  --shadow-lg: 0 20px 60px rgba(0,0,0,.18);

  /* type */
  --font-display: 'Archivo Expanded', 'Archivo', system-ui, sans-serif;
  --font-body:    'Archivo', system-ui, sans-serif;
  --font-mono:    ui-monospace, 'SF Mono', 'Roboto Mono', monospace;
}

/* ── Base ──────────────────────────────────────────────── */
html { height: 100%; }
body {
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  font-size: 15px;
  line-height: 1.55;
  min-height: 100%;
  padding-bottom: var(--bottom-nav-h);
  padding-bottom: calc(var(--bottom-nav-h) + env(safe-area-inset-bottom));
  -webkit-font-smoothing: antialiased;
}
/* Two tiers, and the DISTANCE between them is the point.
   h2, h3 and .section-title are ONE step — the label tier, 0.8rem, weight
   600, uppercase. They are signposts; a section heading is not a display
   moment. Everything below them is body (15px), everything above them is a
   heading or the figure.
   What it replaced was five steps inside a factor of two — 2rem, 1.05rem,
   1rem, .8rem, .7rem over a 15px body — which is a gradient, not a
   hierarchy, and it left the app with no way to say "this number is the
   one that matters". The figure that cleared its floor is now said with
   .stat-hero, at 3.4rem. h1 sits above the label tier untouched: it is the
   view name and a navigational landmark, not a display moment.
   The two label trackings (.04em on h2/.section-title, .06em on h3) are
   carried through unchanged — tokenising tracking is a separate piece of
   work and rewriting these values here would silently move them. */
h1 { font-family: var(--font-display); font-size: 1.5rem; font-weight: 700; letter-spacing: var(--track-caps-lg); text-transform: uppercase; }
h2 { font-family: var(--font-display); font-size: .8rem;  font-weight: 600; text-transform: uppercase; letter-spacing: var(--track-caps); }
h3 { font-family: var(--font-display); font-size: .8rem;  font-weight: 600; text-transform: uppercase; letter-spacing: var(--track-caps); }
p  { text-wrap: pretty; }
a  { color: var(--accent); text-decoration: none; }
/* tabular numerals wherever data is shown */
.tnum, td, th { font-variant-numeric: tabular-nums; }
/* oversized display type steps up only on wider screens — render-scan checks
   393px for horizontal overflow (spec C6). The label tier does NOT step up:
   it is one size at every width, which is what makes it a tier. */
@media (min-width: 640px) {
  h1 { font-size: 2rem; }
}

/* ── Layout primitives (spec §7.1) ─────────────────────────────────────
   Small, composable, var()-driven. These replaced the 35 inline style=""
   blocks in index.html: an inline style cannot be re-themed, cannot answer
   a media query, and is invisible to every check that reads the stylesheet.
   No component styling belongs in here. */
.pad     { padding-inline: var(--s4); }
.pad-y   { padding-block: var(--s4); }
.pad-b   { padding-bottom: var(--s4); }
.stack > * + * { margin-top: var(--gap, var(--s4)); }
.cluster { display: flex; flex-wrap: wrap; gap: var(--gap, var(--s2)); align-items: center; }
.row-between { display: flex; align-items: center; justify-content: space-between; gap: var(--s3); }
.grid-auto {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(var(--col, 160px), 1fr));
  gap: var(--gap, var(--s3));
}
/* A link sitting inside a paragraph of body text. */
.link-inline { color: var(--accent); font-weight: 600; text-decoration: none; }
.link-inline:hover { text-decoration: underline; }
/* Explanatory copy under a view heading. */
.view-lede { color: var(--text-muted); font-size: .9rem; margin-bottom: var(--s5); }
/* The no-JS fallback. It has to stand on its own — nothing has run. */
.noscript-note { padding: var(--s6) var(--s5); text-align: center; }

/* Vertical rhythm, on the scale rather than in a style attribute. */
.mt-2 { margin-top: var(--s2); }
.mt-3 { margin-top: var(--s3); }
.mt-4 { margin-top: var(--s4); }
.mt-6 { margin-top: var(--s6); }
.mb-2 { margin-bottom: var(--s2); }
.mb-3 { margin-bottom: var(--s3); }
.mb-5 { margin-bottom: var(--s5); }
.mb-6 { margin-bottom: var(--s6); }
.w-full { width: 100%; }

/* ── Scroll motion (spec Task 15) ──────────────────────────────────────
   Three effects, and one rule that is not negotiable:

     Animate FROM a visible resting state, never TO one.

   Nothing in this section may leave an element at opacity:0,
   visibility:hidden, or translated off its own box in the state the page
   renders in before any JS has run. If the observer never fires — an engine
   without IntersectionObserver, a JS error, a print, a screen reader
   linearising the page, render-scan.js sampling a single frame — the page is
   ALREADY correct. Nothing arrives; things that are already there sharpen.

   The generic version has failed here once. `.section-block` carried a
   staggered `viewFadeIn` with eight nth-child delays; somebody later found
   content invisible for real users and killed it with an override titled
   "ensure content is always visible" (`animation: none !important`), and the
   delays then sat dead for months — because a thing that does not appear is
   indistinguishable from a thing that was never there. Do not reintroduce the
   stagger. test/suites/scroll-motion.js pins all of this.

   Effect 1 (chart draw-on) is JS-only and lives in ScrollMotion. */

/* Effect 2 — the view header condenses.
   DEVIATION FROM THE WRITTEN TASK, stated rather than quietly applied: the
   plan says the header condenses "into the nav", justified by a 393px
   viewport scrolling the title away. But `.top-nav` is `display:none` below
   768px — a phone has no top bar to condense into, and at >=768px the sticky
   nav already marks the active view, so a second sticky header there is
   chrome that says nothing twice. So the header condenses INTO ITSELF, and
   only on a phone, which is where the benefit was claimed.

   The negative inline margin exactly matches `.app-main`'s 1rem side padding,
   so the sticky band is full-bleed and the box is no wider than its parent's
   border edge — a sticky header that overflows is the 605px bug again. */
@media (max-width: 767px) {
  .view-header {
    position: sticky; top: 0; z-index: 90;
    background: var(--bg);
    margin-inline: -1rem; padding-inline: 1rem;
    transition: padding-block var(--dur) ease, box-shadow var(--dur) ease;
  }
  .view-title { transition: font-size var(--dur) ease; }
  html.sm-condensed .view-header {
    padding-block: var(--s2);
    box-shadow: 0 1px 0 var(--line);
  }
  html.sm-condensed .view-title { font-size: 1.05rem !important; }
}

/* Effect 3 — the section rule draws in.
   Deliberately on the 1px rule and NOT on the content: a border is decoration
   by definition, so if it never animates nothing is lost. The resting state is
   scaleX(1) and the keyframe has only a `from`, so it animates TO whatever the
   element already computes to. There is no pending class and nothing is ever
   hidden waiting for JS. */
@keyframes smRuleIn { from { transform: scaleX(0); } }
.section-block.sm-rule::before { animation: smRuleIn calc(var(--dur) * 2) ease-out; }

/* ── Icons — inline SVG sprite (spec §6.1) ─────────────────────────────
   1em square so an icon inherits the size of the text it sits beside;
   currentColor so it inherits the colour too. Squared caps/joins to match
   the chamfer. Set on the element, not the symbol, so a single sprite can
   be tinted per site. */
/* The sprite itself: zero-sized and out of flow. width/height="0" on the
   element already collapses it; this keeps it out of flow too. */
.icon-sprite { position: absolute; }
.icon {
  width: 1em; height: 1em; display: inline-block;
  vertical-align: -.125em; flex: none;
  fill: none; stroke: currentColor;
  stroke-width: 1.6; stroke-linecap: square; stroke-linejoin: miter;
}

/* ── Buttons — chamfered cut-corner geometry ──────────────────────────
   One silhouette: top-left + bottom-right corners sliced by --cut. Three
   weights. clip-path also clips a real border, so .btn-secondary draws its
   outline as a ::before layer sharing the clip. Focus is an outline drawn INSIDE
   the cut (clip-path clips an outline too — see the focus rules below). No
   shadows on buttons — chrome is flat. */
button {
  font-family: var(--font-body);
  cursor: pointer; border: none;
  font-size: .875rem; border-radius: 0;
  transition: background var(--dur), color var(--dur), outline-color var(--dur), filter var(--dur);
}
:where(.btn-primary, .btn-secondary, .btn-danger, .btn-ghost) {
  --cut: 10px;
  position: relative; isolation: isolate;
  display: inline-flex; align-items: center; justify-content: center; gap: .5ch;
  padding: .7rem 1.5rem; min-height: var(--tap);
  font-family: var(--font-display); font-weight: 600;
  font-size: .8rem; text-transform: uppercase; letter-spacing: var(--track-caps);
  clip-path: polygon(var(--cut) 0, 100% 0, 100% calc(100% - var(--cut)), calc(100% - var(--cut)) 100%, 0 100%, 0 var(--cut));
}
.btn-sm { --cut: 7px; padding: .45rem 1rem; min-height: 0; font-size: .72rem; }

.btn-primary { background: var(--accent); color: var(--accent-ink); }
.btn-primary:hover { background: var(--forest); }

.btn-danger { background: var(--red); color: var(--on-fill); }
.btn-danger:hover { filter: brightness(.92); }

/* border = the element's own background showing through a 1.5px inset ::before */
.btn-secondary { background: var(--line-strong); color: var(--text); }
.btn-secondary::before {
  content: ""; position: absolute; inset: 1.5px; z-index: -1;
  background: var(--surface); clip-path: inherit;
}
.btn-secondary:hover { background: var(--accent); }
.btn-secondary:hover::before { background: var(--surface); }

.btn-ghost { background: none; color: var(--text-muted); }
.btn-ghost:hover { color: var(--text); }

button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* The ring goes INSIDE wherever outside would be cut off (R20/V26). clip-path
   clips an outline like everything else, so every chamfered button drew its
   focus ring into the clipped-away region — invisible. And a control inside a
   box that clips (overflow hidden or auto) loses whichever edge crosses it:
   every Settings row, the drill tabs, the club chips. currentColor, because an
   inset ring sits on the control's own fill, and an accent ring on the accent
   primary button or an active tab is the same invisible ring by another route.
   render-scan.js measures every focusable control for both. */
:is(.settings-section, .drill-tabs, .filter-row, .tbl-wrap, .doc-table-wrap, .subnav, .fault-card) :focus-visible {
  outline-offset: -2px; outline-color: currentColor;
}
:is(.btn-primary, .btn-secondary, .btn-danger, .btn-ghost, .btn-icon):focus-visible {
  outline-offset: -5px; outline-color: currentColor;
}
:where(.btn-primary, .btn-secondary, .btn-danger, .btn-icon):active { transform: translateY(1px); }
button:disabled, button[disabled] {
  background: var(--surface2); color: var(--text-dim);
  cursor: not-allowed; filter: none;
}
.btn-secondary:disabled::before { background: var(--surface2); }

.btn-icon {
  --cut: 8px;
  background: var(--surface2); color: var(--text-muted);
  width: 36px; height: 36px; padding: 0; font-size: 1rem; line-height: 1;
  display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0;
  clip-path: polygon(0 0, calc(100% - var(--cut)) 0, 100% var(--cut), 100% 100%, 0 100%);
}
.btn-icon:hover { background: var(--surface3); color: var(--text); }
.btn-icon.btn-danger { background: transparent; color: var(--red); clip-path: none; }
.btn-icon.btn-danger:hover { background: var(--accent-weak); }

.btn-back {
  background: none; color: var(--text-muted);
  padding: .3rem 0; font-size: .78rem;
  text-transform: uppercase; letter-spacing: var(--track-caps);
  white-space: nowrap; display: inline-flex; align-items: center; gap: .35rem;
}
.btn-back:hover { color: var(--accent); }

/* ── Top nav (desktop) — flat, hairline, no fill ───────────────────────
   The bar used to be a three-stop gradient with a red glow under it, a
   diagonal stripe overlay and a bordered TOUR badge. That is four decorative
   layers on the one element that is on screen for the whole visit. It is now
   chrome: a surface, a hairline, and an accent tick on the view you are in. */
.top-nav {
  display: none; position: sticky; top: 0; z-index: 100;
  background: var(--surface);
  border-bottom: 1px solid var(--line);
  padding: 0 var(--s5);
  height: var(--nav-h);
  align-items: center; justify-content: space-between; gap: var(--s4);
  box-shadow: none;
}
.top-nav-brand { display: flex; align-items: baseline; gap: .5rem; }
.brand-icon { font-size: 1.15rem; color: var(--accent); align-self: center; }
.brand-name {
  font-family: var(--font-display);
  font-size: 1.15rem; font-weight: 800;
  color: var(--text); letter-spacing: var(--track-caps-lg); text-transform: uppercase;
}
.brand-tag {
  font-family: var(--font-mono);
  font-size: .58rem; font-weight: 600; letter-spacing: var(--track-caps-xs);
  color: var(--text-dim); text-transform: uppercase;
}
.top-nav-links { display: flex; align-items: center; gap: var(--s1); }
.top-nav-links .nav-link {
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-size: .72rem; font-weight: 500; letter-spacing: var(--track-caps);
  text-transform: uppercase;
  padding: .38rem .8rem;
  border-radius: 0; border-bottom: none;
  border-left: 2px solid transparent;
  transition: color var(--dur), border-color var(--dur);
}
.top-nav-links .nav-link:hover { color: var(--text); background: none; }
.top-nav-links .nav-link.active {
  color: var(--text); background: none;
  border-left-color: var(--accent);
}
.top-nav .btn-primary { margin-left: var(--s3); }

/* ── App main ──────────────────────────────────────────── */
.app-main { padding: 0 1rem 2rem; }

/* ── Views ─────────────────────────────────────────────── */
.view { display: none; padding-top: 1.25rem; }
/* No entrance animation. There was one — a viewFadeIn on every view and a
   staggered version on .section-block — and somebody later neutralised it
   with an "ensure content is always visible" override setting
   animation:none !important on both. That override is what you write when an
   entrance animation has left real content invisible for real users. Both
   halves are gone now rather than left cancelling each other out. */
.view.active { display: block; }
.view-header { display: flex; align-items: center; gap: .75rem; margin-bottom: 1.5rem; padding-block: .25rem; }
/* padding-BLOCK, not the `padding: .25rem 0` shorthand this used to be. The
   phone rule above gives the sticky header `margin-inline: -1rem;
   padding-inline: 1rem` to run it full-bleed — but it sits EARLIER in the
   file, and this later shorthand reset padding-inline to 0 at every width.
   The negative margin survived, so on a phone every view title sat flush
   against the screen edge at x=0 while the cards below it sat at 16px.
   cascade-overrides.js now fails on a media declaration a later base rule
   kills. */
.view-title { flex: 1; }
.section-sub {
  font-size: .7rem; color: var(--text-muted);
  font-weight: 400; text-transform: none; letter-spacing: 0;
}

/* ── Bottom nav ────────────────────────────────────────── */
.bottom-nav {
  position: fixed; bottom: 0; left: 0; right: 0;
  height: var(--bottom-nav-h);
  padding-bottom: env(safe-area-inset-bottom);
  background: var(--surface);
  border-top: 1px solid var(--line);
  display: flex; z-index: 100;
  box-shadow: none;
}
.bottom-nav-item {
  flex: 1; min-width: 0; background: none; color: var(--text-dim);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 3px; font-size: .58rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: var(--track-caps-xs);
  border-radius: 0; position: relative; transition: color .15s;
}
.bottom-nav-item:hover { color: var(--text-muted); }
.bottom-nav-item.active { color: var(--accent); }
/* A tick on the edge you came from, not a rounded pill under the label. */
.bottom-nav-item.active::before {
  content: '';
  position: absolute; top: 0; left: 18%; right: 18%;
  height: 2px; background: var(--accent);
  border-radius: 0;
}
.bnav-icon { font-size: 1.25rem; line-height: 1; display: flex; align-items: center; justify-content: center; }
.bnav-label { line-height: 1; white-space: nowrap; }

/* #fabImport is kept in the markup (its route handler and its id are still
   referenced) but not shown: it never had base styles, so it rendered as a
   static button at the foot of the document rather than floating anywhere,
   and both navs already carry an Import action a thumb away. */
.fab { display: none; }

/* ── Import — the one accent block in the bar ──────────────────────────
   A circle is the one shape this design system does not have. It is the
   same cut corner as .btn-primary, at nav scale. */
.bottom-nav-item[data-view="import"] .bnav-icon {
  --cut: 7px;
  background: var(--accent); color: var(--accent-ink);
  width: 38px; height: 32px; border-radius: 0;
  display: flex; align-items: center; justify-content: center;
  font-size: 1.15rem; font-weight: 700;
  box-shadow: none;
  clip-path: polygon(var(--cut) 0, 100% 0, 100% calc(100% - var(--cut)), calc(100% - var(--cut)) 100%, 0 100%, 0 var(--cut));
  transition: background var(--dur);
}
.bottom-nav-item[data-view="import"]:hover .bnav-icon {
  background: var(--forest); box-shadow: none; transform: none;
}
.bottom-nav-item[data-view="import"].active::before { display: none; }
.bottom-nav-item[data-view="import"],
.bottom-nav-item[data-view="import"].active { color: var(--text-dim); }

/* ── Empty state ───────────────────────────────────────── */
.empty-state {
  text-align: center; padding: 5rem 2rem;
  display: flex; flex-direction: column; align-items: center; gap: 1rem;
}
.empty-icon { font-size: 2.5rem; color: var(--text-dim); opacity: .55; line-height: 1; }
.empty-state h2 { font-size: 1.3rem; color: var(--text); }
.empty-state p { color: var(--text-muted); font-size: .8rem; max-width: 250px; }

/* ── Session list ──────────────────────────────────────── */
.session-list { list-style: none; display: flex; flex-direction: column; gap: .55rem; }

.session-card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-left-width: 3px;
  border-left-color: var(--line-strong);
  border-radius: var(--radius);
  padding: var(--s4);
  cursor: pointer;
  transition: border-color var(--dur), background var(--dur);
  display: flex; align-items: flex-start; justify-content: space-between; gap: .75rem;
  box-shadow: none;
}
/* No lift, no shadow: the row is a row. Hover moves the hairline, which is
   the only thing defining the card in the first place. */
.session-card:hover { border-color: var(--line-strong); background: var(--surface2); }

/* Grade-keyed left stripe. Five grades, four token colours: A and B are both
   "good" at different strengths, C and D are both "caution". The palette has
   no separate olive, and inventing one to keep five distinct stripes would be
   five colours carrying four states. */
.session-card[data-grade="A"] { border-left-color: var(--green); }
.session-card[data-grade="B"] { border-left-color: var(--green-light); }
.session-card[data-grade="C"] { border-left-color: var(--yellow); }
.session-card[data-grade="D"] { border-left-color: var(--yellow); }
.session-card[data-grade="F"] { border-left-color: var(--red); }
@keyframes badgePulse { 0%,85%,100%{opacity:1} 92%{opacity:.5} }

/* ── Import flow ───────────────────────────────────────── */
.import-step { display: none; }
.import-step.active { display: block; }

.drop-zone {
  background: var(--surface);
  border: 1.5px dashed var(--line-strong);
  border-radius: var(--radius);
  padding: var(--s7) var(--s5); text-align: center;
  transition: border-color var(--dur), background var(--dur);
  display: flex; flex-direction: column; align-items: center; gap: .875rem;
  cursor: pointer;
}
.drop-zone.dragover {
  border-color: var(--accent);
  border-style: solid;
  background: var(--accent-weak);
  box-shadow: none;
}
.drop-icon { font-size: 2rem; color: var(--text-dim); line-height: 1; }
.drop-label { font-size: 1rem; font-weight: 600; color: var(--text); }
.drop-sub { color: var(--text-muted); font-size: .7rem; }

.import-hint {
  color: var(--text-muted); font-size: .65rem;
  margin-top: 1rem; text-align: center;
  padding: .6rem .875rem;
  background: var(--surface2); border-radius: var(--radius);
  border: 1px solid var(--line);
}
.step-heading { font-family: var(--font-display); font-size: 1.15rem; font-weight: 700; text-transform: uppercase; letter-spacing: var(--track-caps-lg); margin-bottom: .25rem; }
.step-sub { color: var(--text-muted); font-size: .85rem; margin-bottom: 1rem; }
.step-actions { display: flex; gap: .75rem; margin-top: 1.5rem; }
.saving-state { text-align: center; padding: 4rem 2rem; color: var(--text-muted); }
.spinner {
  width: 32px; height: 32px;
  border: 2px solid var(--line);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin .7s linear infinite;
  margin: 0 auto 1rem;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ── Form ──────────────────────────────────────────────── */
.form-group { display: flex; flex-direction: column; gap: .4rem; margin-bottom: 1rem; }
.form-group label { font-size: .65rem; font-weight: 700; color: var(--text-muted); text-transform: uppercase; letter-spacing: var(--track-caps-xs); }
.label-opt { font-weight: 400; text-transform: none; letter-spacing: 0; }
.form-group input, .form-group textarea, .form-group select,
.input, .select {
  background: var(--surface2); border: 1px solid var(--line-strong);
  border-radius: var(--radius); color: var(--text);
  min-height: var(--tap);
  /* 16px, not .9rem. Safari on iOS zooms the whole page when a focused field is
     under 16px and does NOT zoom back out afterwards, so every tap on a form
     left the app scaled up and scrolled sideways. This is the single most
     common reason a decent layout "feels broken" on an iPhone. */
  padding: .7rem 1rem; font-family: var(--font-body); font-size: 16px; outline: none;
  max-width: 100%;
  transition: border-color .15s, box-shadow .15s;
}
.form-group input:focus, .form-group textarea:focus, .form-group select:focus,
.input:focus, .select:focus {
  border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-weak);
}
/* A grid item's default `min-width: auto` means it refuses to shrink below its
   content — so a <select> with a long option, or an input with a wide
   placeholder, forced its column past the 1fr track and pushed the whole import
   form to 496px on a 393px phone. `minmax(0, 1fr)` lets the track actually
   shrink; `min-width: 0` on the child does the same for its own contents. */
.form-row { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: .75rem; }
.form-row > .form-group { min-width: 0; }
/* Under ~380px two columns of a labelled input are unreadable anyway. */
@media (max-width: 380px) { .form-row { grid-template-columns: minmax(0, 1fr); } }
textarea { resize: vertical; }

/* ── Score banner ──────────────────────────────────────── */
.score-banner { margin-bottom: 1.5rem; }
.score-banner-content {
  background: var(--surface);
  border: 1px solid var(--line);
  border-top: 2px solid var(--accent);
  border-radius: var(--radius);
  padding: var(--s5);
  display: flex; align-items: center; gap: var(--s4);
  box-shadow: none;
  position: relative;
}

.score-ring {
  display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}
.score-ring svg { display: block; overflow: visible; }

.score-details { flex: 1; min-width: 0; }
/* This is the screen's one display figure, so it takes .stat-hero's scale
   rather than a fourth near-2rem size of its own. The class is applied at the
   call site in renderScoreBanner — and only on the branch where the mean has
   cleared Metrics.MIN_SHOTS_REPORT — so the tier has a referent a suite can
   find rather than a size that arrives by inheritance. See the Cards &
   section blocks section for the single definition. Only what is specific to
   the score banner lives here. */
.score-max { font-size: .8rem; font-weight: 400; color: var(--text-muted); }
.score-label { font-size: .7rem; color: var(--text-dim); margin-bottom: .6rem; text-transform: uppercase; letter-spacing: var(--track-caps); font-weight: 600; }
.score-bar-row { display: flex; height: 6px; border-radius: 0; overflow: hidden; gap: 1px; }
.score-pip { height: 100%; border-radius: 0; flex-shrink: 0; }
.score-breakdown { display: flex; flex-direction: column; gap: 4px; min-width: 112px; }
.score-bd-row { display: flex; align-items: center; gap: .5rem; }
.score-bd-label { color: var(--text-dim); min-width: 44px; font-size: .62rem; text-transform: uppercase; letter-spacing: var(--track-caps-xs); font-weight: 600; }
.score-bd-bar { height: 5px; border-radius: 0; min-width: 4px; transition: width .5s ease; }
.score-bd-n { color: var(--text-muted); min-width: 16px; text-align: right; font-size: .75rem; font-variant-numeric: tabular-nums; }

/* ── Ring arc animation ────────────────────────────────── */
/* The grade letter inside both score rings. It carried
   font-family="Outfit,sans-serif" as an SVG presentation attribute — Outfit
   being the typeface this redesign replaced, which nothing loads and which
   font-src 'self' would refuse — so the largest glyph on the session detail
   rendered in the browser's default sans beside Archivo Expanded headings.

   A class, not the literal 'Archivo Expanded': a literal in the markup is how
   this happened in the first place. An SVG <text> inherits font-family from
   CSS, so the attribute was never needed. Guarded by fonts-are-tokens.js. */
.grade-glyph { font-family: var(--font-display); }

.score-ring-arc { transition: stroke-dashoffset .95s cubic-bezier(.15,.65,.25,1); }
.scard-ring-arc { transition: stroke-dashoffset .8s  cubic-bezier(.15,.65,.25,1); }

/* ── Metrics strip ─────────────────────────────────────── */
.metrics-strip {
  display: grid; grid-template-columns: repeat(3,1fr);
  gap: .55rem; margin-bottom: 1.5rem;
}
.metric-card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 1.15rem .85rem .9rem;
  position: relative; overflow: hidden;
  transition: border-color var(--dur);
}
/* The rule at the top is the cell's reading — --mc is set per metric from
   its verdict, so it stays coloured. Everything else about the cell is not. */
.metric-card::before {
  content: '';
  position: absolute; top: 0; left: 0; right: 0; height: 2px;
  background: var(--mc, var(--line-strong));
  border-radius: 0;
}
.metric-card:hover { border-color: var(--line-strong); }

.metric-label { font-size: .62rem; font-weight: 600; color: var(--text-dim); text-transform: uppercase; letter-spacing: var(--track-caps-xs); margin-bottom: .2rem; }
.metric-value {
  font-family: var(--font-display);
  font-size: 1.75rem; font-weight: 700; line-height: 1.05;
  letter-spacing: -.01em; font-variant-numeric: tabular-nums; color: var(--text);
}
.metric-unit { font-size: .4em; font-weight: 600; opacity: .5; margin-left: 1px; }
.metric-delta { font-size: .68rem; color: var(--text-muted); margin-top: .15rem; }
.metrics-strip-club { font-size: .75rem; color: var(--text-muted); margin: 0 0 var(--s2); }

/* ── Club filter ───────────────────────────────────────── */
.filter-row {
  display: flex; align-items: center; gap: .5rem;
  margin-bottom: 1.5rem; overflow-x: auto;
  padding-bottom: 4px; scrollbar-width: none;
}
.filter-row::-webkit-scrollbar { display: none; }
.filter-label { font-size: .62rem; color: var(--text-muted); flex-shrink: 0; font-weight: 700; text-transform: uppercase; letter-spacing: var(--track-caps-xs); white-space: nowrap; }
.filter-chips { display: flex; gap: .35rem; flex-wrap: nowrap; }
/* A tab, not a pill. The active one is marked by an accent rule under it —
   the same mark the nav uses — rather than by a filled capsule, so a row of
   chips reads as one control instead of as a row of buttons. */
.chip {
  background: none; border: 1px solid transparent;
  border-bottom: 2px solid transparent;
  border-radius: 0; padding: .35rem .7rem;
  font-size: .73rem; font-weight: 600; cursor: pointer;
  white-space: nowrap; color: var(--text-muted);
  transition: color var(--dur), border-color var(--dur);
}
.chip:hover { background: none; color: var(--text); border-bottom-color: var(--line-strong); }
.chip.active {
  background: none; color: var(--text);
  border-bottom-color: var(--accent);
  box-shadow: none;
}

/* ── Cards & section blocks (spec §7.3) ────────────────────────────────
   A card is defined by a hairline, not by a shadow. The eight staggered
   fade-in delays below were already dead — a later "content is always
   visible" override set animation:none on .section-block — so they are gone
   rather than left looking like a decision. */
.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: var(--s4);
  box-shadow: none;
}

.section-block {
  position: relative;
  margin-bottom: var(--s6);
  padding-top: var(--s5);
}
/* The top rule is a pseudo-element rather than a border so Task 15 can draw
   it in. Its resting state is the DRAWN one — see the scroll-motion section. */
.section-block::before {
  content: ''; position: absolute; inset: 0 0 auto 0; height: 1px;
  background: var(--line); transform-origin: left center;
}
/* The first block under a view header already has the header's rule above it. */
.view-header + .section-block,
.section-block:first-child { padding-top: 0; }
.view-header + .section-block::before,
.section-block:first-child::before { content: none; }

/* The label tier — the same step as h2 and h3, deliberately. A section
   heading names what is below it; it does not compete with it. */
.section-title {
  font-family: var(--font-display);
  font-size: .8rem; font-weight: 600; color: var(--text);
  text-transform: uppercase; letter-spacing: var(--track-caps);
  margin-bottom: var(--s3);
  display: flex; align-items: center; gap: .5rem;
}

/* A small upper-case label above a heading or a number. Half a step BELOW
   the label tier on purpose: it is the one label that sits directly above a
   figure, so it has to read as an attribute of the number rather than as a
   heading that happens to be there. */
.kicker {
  font-family: var(--font-body);
  font-size: .7rem; font-weight: 600; color: var(--text-dim);
  text-transform: uppercase; letter-spacing: var(--track-caps);
  display: block;
}

/* THE DISPLAY TIER — the measured figure, and the whole point of the type
   scale being two tiers instead of five steps. Archivo Expanded at weight
   800, tracking pulled negative, tabular so a column of them does not jitter.

   It was defined here and referenced by NOTHING for the life of the redesign:
   a display tier with no referent, which is the same defect as a gate nothing
   calls. The session-detail score banner carries the class now, on the branch
   where its mean has cleared its sample floor, and nothing reaches it that has
   not (test/suites/display-tier.js fails if a call site is not gated that way):

     - the session-detail score banner — one figure, the screen's protagonist.

   The yardage book's stock carry was a second call site and is NOT one. Carry
   is not a measurement in this app: it is a model output the monitor computes
   from launch conditions, and D3 names a modelled carry among the figures that
   do not enter this tier. The workover plan's Task 2 listed the carry column
   as a site and so contradicted the constraint it was written under; the
   constraint is the authority and the plan is the argument, so the column went
   back to its own size. Nothing was promoted in its place — a "featured carry"
   would have been an invented figure, and which club got it an invented choice.

   D3: the floor is not decoration. A club under Metrics.MIN_SHOTS_REPORT, a
   withheld dispersion figure, a modelled number with no label — none of them
   may enter this tier. A design rule enforcing a measurement rule.

   .score-number is listed here rather than given its own copy of these
   values: it IS the hero numeral on the score banner, and two rules setting
   one size is how a type scale drifts. */
.stat-hero, .score-number {
  font-family: var(--font-display);
  font-weight: 800; font-size: 2.4rem; line-height: 1;
  letter-spacing: -.02em; color: var(--text);
  font-variant-numeric: tabular-nums;
  display: block;
}
@media (min-width: 640px) { .stat-hero, .score-number { font-size: 3.4rem; } }

/* ── Swing DNA ─────────────────────────────────────────── */
.dna-grid { display: flex; flex-direction: column; gap: .4rem; }
.dna-pill {
  display: flex; align-items: center; gap: .75rem;
  background: var(--surface);
  border: 1.5px solid var(--border);
  border-left: 4px solid var(--border-hi);
  border-radius: var(--radius-sm);
  padding: .65rem .9rem;
  transition: background .15s, box-shadow .15s;
}
.dna-pill:hover { background: var(--surface2); box-shadow: var(--shadow-sm); }
.dna-pill.tone-good { border-left-color: var(--green); }
.dna-pill.tone-ok   { border-left-color: var(--yellow); }
.dna-pill.tone-bad  { border-left-color: var(--red); }
.dna-icon  { font-size: 1rem; flex-shrink: 0; }
.dna-text  { flex: 1; min-width: 0; }
.dna-cat   { font-size: .6rem; color: var(--text-dim); text-transform: uppercase; letter-spacing: var(--track-caps-xs); font-weight: 700; }
.dna-val   { font-size: .9rem; font-weight: 600; color: var(--text); margin-top: 2px; }

/* ── Charts ────────────────────────────────────────────── */
.chart-card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: var(--s4);
  box-shadow: none;
}
.chart-card h3 {
  font-size: .7rem; color: var(--text-dim);
  text-transform: uppercase; letter-spacing: var(--track-caps);
  margin-bottom: var(--s3); font-weight: 600;
  display: flex; align-items: center; gap: .4rem;
}
.dispersion-card { padding-bottom: 1.5rem; }
.dispersion-card canvas { height: 280px !important; }
.gapping-card canvas { height: 160px !important; }
canvas { max-height: 180px; }

/* ── Club dot ──────────────────────────────────────────── */
/* A square swatch, not a dot: it is a key to a chart series, and the charts
   in this app draw square bars and square-capped lines. */
.club-dot {
  display: inline-block; width: 8px; height: 8px;
  border-radius: 0; margin-right: 6px;
  vertical-align: middle; flex-shrink: 0;
}

/* ── Launch windows ────────────────────────────────────── */
#launchWindows table { font-size: .78rem; }
#launchWindows th { font-size: .62rem; }
#launchWindows td small { color: var(--text-muted); }

/* ── Fault cards ───────────────────────────────────────── */
.fault-list { display: flex; flex-direction: column; gap: .875rem; }
/* DELIBERATELY EMPTY, and it must stay in the sheet. `.fault-category` is a
   grouping wrapper that needs no style of its own, but class-is-wired.js
   requires every class the markup applies to have a rule or a named reason —
   this rule IS that declaration. A sweep that deletes empty rules will take
   it out and the suite will fail; that already happened once. */
.fault-category {}
.fault-cat-label {
  font-size: .6rem; font-weight: 700; text-transform: uppercase; letter-spacing: var(--track-caps-xs);
  color: var(--text-muted); margin-bottom: .5rem;
  padding-left: .5rem; border-left: 2px solid var(--border-hi);
}
.fault-card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-left: 3px solid var(--yellow);
  border-radius: var(--radius); overflow: hidden; margin-bottom: .4rem;
  transition: border-color var(--dur);
}
.fault-card:hover { border-color: var(--line-strong); }
.fault-card.severity-high { border-left-color: var(--red); }
.fault-card.severity-low  { border-left-color: var(--blue); }
.fault-header {
  display: flex; align-items: center; justify-content: space-between;
  padding: .9rem 1rem; cursor: pointer; gap: .5rem;
  transition: background .15s;
}
.fault-header:hover { background: var(--surface2); }
.fault-header-left { display: flex; align-items: center; gap: .65rem; flex: 1; min-width: 0; }
.fault-icon { font-size: .95rem; flex-shrink: 0; }
.fault-name { font-weight: 600; font-size: .875rem; color: var(--text); }
.fault-count { font-size: .7rem; color: var(--text-muted); margin-top: 1px; }
.fault-toggle {
  color: var(--text-dim); font-size: .75rem;
  transition: transform .25s ease; flex-shrink: 0; width: 20px; text-align: center;
}
.fault-card.open .fault-toggle { transform: rotate(180deg); }
.fault-body { display: none; padding: 0 1rem .9rem; }
.fault-card.open .fault-body { display: block; animation: bodySlide .2s ease; }
@keyframes bodySlide {
  from { opacity: 0; transform: translateY(-5px); }
  to   { opacity: 1; transform: translateY(0); }
}
.fault-desc {
  font-size: .84rem; color: var(--text-muted); margin-bottom: .875rem;
  line-height: 1.65; border-top: 1px solid var(--line);
  padding-top: .875rem; text-wrap: pretty;
}
.fault-section-title { font-size: .6rem; font-weight: 700; text-transform: uppercase; letter-spacing: var(--track-caps-xs); color: var(--text-dim); margin: .875rem 0 .4rem; }
.fault-causes { padding-left: 1.2rem; }
.fault-causes li { font-size: .8rem; color: var(--text-muted); margin-bottom: .25rem; line-height: 1.6; }
.fault-drills { display: flex; flex-direction: column; gap: .5rem; }

/* ── The inset drill card ────────────────────────────────────
   THIS IS A DIFFERENT COMPONENT FROM `.drill-card`, and for the whole life
   of the redesign it was spelled the same. `.drill-card` is declared again
   ~1,500 lines below this — a standalone, tappable recommendation card on
   --surface with an accent left rule — and the later rule won every one of
   these declarations, so what a fault card actually rendered was a pointer
   cursor on something that is not tappable, an accent rule it never asked
   for, and `margin-bottom: .8rem` stacked on top of this flex `gap`.

   The two are not a duplicate to merge. One sits INSIDE a fault card and has
   to read as inset against it (--surface2 on --surface, tighter padding, no
   left rule); the other sits on the page. So this one gets its own name and
   is applied alongside the base, and the cascade stops being the thing that
   decides which component you get. The fault-drill render site is the only
   caller — the other three want the standalone treatment they were already
   getting by accident. */
.drill-card--inset {
  background: var(--surface2); border: 1px solid var(--line);
  border-left-width: 1px; border-left-color: var(--line);
  border-radius: var(--radius); padding: .7rem .9rem;
  margin-bottom: 0; cursor: default;
  transition: background var(--dur);
}
.drill-card--inset:hover { background: var(--surface3); border-color: var(--line); }
.drill-name { font-size: .85rem; font-weight: 700; margin-bottom: .25rem; color: var(--text); }
.drill-card--inset .drill-desc { font-size: .78rem; color: var(--text-muted); line-height: 1.65; text-wrap: pretty; }
.fault-optimal { font-size: .75rem; color: var(--accent); margin-top: .75rem; font-weight: 600; }
.fault-shots { font-size: .7rem; color: var(--text-dim); margin-top: .4rem; }
.no-faults { color: var(--green); font-size: .875rem; font-weight: 500; }

/* ── Benchmarking ──────────────────────────────────────── */
.bench-ref { color: var(--text-dim); font-size: .63rem; }

/* ── Tables ────────────────────────────────────────────── */
.table-wrap {
  overflow-x: auto; border-radius: var(--radius);
  border: 1px solid var(--line);
  -webkit-overflow-scrolling: touch;
}
table { width: 100%; border-collapse: collapse; font-size: .8rem; }
/* Instrument table: the header is a label row, not a filled band. The rule
   under it is the heaviest line in the table because it is the only one that
   separates two different kinds of thing. */
th {
  background: var(--surface); color: var(--text-dim);
  text-align: left; padding: .6rem .875rem;
  font-weight: 600; font-size: .7rem;
  text-transform: uppercase; letter-spacing: var(--track-caps);
  border-bottom: 1.5px solid var(--line-strong);
  white-space: nowrap; line-height: 1.4;
}
/* Only a header that sorts looks like it does something (R4). Every <th> in
   the app used to take a pointer and a hover, and one table sorts. */
th[data-field] { cursor: pointer; user-select: none; }
th[data-field]:hover { color: var(--text); background: var(--surface2); }
/* Tokens, not `inherit`: a button does not inherit colour by default, and an
   inherited colour is one contrast.js cannot measure. Same values as the th. */
.th-sort { background: none; border: none; padding: 0; font: inherit; color: var(--text-dim);
  text-transform: inherit; letter-spacing: inherit; text-align: inherit; cursor: pointer; }
th[data-field]:hover .th-sort { color: var(--text); }
.shot-open { background: none; border: none; padding: 0; font: inherit; color: var(--text); cursor: pointer;
  min-width: 1.5rem; text-align: left; }
.session-open { background: none; border: none; padding: 0; font-family: inherit; text-align: left; cursor: pointer; }
td {
  padding: .55rem .875rem; border-top: 1px solid var(--line);
  white-space: nowrap; color: var(--text);
  font-variant-numeric: tabular-nums;
}
tr:hover td { background: var(--surface2); }
/* The row's score as a mark on its FIRST cell. Written as `td`, it drew a
   coloured rule down the left of every cell, so the table read as a grid of
   green, yellow and red columns. */
.row-good td:first-child { border-left: 2px solid var(--green); }
.row-ok   td:first-child { border-left: 2px solid var(--yellow); }
.row-bad  td:first-child { border-left: 2px solid var(--red); }
.shot-score {
  font-family: var(--font-display);
  font-weight: 800; font-size: .9rem; font-variant-numeric: tabular-nums;
}
.status-dot {
  display: inline-block; width: 6px; height: 6px;
  border-radius: 50%; margin-right: 4px; flex-shrink: 0; vertical-align: middle;
}
.status-dot.green  { background: var(--green); animation: dotPulse 2.4s ease-in-out infinite; }
.status-dot.yellow { background: var(--yellow); }
.status-dot.red    { background: var(--red); }
.status-dot.na     { background: var(--border-hi); }
@keyframes dotPulse {
  0%,100% { box-shadow: 0 0 0 0 var(--green-glow); }
  50%     { box-shadow: 0 0 0 3px transparent; }
}

/* ── Detail header ─────────────────────────────────────── */
.detail-header-info { flex: 1; min-width: 0; }
.detail-notes {
  font-size: .78rem; color: var(--text-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-top: 2px;
}

/* ── Progress ──────────────────────────────────────────── */
.progress-filter { max-width: 220px; margin-bottom: 1.25rem; }
.charts-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: var(--s3); }

/* ── Trend box ─────────────────────────────────────────── */
.trend-box {
  background: var(--surface); border: 1px solid var(--line);
  border-top: 2px solid var(--accent);
  border-radius: var(--radius); padding: var(--s4);
  margin-bottom: var(--s5); box-shadow: none;
}
.trend-heading {
  font-size: .7rem; font-weight: 600; text-transform: uppercase;
  letter-spacing: var(--track-caps); color: var(--text-dim); margin-bottom: .875rem;
}
.trend-row { display: flex; align-items: center; gap: .65rem; padding: .4rem 0; border-top: 1px solid var(--border); font-size: .82rem; }
.trend-row:first-of-type { border-top: none; }
.trend-icon { font-size: .875rem; min-width: 1rem; text-align: center; flex-shrink: 0; }
.trend-label { flex: 1; color: var(--text-muted); font-size: .82rem; }
.trend-val { font-weight: 700; font-size: .82rem; font-variant-numeric: tabular-nums; }
.trend-positive .trend-icon, .trend-positive .trend-val { color: var(--green); }
.trend-negative .trend-icon, .trend-negative .trend-val { color: var(--red); }
.trend-neutral  .trend-icon, .trend-neutral  .trend-val { color: var(--text-muted); }

/* ── Settings ──────────────────────────────────────────── */
.settings-list { display: flex; flex-direction: column; gap: 1.5rem; }
.settings-section { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; box-shadow: none; }
/* A label row, not a filled band. Eight solid accent bars stacked down the
   settings page made the page look like eight warnings. */
.settings-section-title {
  font-size: .7rem; font-weight: 600; color: var(--text-dim);
  text-transform: uppercase; letter-spacing: var(--track-caps);
  padding: .7rem 1rem;
  background: var(--surface2);
  border-bottom: 1px solid var(--line);
}
.settings-row {
  width: 100%; display: flex; align-items: center; justify-content: space-between;
  padding: .9rem 1rem; background: none; color: var(--text); text-align: left;
  border-top: 1px solid var(--line); font-size: .875rem; transition: background var(--dur);
  min-height: var(--tap);
}
.settings-row { cursor: pointer; }
/* One row in the list is a real link to a real page, not a modal opener. */
a.settings-row { text-decoration: none; }
.settings-row:first-of-type { border-top: none; }
.settings-row:not(.static):hover { background: var(--surface2); }
.settings-row.static { cursor: default; }
.settings-row.danger { color: var(--red); }
.settings-arrow { color: var(--text-dim); font-size: .8rem; }
.settings-value { color: var(--text-muted); font-size: .8rem; text-align: right; max-width: 60%; }

/* ── Insights / Coach's notes ──────────────────────────── */
.insights-card {
  background: var(--surface);
  border: 1.5px solid var(--border);
  border-top: 3px solid var(--pine);
  border-radius: var(--radius);
  padding: 1.1rem 1.25rem; margin-bottom: 1.5rem;
  box-shadow: var(--shadow-sm);
}
.insights-card:empty { display: none; }
.insights-head { display: flex; align-items: center; gap: .5rem; margin-bottom: .875rem; }
.insights-icon { font-size: 1rem; }
.insights-title { font-size: .62rem; font-weight: 700; text-transform: uppercase; letter-spacing: var(--track-caps-xs); color: var(--pine); }
.insights-cols { display: flex; flex-direction: column; gap: 1rem; }
.insights-block { flex: 1; }
.insights-label { font-size: .68rem; font-weight: 700; text-transform: uppercase; letter-spacing: var(--track-caps-xs); margin-bottom: .4rem; }
.insights-label.good { color: var(--green); }
.insights-label.bad  { color: var(--yellow); }
.insights-list { list-style: none; display: flex; flex-direction: column; gap: .4rem; }
.insights-list li {
  font-size: .84rem; color: var(--text); line-height: 1.55;
  padding-left: .6rem; border-left: 2px solid var(--border);
  text-wrap: pretty; transition: border-color .15s, background .15s;
}
.insights-list li:hover { border-left-color: var(--pine); background: var(--surface2); padding-right: .4rem; border-radius: 0 4px 4px 0; }

/* ── Dispersion stats ──────────────────────────────────── */
.dispersion-stats {
  display: grid; grid-template-columns: repeat(3,1fr);
  gap: .5rem; margin-top: .875rem;
}
.disp-stat {
  background: var(--surface); border: 1.5px solid var(--border);
  border-radius: var(--radius-sm); padding: .65rem .5rem; text-align: center;
  transition: border-color .15s, box-shadow .15s;
}
.disp-stat:hover { border-color: var(--pine); box-shadow: var(--shadow-sm); }
.disp-stat-val {
  font-family: var(--font-display);
  font-size: 1.05rem; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--text);
}
.disp-stat-label { font-size: .6rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: var(--track-caps-xs); margin-top: 3px; }

/* ── Practice plan ─────────────────────────────────────── */
.practice-plan { display: flex; flex-direction: column; gap: .6rem; }
.plan-intro { font-size: .84rem; color: var(--text-muted); margin-bottom: .5rem; line-height: 1.55; }
.plan-item {
  display: flex; gap: .875rem;
  background: var(--surface); border: 1.5px solid var(--border);
  border-left: 4px solid var(--yellow);
  border-radius: var(--radius); padding: .9rem 1rem;
  transition: background .15s, transform .15s;
}
.plan-item:hover { background: var(--surface2); transform: translateX(2px); }
.plan-item.severity-high { border-left-color: var(--red); }
.plan-item.severity-low  { border-left-color: var(--blue); }
.plan-num {
  width: 24px; height: 24px; border-radius: 50%;
  background: var(--accent); color: var(--accent-ink);
  display: flex; align-items: center; justify-content: center;
  font-size: .68rem; font-weight: 800; flex-shrink: 0;
  font-family: var(--font-mono);
}
.plan-body { flex: 1; min-width: 0; }
.plan-head { display: flex; justify-content: space-between; align-items: center; font-weight: 700; font-size: .875rem; margin-bottom: .4rem; gap: .5rem; }
.plan-min { color: var(--pine); font-size: .72rem; white-space: nowrap; font-weight: 700; }
.plan-drill { font-size: .8rem; color: var(--text-muted); line-height: 1.6; text-wrap: pretty; }

/* Practice-plan evidence line: how many shots the fault actually appeared on,
   so a "tentative" call is visibly weaker than a confirmed one. */
.plan-evidence{
  margin-top:.35rem; font-size:.72rem; color:var(--text-muted);
  font-variant-numeric:tabular-nums; letter-spacing:.01em;
}
.plan-note{
  margin-top:.8rem; padding:.6rem .75rem; font-size:.78rem; line-height:1.45;
  color:var(--text-dim); background:var(--surface2);
  border-left:2px solid var(--pine); border-radius:var(--radius-sm);
}

/* Measurement caveats — deliberately shown ABOVE prescriptions, because they
   decide whether the prescription is admissible at all. */
.caveat-block{
  margin:0 1rem 1rem; padding:.85rem 1rem;
  background:var(--surface2); border-left:3px solid var(--yellow);
  border-radius:var(--radius-sm);
}
.caveat-head{
  font-size:.7rem; letter-spacing: var(--track-caps); text-transform:uppercase;
  font-weight:700; color:var(--text-dim); margin-bottom:.5rem;
}
.caveat-item{
  font-size:.82rem; line-height:1.5; color:var(--text);
  padding-left:.9rem; position:relative; margin-bottom:.45rem;
}
.caveat-item:last-child{margin-bottom:0}
.caveat-item::before{content:'·'; position:absolute; left:.15rem; color:var(--yellow); font-weight:700}
.form-hint{
  margin:-.3rem 0 .8rem; font-size:.75rem; line-height:1.45; color:var(--text-muted);
}

.sm-pm{color:var(--text-muted); font-size:.78em; font-variant-numeric:tabular-nums}
.sm-warn{color:var(--yellow); font-size:.72rem; font-weight:600}
.sm-note{color:var(--text-muted); font-size:.72rem; font-style:italic}

.check-row{display:flex;align-items:flex-start;gap:.55rem;cursor:pointer;font-size:.85rem;line-height:1.4}
.check-row input{margin-top:.15rem;flex:0 0 auto}

.tbl-wrap{overflow-x:auto;margin:.6rem 0 1rem}
.ref-tbl{width:100%;border-collapse:collapse;font-size:.8rem;min-width:420px}
.ref-tbl th{text-align:left;font-size:.68rem;letter-spacing: var(--track-caps-xs);text-transform:uppercase;
  color:var(--text-dim);font-weight:700;padding:.4rem .6rem .4rem 0;border-bottom:1px solid var(--border)}
.ref-tbl td{padding:.5rem .6rem .5rem 0;border-bottom:1px solid var(--border);
  color:var(--text);vertical-align:top;font-variant-numeric:tabular-nums}
.ref-tbl tr.unmeasured td{color:var(--text-muted)}
.ref-src{font-size:.68rem;color:var(--text-muted);margin-top:.15rem;font-variant-numeric:normal}
.ref-note{font-size:.78rem;line-height:1.5;color:var(--text-dim);margin:.3rem 0 .6rem}

.probe-block{margin:0 1rem 1rem;padding:.85rem 1rem;background:var(--surface2);
  border-left:3px solid var(--pine);border-radius:var(--radius-sm)}
.probe-block.pending{border-left-color:var(--text-muted)}
.probe-head{font-size:.7rem;letter-spacing: var(--track-caps);text-transform:uppercase;font-weight:700;
  color:var(--text-dim);margin-bottom:.5rem}
.probe-item{position:relative;padding-left:1rem;font-size:.83rem;line-height:1.55;
  color:var(--text);margin-bottom:.45rem}
.probe-item:last-child{margin-bottom:0}
.probe-dot{position:absolute;left:0;top:.5rem;width:7px;height:7px;border-radius:50%;
  background:var(--text-muted)}
.outcome-retained .probe-dot{background:var(--green)}
.outcome-regressed .probe-dot{background:var(--red)}
.outcome-no-change .probe-dot{background:var(--yellow)}

/* ── Dispersion tail audit ──────────────────────────────
   The one place in the app that shows a strokes figure, so it is also the one
   place where the caveats are part of the component rather than a footnote. */
.tail-block{margin:.875rem 0 0;padding:.85rem 1rem;background:var(--surface2);
  border-left:3px solid var(--pine);border-radius:var(--radius-sm)}
.tail-block.pending{border-left-color:var(--text-muted)}
.tail-head{font-size:.7rem;letter-spacing: var(--track-caps);text-transform:uppercase;font-weight:700;
  color:var(--text-dim);margin-bottom:.6rem;display:flex;justify-content:space-between;gap:.5rem}
.tail-n{color:var(--text-muted);font-weight:600;letter-spacing:.04em;font-variant-numeric:tabular-nums}
.tail-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:.5rem;margin-bottom:.7rem}
.tail-item{position:relative;padding-left:1rem;font-size:.83rem;line-height:1.55;
  color:var(--text);margin-bottom:.5rem}
.tail-item::before{content:'·';position:absolute;left:.2rem;color:var(--pine);font-weight:700}
.tail-item.heavy::before{color:var(--yellow)}
.tail-note{font-size:.75rem;line-height:1.5;color:var(--text-muted);margin-bottom:.4rem}
.tail-note:last-child{margin-bottom:0}
/* Accessibility. These replace three tokens the JS used to set that no rule
   ever read: --contrast, --animation-duration, and a `keyboard-focus` body
   class. A media query and :focus-visible do the same jobs without JS, and
   unlike the tokens they actually apply. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important; animation-iteration-count: 1 !important;
    transition-duration: .01ms !important; scroll-behavior: auto !important;
  }
}
:focus-visible {
  outline: 2px solid var(--pine); outline-offset: 2px; border-radius: 2px;
}
/* The page behind an open dialog must not scroll under it. */
body.modal-open { overflow: hidden; }

.import-error{margin:.75rem 0 .25rem;padding:.7rem .85rem;font-size:.83rem;line-height:1.55;
  color:var(--text);background:var(--surface2);border-left:3px solid var(--red);
  border-radius:var(--radius-sm);text-wrap:pretty}
.fault-causes-body li{color:var(--text-muted)}
/* Drills that are body feels get the same visual demotion the body causes get:
   still there, plainly less load-bearing than the ones the ball checks. */
.fault-drills-feel .drill-card--inset{opacity:.82;border-left:2px solid var(--yellow)}
/* A club under the sample floor keeps its row — it is still in the bag — but
   prints what it needs instead of a number that would read like a yardage. */
.yard-thin td{color:var(--text-muted);font-style:italic}
.yard-thin td strong{font-style:normal}
/* A record that had a higher reading screened out of it says so on the card —
   the shot is not hidden, it is flagged for the golfer to go and look at. */
.record-note{margin-top:.35rem;font-size:.68rem;line-height:1.4;color:var(--text-muted);text-wrap:pretty}
/* The quick-stat row names the club it is about. Pooled across a bag those
   numbers measure which clubs you hit, not how you hit them. */
.quick-stats-label{margin:.3rem 0 .2rem;text-align:center;font-size:.7rem;letter-spacing: var(--track-caps);
  color:var(--text-muted);text-transform:uppercase}
.quick-stat-wide{flex:3 1 auto}
.quick-stat-note{font-size:.7rem;line-height:1.45;color:var(--text-muted);text-wrap:pretty;text-align:left}
/* A flight with no launch reading is not drawn at all — an invented 12° with
   "12.0° launch" written under it is a measurement that does not exist. */
.traj-none{padding:1rem;font-size:.8rem;line-height:1.5;color:var(--text-muted);text-align:center;text-wrap:pretty}
.traj-note{margin-top:.3rem;font-size:.68rem;line-height:1.4;color:var(--text-muted);text-align:center;text-wrap:pretty}

/* ── View preferences ─────────────────────────────────────────────
   Driven by a class on <html> rather than by setting `hidden` on the
   sections. A section's markup is replaced wholesale by the next
   `innerHTML =`, which would wipe an inline `hidden` and quietly turn the
   setting off again — which is roughly how these toggles came to do nothing
   in the first place. A class on the root element survives every re-render. */
html.pref-no-heatmap .dash-heatmap-wrap { display: none; }
html.pref-no-faults #secFaults { display: none; }
html.pref-no-gapping #gappingSection { display: none; }
html.pref-no-comparison #compareHost { display: none; }

/* Compact view: tighter blocks and smaller section gaps, nothing hidden. */
html.pref-dense .section-block { margin-bottom: 1rem; padding: .85rem; }
html.pref-dense .section-title { font-size: .95rem; margin-bottom: .5rem; }
html.pref-dense .quick-stat { padding: .55rem; }
html.pref-dense .drill-card { padding: .7rem; }
html.pref-dense table td, html.pref-dense table th { padding: .3rem .45rem; }
.fault-inference-note{margin:.4rem 0 .2rem;padding-left:.7rem;border-left:2px solid var(--yellow);
  font-size:.74rem;line-height:1.5;color:var(--text-muted);text-wrap:pretty}
.cmp-caveat{margin-bottom:.55rem;padding:.55rem .7rem;font-size:.75rem;line-height:1.5;
  color:var(--text-dim);background:var(--surface2);border-left:3px solid var(--yellow);
  border-radius:var(--radius-sm);text-wrap:pretty}
.cmp-row.cmp-withheld{opacity:.72}
.cmp-row.cmp-withheld .cmp-label small{color:var(--yellow);font-weight:600}
.plan-gate{margin-top:.35rem;font-size:.72rem;line-height:1.45;color:var(--text-muted)}
/* Ticking a block off is the only write path PracticeLog has, so it has to
   read as a control rather than as a label. */
.plan-done{margin-top:.6rem;padding:.4rem .8rem;font:inherit;font-size:.74rem;font-weight:600;
  border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--surface);
  color:var(--text);cursor:pointer}
.plan-done:hover{border-color:var(--accent);color:var(--accent)}
.plan-done.logged{border-color:var(--green);color:var(--green);cursor:default;opacity:.85}
.plan-locked{margin-top:.4rem;padding-left:.7rem;border-left:2px solid var(--yellow);
  font-size:.75rem;line-height:1.5;color:var(--text-dim)}
.plan-item.plan-wrapper{border-left-color:var(--pine)}
.drill-tabs{display:flex;gap:.35rem;overflow-x:auto;padding-bottom:.4rem;margin-bottom:.6rem;
  scrollbar-width:none}
.drill-tabs::-webkit-scrollbar{display:none}
.drill-tab{flex:0 0 auto;padding:.45rem .7rem;font-size:.72rem;font-weight:600;white-space:nowrap;
  cursor:pointer;background:none;color:var(--text-dim);
  border:none;border-bottom:2px solid transparent;border-radius:0;
  transition:color var(--dur),border-color var(--dur)}
.drill-tab:hover{color:var(--text);border-bottom-color:var(--line-strong)}
.drill-tab.on{background:none;border-bottom-color:var(--accent);color:var(--text)}
.drill-row{padding:.7rem .85rem;margin-bottom:.4rem;background:var(--surface2);
  border-left:3px solid var(--accent);border-radius:var(--radius)}
.drill-row.locked{border-left-color:var(--withheld);opacity:.72}
.drill-row.flagged{border-left-color:var(--yellow)}
.drill-row-head{display:flex;justify-content:space-between;gap:.6rem;align-items:baseline}
.drill-row-name{font-size:.86rem;font-weight:650;color:var(--text)}
.drill-row-state{flex:0 0 auto;font-size:.62rem;letter-spacing: var(--track-caps-xs);text-transform:uppercase;
  color:var(--text-muted);font-weight:700}
.drill-row.locked .drill-row-state{color:var(--text-muted)}
.drill-row.flagged .drill-row-state{color:var(--yellow)}
.drill-row-desc{font-size:.8rem;line-height:1.55;color:var(--text-dim);margin-top:.25rem;text-wrap:pretty}
.drill-row-why{font-size:.74rem;line-height:1.5;color:var(--text-muted);margin-top:.35rem;
  padding-left:.7rem;border-left:2px solid var(--border)}
.next-step{border-left:4px solid var(--pine)}
.next-why{margin-top:.45rem;padding-left:.7rem;border-left:2px solid var(--border);
  font-size:.73rem;line-height:1.5;color:var(--text-muted);text-wrap:pretty}
.rd-form{display:grid;grid-template-columns:1fr 1fr;gap:.55rem;margin-bottom:.7rem}
.rd-form .qe-field input,.rd-form .qe-field select{width:100%}
.rd-stat{margin-bottom:.7rem}
.rd-stat-head{display:flex;justify-content:space-between;gap:.5rem;align-items:baseline;
  font-size:.82rem;color:var(--text);font-weight:600}
.rd-stat-val{font-variant-numeric:tabular-nums;color:var(--text)}
.rd-bar{height:6px;margin:.3rem 0 .2rem;background:var(--surface3);border-radius:999px;overflow:hidden}
.rd-bar-fill{height:100%;background:var(--pine);border-radius:999px}
.rd-stat-foot{font-size:.7rem;color:var(--text-muted)}
.sg-row{border-left-color:var(--accent)}
.sg-tier{flex:0 0 auto;font-size:.62rem;letter-spacing: var(--track-caps-xs);text-transform:uppercase;font-weight:600;
  padding:.15rem .4rem;border-radius:var(--radius-sm);white-space:nowrap;background:var(--surface3)}
.sg-tier-strong{color:var(--green)}
.sg-tier-moderate{color:var(--yellow)}
.sg-tier-weak{color:var(--text-muted)}
.sg-structs{display:flex;flex-wrap:wrap;gap:.3rem;margin-top:.4rem}
.sg-struct{font-size:.65rem;letter-spacing: var(--track-caps-xs);text-transform:uppercase;font-weight:600;
  padding:.15rem .45rem;border:1px solid var(--line);border-radius:var(--radius-sm);color:var(--text-muted)}
.qe-form{display:flex;flex-direction:column;gap:.6rem}
.qe-field{display:flex;flex-direction:column;gap:.25rem;font-size:.78rem;color:var(--text-dim)}
.qe-field input{padding:.55rem .7rem;font-size:16px;font-variant-numeric:tabular-nums;
  min-height:var(--tap);
  background:var(--surface2);color:var(--text);border:1px solid var(--line-strong);
  border-radius:var(--radius)}
.qe-field input:focus{border-color:var(--accent);outline:none;box-shadow:0 0 0 3px var(--accent-weak)}
.probe-ask{margin-bottom:.6rem}
.probe-btns{display:flex;gap:.45rem;flex-wrap:wrap;margin:.5rem 0 0 1rem}
.probe-btn{padding:.5rem .85rem;font-size:.8rem;font-weight:600;cursor:pointer;
  min-height:var(--tap);
  background:var(--surface);color:var(--text);border:1px solid var(--line-strong);
  border-radius:var(--radius)}
.probe-btn:hover{border-color:var(--accent);color:var(--accent)}
.probe-btn.ghost{color:var(--text-muted);font-weight:500}
.tail-spark{display:block;width:100%;height:28px;margin:.1rem 0 .2rem;color:var(--accent);opacity:.9}
.tail-spark-key{font-size:.65rem;letter-spacing: var(--track-caps-xs);text-transform:uppercase;color:var(--text-muted);margin-bottom:.7rem}
.tail-value{margin:.7rem 0 .5rem;padding:.7rem .8rem;background:var(--surface);
  border:1.5px solid var(--border);border-radius:var(--radius-sm)}
.tail-value-num{font-family:var(--font-display);font-size:1.25rem;font-weight:700;
  font-variant-numeric:tabular-nums;color:var(--text)}
.tail-value-sub{font-size:.78rem;line-height:1.5;color:var(--text-dim);margin-top:.25rem}

/* ── Launch-monitor setup guide ─────────────────────────── */
.modal-scroll{max-height:72vh;overflow-y:auto;padding:1rem 1.1rem 1.2rem}
.setup-guide{color:var(--text)}
.setup-summary{
  margin:0 0 1.1rem; padding:.8rem .9rem; font-size:.85rem; line-height:1.55;
  background:var(--surface2); border-left:3px solid var(--pine);
  border-radius:var(--radius-sm); color:var(--text);
}
.setup-h{
  font-size:.72rem; letter-spacing: var(--track-caps); text-transform:uppercase; font-weight:700;
  color:var(--text-dim); margin:1.3rem 0 .7rem; padding-bottom:.35rem;
  border-bottom:1px solid var(--border);
}
.setup-steps{list-style:none; margin:0; padding:0; display:grid; gap:.85rem}
.setup-step{display:grid; grid-template-columns:26px 1fr; gap:.7rem; align-items:start}
.setup-step-n{
  width:26px; height:26px; border-radius:50%; display:grid; place-items:center;
  background:var(--accent); color:var(--accent-ink); font-size:.78rem; font-weight:700;
  font-variant-numeric:tabular-nums;
}
.setup-step-title{font-weight:650; font-size:.92rem; margin-bottom:.2rem; line-height:1.3}
.setup-step-text{font-size:.83rem; line-height:1.55; color:var(--text)}
.setup-step-text strong{font-weight:700}
.setup-step-why{
  margin-top:.35rem; font-size:.75rem; line-height:1.45; color:var(--text-muted);
  padding-left:.6rem; border-left:2px solid var(--border);
}
.setup-reqs{display:grid; gap:.5rem}
.setup-req{
  padding:.6rem .75rem; background:var(--surface2);
  border-left:3px solid var(--border); border-radius:var(--radius-sm);
}
.setup-req.ok{border-left-color:var(--green)}
.setup-req.warn{border-left-color:var(--yellow)}
.setup-req-metric{font-size:.8rem; font-weight:650; margin-bottom:.2rem}
.setup-req-needs{font-size:.78rem; line-height:1.5; color:var(--text-dim)}

/* ── Yardage book ──────────────────────────────────────── */
.yardage-intro { font-size: .9rem; color: var(--text-muted); margin-bottom: var(--s3); line-height: 1.55; }
.yardage-meta {
  display: block; font-size: .68rem; margin-top: .25rem; color: var(--text-dim);
  font-family: var(--font-mono); letter-spacing: .04em;
}
#yardageTable td { font-size: .84rem; }

/* ── Personal bests ────────────────────────────────────── */
.records-grid { display: grid; grid-template-columns: repeat(2,1fr); gap: .6rem; }
.record-card {
  background: var(--surface); border: 1px solid var(--line);
  border-top: 2px solid var(--accent);
  border-radius: var(--radius); padding: var(--s4);
  transition: border-color var(--dur);
}
.record-card:hover { border-color: var(--line-strong); }
.record-value {
  font-family: var(--font-display);
  font-size: 1.7rem; font-weight: 800; line-height: 1;
  font-variant-numeric: tabular-nums; letter-spacing: -.01em; color: var(--text);
}
.record-unit { font-size: .52em; font-weight: 600; color: var(--text-muted); margin-left: 2px; }
.record-label { font-size: .75rem; font-weight: 600; margin-top: .35rem; color: var(--text); }
.record-meta { font-size: .68rem; color: var(--text-muted); margin-top: 2px; }

/* ── Shot detail modal ─────────────────────────────────── */
.modal-wide { max-width: 440px; }
.modal-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--s4); padding-bottom: .875rem; border-bottom: 1px solid var(--line); }
.modal-head .btn-icon { width: 30px; height: 30px; font-size: .9rem; }
.sm-grade { font-size: .85rem; font-weight: 700; margin-left: .4rem; }
.sm-table { width: 100%; border-collapse: collapse; }
.sm-table td { padding: .42rem .25rem; border-top: 1px solid var(--line); white-space: nowrap; }
.sm-table tr:first-child td { border-top: none; }
.sm-k { color: var(--text-muted); font-size: .8rem; }
.sm-v { font-weight: 600; font-size: .85rem; text-align: right; font-variant-numeric: tabular-nums; }
.sm-c { font-size: .7rem; text-align: right; padding-left: .5rem !important; }
.sm-cmp.up   { color: var(--green); }
.sm-cmp.down { color: var(--red); }
.sm-faults-title { font-size: .62rem; font-weight: 700; text-transform: uppercase; letter-spacing: var(--track-caps-xs); color: var(--text-muted); margin: 1rem 0 .5rem; }
.sm-fault {
  font-size: .84rem; padding: .4rem .7rem; border-radius: var(--radius);
  background: var(--surface2); border-left: 3px solid var(--yellow); margin-bottom: .35rem;
}
.sm-fault.severity-high { border-left-color: var(--red); }
.sm-fault.severity-low  { border-left-color: var(--blue); }
.sm-clean { color: var(--green); font-size: .85rem; margin-top: 1rem; font-weight: 500; }
.shot-row { cursor: pointer; }

/* ── Modal overlay ─────────────────────────────────────── */
.modal-overlay {
  position: fixed; inset: 0; z-index: 200;
  /* Was rgba(12,31,20,.4) — a green-black left over from the pine theme,
     which tints everything behind it. --overlay is the one scrim value. */
  background: var(--overlay);
  backdrop-filter: blur(2px); -webkit-backdrop-filter: blur(2px);
  display: flex; align-items: flex-start; justify-content: center;
  /* Respect notch/home-bar safe areas */
  padding-top: max(1.5rem, env(safe-area-inset-top));
  padding-right: max(1.5rem, env(safe-area-inset-right));
  padding-bottom: max(1.5rem, env(safe-area-inset-bottom));
  padding-left: max(1.5rem, env(safe-area-inset-left));
  animation: overlayIn .15s ease;
  overflow-y: auto;              /* overlay scrolls when modal is taller than viewport */
  overscroll-behavior: contain;  /* Android: stop scroll bleeding through to body */
  -webkit-overflow-scrolling: touch;
}
@keyframes overlayIn { from{opacity:0} to{opacity:1} }
.modal-overlay[hidden] { display: none; }

/* Lock body scroll when any modal is open (toggled by JS MutationObserver).
   iOS Safari ignores overflow:hidden on body; position:fixed + top = saved scrollY
   is the only reliable cross-browser fix for scroll-behind. */
body.modal-open {
  overflow: hidden;
  position: fixed;
  width: 100%;
  top: var(--scroll-y, 0);
}

/* ── Agreement gate (blocking first-run consent) ───────── */
.agreement-gate { z-index: 400; background: var(--overlay); }
/* Terms/Privacy must open ABOVE the gate when reviewed from it */
#privacyModal, #termsModal { z-index: 500; }
.agreement-gate-body { max-height: 60vh; overflow-y: auto; }
.agreement-gate-check {
  display: flex; align-items: flex-start; gap: .6rem;
  margin-top: 1rem; cursor: pointer; font-size: .85rem;
}
.agreement-gate-check input { margin-top: .2rem; flex: 0 0 auto; width: 18px; height: 18px; accent-color: var(--accent); }
.agreement-gate-check + .agreement-gate-check { margin-top: .6rem; }

/* The risk statement is part of what is being agreed to, not a second modal
   stacked on top of the first. A separate "are you sure" dialog after accept
   would be a third interstitial before the app appears, and each one added
   makes all of them less likely to be read — while a paraphrase of the Terms
   sitting outside the Terms is exactly the ambiguity s.1812(2) of the Czech
   Civil Code resolves against whoever drafted it. */
/* The beta notice. Deliberately not small print — it is the same size as the
   body text around it, because a notice set smaller than what it qualifies is
   decoration rather than disclosure. */
.beta-notice {
  margin-top: var(--s4);
  padding: var(--s4);
  background: var(--surface2);
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
  font-size: .85rem; line-height: 1.6;
}
.beta-notice p { margin: 0 0 .6rem; }
.beta-notice p:last-child { margin-bottom: 0; }
.beta-notice-head {
  font-family: var(--font-display);
  font-weight: 800; text-transform: uppercase; letter-spacing: var(--track-caps);
  font-size: .8rem; color: var(--accent);
}
/* A persistent, dismissible marker that this is beta, on the app chrome. */
.beta-tag {
  font-family: var(--font-mono);
  font-size: .58rem; font-weight: 600; letter-spacing: var(--track-caps-xs);
  text-transform: uppercase;
  color: var(--accent-ink); background: var(--accent);
  padding: 2px 5px 1px; border-radius: var(--radius-sm);
  white-space: nowrap;
}

/* ── Standalone legal pages (/terms, /privacy, /contact) ───────────────
   These share style.css with the app so there is one design system, but they
   are ordinary documents rather than app views: always-visible nav, a
   measured column, and no view machinery. */
.legal-nav { display: flex !important; position: static; }
.legal-home { text-decoration: none; display: flex; align-items: baseline; gap: .5rem; }
.legal-page {
  max-width: 46rem; margin: 0 auto;
  padding: var(--s6) var(--s4) var(--s7);
}
.legal-page .modal-document { max-height: none; overflow: visible; padding: 0; }
/* On its own page the document's <h1> IS the heading, so it comes back. */
.legal-page .doc-standalone h1 {
  display: block;
  font-family: var(--font-display);
  font-size: 1.5rem; font-weight: 800; letter-spacing: var(--track-caps-lg);
  text-transform: uppercase;
  margin: 0 0 var(--s4);
  padding-bottom: var(--s3);
  border-bottom: 2px solid var(--accent);
}
@media (min-width: 640px) { .legal-page .doc-standalone h1 { font-size: 2rem; } }
@media print {
  .legal-nav { display: none !important; }
  .legal-page { max-width: none; padding: 0; }
}

/* ── Safety notices (audit §9) ─────────────────────────────────────────
   Rendered above the drill catalogue and on every surface that prescribes
   physical activity. Deliberately not small print: a warning set smaller
   than the thing it warns about is decoration. */
.safety-note {
  border: 1px solid var(--line);
  border-left: 3px solid var(--yellow);
  background: var(--surface2);
  padding: var(--s3) var(--s4);
  margin-bottom: var(--s4);
  font-size: .84rem; line-height: 1.55;
}
.safety-note p { margin: 0; }
.safety-note p + p { margin-top: .5rem; }
.safety-pain { color: var(--text-muted); }
.safety-inline { margin: var(--s3) 0 var(--s4); }
/* On an entry that prescribes barbell, plyometric or overspeed work. */
.drill-row-risk { color: var(--yellow); }
.rc-safety {
  margin-top: var(--s3); padding-top: var(--s3);
  border-top: 1px solid var(--line);
  font-size: .82rem; color: var(--text-muted); line-height: 1.5;
}

/* ── Declined ──────────────────────────────────────────────────────────
   Replaces the document when the terms are refused. window.close() only
   works for a script-opened window and history.back() only when there is
   history, so this is what a golfer who typed the address actually sees.
   It is a terminal state, not a redirect to somebody else's site. */
.declined-screen {
  position: fixed; inset: 0; z-index: 9999;
  display: flex; align-items: center; justify-content: center;
  padding: var(--s5);
  background: var(--bg); color: var(--text);
  font-family: var(--font-body);
}
.declined-card {
  max-width: 32rem; text-align: center;
  border-top: 2px solid var(--accent);
  background: var(--surface);
  border: 1px solid var(--line); border-top: 2px solid var(--accent);
  padding: var(--s5);
}
.declined-title {
  font-family: var(--font-display);
  font-size: 1.3rem; font-weight: 800; text-transform: uppercase;
  letter-spacing: var(--track-caps-lg); margin-bottom: var(--s3);
}
.declined-card p { color: var(--text-muted); font-size: .9rem; line-height: 1.6; margin-bottom: var(--s4); }
.link-btn {
  background: none; border: none; padding: 0; cursor: pointer;
  color: var(--accent); text-decoration: underline;
  font: inherit;
}
.btn-primary:disabled { opacity: .5; cursor: not-allowed; }
.modal {
  background: var(--surface);
  border: 1px solid var(--line);
  border-top: 2px solid var(--accent);
  border-radius: var(--radius); padding: var(--s5);
  max-width: 380px; width: 100%;
  /* --shadow-md, not lg: a modal floats above the page, it does not hover
     over a hole in it. The agreement gate keeps lg — it is the one overlay
     that blocks the whole app. */
  box-shadow: var(--shadow-md);
  animation: modalIn .22s ease;
  /* Center vertically when overlay has free space; allow scrolling if modal is tall */
  margin: auto;
  max-height: calc(100dvh - 3rem);
  overflow-y: auto;
  overscroll-behavior: contain;
}
/* Rises, does not zoom. A scale-up on a square-cornered panel reads as a
   dialog box from a different design system. */
@keyframes modalIn {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}
.agreement-gate .modal { box-shadow: var(--shadow-lg); }
.modal-title { margin-bottom: .5rem; }
.modal-body { color: var(--text-muted); font-size: .875rem; margin-bottom: 1.25rem; line-height: 1.6; }
/* ── Legal documents ───────────────────────────────────────────────────
   Set as a document, not as app UI: a measured column, dense leading, a
   clear heading hierarchy and real tables. Both documents are mostly tables
   and numbered clauses, so those two things carry the typography. */
.doc-head-actions { display: flex; align-items: center; gap: var(--s2); }
.modal-document {
  max-height: 70vh; overflow-y: auto; padding: 0 var(--s1) var(--s4) 0;
  color: var(--text); font-size: .84rem; line-height: 1.62;
  text-wrap: pretty;
}
/* On screen the modal head already names the document, so its own <h1>
   would be the title twice. It is kept in the markup and shown only in
   print, where the modal head is the part that does not belong. */
.modal-document h1 { display: none; }
.modal-document h2 {
  font-family: var(--font-display);
  font-size: .95rem; font-weight: 700; letter-spacing: var(--track-caps-lg);
  text-transform: uppercase;
  margin: var(--s5) 0 var(--s2);
  padding-top: var(--s3); border-top: 1px solid var(--line);
}
.modal-document h1 + p,
.modal-document h1 ~ p:nth-of-type(-n+3) { color: var(--text-muted); font-size: .78rem; margin: .15rem 0; }
.modal-document h3 {
  font-size: .84rem; font-weight: 700; color: var(--text);
  margin: var(--s4) 0 var(--s2);
}
.modal-document p { margin: 0 0 .7rem; }
.modal-document ul { margin: 0 0 .7rem; padding-left: 1.15rem; }
.modal-document li { margin: 0 0 .3rem; padding-left: .15rem; }
.modal-document strong { font-weight: 600; color: var(--text); }
.modal-document code {
  font-family: var(--font-mono); font-size: .92em;
  background: var(--surface2); padding: .05em .3em; border-radius: var(--radius-sm);
}
.modal-document a { color: var(--accent); }
/* Legal tables are reference material: tight, ruled, and allowed to scroll
   sideways on a phone rather than crushing a column to two characters. */
.doc-table-wrap { overflow-x: auto; margin: 0 0 var(--s4); -webkit-overflow-scrolling: touch; }
.doc-table { width: 100%; border-collapse: collapse; font-size: .76rem; min-width: 22rem; }
.doc-table th {
  background: none; color: var(--text-dim);
  font-size: .66rem; font-weight: 600; letter-spacing: var(--track-caps-xs); text-transform: uppercase;
  text-align: left; padding: .35rem .55rem .35rem 0;
  border-bottom: 1.5px solid var(--line-strong);
  cursor: default; white-space: nowrap;
}
.doc-table td {
  padding: .4rem .55rem .4rem 0; border-top: 1px solid var(--line);
  vertical-align: top; white-space: normal; line-height: 1.5;
}
.doc-table td:first-child { font-weight: 600; }
.modal-document blockquote {
  margin: 0.75rem 0; padding: .65rem .9rem;
  border-left: 3px solid var(--accent);
  background: var(--surface2); border-radius: var(--radius);
}
.modal-actions { display: flex; justify-content: flex-end; gap: .75rem; flex-wrap: wrap; }
/* Refusing must not be harder to find than accepting. Same row, same size;
   the accent fill marks the primary path, not the only one. */
.agreement-gate .modal-actions { justify-content: space-between; }
@media (max-width: 420px) {
  .agreement-gate .modal-actions { flex-direction: column-reverse; }
  .agreement-gate .modal-actions button { width: 100%; }
}

/* Reduce overlay/modal padding on very small phones (< 390px) */
@media (max-width: 389px) {
  .modal-overlay {
    padding-top: max(.75rem, env(safe-area-inset-top));
    padding-right: max(.75rem, env(safe-area-inset-right));
    padding-bottom: max(.75rem, env(safe-area-inset-bottom));
    padding-left: max(.75rem, env(safe-area-inset-left));
  }
  .modal { padding: 1.1rem; }
}

/* ── Preview table ─────────────────────────────────────── */
.preview-table th, .preview-table td { font-size: .74rem; padding: .4rem .65rem; }
.preview-table { min-width: 500px; }

/* ── Custom scrollbar ──────────────────────────────────── */
::-webkit-scrollbar { width: 5px; height: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border-hi); border-radius: 99px; }
::-webkit-scrollbar-thumb:hover { background: var(--text-dim); }

/* ── Mobile sticky bench col ───────────────────────────── */
@media (max-width: 767px) {
  .bench-table td:first-child, .bench-table th:first-child {
    position: sticky; left: 0;
    background: var(--surface); z-index: 1;
    border-right: 1px solid var(--line);
  }
}

/* ── Desktop ───────────────────────────────────────────── */
@media (min-width: 768px) {
  /* The bar is position:sticky, so it occupies its own space in flow. The
     padding-top here was stacking a second nav-h ON TOP of it and opening a
     60px band above the bar — visible the whole time the gradient was there
     to disguise it. */
  body { padding-bottom: 0; padding-top: 0; }
  .top-nav { display: flex; }
  .bottom-nav { display: none; }
  .app-main { max-width: 980px; margin: 0 auto; padding: 1.75rem 2.25rem 3rem; }
  .metrics-strip { grid-template-columns: repeat(6,1fr); gap: .75rem; }
  .charts-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
  .dna-grid { display: grid; grid-template-columns: repeat(2,1fr); gap: .4rem; }
  .dispersion-card canvas { height: 360px !important; }
  .insights-cols { flex-direction: row; }
  .insights-block { flex: 1; }
  .dispersion-stats { grid-template-columns: repeat(6,1fr); }
  .records-grid { grid-template-columns: repeat(3,1fr); }
  /* A `.score-ring svg { 100px }` lived here and never applied at any width:
     the base rule below it sets 110px !important, and the desktop block
     further down sets 130px !important. 130 is what desktop has always
     shown, so the dead line went rather than the live one. */
}

/* ═══════════════════════════════════════════════════════════
   v3 — FIELD BULLETIN / EDITORIAL ENHANCEMENT LAYER
   Mixed type (serif + mono + condensed) · Measurement grid
   Custom dropdowns · Decorative ornaments · Hero score
   ═══════════════════════════════════════════════════════════ */

/* The webfont @import that used to sit here was dead: a CSS @import is only
   honoured before every other rule, and this one was ~1200 lines in, so the
   browser dropped it. DM Serif Display and JetBrains Mono never loaded once.
   --font-mono keeps the spec §4.1 stack it was already falling back to. */

/* The measurement-grid background lived here: a 28px graph-paper lattice
   painted behind the whole app in three !important declarations spread over
   600 lines, the first of them still tinted with the retired red.

   It is gone, and subtraction is the whole fix. A graph-paper backdrop is
   theming by prop — it says "instrument" by drawing the object rather than by
   being one, and the app already has a real grid: hairline card borders,
   section rules, table rows. The painted one sat behind those at its own
   pitch, so every real line crossed a decorative line at an arbitrary offset.
   At .035 alpha it was under the threshold of reading as a decision and over
   the threshold of being visible, which is the worst place for a mark to sit.

   Both systems the design reference draws on refuse exactly this: BMW M keeps
   empty space as empty canvas and adds no atmospheric backdrop; Linear's
   don't-list names atmospheric gradients outright. The canvas is --bg, flat.
   See docs/reference/README.md. */

/* ── Custom select dropdown ─────────────────────────────────── */
.form-group select, .select {
  appearance: none;
  -webkit-appearance: none;
  background-image: var(--chevron);
  background-repeat: no-repeat;
  background-position: right 14px center;
  padding-right: 42px;
  cursor: pointer;
  font-family: var(--font-body);
  font-weight: 600;
  font-size: .9rem;
  letter-spacing: .01em;
  transition: background-color var(--dur), border-color var(--dur), box-shadow var(--dur);
}
.form-group select:hover, .select:hover {
  background-color: var(--surface3);
  border-color: var(--accent);
}

/* ── Date input: monospace ─────────────────────────────────── */
.form-group input[type="date"] {
  font-family: var(--font-mono);
  font-weight: 500;
  letter-spacing: .02em;
}

/* ── HERO SCORE BANNER ─────────────────────────────────────── */
.score-banner-content {
  padding: var(--s5) var(--s5) var(--s4) !important;
  background: var(--surface) !important;
  position: relative;
}
.score-ring svg { width: 110px !important; height: 110px !important; }

.score-label {
  display: flex !important;
  align-items: center;
  gap: 7px;
  font-family: var(--font-body) !important;
}
/* ── EDITORIAL SESSION CARDS ───────────────────────────────── */
/* The base rule sets --s4 and nothing else competes for it now, so the
   !important that was holding this padding against the folded override
   has nothing left to beat. */
.session-card { padding: var(--s4); }

/* ── METRIC CARDS — auto numbered ──────────────────────────── */
.metrics-strip { counter-reset: metric; }
.metric-card {
  counter-increment: metric;
  padding: 1.15rem .85rem .9rem !important;
}
.metric-card::after {
  content: counter(metric, decimal-leading-zero);
  position: absolute;
  top: 9px; right: 12px;
  font-family: var(--font-mono);
  font-size: .58rem;
  font-weight: 500;
  color: var(--text-dim);
  letter-spacing: .1em;
  opacity: .7;
}
.metric-label {
  font-family: var(--font-mono) !important;
  font-weight: 500 !important;
  font-size: .55rem !important;
  letter-spacing: var(--track-caps-xs) !important;
}

.chart-card, .trend-box, .insights-card { position: relative; }

.trend-heading {
  display: flex !important;
  align-items: center !important;
  gap: 8px;
}

/* ── Insights title — mono ─────────────────────────────────── */
.insights-title {
  font-family: var(--font-mono) !important;
}

/* ── Fault labels — mono ──────────────────────────────────── */
.fault-cat-label,
.fault-section-title,
.sm-faults-title,
.disp-stat-label,
.score-bd-label,
.filter-label,
.dna-cat,
.metric-label,
.settings-section-title {
  font-family: var(--font-mono) !important;
}

/* ── Record cards ───────────────────────────────────────────── */
/* This block used to force an "editorial serif" on the record value. The
   serif it named never loaded (its @import was dead — see Task 9), so it was
   really applying weight 400 at 2rem to the display face. Kept as a .kicker
   label and a --stat-hero-scale number, defined once in the base rule. */
.record-label {
  font-size: .7rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: var(--track-caps);
  color: var(--text-dim);
  margin-top: .45rem;
}
.record-meta {
  font-size: .68rem; letter-spacing: .02em;
  color: var(--text-muted); opacity: 1;
}

/* (button display type + geometry now live in the base Buttons block) */

/* ── Chips — mono uppercase ────────────────────────────────── */
.chip {
  font-family: var(--font-mono);
  font-weight: 500;
  letter-spacing: var(--track-caps-xs);
  text-transform: uppercase;
  font-size: .68rem;
}

/* ── Bottom nav labels — mono ──────────────────────────────── */
.bnav-label {
  font-family: var(--font-mono) !important;
  font-size: .55rem !important;
  font-weight: 500 !important;
  /* The xs rung, and the bottom nav is the tightest place it lands: seven
     equal slots at 393px give each label 56.1px, and the widest ("Sessions")
     measures 52.2px here against 44.5px untracked. It fits, with ~2px of
     gutter each side. Nothing would overflow the PAGE if it stopped fitting,
     so render-scan cannot see this one — measure the label box against its
     slot before widening the rung or adding an eighth nav item. */
  letter-spacing: var(--track-caps-xs) !important;
}

/* ── DNA pill: refined typography ──────────────────────────── */
.dna-val {
  font-family: var(--font-display);
  font-weight: 600;
  letter-spacing: .005em;
}

/* ── Plan items: editorial num ─────────────────────────────── */
.plan-num {
  letter-spacing: .03em;
}
.plan-min {
  font-family: var(--font-mono) !important;
  letter-spacing: .04em;
}

/* ── Drill card: condensed name ────────────────────────────── */
.drill-name {
  font-family: var(--font-display) !important;
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
}

/* ── Modal title editorial ─────────────────────────────────── */
.modal-title {
  font-family: var(--font-display);
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
}

/* ── View title — editorial mix ────────────────────────────── */
.view-title {
  font-family: var(--font-display);
  font-size: 1.7rem !important;
  font-weight: 700;
  letter-spacing: var(--track-caps-lg);
  text-transform: uppercase;
}

/* ── Step heading mono accent ──────────────────────────────── */
.step-heading {
  font-family: var(--font-display);
  text-transform: uppercase;
  letter-spacing: var(--track-caps-lg);
}

/* ── Drop zone: refined hint ───────────────────────────────── */
.drop-label { font-family: var(--font-display); text-transform: uppercase; letter-spacing: var(--track-caps-lg); }
.drop-sub { font-family: var(--font-mono); letter-spacing: var(--track-caps); text-transform: uppercase; }
.import-hint { font-family: var(--font-mono); letter-spacing: .05em; }

/* ── Yardage meta — mono ───────────────────────────────────── */
.yardage-meta {
  font-family: var(--font-mono);
}

/* ── Empty state polish ────────────────────────────────────── */
.empty-state h2 {
  font-family: var(--font-display);
  text-transform: uppercase;
  letter-spacing: var(--track-caps-lg);
}
.empty-state p { font-family: var(--font-mono); letter-spacing: .03em; }

/* ── Form labels — mono ────────────────────────────────────── */
.form-group label {
  font-family: var(--font-mono) !important;
}

/* ── Custom number values throughout ───────────────────────── */
.trend-val, .score-bd-n, .sm-v, .disp-stat-val, .score-number, .score-max {
  font-variant-numeric: tabular-nums;
}

/* ── Desktop — hero proportions ────────────────────────────── */
@media (min-width: 768px) {
  .score-ring svg { width: 130px !important; height: 130px !important; }
  .record-value { font-size: 2.2rem !important; }
  .metric-value { font-size: 1.95rem !important; }
  .view-title { font-size: 2rem !important; }
}

/* ── v3 fixes ─────────────────────────────────────────────── */
.top-nav .btn-primary,
.top-nav .btn-sm {
  white-space: nowrap !important;
  font-size: .7rem !important;
  padding: .45rem .85rem !important;
}

/* ═══════════════ LAYOUT v2 — Dashboard · Trajectory · Subnav ═══════════════ */

/* Dashboard bento */
.dashboard { margin-bottom: 1.75rem; }
.dash-grid { display:grid; grid-template-columns:repeat(2,1fr); gap:.7rem; }
.dash-hero {
  grid-column:span 2; display:flex; align-items:center; gap:1.1rem;
  background:var(--surface); border:1px solid var(--line); border-radius:var(--radius);
  padding:var(--s4); box-shadow:none;
}
.dash-hero-ring { position:relative; width:96px; height:96px; flex-shrink:0; }
.dash-hero-ring svg { width:96px; height:96px; transform:rotate(-90deg); }
.dh-track { fill:none; stroke:var(--line); stroke-width:8; }
.dh-arc { fill:none; stroke-width:8; stroke-linecap:butt; transition:stroke-dashoffset 1.1s cubic-bezier(.4,0,.2,1); }
.dash-hero-num { position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; line-height:1; }
.dh-grade { font-family:var(--font-display); font-size:1.7rem; font-weight:800; }
.dh-score { font-size:.72rem; color:var(--text-muted); font-weight:600; margin-top:2px; }
.dh-score small { font-size:.85em; opacity:.7; }
.dash-hero-meta { flex:1; min-width:0; }
.dash-hero-title { font-family:var(--font-display); font-size:1.25rem; font-weight:700; text-transform:uppercase; letter-spacing: var(--track-caps-lg); }
.dash-hero-sub { font-size:.8rem; color:var(--text-muted); margin-top:2px; }
.dash-trend { font-size:.8rem; font-weight:700; margin-top:.5rem; }
.dash-trend.up { color:var(--green); }
.dash-trend.down { color:var(--red); }

.dash-tile {
  background:var(--surface); border:1px solid var(--line); border-radius:var(--radius);
  padding:var(--s3) var(--s4); box-shadow:none;
  display:flex; flex-direction:column; justify-content:center; min-height:78px;
}
/* The "accent" tile was a two-stop red-to-green gradient wash. A tile that is
   tinted differently from its neighbours reads as a different KIND of
   reading; these are the same kind. It gets the accent rule instead. */
.dash-tile.accent { background:var(--surface); border-top:2px solid var(--accent); }
.dash-tile.wide { grid-column:span 2; }
.dash-tile.clickable { cursor:pointer; transition:border-color var(--dur), background var(--dur); }
.dash-tile.clickable:hover { border-color:var(--line-strong); background:var(--surface2); }
.dt-val { font-family:var(--font-display); font-size:1.85rem; font-weight:800; line-height:1; color:var(--text); }
.dt-val small { font-size:.5em; font-weight:600; color:var(--text-muted); margin-left:2px; }
.dt-label { font-size:.7rem; color:var(--text-dim); text-transform:uppercase; letter-spacing: var(--track-caps); margin-top:.35rem; font-weight:600; }
.dt-lastline { margin-top:.4rem; font-size:.88rem; font-weight:600; }
.dt-fault { color:var(--red); }
.dt-clean { color:var(--green); }
.dt-cta { margin-top:.4rem; font-size:.78rem; color:var(--accent); font-weight:600; }

/* Heatmap */
.dash-heatmap-wrap { margin-top:1rem; background:var(--surface); border:1.5px solid var(--border); border-radius:var(--radius); padding:1rem 1.1rem; box-shadow:var(--shadow-sm); }
.dash-sub-title { font-size:.72rem; font-weight:700; text-transform:uppercase; letter-spacing: var(--track-caps); color:var(--text-muted); margin-bottom:.7rem; display:flex; align-items:center; justify-content:space-between; gap:.5rem; }
.hm-legend { display:flex; align-items:center; gap:3px; font-weight:600; text-transform:none; letter-spacing:0; }
.heatmap { display:grid; grid-auto-flow:column; grid-template-rows:repeat(7,13px); gap:3px; overflow-x:auto; padding-bottom:2px; }
.hm-cell { width:13px; height:13px; border-radius:var(--radius-sm); background:var(--surface3); }
.hm-cell.hm-empty { background:transparent; }
/* Level 0 is a day with no shots — a neutral surface, the same ground as an
   unfilled cell. Levels 1-3 are alpha steps of the accent and level 4 is the
   accent itself. These were a pale mint and three steps of the retired red,
   the palette the app stopped using, so the one component on the home view
   that is pure colour was the one still wearing the old one. */
.hm-l0 { background:var(--surface3); }
.hm-l1 { background:var(--hm1); }
.hm-l2 { background:var(--hm2); }
.hm-l3 { background:var(--hm3); }
.hm-l4 { background:var(--accent); }
.hm-legend .hm-cell { width:11px; height:11px; }

.recent-title { margin-bottom:.75rem; }

/* Sticky in-page subnav — a tab row, marked by a rule, not by eight pills */
.subnav {
  position:sticky; top:0; z-index:40;
  display:flex; gap:0; overflow-x:auto;
  background:var(--bg); margin:0 -1rem var(--s4); padding:0 var(--s4);
  border-bottom:1px solid var(--line); -webkit-overflow-scrolling:touch;
}
.subnav-link {
  flex-shrink:0; cursor:pointer;
  font-family:var(--font-mono); font-size:.68rem; font-weight:500;
  text-transform:uppercase; letter-spacing: var(--track-caps-xs);
  color:var(--text-dim);
  padding:.6rem .7rem; border-radius:0; white-space:nowrap;
  border:none; border-bottom:2px solid transparent; background:none;
  margin-bottom:-1px;
  transition:color var(--dur), border-color var(--dur);
}
/* The tab row scrolls sideways at phone width and nothing said so: "FAU…"
   simply stopped at the edge (V6). A fade in the page ground pinned to the
   right edge says there is more. Sticky inside the scroller, so it rides the
   edge while scrolling and comes to rest in its own space after the last tab
   at the end — no negative margin, or it would sit on "Shots". */
.subnav::after {
  content: ""; position: sticky; right: 0; flex: 0 0 1.75rem;
  background: linear-gradient(to right, transparent, var(--bg)); pointer-events: none;
}
.subnav-link:hover { color:var(--text); border-bottom-color:var(--line-strong); }
.subnav-link.active { color:var(--text); border-bottom-color:var(--accent); }

/* Trajectory */
.traj-card { padding:.5rem; }
.traj-svg { width:100%; height:auto; display:block; }
.traj-lbl { fill:var(--text-muted); font-size:10px; font-family:var(--font-body); font-weight:700; }
.sm-traj { background:var(--surface2); border:1px solid var(--border); border-radius:var(--radius-sm); padding:.4rem; margin-bottom:.9rem; }

/* Desktop */
@media (min-width: 768px) {
  .dash-grid { grid-template-columns:repeat(4,1fr); }
  .dash-hero { grid-column:span 2; grid-row:span 2; flex-direction:column; text-align:center; justify-content:center; }
  .dash-hero-ring { width:120px; height:120px; }
  .dash-hero-ring svg { width:120px; height:120px; }
  .subnav { top:var(--nav-h); }
  .heatmap { justify-content:start; }
}

/* Auth modal */
.auth-tabs { display:flex; gap:0; border-bottom:1px solid var(--line); margin-bottom:1.5rem; }
.auth-tab {
  flex:1; padding:.75rem; font-size:.85rem; font-weight:600; background:none; border:none;
  color:var(--text-muted); cursor:pointer; border-bottom:2px solid transparent;
  min-height:var(--tap);
  transition:color var(--dur), border-color var(--dur);
}
.auth-tab:hover { color:var(--text); border-bottom-color:var(--line-strong); }
.auth-tab.active { color:var(--text); border-bottom-color:var(--accent); }
.auth-form { display:none; }
.auth-form.active { display:block; }
.auth-error { color:var(--red); font-size:.82rem; margin-top:.75rem; }
.auth-hint { font-size:.8rem; color:var(--text-muted); margin-top:.75rem; text-align:center; }
.auth-hint a { color:var(--accent); text-decoration:none; font-weight:600; }

.auth-social { display:flex; flex-direction:column; gap:.55rem; margin-bottom:1.1rem; }
.auth-social-btn {
  display:flex; align-items:center; justify-content:center; gap:.6rem;
  width:100%; padding:.65rem 1rem; font-size:.85rem; font-weight:600;
  background:var(--surface); color:var(--text); border:1px solid var(--line-strong);
  border-radius:var(--radius); min-height:var(--tap);
}
.auth-social-btn:hover { background:var(--surface2); border-color:var(--accent); }
.auth-social-btn svg { flex-shrink:0; }

.auth-divider {
  display:flex; align-items:center; gap:.7rem; margin-bottom:1.1rem;
  color:var(--text-muted); font-size:.72rem; text-transform:uppercase; letter-spacing: var(--track-caps);
}
.auth-divider::before, .auth-divider::after { content:''; flex:1; height:1px; background:var(--line); }

.auth-guest {
  margin-top:1.1rem; padding-top:1.1rem; border-top:1px solid var(--line);
}
.auth-guest-note { font-size:.72rem; color:var(--text-muted); margin-top:.5rem; text-align:center; }

/* Toast */
.toast {
  position:fixed; left:50%; bottom:calc(var(--bottom-nav-h) + 1rem);
  transform:translate(-50%, 1rem); z-index:300;
  background:var(--accent); color:var(--accent-ink); font-weight:600; font-size:.85rem;
  padding:.7rem 1.1rem; border-radius:var(--radius); box-shadow:var(--shadow-md);
  opacity:0; pointer-events:none; transition:opacity .25s, transform .25s; max-width:90vw; text-align:center;
}
.toast.show { opacity:1; transform:translate(-50%, 0); }
@media (min-width:768px) { .toast { bottom:1.5rem; } }

/* ── Paywall (blur for guests) ─────────────────────────────── */
.paywall-wrap {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius);
}
.paywall-blur {
  filter: blur(5px);
  pointer-events: none;
  user-select: none;
  transform: scale(1.01);
}
.paywall-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: .6rem;
  /* was rgba(237,244,238,.82) — a pale green wash from the pine palette,
     which read as a tint over the data rather than as a cover on it */
  background: color-mix(in srgb, var(--surface) 88%, transparent);
  border-radius: var(--radius);
  z-index: 2;
}
.paywall-lock { font-size: 1.5rem; }
.paywall-msg {
  font-size: .82rem; font-weight: 600; color: var(--accent);
  text-align: center; padding: 0 1.5rem; line-height: 1.45;
  font-family: var(--font-mono);
  letter-spacing: .04em;
}

/* ═══════════════════════════════════════════════════════════════
   v4 — FEATURES + DARK MODE + POLISH
   Streak · Focus · Achievements · Compare · Search · Dark theme
   ═══════════════════════════════════════════════════════════════ */

/* ── Feature row on dashboard ──────────────────────────────────── */
.feature-row { margin-top: 1rem; display: flex; flex-direction: column; gap: .7rem; }
.streak-focus { display: grid; grid-template-columns: auto 1fr; gap: .7rem; align-items: stretch; }

/* Streak chip */
.streak-chip {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 2px; min-width: 92px; padding: .8rem .6rem;
  /* Was a peach gradient (#fff7ed -> #ffedd5) with a #fed7aa border, from the
     palette this app retired. Flat surface + hairline, like every other block:
     the chip earns its emphasis from .is-active, not from being tinted. */
  background: var(--surface2);
  border: 1.5px solid var(--line); border-radius: var(--radius);
  box-shadow: var(--shadow-sm); text-align: center;
}
.streak-chip.is-active { border-color: var(--accent); box-shadow: none; }
.streak-chip.streak-dim { background: var(--surface2); border-color: var(--line); opacity: .85; }
.streak-flame { font-size: 1.15rem; line-height: 1; color: var(--accent); }
.streak-n {
  font-family: var(--font-display); font-size: 1.7rem; font-weight: 800;
  line-height: 1; color: var(--accent); font-variant-numeric: tabular-nums;
}
.streak-lbl {
  font-family: var(--font-mono); font-size: .54rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: var(--track-caps-xs); color: var(--text-muted); line-height: 1.25;
}

/* Focus card */
.focus-card {
  background: var(--surface); border: 1.5px solid var(--border);
  border-left: 4px solid var(--yellow); border-radius: var(--radius);
  padding: .85rem 1rem; box-shadow: var(--shadow-sm);
  display: flex; flex-direction: column; justify-content: center;
}
.focus-card.sev-high { border-left-color: var(--red); }
.focus-card.sev-low  { border-left-color: var(--blue); }
.focus-card.focus-clean { border-left-color: var(--green); }
.focus-head { display: flex; align-items: center; gap: .5rem; margin-bottom: .4rem; }
.focus-icon { font-size: 1rem; }
.focus-kicker {
  font-family: var(--font-mono); font-size: .56rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: var(--track-caps-xs); color: var(--text-muted);
}
.focus-conf {
  margin-left: auto; font-family: var(--font-mono); font-size: .58rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: var(--track-caps-xs); padding: 2px 7px; border-radius: var(--radius-sm);
}
.conf-high   { background: var(--surface3); color: var(--red); }
.conf-medium { background: var(--surface3); color: var(--yellow); }
.conf-low    { background: var(--surface3); color: var(--text-muted); }
.focus-title { font-family: var(--font-display); font-size: 1rem; font-weight: 700; text-transform: uppercase; letter-spacing: var(--track-caps-lg); color: var(--text); }
.focus-sub { font-size: .78rem; color: var(--text-muted); margin-top: .25rem; line-height: 1.45; }
.focus-sub strong { color: var(--text); font-weight: 700; }

/* Achievements strip */
.ach-strip {
  display: flex; align-items: center; gap: .55rem;
  /* A two-colour wash from the retired red to the retired green. Two accents
     in one decorative fill is the thing both reference systems refuse most
     explicitly, and neither colour was in the palette any more. */
  background: var(--surface);
  border: 1.5px solid var(--border-hi); border-radius: var(--radius);
  padding: .7rem 1rem; cursor: pointer;
  transition: border-color .15s, transform .12s, box-shadow .15s;
}
.ach-strip:hover { border-color: var(--pine); transform: translateY(-1px); box-shadow: var(--shadow-sm); }
.ach-trophy { font-size: 1.2rem; }
.ach-count { font-family: var(--font-display); font-size: 1.15rem; font-weight: 800; color: var(--text); }
.ach-lbl { font-family: var(--font-mono); font-size: .6rem; text-transform: uppercase; letter-spacing: var(--track-caps-xs); color: var(--text-muted); }
.ach-go { margin-left: auto; font-family: var(--font-display); font-size: .8rem; font-weight: 700; color: var(--pine); text-transform: uppercase; letter-spacing: var(--track-caps); }

/* Achievements modal grid */
.ach-head-count { font-family: var(--font-mono); font-size: .6rem; font-weight: 500; color: var(--text-muted); letter-spacing: .06em; margin-left: .4rem; }
.ach-grid { display: flex; flex-direction: column; gap: .5rem; max-height: 60vh; overflow-y: auto; }
.ach-item {
  display: flex; align-items: center; gap: .8rem;
  padding: .7rem .85rem; border-radius: var(--radius);
  border: 1px solid var(--line); background: var(--surface);
  transition: background var(--dur), border-color var(--dur);
}
/* Earned is marked by the accent rule and full opacity; locked is dimmed.
   The gradient wash said the same thing a third time. */
.ach-item.got { background: var(--surface); border-left: 2px solid var(--accent); }
.ach-item.locked { opacity: .62; }
.ach-item-icon { font-size: 1.5rem; flex-shrink: 0; width: 32px; text-align: center; }
.ach-item-text { flex: 1; min-width: 0; }
.ach-item-name { font-family: var(--font-display); font-size: .92rem; font-weight: 700; text-transform: uppercase; letter-spacing: var(--track-caps); color: var(--text); }
.ach-item-desc { font-size: .74rem; color: var(--text-muted); margin-top: 1px; }
.ach-item-check { color: var(--green); font-weight: 800; font-size: 1.1rem; flex-shrink: 0; }

/* ── Compare ───────────────────────────────────────────────────── */
.compare-host { margin-bottom: 1.5rem; }
.compare-card { background: var(--surface); border: 1.5px solid var(--border); border-top: 3px solid var(--pine); border-radius: var(--radius); padding: 1.1rem 1.2rem; box-shadow: var(--shadow-sm); }
.compare-selects { display: flex; align-items: center; gap: .6rem; margin-bottom: 1rem; }
.cmp-sel {
  flex: 1; min-width: 0; background: var(--surface); border: 1.5px solid var(--border-hi);
  border-radius: var(--radius-sm); color: var(--text); padding: .55rem .7rem;
  font-family: var(--font-mono); font-size: .72rem; outline: none;
  appearance: none; -webkit-appearance: none; cursor: pointer;
}
.cmp-sel:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-weak); }
.cmp-vs { font-family: var(--font-display); font-size: .8rem; font-weight: 800; color: var(--text-muted); text-transform: uppercase; letter-spacing: var(--track-caps); flex-shrink: 0; }
.compare-result { display: flex; flex-direction: column; gap: 0; }
.cmp-row {
  display: grid; grid-template-columns: 1fr auto auto auto; gap: .6rem; align-items: center;
  padding: .55rem 0; border-top: 1px solid var(--border); font-size: .82rem;
}
.cmp-row:first-child { border-top: none; }
.cmp-label { color: var(--text-muted); font-family: var(--font-mono); font-size: .62rem; text-transform: uppercase; letter-spacing: var(--track-caps-xs); }
.cmp-a, .cmp-b { font-family: var(--font-display); font-weight: 700; font-variant-numeric: tabular-nums; color: var(--text); min-width: 56px; text-align: right; }
.cmp-a small, .cmp-b small { font-size: .6em; color: var(--text-dim); margin-left: 1px; font-weight: 600; }
.cmp-delta { font-family: var(--font-mono); font-size: .68rem; font-weight: 600; min-width: 48px; text-align: center; font-variant-numeric: tabular-nums; }
.cmp-delta.good { color: var(--green); }
.cmp-delta.bad { color: var(--red); }
.cmp-delta.neutral { color: var(--text-dim); }
.cmp-legend { display: flex; justify-content: space-between; margin-top: .7rem; padding-top: .6rem; border-top: 1px dashed var(--border); font-family: var(--font-mono); font-size: .56rem; text-transform: uppercase; letter-spacing: var(--track-caps-xs); color: var(--text-dim); }
.cmp-hint { font-size: .8rem; color: var(--text-muted); text-align: center; padding: .5rem; }

/* ── Search bar ────────────────────────────────────────────────── */
.search-bar {
  display: flex; align-items: center; gap: .5rem;
  background: var(--surface); border: 1.5px solid var(--border-hi);
  border-radius: 99px; padding: .5rem .95rem; margin-bottom: .8rem;
  box-shadow: var(--shadow-sm); transition: border-color .15s, box-shadow .15s;
}
.search-bar:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-weak); }
.search-ico { font-size: .85rem; opacity: .6; flex-shrink: 0; }
#sessionSearch {
  flex: 1; border: none; outline: none; background: none; color: var(--text);
  font-family: var(--font-body); font-size: .85rem;
}
#sessionSearch::placeholder { color: var(--text-dim); }

/* ── Dark-mode toggle switch ───────────────────────────────────── */
.theme-switch { width: 42px; height: 24px; border-radius: 0; background: var(--surface3); border: 1px solid var(--line-strong); position: relative; transition: background var(--dur), border-color var(--dur); flex-shrink: 0; }
.theme-switch .theme-knob { position: absolute; top: 2px; left: 2px; width: 18px; height: 18px; border-radius: 0; background: var(--text-dim); box-shadow: none; transition: transform var(--dur), background var(--dur); }
.theme-switch.on { background: var(--accent-weak); border-color: var(--accent); }
.theme-switch.on .theme-knob { background: var(--accent); }
.theme-switch.on .theme-knob { transform: translateX(20px); }

/* Desktop: feature row spans cleanly */
@media (min-width: 768px) {
  .streak-focus { grid-template-columns: auto 1fr; }
}

/* ═══════════════════════════════════════════════════════════════
   DARK MODE — token overrides only (everything else inherits)
   ═══════════════════════════════════════════════════════════════ */
html.dark {
  color-scheme: dark;
  --bg:        #0B0D10;
  --surface:   #14171C;
  --surface2:  #1C2026;
  --surface3:  #232830;
  --line:      #2A2F37;
  --line-strong: #3A404A;
  --text:       #F4F6F8;
  --text-muted: #B9C2CD;
  --text-dim:   #949DAA;
  --accent:      #FF6A2B;
  --on-fill:     #0B0D10;   /* the fills are light here, so the ink is dark */
  --chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%23FF6A2B' stroke-width='1.6' stroke-linecap='square' stroke-linejoin='miter'%3E%3Cpolyline points='5 9 12 16 19 9'/%3E%3C/svg%3E");
  --accent-weak: rgba(255,106,43,.12);
  --hm1: rgba(255,106,43,.26);
  --hm2: rgba(255,106,43,.50);
  --hm3: rgba(255,106,43,.74);
  --forest:  #FF7F49;
  --green:       #35C08A;
  --green-light: #37B082;
  --green-glow:  rgba(53,192,138,.45);
  --withheld:    #8B93A0;
  --yellow:      #E0A93A;
  --red:         #FE6F66;
  --blue:        #4FA8E8;
  --overlay: rgba(0,0,0,.72);
  --shadow-md: 0 8px 30px rgba(0,0,0,.45);
  --shadow-lg: 0 20px 60px rgba(0,0,0,.6);
  /* --border, --border-hi, --turf, --pine, --accent-glow and --accent-ink
     inherit via their :root var() aliases, which re-resolve against the
     values above — --accent-ink therefore follows --on-fill into the dark */
}
/* Table headers stay legible */
html.dark th { background: var(--surface2); color: var(--text); }
html.dark th:hover { background: var(--surface3); }
/* Both bars read their tokens directly now, so dark needs no nav override. */
html.dark .settings-section-title { background: var(--surface2); }
/* Streak chip stays warm but tuned for dark */
html.dark .streak-chip.streak-dim { background: var(--surface2); border-color: var(--border); }
html.dark .paywall-overlay { background: color-mix(in srgb, var(--surface) 88%, transparent); }

/* ─────────────────────────────────────────────────────────────
   NEW FEATURES: Goals, View Preferences, Customization
   ───────────────────────────────────────────────────────────── */

.pref-toggle {
  display: inline-block; width: 20px; text-align: center; font-weight: 700;
  color: var(--accent); transition: color var(--dur);
}

.goal-inputs {
  background: var(--surface2); padding: var(--s3); border-radius: var(--radius);
  border: 1px solid var(--line);
}

.goal-item {
  display: grid; grid-template-columns: 1fr auto; gap: .5rem;
  align-items: center; padding: .6rem; background: var(--surface2);
  border-radius: var(--radius-sm); margin-bottom: .5rem; transition: background .15s;
}

.goal-metric { font-weight: 600; color: var(--text); font-size: .9rem; }
.goal-progress { font-size: .75rem; color: var(--text-muted); margin: .4rem 0 .4rem; }
.goal-bar { height: 6px; background: var(--surface3); border-radius: 0; overflow: hidden; }
.goal-bar-fill { height: 100%; background: var(--accent); transition: width .3s; }

/* The severity lives in the left rule. The tinted fill behind it was a
   second encoding of the same fact, in colours mixed by hand rather than
   taken from the palette — so a "decline" was a different red from --red. */
.alert-item {
  padding: .7rem .9rem; border-left: 3px solid var(--yellow); background: var(--surface2);
  border-radius: var(--radius); margin-bottom: .6rem; font-size: .9rem;
}
.alert-item.improvement { border-left-color: var(--green); }
.alert-item.decline { border-left-color: var(--red); }

.benchmark-table {
  width: 100%; border-collapse: collapse; font-size: .85rem;
  margin-top: .8rem;
}
.benchmark-table th {
  padding: .5rem; text-align: left; background: var(--surface);
  font-weight: 600; border-bottom: 1.5px solid var(--line-strong);
}
.benchmark-table td {
  padding: .5rem; border-bottom: 1px solid var(--line);
}
.benchmark-table tr:hover { background: var(--surface2); }

/* Smooth theme transition */
body, .session-card, .dash-tile, .chart-card, .metric-card, .settings-section,
.focus-card, .compare-card, .ach-strip, .streak-chip, .search-bar, .goal-item,
.alert-item, .benchmark-table {
  transition: background-color .25s ease, border-color .25s ease, color .25s ease;
}

/* ═══════════════════════════════════════════════════════════════
   REDESIGN v5 — UX/NAVIGATION OVERHAUL
   Information architecture, visual hierarchy, cognitive flow
   ═══════════════════════════════════════════════════════════════ */

/* Quick stats always visible (sticky mini-dashboard) */
/* One strip, divided by hairlines rather than four separate filled boxes:
   these are four readings off the same session, not four cards. */
.quick-stats {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 0;
  padding: var(--s3) var(--s4); background: var(--surface);
  border-top: 1px solid var(--line); border-bottom: 1px solid var(--line);
  font-size: .75rem; font-weight: 600; text-transform: uppercase; letter-spacing: var(--track-caps);
  position: sticky; top: 0; z-index: 50;
}
.quick-stat {
  text-align: center; padding: .4rem var(--s2);
  border-radius: 0; background: none;
  border-left: 1px solid var(--line);
}
.quick-stat:first-child { border-left: none; }
.quick-stat-value {
  font-size: 1.15rem; font-weight: 800; color: var(--text);
  font-family: var(--font-display); font-variant-numeric: tabular-nums;
}
.quick-stat-label { color: var(--text-dim); margin-top: 2px; letter-spacing: .08em; }

/* The session card's LAYOUT only. This block used to redeclare the whole
   card — border, background, a drop shadow and a hover state with a red glow
   and a 2px lift — and being later in the file it quietly won over the
   reskin, so every session row still carried the retired accent and a shadow
   the spec forbids on chrome. Found by a QC pass that walked every view in
   both themes looking for a computed box-shadow, because reading the
   stylesheet had not found it in two passes. The card's appearance now lives
   in exactly one place, in the Session list section. */
.session-card {
  overflow: visible;
  display: grid; grid-template-columns: 1fr auto; gap: .8rem;
}
.session-card-date { font-weight: 600; color: var(--text); font-size: 1rem; }
.session-card-meta { font-size: .8rem; color: var(--text-muted); margin: .3rem 0; }
.session-card-badges { display: flex; gap: .4rem; margin-top: .5rem; flex-wrap: wrap; }
.session-badge { display: inline-block; padding: 3px 9px; background: var(--accent); color: var(--accent-ink);
  font-size: .65rem; font-weight: 700; border-radius: var(--radius-sm); text-transform: uppercase;
  letter-spacing: var(--track-caps-xs); }
.session-badge.fault { background: var(--red); color: var(--on-fill); }
.session-badge.improvement { background: var(--green); color: var(--on-fill); }

.session-card-score { text-align: right; }
.session-score-text { display: block; }
.session-score-ring { position: relative; width: 70px; height: 70px; }
/* The score ring had a soft drop-shadow from the skin before this one.
   Chrome here is flat — --shadow-sm is literally `none`, and elevation is a
   1px hairline. It is the only lifted thing left on the home view. */
.session-score-num { font-size: 1.8rem; font-weight: 800; font-family: var(--font-display);
  line-height: 1; text-align: center; }
.session-score-label { font-size: .65rem; color: var(--text-muted); text-transform: uppercase;
  letter-spacing: var(--track-caps-xs); text-align: center; }

/* Drill recommendation cards */
.drill-card {
  padding: var(--s4); background: var(--surface);
  border: 1px solid var(--line); border-left: 2px solid var(--accent);
  border-radius: var(--radius);
  transition: border-color var(--dur), background var(--dur); margin-bottom: .8rem;
}
/* Pointer and hover only on a card that goes somewhere (R4) — the practice
   plan's cards are read, not pressed, and looked pressable. */
.drill-card[role="button"] { cursor: pointer; }
.drill-card[role="button"]:hover { border-color: var(--line-strong); border-left-color: var(--accent); background: var(--surface2); }
.drill-icon { font-size: 1.5rem; margin-bottom: .4rem; }
.drill-title { font-weight: 700; color: var(--text); margin-bottom: .2rem; }
.drill-desc { font-size: .85rem; color: var(--text-muted); line-height: 1.4; }
.drill-time { font-size: .75rem; color: var(--text-dim); margin-top: .4rem; }
.stat-card {
  padding: var(--s3); background: var(--surface); border-radius: var(--radius);
  border: 1px solid var(--line); text-align: center;
}
.stat-value { font-size: 1.5rem; font-weight: 800; color: var(--text);
  font-family: var(--font-display); font-variant-numeric: tabular-nums; }
.stat-label { font-size: .7rem; color: var(--text-dim); text-transform: uppercase;
  letter-spacing: var(--track-caps); margin-top: .3rem; }

/* Mobile optimizations */
@media (max-width: 640px) {
  .quick-stats { grid-template-columns: repeat(2, 1fr); padding: .5rem .8rem; }
  /* on two rows the third and fourth cell start a new row, so the divider
     on the left of cell 3 would be a stray vertical rule */
  .quick-stat:nth-child(3) { border-left: none; }
  .quick-stat:nth-child(n+3) { border-top: 1px solid var(--line); }
  .session-card { grid-template-columns: 1fr; }
}


/* Mobile-first optimizations for touch interaction */
@media (max-width: 480px) {
  .quick-stats { grid-template-columns: repeat(2, 1fr); padding: .4rem .6rem; }
  .quick-stat { padding: .3rem; }
  .quick-stat-value { font-size: 0.95rem; }
  .session-card { padding: .8rem; gap: .6rem; }
  .stat-card { padding: .6rem; }
  .stat-value { font-size: 1.2rem; }
  /* Shrink action buttons on very small screens, but NOT icon/close buttons */
  .btn-primary, .btn-secondary, .btn-danger { padding: .55rem .9rem; font-size: .8rem; }
}

/* Better visual feedback on hover/active */
.session-card:active { transform: scale(0.99); }

/* Smooth animations */
/* Translate only, from a `from` with no `to`: the content is visible for the
   whole animation, and the resting state is whatever the element computes to
   (the scroll-motion rule). It used to fade in from opacity 0 — and a second
   `@keyframes slideUp` below, the cookie banner's, won the name outright, so
   plan cards and the stats strip entered from translateY(100%) and invisible
   (V36). The banner's keyframe is `cookieIn` now. */
@keyframes slideUp {
  from { transform: translateY(10px); }
}

.quick-stats { animation: slideUp .3s ease; }
.drill-card { animation: slideUp .4s ease; }

/* Better form styling */
input[type="number"], input[type="text"], input[type="email"],
input[type="password"], textarea, select {
  appearance: none; -webkit-appearance: none;
  background-image: none;
  /* iOS Safari zooms the page when a focused input is < 16px.
     max() keeps the visual size at the designed 1em on desktop, 16px floor on mobile. */
  font-size: max(16px, 1em);
}

select::-webkit-search-cancel-button,
select::-webkit-outer-spin-button,
select::-webkit-inner-spin-button {
  -webkit-appearance: none;
}

/* Touch-friendly tap targets — scoped so inline text links and icon buttons aren't distorted */
.btn-primary, .btn-secondary, .btn-danger, .btn-back,
.bottom-nav-item, .nav-link, .top-nav-links .nav-link,
[data-view], [data-close-modal], [data-close] {
  min-height: 44px;
}
/* Fast tap on all interactive elements — removes 300ms delay on mobile */
a, button { touch-action: manipulation; }

/* Better contrast for readability */

/* Improved chart sizing */
canvas {
  image-rendering: crisp-edges;
  image-rendering: pixelated;
}

/* ── Cookie Consent Banner ─────────────────────────────── */
.cookie-consent {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 1000;
  background: var(--surface);
  backdrop-filter: none;
  border-top: 1px solid var(--line-strong);
  padding: 1.2rem;
  padding-bottom: calc(1.2rem + env(safe-area-inset-bottom));
  animation: cookieIn .3s ease;
}

.cookie-consent-content {
  max-width: 600px;
  margin: 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.2rem;
}

.cookie-consent-text {
  color: var(--text);
  flex: 1;
}

.cookie-consent-text strong {
  display: block;
  margin-bottom: 0.5rem;
  font-size: 0.95rem;
}

.cookie-consent-text p {
  font-size: 0.85rem;
  color: var(--text-muted);
  margin: 0;
}

.cookie-consent-text a {
  color: var(--accent);
  text-decoration: underline;
  font-weight: 600;
}

.cookie-consent-actions {
  display: flex;
  gap: 0.6rem;
  flex-shrink: 0;
}

.cookie-consent-actions button {
  white-space: nowrap;
}

@media (max-width: 640px) {
  .cookie-consent-content {
    flex-direction: column;
    align-items: stretch;
  }

  .cookie-consent-actions {
    flex-direction: column;
    width: 100%;
  }

  .cookie-consent-actions button {
    width: 100%;
  }
}

/* The banner is fixed to the bottom edge, so it rises from below the screen
   rather than fading: off-screen is not a resting state it ever holds, and
   without the animation it simply sits where it belongs. */
@keyframes cookieIn {
  from { transform: translateY(100%); }
}

/* ── Range card ───────────────────────────────────────────────────
   The plan at the mat: one block filling the screen, readable at arm's
   length in daylight. Sizes here are deliberately larger than anywhere
   else in the app — this is the one surface read from a metre away with
   a club in one hand. */
html.range-open, html.range-open body { overflow: hidden; }

/* iOS paints a translucent grey rectangle over anything tappable on touch.
   The app has its own :active states, so the default just reads as a smudge. */
a, button, .bottom-nav-item, .chip, [role="button"] { -webkit-tap-highlight-color: transparent; }
.range-card{
  position:fixed;inset:0;z-index:1200;background:var(--bg);
  display:flex;align-items:stretch;justify-content:center;
  padding:env(safe-area-inset-top,0) 0 env(safe-area-inset-bottom,0);
}
.rc-shell{display:flex;flex-direction:column;width:100%;max-width:560px;padding:1rem 1.2rem 1.2rem}
.rc-x{position:absolute;top:.8rem;right:.9rem;width:2.4rem;height:2.4rem;border:none;border-radius:50%;
  background:var(--surface);color:var(--text-muted);font-size:1.1rem;cursor:pointer}
.rc-dots{display:flex;gap:.4rem;justify-content:center;margin:.4rem 0 1.2rem}
.rc-dot{width:.6rem;height:.6rem;padding:0;border:none;border-radius:50%;background:var(--border);cursor:pointer}
.rc-dot.on{background:var(--accent);transform:scale(1.35)}
.rc-dot.done{background:var(--green)}
.rc-body{flex:1;display:flex;flex-direction:column;justify-content:center;gap:.5rem;min-height:0;overflow-y:auto}
.rc-step{font-size:.78rem;letter-spacing: var(--track-caps);text-transform:uppercase;color:var(--text-muted)}
.rc-name{font-size:1.75rem;font-weight:800;line-height:1.15}
.rc-count{font-size:1.15rem;font-weight:700;color:var(--accent)}
.rc-cue{margin-top:.8rem;padding:.9rem 1rem;border-radius:var(--radius);background:var(--surface);
  border:1px solid var(--border)}
.rc-cue-name{font-size:1.05rem;font-weight:700;margin-bottom:.35rem}
.rc-cue-desc{font-size:.95rem;line-height:1.5;color:var(--text-dim);margin:0}
.rc-note{font-size:.82rem;line-height:1.45;color:var(--text-muted);margin:.5rem 0 0}
.rc-space{font-size:.82rem;color:var(--text-muted);margin-top:.9rem}
.rc-foot{display:flex;gap:.6rem;align-items:center;padding-top:1rem}
.rc-btn{flex:0 0 auto;padding:.9rem 1.1rem;font:inherit;font-size:1rem;font-weight:600;
  border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--surface);
  color:var(--text);cursor:pointer;min-width:3.4rem}
.rc-btn.primary{flex:1;background:var(--accent);border-color:var(--accent);color:var(--accent-ink)}
.rc-btn.primary.logged{background:var(--green);border-color:var(--green)}
.rc-btn.ghost{color:var(--text-muted)}
.rc-btn[disabled]{opacity:.35;cursor:default}
.rc-end{text-align:center}
.rc-end-h{font-size:1.6rem;font-weight:800}
/* Focused by script after each repaint so the new block is announced; they
   are headings, not controls, so they draw no ring (R22). */
.rc-name:focus, .rc-end-h:focus { outline: none; }
.rc-end-n{font-size:1.05rem;font-weight:700;color:var(--accent);margin-top:.3rem}
.rc-end-note{font-size:.85rem;line-height:1.5;color:var(--text-muted);margin:1rem 0 0}

/* ── Since last comparable session ────────────────────────────────
   Deltas only get a colour when Metrics.changeIsReal says the move
   cleared the golfer's own spread. `flat` is the default, not the
   exception — most week-to-week movement is noise. */
.since-block{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);
  padding:1rem 1.1rem;margin-bottom:1rem}
.since-head{font-size:.95rem;font-weight:700;margin-bottom:.7rem}
.since-rows{display:flex;flex-direction:column;gap:.45rem}
.since-row{display:grid;grid-template-columns:auto auto 1fr;gap:.5rem;align-items:baseline;
  font-size:.85rem;padding-bottom:.4rem;border-bottom:1px solid var(--border)}
.since-row:last-child{border-bottom:none;padding-bottom:0}
.since-label{font-weight:600}
.since-delta{font-weight:700;font-variant-numeric:tabular-nums;color:var(--text-muted)}
.since-row.good .since-delta{color:var(--green)}
.since-row.bad  .since-delta{color:var(--accent)}
.since-was{font-size:.78rem;color:var(--text-dim);font-variant-numeric:tabular-nums}
.since-tag{grid-column:1/-1;font-size:.72rem;color:var(--text-muted)}
.since-note{font-size:.76rem;line-height:1.5;color:var(--text-muted);margin:.7rem 0 0}

/* ── Probe deadlines ──────────────────────────────────────────────
   A task that expires and does not say so is a task the golfer misses. */
.next-deadline{margin-top:.5rem;font-size:.78rem;font-weight:700;color:var(--accent)}
.drill-card.next-step.has-deadline{border-left:3px solid var(--accent)}
.probe-deadline{margin-top:.45rem;font-size:.78rem;font-weight:700;color:var(--accent)}
.probe-block.lapsed{opacity:.9}
.probe-block.lapsed .probe-head{color:var(--text-muted)}

/* ── Yardage-book trend column ────────────────────────────────────
   The sparkline is drawn only when the verdict is real — see the
   legend. `flat` is the default state, and it is a finding. */
.yard-trend{white-space:nowrap;font-size:.76rem;color:var(--text-muted)}
.yard-trend.up{color:var(--green)}
.yard-trend.down{color:var(--accent)}
.yard-spark{display:block;margin-bottom:.15rem}
.yard-trend-label{display:block;font-weight:600}
.yard-trend-n{display:block;color:var(--text-muted);font-weight:400;font-size:.7rem}

/* ── The yardage book on a phone ─────────────────────────────
   Seven columns do not fit 393px: the table measured 723px inside a 359px
   box, so the book scrolled sideways and the trend column — the one new thing
   it can say — sat off-screen on the screen a golfer stands over a shot with.
   Under 640px each club becomes a block: club and stock carry on the first
   line, where the eye goes; the trend under it; the four secondary figures as
   a labelled row beneath. SCREEN only — a card printed from a phone still
   prints the full table, which is what the print head is written against. */
@media screen and (max-width: 640px) {
  #yardageTable, #yardageTable tbody { display: block; }
  #yardageTable thead { display: none; }
  #yardageTable tr {
    display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: var(--s1) var(--s3); padding: var(--s3) 0;
    border-bottom: 1px solid var(--line);
  }
  #yardageTable td { display: block; padding: 0; border: 0; min-width: 0; }
  #yardageTable .yc-club  { grid-column: 1 / 3; align-self: center; }
  #yardageTable .yc-carry { grid-column: 3 / 5; text-align: right; white-space: nowrap; }
  #yardageTable .yc-trend { grid-column: 1 / -1; white-space: normal; }
  #yardageTable .yc-note  { grid-column: 1 / -1; }
  #yardageTable .yard-thin .yc-shots { grid-column: 1 / -1; }
  #yardageTable td[data-label]::before {
    content: attr(data-label); display: block;
    font-family: var(--font-mono); font-size: .6rem; font-style: normal;
    text-transform: uppercase; letter-spacing: var(--track-caps-xs);
    color: var(--text-dim); margin-bottom: 2px;
  }
}

/* ── Session tags ─────────────────────────────────────────────────
   A finder, not a variable — see SessionTags. They look like chips
   rather than badges so nothing reads them as a verdict on a session. */
.tag-row{display:flex;flex-wrap:wrap;gap:.4rem;margin:.6rem 0 .9rem}
.tag-chip{padding:.3rem .6rem;font:inherit;font-size:.75rem;font-weight:600;border:1px solid var(--border);
  border-radius:999px;background:var(--surface);color:var(--text-dim);cursor:pointer}
.tag-chip.on{border-color:var(--accent);color:var(--accent)}
.tag-n{margin-left:.3rem;color:var(--text-muted);font-weight:400}
.tag-note{flex-basis:100%;font-size:.72rem;line-height:1.45;color:var(--text-muted);margin-top:.2rem}
.session-tags{display:flex;flex-wrap:wrap;gap:.3rem;margin-top:.55rem}
.session-tag{padding:.15rem .45rem;font:inherit;font-size:.7rem;font-weight:600;border:1px solid var(--border);
  border-radius:999px;background:transparent;color:var(--text-muted);cursor:pointer}
.session-tag:hover{border-color:var(--accent);color:var(--accent)}

/* ── Correcting the alignment flag ────────────────────────────────
   A caveat that names what is withheld and offers no way to answer it
   is a dead end. Sized as a quiet link, not a call to action: it is a
   claim about what happened, not a setting to flip. */
.caveat-fix{margin-top:.7rem}
.caveat-fix .link-btn{font-size:.78rem}

/* ── The printed yardage card ─────────────────────────────────────
   A card lives in a golf bag and outlives the screen it came from, so
   the caveats print WITH it. Anything not on the paper is gone: on
   screen a caveat is a paragraph you can scroll back to, on paper it
   either printed or it did not. */
.print-only { display: none; }
@media print {
  /* Nothing but the book. Nav, other views, the drill finder, the
     records grid and every control are screen furniture. */
  /* `.top-bar` was a class that does not exist in this codebase, so the
     desktop nav printed on the card for as long as this rule has been here —
     the element is `.top-nav`. C4 says everything but the book is hidden. */
  .bottom-nav, .top-nav, .view-header, .no-print, .modal-overlay, .cookie-consent,
  #drillFinderHost, #recordsGrid, #yardages-empty, .paywall-overlay,
  .tag-row, .search-bar, .range-card { display: none !important; }
  .view:not(#view-yardages) { display: none !important; }
  #view-yardages { display: block !important; padding: 0 !important; }
  .print-only { display: block; }

  /* Print in ink on paper, whatever theme the screen was in. A dark
     theme sent to a printer is a solid black page. */
  html, body, #view-yardages { background: #fff !important; color: #000 !important; }
  * { color: #000 !important; box-shadow: none !important; }
  .table-wrap { overflow: visible !important; }
  #yardageTable { width: 100%; border-collapse: collapse; font-size: 11pt; }
  #yardageTable th, #yardageTable td { border-bottom: 1px solid #999; padding: .35rem .3rem; text-align: left; }
  .yard-spark { display: none; }          /* a sparkline is meaningless in one ink colour */
  .club-dot { display: none; }            /* so is a colour key */
  .print-title { font-size: 18pt; font-weight: 800; margin-bottom: .2rem; }
  .print-sub { font-size: 9pt; margin-bottom: .15rem; }
  .print-caveat { font-size: 8.5pt; line-height: 1.4; margin: .5rem 0 .8rem; }
  .yardage-intro, .yardage-meta { font-size: 9pt; }
  @page { margin: 12mm; }
}

/* ── Legal document -> PDF ─────────────────────────────────────────────
   The "PDF" button on a legal modal puts .legal-print on <html> and calls
   window.print(). The browser's own print-to-PDF then produces the file.

   This is not a fallback for a real PDF library — it is the only route the
   app's own CSP leaves open. object-src is 'none' and frame-src allows only
   accounts.google.com, so a PDF cannot be embedded or framed in the page,
   and widening either directive on a page that holds an auth token in
   localStorage costs real security for a cosmetic gain. It also keeps ONE
   copy of each document: the PDF is rendered from the same markdown the
   screen renders, so the two cannot drift.

   When .legal-print is on, the rule below outranks the print block above,
   which would otherwise hide the modal along with everything else. */
@media print {
  /* A modal being open puts body.modal-open into position:fixed (the iOS
     scroll-lock). A fixed body cannot paginate, so the print came out as a
     single page with everything past the first viewport clipped off — a
     four-page policy silently becoming one. This has to be undone first. */
  html.legal-print body.modal-open,
  html.legal-print body {
    position: static !important; overflow: visible !important;
    top: auto !important; width: auto !important; height: auto !important;
  }
  html.legal-print body > *:not(.modal-overlay) { display: none !important; }
  html.legal-print .modal-overlay[hidden] { display: none !important; }
  html.legal-print .modal-overlay {
    display: block !important;
    position: static !important;
    background: none !important;
    backdrop-filter: none !important;
    padding: 0 !important;
    overflow: visible !important;
  }
  html.legal-print .modal {
    max-width: none !important; max-height: none !important;
    border: none !important; box-shadow: none !important;
    padding: 0 !important; margin: 0 !important;
    overflow: visible !important; animation: none !important;
  }
  html.legal-print .modal-document {
    max-height: none !important; overflow: visible !important;
    padding: 0 !important;
    font-size: 9.5pt !important; line-height: 1.5 !important;
  }
  /* Ink on paper, and the controls are not part of the document. */
  html.legal-print, html.legal-print body, html.legal-print .modal {
    background: #fff !important; color: #000 !important;
  }
  html.legal-print * { color: #000 !important; box-shadow: none !important; }
  html.legal-print .btn-icon,
  html.legal-print .doc-head-actions,
  html.legal-print .modal-head,
  html.legal-print .no-print { display: none !important; }
  /* The document's own title block, which carries the effective date and
     version directly under it — the part that matters on a filed copy. */
  html.legal-print .modal-document h1 {
    display: block !important;
    font-size: 16pt !important; margin: 0 0 4pt !important;
    padding-bottom: 6pt !important; border-bottom: 1.5pt solid #000 !important;
  }
  /* A legal document is read clause by clause; a heading orphaned at the
     foot of a page separates it from the clause it governs. */
  html.legal-print .modal-document h1,
  html.legal-print .modal-document h2,
  html.legal-print .modal-document h3 { break-after: avoid; page-break-after: avoid; }
  html.legal-print .modal-document h2 {
    border-top: 1px solid #999 !important;
    font-size: 11pt !important; margin-top: 14pt !important;
  }
  html.legal-print .doc-table-wrap { overflow: visible !important; }
  html.legal-print .doc-table { font-size: 8.5pt !important; min-width: 0 !important; }
  html.legal-print .doc-table th { border-bottom: 1pt solid #000 !important; }
  html.legal-print .doc-table td { border-top: .5pt solid #999 !important; }
  html.legal-print .doc-table tr,
  html.legal-print .modal-document li { break-inside: avoid; page-break-inside: avoid; }
  html.legal-print .modal-document a { text-decoration: none !important; }
  /* A URL is worth nothing on paper unless it is printed. */
  html.legal-print .modal-document a[href^="http"]::after {
    content: " (" attr(href) ")"; font-size: 7.5pt; word-break: break-all;
  }
  html.legal-print .modal-document code { background: none !important; }
}

/* ── First-run orientation ────────────────────────────────────────
   Long-form on purpose: it is a statement of method, not a tour. */
.intro-body{max-height:70vh;overflow-y:auto}
.intro-lead{font-size:.92rem;line-height:1.6;margin:0 0 1.1rem}
.intro-h{font-size:.88rem;font-weight:700;margin:1.3rem 0 .45rem;color:var(--accent)}
.intro-p{font-size:.84rem;line-height:1.6;color:var(--text-dim);margin:0 0 .55rem}
.intro-p strong{color:var(--text)}
.intro-actions{display:flex;flex-wrap:wrap;gap:.6rem;margin-top:1.4rem}
.intro-foot{font-size:.75rem;color:var(--text-muted);margin:.8rem 0 0}

/* ── Cloud reachability warning ───────────────────────────────────
   Rendered above the home view because it changes what everything
   below it means, not merely how it looks. */
.sync-warn{margin:1rem;padding:1rem 1.1rem;border:1px solid var(--accent);border-left-width:4px;
  border-radius:var(--radius);background:color-mix(in srgb, var(--accent) 8%, transparent)}
.sync-warn-head{font-size:.95rem;font-weight:800;color:var(--accent);margin-bottom:.5rem}
.sync-warn-p{font-size:.83rem;line-height:1.55;margin:0 0 .5rem}
.sync-warn-why{font-size:.74rem;color:var(--text-muted);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
  margin:.6rem 0 .8rem;word-break:break-word}

/* ── Drill library groups ─────────────────────────────────────────
   The list was 104 entries all called drills, a quarter of which were
   not. Grouping is the fix: the range work reads as the list, and the
   screen-reads and gym sessions read as what they are. */
.drill-group-head{margin:1.4rem 0 .5rem;padding-top:.9rem;border-top:1px solid var(--border)}
.drill-group-name{display:block;font-size:.85rem;font-weight:700;color:var(--text)}
.drill-group-blurb{display:block;font-size:.76rem;line-height:1.5;color:var(--text-muted);margin-top:.25rem}
/* Not range work — visually stepped back so it cannot be mistaken for it. */
.drill-group-review .drill-row,
.drill-group-fitness .drill-row,
.drill-group-equipment .drill-row{opacity:.82}

/* ── Chipping logger ──────────────────────────────────────────────
   Distance and lie are set once; the rest is two big targets you can
   hit standing on a green without looking at the phone properly. */
.sg-setup{display:grid;grid-template-columns:1fr 1fr;gap:.6rem}
.sg-tap{display:grid;grid-template-columns:1fr 1fr;gap:.6rem;margin:.8rem 0 .6rem}
.sg-tap-btn{padding:1.1rem .6rem;font:inherit;font-size:1rem;font-weight:700;cursor:pointer;
  min-height:var(--tap);
  border-radius:var(--radius);border:1px solid var(--line-strong);background:var(--surface);color:var(--text)}
.sg-tap-btn.yes{border-color:var(--green);color:var(--green)}
.sg-tap-btn.no{border-color:var(--accent);color:var(--accent)}
.sg-tap-btn:active{transform:scale(.97)}
.sg-dist-rows{margin-top:.8rem;display:flex;flex-direction:column;gap:.35rem}
.sg-dist-row{display:grid;grid-template-columns:5rem 4rem 1fr;gap:.5rem;align-items:baseline;
  font-size:.82rem;padding-bottom:.3rem;border-bottom:1px solid var(--line)}
.sg-dist-row:last-child{border-bottom:none}
.sg-dist-row.thin{opacity:.6}
.sg-dist-yd{font-weight:600}
.sg-dist-rate{font-weight:700;font-variant-numeric:tabular-nums}
.sg-dist-n{font-size:.75rem;color:var(--text-muted);font-variant-numeric:tabular-nums}

/* ── Bands — three surface modes, assigned by ROLE ────────────────────
   The home view renders seven insight surfaces and every one of them was a
   .card on --surface with the same 1px hairline, stacked. Nothing on that
   screen was more important than anything else, which undercuts the one
   design decision the screen actually has: SmartRecommendations.getNextStep
   returns exactly ONE ranked card, deliberately, because rule 9 of the
   research base is one cue and never a checklist — and it was rendered in
   the same treatment as the day-streak counter.

   Three modes, and the mode is a property of what a surface IS:

     .band--canvas   the band is the GROUND. Its content already supplies its
                     own cards, so a second ground behind them would be a card
                     behind cards. Caveats, rows of cards, lists.
     .band--surface  the band IS the card. Its content is uncarded, so the
                     band provides the ground and the hairline.
     .band--signal   the inverted band: --text ground, --bg type. Exactly one
                     per view and it is the ranked card.

   Positional alternation (`:nth-child`) is the obvious implementation and it
   is broken here: ViewPrefs hides sections with a class on <html>, and CSS
   cannot count VISIBLE siblings — so a band would land on whichever child
   happened to be fifth, with no error anywhere. No pref hides a whole band on
   the home view today (the heatmap it hides sits inside one), which is exactly
   why a positional scheme would pass a test written now and rot on the next
   pref added — the same defect class as setting `hidden` on a section that
   gets re-rendered. The mode is declared at each surface's own call site in
   app.js, and test/suites/bands.js re-renders the view under every pref class
   the stylesheet knows about to prove the assignment does not move.

   The band spans the app-main gutter and brings its own: a band whose ground
   stopped at the content column would just be a card. It deliberately does
   NOT set `display` — #dashboard and #syncBanner are toggled with the
   `hidden` attribute, and an author `display` here would beat the UA sheet's
   [hidden] rule and leave both permanently visible. */
.band {
  margin-inline: calc(-1 * var(--s4));
  padding: var(--s5) var(--s4);
}
.band--canvas { background: var(--bg); }
.band--surface {
  background: var(--surface);
  border-block: 1px solid var(--line);
}

/* The one high-contrast moment on the screen, and the one place the accent
   steps aside. Measured, not assumed (audited 12 Sep 2026):

     --bg type on --text ground   18.29:1 light   17.96:1 dark   PASS
     --accent on the same ground   3.04:1 light    2.64:1 dark   FAIL

   Both halves now fail, and that is newer than the band. Until the contrast
   fix the light accent cleared this ground at 4.80:1 and stepped aside for
   consistency with dark rather than because it had to. Darkening it to read
   on a WHITE ground necessarily cost it the near-black one — an accent
   cannot clear both ends of the ladder — so the rule that was a judgement
   call in light is now a measurement in both. The dark side is unchanged:
   the inversion puts a near-white ground under the dark-mode accent, and no
   token in the palette is an orange that works on a light ground in dark
   mode. The inversion IS the signal; a hue on top of it would be
   a second one. Emphasis inside the band comes from weight and from --bg at
   a different opacity, never from a hue. An accent FILL is still available
   if one is ever needed: --on-fill on --accent passes at 6.22/6.81,
   because that is ink on a fill rather than a hue on an inverted ground. */
.band--signal {
  background: var(--text);
  color: var(--bg);
}
/* The band is the card. The ranked recommendation keeps its markup — the tap
   target, the icon, the deadline, the "why" — and loses the card ground it
   no longer needs: a --surface box on an inverted band puts a second,
   brighter surface under the one thing the inversion exists to make
   unmissable. The 4px accent left rule goes with it. */
.band--signal .drill-card,
.band--signal .next-step {
  background: none;
  border: 0;
  padding: 0;
  margin-bottom: 0;
  box-shadow: none;
}
.band--signal .drill-card:hover { background: none; opacity: .88; }
/* Every text node in the card names its own colour, so inheritance is not
   enough — and on the dark-mode inversion the card's own title is the extreme
   case: `.drill-title` is `--text`, the band's ground IS `--text`, so the
   ranked card's heading renders at 1.00:1. Literally invisible, with no error
   anywhere. The rest are legibility, not absence: the "why" line (the argument
   for the one recommendation the app makes) at 2.17:1, the description 2.17,
   the deadline's accent 2.64, "Tap to go" 4.37 — all measured in dark mode,
   the theme this app is mostly used in. */
.band--signal .drill-icon,
.band--signal .drill-title,
.band--signal .drill-time,
.band--signal .next-deadline { color: var(--bg); }
.band--signal .drill-desc,
.band--signal .next-why { color: var(--bg); opacity: .82; }
/* The rule beside the "why" was --border, a mid-tone grey chosen for a card.
   On the inverted ground it reads as a hairline from another system; it fades
   with the sentence it brackets instead. */
.band--signal .next-why { border-left-color: var(--bg); }

/* A band supplies the gutter. The sync warning brings `margin: 1rem` of its
   own and would otherwise inset itself a second time inside its band. */
.band > .sync-warn { margin: 0; }

/* The tip of the day, as one line on its band. It used to be a box tinted
   with --accent-weak — an accent wash used as a surface, which is the one
   thing this palette refuses twice over. The band is the card now. */

/* The anchored quick-stat row is a band already and takes no mode class: an
   opaque --surface ground, a hairline top and bottom, hairline-divided cells,
   sticky. It IS the surface mode — the band system was taken from it — so a
   class here would be a no-op that claims to do something. The exemption is
   asserted in test/suites/bands.js against those three properties, so it
   cannot rot into an unearned one.

   It does have to reach the same gutter every other band reaches, though, or
   the one strip that IS a surface mode reads as a card floating inside a page
   of bands. Margin only: its own padding stays, which is why this is not the
   .band class (that would also add --s5 of block padding to a sticky row). */
.quick-stats { margin-inline: calc(-1 * var(--s4)); }


/* ── iOS: never let a focusable text control fall under 16px ──────────
   Safari on iOS zooms the whole page when a focused field is smaller than
   16px, and it does not zoom back out — so one tap on a dropdown leaves the
   app scaled up and scrolling sideways for the rest of the visit. This is the
   single most common reason a sound layout "feels broken" on an iPhone.

   Keyed on WIDTH rather than `pointer: coarse`: the pointer media feature is
   not reliably emulated by headless Chromium, so a rule written against it
   cannot be verified by the browser checks in test/browser — and an unverifiable
   fix for a bug you cannot reproduce is a guess. Phone widths are where the zoom
   happens anyway. Last in the file so it beats the per-component rules that set
   .88rem. Checkboxes, radios and file inputs are excluded: they never trigger
   the zoom, and a file input's font-size drives its button size. */
@media (max-width: 640px) {
  /* !important is deliberate and is the right tool here. This is a platform
     constraint, not a design preference: component rules legitimately set their
     own type scale (there are two separate `.form-group select` rules alone,
     which is how the first attempt at this silently lost), and a bare element
     selector can never outrank a class no matter where it sits in the file.
     Anything that must hold across every component regardless of what that
     component wants is exactly what this is for. */
  select,
  textarea,
  input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="range"]) {
    font-size: 16px !important;
  }
}

/* ── Phones: one sticky bar, and the score stays in the card's header ──
   V1: below 768px the view header is already sticky at top:0, and so was the
   quick-stats strip, so the two stacked while scrolling: the header covered
   the strip's top row and left an orphan label, while the conditions line
   that qualifies those numbers scrolled away. On a phone the strip scrolls.
   V2: `.session-card` went to one column under 640px, which dropped the grade
   ring and score below Share/Export, hanging off the card's left rule. It
   stays in the header corner instead, compact, and the text leaves it room.
   After every base rule for these selectors, so none of them can override it. */
@media (max-width: 767px) {
  .quick-stats { position: static; }
}
@media (max-width: 640px) {
  .session-card { position: relative; }
  .session-card-score { position: absolute; top: var(--s3); right: var(--s3); }
  .session-card-score .session-score-ring { width: auto; height: auto; display: flex; align-items: center; gap: .4rem; }
  .session-card-score .session-score-ring svg { width: 40px; height: 40px; }
  .session-card-score .session-score-num { font-size: 1.2rem; text-align: left; }
  .session-card-score .session-score-label { text-align: left; }
  .session-card .session-card-date, .session-card .session-card-meta { padding-right: 88px; }
}

/* R14: the first Tab stop on every page. Off-screen until focused, then on
   top of everything, so a keyboard user can pass the header and nav once. */
.skip-link {
  position: absolute; left: var(--s3); top: -3rem; z-index: 10000;
  padding: .6rem 1rem; background: var(--accent); color: var(--on-fill);
  font-weight: 600; text-decoration: none;
}
.skip-link:focus { top: var(--s3); }
#appMain:focus { outline: none; }
