/* ============================================================
   BASE — design tokens, reset, typography, auth, nav, shared
   page chrome (page-inner / panel), role picker, undo bar.
   ============================================================ */

*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{
  /* ── THE BRAND PALETTE ────────────────────────────────────────────
     Three colours, fixed, theme-independent. Everything below is a
     ROLE that points at one of them (or at a neutral derived from
     one), so the palette is stated once and read everywhere.

       --brand-lav  lavender, the accent on light grounds
       --brand-grn  green, the accent on dark grounds
       --brand-ink  the darkest neutral: the dark theme's page, and
                    the light theme's text

     Why the accent SWAPS by theme rather than staying one colour:
     lavender on the dark ground manages 3.5:1 and green on the light
     ground manages 1.6:1 — each of the two is unreadable on the
     other's background, and each is excellent on its own (green on
     ink 11.8:1, lavender on paper 4.6:1). One accent for both themes
     would have meant one of them being wrong everywhere.

     NOT covered by this palette, on purpose: the per-action TAG
     colours (TC[] in js/data.js, the swatch list in js/custom.js and
     the label map in js/tagcsv.js). Those encode which action a
     button is, they are saved into every user's panels, and
     repainting them would change what people's own data means. Same
     for the chart palette in js/report.js. A colour that carries
     information is not chrome. */
  --brand-lav:#635be0;
  --brand-grn:#afe259;
  --brand-ink:#00192d;

  /* THE DARK GROUND IS NOT THE BRAND INK. It was, until 2026-09-02, and
     that is why this line and --brand-ink above look like a near-miss
     rather than a typo. The ink is a BRAND colour — it is the body text of
     the light theme (css/theme-light.css --t) and the tint every hairline
     and shadow there is mixed from — and it stayed exactly where it was.
     What moved is the dark theme's ground, to a deeper, cooler near-black.

     The surface ramp moved WITH it, and had to. These four are one family
     or they are nothing: a card is legible as a card because it is a known
     step up from the ground behind it, and leaving the old navy surfaces
     (#07253e / #10334e / #1b405d) on a near-black ground made every panel
     read as a lighter blue sheet floating above the page rather than a
     raised part of it. Same hue, same step sizes, re-based on the new
     ground. Changing --bg alone is the one edit to never make here. */
  --bg:#010b16;--s1:#071726;--s2:#0e2236;--s3:#182f46;
  --b:rgba(255,255,255,0.07);--bm:rgba(255,255,255,0.12);--bs:rgba(255,255,255,0.2);
  --t:#f0f2f5;--t2:#93a5b7;--t3:#556478;
  --ac:#afe259;--acd:rgba(175,226,89,0.14);--acb:rgba(175,226,89,0.32);
  /* The accent's channels on their own, so a one-off tint can be written
     as rgba(var(--ac-rgb),.25) and follow the theme instead of freezing a
     literal. Half a dozen accent tints across review/tagger/pages had
     frozen the pre-2026-09 green exactly that way and were still showing
     it after the palette moved. */
  --ac-rgb:175,226,89;
  /* Success. In the dark theme it IS the accent; the light theme darkens
     it, because #afe259 on paper is 1.6:1 and unreadable as text. */
  --ok:#afe259;--okd:rgba(175,226,89,0.14);--okb:rgba(175,226,89,0.4);
  /* Danger has two jobs and needs two names, not three literals. --rd is
     the CHROME red — a dot, a bar, a tint. --rd-str is danger that has to
     READ: an error line under a form field, a quota that is actually full.
     Before this, --rd was the token, #e5484d was written out nine times for
     the second job, and #b3261e was written out separately in landing.css
     and pages.css for the same job on the public pages. */
  --rd:#ff4d4d;--rdd:rgba(255,77,77,0.12);--rd-str:#e5484d;
  --bl:#4d9fff;--bld:rgba(77,159,255,0.12);
  --am:#f5a623;--amd:rgba(245,166,35,0.1);
  /* Semantic chrome tokens — the light/pastel theme re-points these
     (see css/theme-light.css). Kept here so the dark default is self-
     contained and every module keeps reading from one place. */
  --on-ac:#010b16;                  /* text/icons on a bright accent fill */
  --nav-bg:rgba(1,11,22,.94);       /* sticky top bar */
  --scrim:rgba(0,0,0,.6);           /* modal / drawer backdrop */
  --shadowlg:0 8px 26px rgba(0,0,0,.5);
  --shadowmd:0 10px 28px rgba(0,0,0,.4);
  --shadowxl:0 20px 60px rgba(0,0,0,.5);
  --r:10px;--rsm:6px;--nh:54px;

  /* ── CONTROL SIZES ───────────────────────────────────────────────
     Four heights for everything a finger or a pointer lands on. Before
     this the primary action button existed at eight heights (30–44px),
     the text input at six (26–42px) and the pill at three (30/34px) —
     which is not a hierarchy, it is the same control drawn from memory
     in fourteen different files.

       --ctl-h-lg  a full-width form submit; also the touch floor
       --ctl-h     THE DEFAULT: a primary action, a filter button, an
                   input, a search box
       --ctl-h-sm  a pill — chip, tab, compact inline action
       --ctl-h-xs  a figure box inside a dense row (a keycap, a minutes
                   field, a clip length). A fourth size on purpose: these
                   sit INSIDE a row of other controls, so matching the
                   row's height would break the row.

     NOT covered, same exemption as the type scale: the tag buttons and
     the fullscreen camera HUD, which are sized for a thumb at arm's
     length rather than for a pointer. */
  --ctl-h-lg:44px;--ctl-h:38px;--ctl-h-sm:32px;--ctl-h-xs:28px;

  /* ── MOTION ──────────────────────────────────────────────────────
     Ten durations between .1s and .3s were in use with no token, most of
     them .15s or .12s and the rest one-offs. Two roles now: --dur for a
     hover or state change, --dur-slow for something that moves or grows. */
  --dur:.15s;--dur-slow:.25s;

  /* The uppercase micro-label had eight letter-spacings (.02em → .1em) on
     what is one thing. It has one now. */
  --ls-label:.08em;
  --mono:'DM Mono',monospace;--sans:'Outfit',sans-serif;

  /* ── TYPE SCALE ──────────────────────────────────────────────────
     One family (Outfit) and six sizes for everything structural, so a
     heading is the same size wherever it appears and a description is
     the same size wherever it appears. Before this there were 25
     distinct font sizes across the stylesheets, most of them within a
     pixel of each other — 13px and 13.5px and 14px all meaning "an item
     name", which reads as sloppiness rather than hierarchy.

       --fs-display  the one big title on a page (Home)
       --fs-h1       the title of a page or an overlay
       --fs-h2       the title of a section or a card
       --fs-h3       the name of a row, tile or list item
       --fs-body     normal reading text
       --fs-sm       secondary text: descriptions, meta, captions
       --fs-xs       micro labels, almost always mono + uppercase

     NOT covered on purpose: the tag buttons, the fullscreen camera HUD
     and the mono stat figures. Those are sized for a thumb at arm's
     length in bright sun, and a shared scale is the wrong master for
     them — see the note above .tag-btn in css/tagger.css.

     DM Mono is kept for FIGURES ONLY (timestamps, counts, keyboard
     chips), never for prose. */
  /* The top three roles are FLUID: they interpolate continuously between a
     320px phone and a 640px tablet instead of jumping at one breakpoint, so a
     heading is in proportion to the screen it is actually on rather than to
     whichever side of 640 that screen fell. Each clamp is written as
     min + (max−min)×(100vw−320px)/320, which lands exactly on the old phone
     value at 320px and the old desktop value at 640px — so nothing moved at
     either end, the steps between them were filled in.
     The four roles below --fs-h2 stay FIXED on purpose: they are already at
     the floor for comfortable reading, and making them fluid could only make
     them smaller on a small screen, which is the opposite of the point. */
  --fs-display:clamp(24px,20px + 1.25vw,28px);
  --fs-h1:clamp(19px,16px + 0.94vw,22px);
  --fs-h2:clamp(14.5px,14px + 0.16vw,15px);
  --fs-h3:13.5px;
  --fs-body:13px;
  --fs-sm:12px;
  --fs-xs:10.5px;
  /* An uppercase, letter-spaced rule label ("BY ACTION", "PLAYERS"). It is a
     signpost, not a heading, so it has its own size — putting it on --fs-h2
     would set 15px uppercase, which shouts. */
  --fs-label:11px;
  --lh-tight:1.25;
  --lh-body:1.6;

  /* The marketing site and the legal pages read at arm's length on a laptop,
     not at a glance on a touchline, so they get their own larger scale.
     Same six roles, same discipline — just editorial rather than dense. */
  --fs-ld-h1:clamp(28px,3.6vw,38px);
  --fs-ld-h2:clamp(20px,2.4vw,25px);
  --fs-ld-h3:16px;
  --fs-ld-body:14px;
}
/* (The old max-width:640px step-down for --fs-display / --fs-h1 / --fs-h2 is
   gone — the clamp()s above express the same two endpoints as a continuous
   ramp, so the breakpoint had nothing left to do.) */
html,body{height:100%;background:var(--bg);font-family:var(--sans);color:var(--t);-webkit-font-smoothing:antialiased;overflow-x:hidden;font-size:14px}

/* Form controls do NOT inherit font-family — the UA stylesheet gives them its
   own default, which is Arial here. Every button in the app that remembered to
   say `font-family:var(--sans)` was fine; the ones that forgot were silently
   rendering in a THIRD typeface. Seven of them in the app and two on the
   landing page, found by walking computed styles rather than reading the CSS,
   because nothing in the stylesheets says "Arial" — it is what you get when
   nobody says anything at all.
   Inherit rather than var(--sans) so a control that deliberately sits in a
   mono context follows its parent; the handful that genuinely want DM Mono
   (e.g. the speed buttons) set it on a class, which outranks this. */
button,input,select,textarea{font-family:inherit}

/* ── THE PRIMARY ACTION BUTTON ──────────────────────────────────────
   One control, fourteen names, and until now fourteen drawings of it:
   eight heights between 30 and 44px, four font sizes, three radii, and
   one (.rec-btn) at weight 600 where the other thirteen were 700. Where
   a button lands and how big it is were being decided again in every
   file that needed one.

   The box is stated here once. Each of these keeps only what is
   genuinely its own — .auth-btn is full width, .ce-pick-go must not
   flex, .rev-up-chip hides a file input — and nothing re-states the
   height, the type or the fill.

   Deliberately NOT in this list: .hp-go on the Help page. It looks like
   the fourteenth variant and is not one — it is a DRAWING of the record
   button inside a phone mockup, sized to sit beside .hp-fld (30px) and
   .hp-tick (13px). Putting it on the real scale would break the mock,
   the same way .tag-btn is exempt from the type scale. */
.auth-btn,.rev-up-chip,.pr-new-btn,.save-custom-btn,.rec-btn,.fi-modal-go,
.onboard-go,.home-empty-cta,.pr-empty-cta,.stat-save,.vid-url-load,
.ce-pick-go,.rec-restore{
  display:inline-flex;align-items:center;justify-content:center;gap:7px;
  height:var(--ctl-h);padding:0 16px;border:none;border-radius:var(--rsm);
  background:var(--ac);color:var(--on-ac);
  font-family:var(--sans);font-size:var(--fs-body);font-weight:700;
  white-space:nowrap;text-decoration:none;cursor:pointer;
  transition:filter var(--dur)}
.auth-btn:hover,.rev-up-chip:hover,.pr-new-btn:hover,.save-custom-btn:hover,
.rec-btn:hover,.fi-modal-go:hover,.onboard-go:hover,.home-empty-cta:hover,
.pr-empty-cta:hover,.stat-save:hover,.vid-url-load:hover,.ce-pick-go:hover,
.rec-restore:hover{filter:brightness(1.05)}
.auth-btn svg,.rev-up-chip svg,.pr-new-btn svg,.home-empty-cta svg,
.pr-empty-cta svg,.ce-pick-go svg,.rec-restore svg{width:15px;height:15px}

/* ── THE PUBLIC-PAGE BUTTON ─────────────────────────────────────
   The landing page and the legal / support pages are one surface to a
   visitor, and drew two button systems a few pixels apart: .ld-btn at
   40px / 13.5px / 700 / --rsm against .pg-btn at 42px / 14px / 600 /
   --r. The difference is invisible on either page alone and obvious the
   moment someone crosses between them.

   It lives here rather than in landing.css because the pages that use
   .pg-btn — contact, help, report, the payment results — load base.css
   and pages.css and never load landing.css.

   The public scale is editorial (--fs-ld-body) and the height is the
   touch floor: these are the pages a phone lands on first. */
.ld-btn,.pg-btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;
  height:var(--ctl-h-lg);padding:0 20px;border-radius:var(--rsm);
  font-family:var(--sans);font-size:var(--fs-ld-body);font-weight:700;
  cursor:pointer;text-decoration:none;border:1px solid transparent;white-space:nowrap;
  transition:filter var(--dur),background var(--dur),border-color var(--dur)}

/* ── EMPTY STATES ───────────────────────────────────────────────────
   "Nothing here yet" existed thirteen times, in five font sizes, two
   text colours, eight paddings, and with a dashed border on four of the
   thirteen — so the same sentence looked like a different kind of thing
   on every screen it appeared on.

   There are two real patterns, not thirteen:

     .fi-blank  a PANEL with nothing in it. The content area of a page
                or a card, so it is centred, has room, and carries the
                dashed edge that says something belongs here.
     .fi-none   an INLINE note where a short list would be. One line of
                muted text; a border would draw more attention to the
                absence than to the page.

   Each screen's own class is aliased onto one of the two rather than
   renamed in the markup, so nothing had to be re-tagged and a screen
   that wants a taller box still says so in its own file.

   .tag-empty is not here: it is the fullscreen camera HUD, exempt for
   the same reason the tag buttons are. */
.fi-blank,.home-empty,.ce-empty,.dh-empty,.pr-empty,.mw-empty,.tl-empty{
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:13px;padding:32px 22px;text-align:center;
  background:var(--s1);border:1px dashed var(--bm);border-radius:var(--r);
  font-size:var(--fs-sm);color:var(--t2);line-height:var(--lh-body)}
.fi-blank b,.home-empty b,.ce-empty b,.dh-empty b,.pr-empty b,.mw-empty b,
.tl-empty b{color:var(--t)}
.fi-blank p,.home-empty p,.ce-empty p,.dh-empty p,.pr-empty p{
  font-size:var(--fs-sm);line-height:var(--lh-body);max-width:420px;margin:0}
.fi-none,.rv-empty,.pp-empty,.stat-empty,.storage-empty,.fi-pick-empty,
.log-empty{font-size:var(--fs-sm);color:var(--t3);line-height:1.55;
  padding:10px 2px}

/* ── FOCUS ──────────────────────────────────────────────────────────
   Nine stylesheets — pages, datahub, fullscreen, billing, camera,
   mobile, audience, repair, tour — had no :focus-visible rule at all,
   which includes every supporting page and the whole of Insights. On
   those screens a keyboard user could not see where they were.

   :where() so this carries ZERO specificity: it is the floor, and every
   component that already draws its own focus ring still wins. */
:where(a,button,select,input,textarea,summary,[tabindex]):focus-visible{
  outline:2px solid var(--ac);outline-offset:2px;border-radius:2px}

/* ── TYPOGRAPHIC ORPHANS ────────────────────────────────────────────
   A single word stranded on the last line of a heading is the most
   visible typesetting fault a page has, and because it moves with the
   viewport it cannot be fixed by hand at one breakpoint — the width
   that fixes a laptop creates one on a tablet.

   `balance` evens the line lengths of a heading, which is what stops
   the orphan; browsers cap it at a handful of lines, which is exactly
   the range headings live in and why it is not applied to prose.
   `pretty` leaves normal paragraph rag alone and only refuses to end a
   block on a single word, which is what body copy needs.

   Degrades safely everywhere: an unsupported value falls back to
   `wrap`, i.e. today's behaviour. Firefox currently has `balance` and
   not `pretty`, so its headings balance and its paragraphs are
   unchanged. The hand-set &nbsp; in the marketing headings sits ON TOP
   of this on purpose — it is the only part that holds in a browser
   with neither, and it costs nothing in one that has both.

   Deliberately NOT applied to the tag buttons, the fullscreen HUD or
   any mono figure: those are single words or numbers, where balancing
   has nothing to do and only costs layout work. */
h1,h2,h3,h4,h5,h6,
.hp-check-h,.hp-qs-h,.hp-step-h,.hp-manual-h3,.ld-price-flag{text-wrap:balance}
p,li,dd,figcaption,
.ld-lead,.ld-sec-lead,.ld-hero-note,.page-hdr p{text-wrap:pretty}

/* ── THE SITE FOOTER ────────────────────────────────────────────────
   One footer — Platform / Resources / Legal — on the landing page
   and on all eight support pages, and a two-column cut of it (Resources
   / Legal only) at the foot of the app's Home dashboard, which had no
   footer at all once you signed in. The app copy drops Platform and Book
   a demo on purpose: a signed-in user is past the marketing funnel and
   every one of those links pointed back out to index.html.
   It replaced two different things: a four-column footer on index.html
   whose headings (Product / Get started / Support / Company) split the
   same links four ways, and a bare row of five or six links at the
   bottom of the support pages, no two of which listed the same five.

   It lives HERE rather than in landing.css because base.css is the one
   stylesheet every page already links: putting it here is what let the
   markup drop into ten pages without wiring a new <link> into ten
   <head>s — landing.css is loaded by index.html and nothing else.

   The markup IS duplicated per page, on purpose. There is no build
   step and no templating in this project, and a footer injected by
   JavaScript would take the Legal column down with it the first time a
   script fails to load — and the Legal column is what carries the
   route we are actually obliged to keep reachable.

   Legal was shortened on 2026-09-03: Security and Sub-processors came
   out, and they now sit one click deeper, behind the Privacy Policy link
   (privacy.html #security and #processors). Grievance Officer did NOT
   come out, and must not. It was cut in that same pass and put straight
   back, because the IT Rules 2021 §79 safe-harbour condition is that the
   officer's details are published and the route answered — and a link
   from every page is the cheapest way to be sure of it. Trim this column
   again if you like; leave those two links (Privacy Policy, Grievance
   Officer) alone. Every copy was generated from a single string; grep
   `class="sf"` to find them all. */
.sf{border-top:1px solid var(--b);background:var(--s1);padding:44px 0 32px;margin-top:64px}
.sf-in{width:100%;max-width:1180px;margin:0 auto;padding:0 22px}
.sf-top{display:flex;flex-wrap:wrap;gap:30px;justify-content:space-between;margin-bottom:30px}
.sf-brand{max-width:280px}
.sf-lock{display:inline-flex;align-items:center;gap:10px;font-weight:700;letter-spacing:-.3px;font-size:16px;color:var(--t);text-decoration:none}
.sf-lm{width:30px;height:30px;flex-shrink:0;display:block}
.sf-lm svg{width:100%;height:100%;display:block}
.sf-brand p{margin-top:12px;font-size:var(--fs-body);color:var(--t2);line-height:1.6}
.sf-cols{display:flex;flex-wrap:wrap;gap:46px}
.sf-col h4{font-size:var(--fs-sm);text-transform:uppercase;letter-spacing:var(--ls-label);color:var(--t3);margin-bottom:12px}
.sf-col a{display:block;font-size:var(--fs-h3);color:var(--t2);text-decoration:none;margin-bottom:9px}
.sf-col a:hover{color:var(--ac)}
.sf-bot{display:flex;flex-wrap:wrap;gap:10px;justify-content:space-between;align-items:center;padding-top:22px;border-top:1px solid var(--b);font-size:var(--fs-sm);color:var(--t3)}
.sf-bot a{color:var(--t3);text-decoration:none}
.sf-bot a:hover{color:var(--t2)}
/* Touch targets: the same 44px floor the rest of the app holds to. The
   links are stacked, so the height is spent as padding, not type size. */
@media(max-width:760px),(hover:none) and (pointer:coarse){
  .sf{padding:34px 0 26px;margin-top:44px}
  .sf-cols{gap:22px 40px}
  .sf-col a{padding:12px 0;margin-bottom:0}
  .sf-bot a{display:inline-block;padding:10px 0}
}

/* ── THE 16px iOS ZOOM FLOOR — every page, not just the app ──
   Safari on iOS zooms the viewport the moment a focused field's font-size is
   under 16px, and the reader then has to pinch back out mid-form. This floor
   used to live only in mobile-enhance.css, which is loaded by app.html and by
   nothing else — so it covered the app and missed every form a prospective
   customer meets first: the demo request on the landing page (14px), the
   contact form (14px) and Report a problem (14px, and a 12.5px textarea).
   It lives here now because base.css is the one stylesheet every page links.

   !important is deliberate and is the same call mobile-enhance.css documented:
   this is a platform-behaviour floor, not a style preference, and a bare
   `input` selector loses to any of the id- or class-scoped sizes above it.
   The third condition catches tablets, which are all touch and no hover. */
@media(max-width:760px),(orientation:landscape) and (max-height:520px),(hover:none) and (pointer:coarse){
  body input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=color]),
  body select,
  body textarea{ font-size:16px !important }
}

/* THE SAME BUG AS THE LINE ABOVE, in the other direction: nothing in any
   stylesheet said what a link looks like, so a link nobody had thought about
   got the UA's #0000EE — found the same way, by walking computed styles.
   In the dark theme that is navy on near-black, and it was shipping: the
   "Report Content" link in the support dialog (js/support.js) measured
   rgb(0,0,238) against a #14181f panel.
   Accent is what every OTHER surface already uses for a prose link — the
   privacy, terms and help pages all resolve to it through their own scoped
   rules — so this makes the default agree with the house style instead of
   with the browser. A BARE `a` SELECTOR IS THE POINT: every deliberate link
   colour in this app is set through a class (.auth-agree a, .set-ds a,
   .fi-pf-consent a, .tm-link…), all of which outrank it, so this can only
   ever reach a link that nothing else has an opinion about. Verified by
   diffing computed colour on every anchor across app.html and the six static
   pages before and after: exactly one changed. */
a{color:var(--ac)}

/* ── FIXED-CHROME HEIGHTS ──
   Declared on <body>, not :root, and that is deliberate. A var() inside a
   custom property is substituted against the element the property is
   DECLARED on — so if --nh lived on :root it would keep resolving against
   :root's --navbarh no matter what a body-level class said. Keeping the
   whole chain on body means the two toggles below re-point ONE input and
   every consumer (.page padding, the sticky #cam-panel, the mobile drawer,
   .recover-bar, #standtag-clockbar…) follows for free.

     --navbarh  the nav bar itself
     --navtop   bottom edge of the nav bar, notch included
     --nh       bottom edge of ALL fixed chrome — what layout code reads.
                Equal to --navtop now that the nav is the only fixed chrome;
                kept as its own name because every consumer reads it. */
body{
  --navbarh:54px;
  --safetop:env(safe-area-inset-top,0px);
  --navtop:calc(var(--safetop) + var(--navbarh));
  --nh:var(--navtop);
  /* ── THE FOUR BEZELS, NAMED ONCE ──────────────────────────────
     Every page here ships `viewport-fit=cover` (the meta at the top of each
     .html), which is what lets the camera fill an iPhone edge to edge — and
     which also means the LAYOUT VIEWPORT runs underneath the status bar, the
     notch and the home indicator. A `position:fixed; inset:0` overlay
     therefore starts ABOVE the visible screen, not at the top of it, and in an
     installed PWA there is no browser chrome pushing it back down.

     That is one bug wearing many hats, all of them reported from a phone:
     a help chapter whose × was under the clock, a settings sheet you could
     scroll but never close, a notice whose first two lines were behind the
     notch. `--safetop` existed for the nav bar alone; the other three sides
     had no name, so every overlay either ignored them or invented its own
     env() call.

     --gutter is the pattern to reach for: the design's own padding, or the
     bezel, whichever is larger. Never their SUM — 20px + a 59px notch is 79px
     of dead space at the top of a 390px phone. */
  --sat:env(safe-area-inset-top,0px);
  --sar:env(safe-area-inset-right,0px);
  --sab:env(safe-area-inset-bottom,0px);
  --sal:env(safe-area-inset-left,0px);
  --gutter:20px;
  --safe-pad:max(var(--gutter),var(--sat)) max(var(--gutter),var(--sar))
             max(var(--gutter),var(--sab)) max(var(--gutter),var(--sal));
  /* What is left of the screen once both bezels are taken out. Anything that
     wants to be "as tall as it can be" measures against this, not 100dvh. */
  --safeh:calc(100dvh - var(--sat) - var(--sab));
}

/* Keyboard focus ring — interactive elements (cards, tabs, icon buttons)
   had no visible focus state. Drawn in the accent so it reads in both
   themes; pointer clicks (:focus, not :focus-visible) stay ring-free. */
:focus-visible{outline:2px solid var(--ac);outline-offset:2px}

/* ── FOLDER CHIP BAR (shared: Matches + Presentations) ── */
.fi-folderbar{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin:2px 0 14px}
.fi-fchip{display:inline-flex;align-items:center;gap:6px;background:var(--s2);border:1px solid var(--b);
  border-radius:99px;color:var(--t2);font-family:var(--sans);font-size:var(--fs-sm);font-weight:600;
  padding:6px 12px;cursor:pointer;transition:border-color var(--dur),color var(--dur),background var(--dur)}
.fi-fchip:hover{border-color:var(--bm);color:var(--t)}
.fi-fchip.active{background:var(--acd);border-color:var(--acb);color:var(--t)}
.fi-fchip .fi-ic{width:14px;height:14px}
/* 500, not the chip's 600: DM Mono has no face above 500 and the browser
   synthesises one by smearing (css/fonts.css). */
.fi-fchip-n{background:var(--s3);border-radius:var(--r);padding:0 7px;font-size:var(--fs-label);font-family:var(--mono);font-weight:500;color:var(--t2);min-width:18px;text-align:center}
.fi-fchip.active .fi-fchip-n{background:var(--acb);color:var(--t)}
.fi-fchip-menu{display:inline-flex;align-items:center;opacity:.55;margin:-2px -4px -2px 2px;padding:2px;border-radius:var(--rsm)}
.fi-fchip-menu:hover{opacity:1;background:var(--bm)}
.fi-fchip-new{border-style:dashed;color:var(--t3)}
.fi-fchip-new:hover{color:var(--ac);border-color:var(--acb)}

/* The folder button used to float over the presentation thumbnail, hidden
   until .pr-card:hover — so on a tablet it did not exist. It lives in the
   card's action row now and wears .match-ibtn; see css/review.css. */

/* ── AUTH ── */
/* Same bezel story as .settings-overlay below — and worse here, because this
   is the FIRST screen of an installed PWA, where there is no browser chrome
   holding it clear of the notch. */
#auth-overlay{position:fixed;inset:0;z-index:999;background:var(--bg);display:flex;align-items:center;justify-content:center;padding:var(--safe-pad);overflow-y:auto;-webkit-overflow-scrolling:touch}
/* Cap the card to the viewport and let it scroll inside itself. This is the
   reliable way to keep it centred yet reachable when it's taller than the
   screen — flexbox centring alone clips the overflow with no way to scroll.

   CENTRED, at every width. The card is the whole screen now — the branded
   hero column that used to sit beside it on laptops is gone — and a left-rag
   column of three short lines floating in the middle of a wide card reads as
   something that failed to load. Everything that is prose or a control
   centres; the FORM ROWS are pulled back to the left below, because a label
   above a full-width input is read down its left edge and centring the pair
   breaks the one alignment the eye is using. */
#auth-box{width:100%;max-width:420px;max-height:calc(var(--safeh) - 2*var(--gutter));overflow-y:auto;overscroll-behavior:contain;background:var(--s1);border:1px solid var(--bm);border-radius:16px;padding:40px 36px;text-align:center}
/* The two exceptions to the centring, and the reason for each: a label reads
   down the left edge of the field it names, and typed text that centres itself
   as you type is the single most disorienting thing an input can do. */
#auth-box .fi-pf-row,#auth-box .afl{text-align:left}
#auth-box .ai{text-align:left}
.auth-back{display:inline-block;margin-bottom:14px;font-size:var(--fs-sm);font-weight:600;color:var(--t2);text-decoration:none;cursor:pointer}
.auth-back:hover{color:var(--ac)}
.auth-logo{display:flex;align-items:center;justify-content:center;gap:10px;margin-bottom:28px}
.auth-lm{width:36px;height:36px;flex:none;display:block}
.auth-lm svg{width:100%;height:100%;display:block}
.auth-ln{font-size:var(--fs-h1);font-weight:700;letter-spacing:-0.5px}
.auth-title{font-size:var(--fs-h1);font-weight:600;letter-spacing:-0.4px;margin-bottom:5px}
.auth-sub{font-size:var(--fs-sm);color:var(--t2);margin-bottom:24px;line-height:1.5}
.auth-tabs{display:flex;background:var(--s2);border-radius:var(--rsm);padding:3px;margin-bottom:20px}
.auth-tab{flex:1;height:30px;border:none;background:transparent;font-family:var(--sans);font-size:var(--fs-body);font-weight:500;color:var(--t2);cursor:pointer;border-radius:4px;transition:all var(--dur)}
.auth-tab.active{background:var(--s3);color:var(--t)}
.afl{font-size:var(--fs-xs);font-weight:600;letter-spacing:var(--ls-label);text-transform:uppercase;color:var(--t3);margin-bottom:5px;display:block}
.ai{width:100%;height:var(--ctl-h);padding:0 13px;background:var(--s2);border:1px solid var(--bm);border-radius:var(--rsm);font-family:var(--sans);font-size:14px;color:var(--t);outline:none;margin-bottom:12px;transition:border-color var(--dur)}
.ai:focus{border-color:var(--acb)}
.ai::placeholder{color:var(--t3)}
/* Native selects don't inherit .ai's look — restate the box, kill the OS
   arrow and draw our own so the four registration fields read as one set. */
select.ai{appearance:none;-webkit-appearance:none;cursor:pointer;padding-right:34px;
  background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5L6 6.5L11 1.5' stroke='%23888' stroke-width='1.6' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat:no-repeat;background-position:right 12px center;background-size:11px 8px}
/* Registration details (js/profile.js) — sport and age group share a row on
   anything wider than a small phone; they're two short answers, not two
   screens. */
.fi-pf-grid{display:grid;grid-template-columns:1fr 1fr;gap:0 10px}
@media(max-width:420px){.fi-pf-grid{grid-template-columns:1fr}}
/* AGE GROUPS — a set, because a club is usually more than one (js/profile.js).
   Chips rather than a bare checkbox list: they have to sit in the half-row a
   <select> used to occupy, beside Sport, and four labelled boxes stacked
   vertically would make that column three times the height of its neighbour.
   They wrap instead, two per line on a phone. */
.fi-pf-ticks{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:12px}
.fi-pf-tick{display:flex;align-items:center;gap:7px;flex:1 1 auto;min-height:var(--ctl-h);
  padding:0 11px;background:var(--s2);border:1px solid var(--bm);border-radius:var(--rsm);
  font-size:var(--fs-sm);color:var(--t2);cursor:pointer;white-space:nowrap}
/* Tinted on tick, exactly as .fi-pf-consent below is, so "chosen" reads the
   same way everywhere in this form. */
.fi-pf-tick.on{border-color:var(--acb);background:var(--acd);color:var(--t)}
.fi-pf-tick input{width:15px;height:15px;flex:none;margin:0;accent-color:var(--ac)}
/* The one-line instruction beside a group label. Lower case and lighter than
   the label it follows: it is guidance, not a second heading. */
.afl-hint{font-weight:500;text-transform:none;letter-spacing:0;color:var(--t3);opacity:.85}
/* The club's answers, shown to everyone who cannot change them (js/profile.js
   fiProfileClubLocked). DELIBERATELY NOT A DISABLED INPUT: a greyed-out box
   still reads as a field you might be able to type in, and on a phone people
   tap it to find out. A stated fact does not invite the tap, and it can say
   whose fact it is, which a disabled input cannot. */
.fi-pf-fixed{background:var(--s2);border:1px solid var(--b);border-radius:var(--rsm);
  padding:11px 13px;margin:0 0 12px}
.fi-pf-fixed b{display:block;font-size:14px;font-weight:600;color:var(--t)}
.fi-pf-fixed span{display:block;margin-top:2px;font-size:var(--fs-sm);color:var(--t2)}
.fi-pf-fixed em{display:block;margin-top:6px;font-style:normal;font-size:var(--fs-xs);color:var(--t3);line-height:1.5}
/* The parental-consent confirmation, revealed when a youth age group is
   picked (js/profile.js). Styled as a panel rather than a bare checkbox
   because it is a statement someone is making, not a preference they are
   setting — and it tints on tick so the answer is legible at a glance in a
   form of otherwise identical grey boxes. Matches .su-warr in setup.css;
   the two are the same commitment asked in two places. */
.fi-pf-consent{display:flex;gap:11px;align-items:flex-start;background:var(--s2);
  border:1px solid var(--bm);border-radius:var(--rsm);padding:12px 13px;margin:2px 0 4px;cursor:pointer}
.fi-pf-consent.on{border-color:var(--acb);background:var(--acd)}
.fi-pf-consent input{width:17px;height:17px;flex:none;margin-top:1px;accent-color:var(--ac)}
.fi-pf-consent span{font-size:var(--fs-sm);color:var(--t2);line-height:1.6}
.fi-pf-consent b{color:var(--t);font-weight:600}
.fi-pf-consent a{color:var(--t2);text-decoration:underline}
/* The agreement line above Continue. Deliberately full-size body text, not
   the 11px whisper it replaced: this is the moment someone accepts the terms,
   and type small enough to be missed is the whole complaint about how consent
   is usually collected. */
.auth-agree{margin:14px 0 10px;font-size:var(--fs-sm);line-height:1.6;color:var(--t2);text-align:center}
.auth-agree a{color:var(--t);text-decoration:underline;font-weight:600}
/* The one full-width submit in the app, so it takes the large box. */
.auth-btn{width:100%;height:var(--ctl-h-lg);font-size:var(--fs-h3);margin-top:4px}
/* The sign-in status line. Three states, and the same accent/amber pair the
   team panel already uses for .tm-msg — a class swap rather than an inline
   style, so js/auth.js's say() helper can move between them without knowing
   any colours. Without these two rules the ok/bad distinction the handlers
   make would render identically and be decorative. */
.auth-msg{font-size:var(--fs-sm);line-height:1.5;color:var(--t2)}
.auth-msg.ok{color:var(--ac)}
.auth-msg.bad{color:var(--am)}
/* There is no global anchor style in this file, so a link dropped into a
   .auth-sub line would render in the browser's default blue on a dark
   surface. Same treatment as .auth-foot a, which solved this once already. */
.auth-sub a{color:var(--t2);text-decoration:underline;cursor:pointer}
.auth-div{display:flex;align-items:center;gap:12px;margin:16px 0;color:var(--t3);font-size:var(--fs-sm)}
.auth-div::before,.auth-div::after{content:'';flex:1;height:1px;background:var(--b)}
.auth-gb{width:100%;height:42px;border:1px solid var(--bm);border-radius:var(--rsm);background:var(--s2);font-family:var(--sans);font-size:var(--fs-body);font-weight:500;color:var(--t2);cursor:pointer;display:flex;align-items:center;justify-content:center;gap:8px}
/* A submit button spends the round trip disabled. Without this it looks
   exactly like a button that is simply ignoring you. */
.auth-gb:disabled{opacity:.5;cursor:default}
.auth-btn:disabled{opacity:.5;cursor:default;filter:none}
/* ── The three sign-in screens (S1, S2, solo) ────────────────────────────
   One overlay, three screens, swapped by js/auth.js fiAuthGo. They are
   screens rather than pages because sign-in has to finish where it started —
   a page navigation mid-flow is how the old build lost people between
   pressing send and being signed in. Only display is toggled; nothing here
   positions anything, so the card keeps sizing itself to whichever screen is
   showing. */
.auth-scr{display:flex;flex-direction:column}

/* ── THE SIGN-IN LOADER ────────────────────────────────────────────
   The mark, animated, for the stretch between "you are who you say" and
   "here is your club" (see the ══ THE WAIT ══ banner in app.html for why
   that stretch exists and why it cannot be shortened away entirely).

   IT IS THE BRAND MARK AND NOT A SPINNER, on purpose. A generic spinner
   says only "something". The mark says "this app, still yours, still
   working" at the one moment a person is deciding whether the thing they
   just paid attention to is broken.

   The four shapes are lit in reading order — left panel, top-right,
   bottom-right, and the green play mark LAST, so each cycle resolves on the
   one shape that means "go" instead of merely looping. The stagger is 0.16s
   against a 1.8s cycle, which leaves roughly six-tenths of a second of calm
   at the end of every pass; a wave with no rest in it reads as agitation. */
#auth-box.busy .auth-logo{display:none}
.auth-load{width:64px;height:64px;margin:0 auto 22px}
.auth-load svg{width:100%;height:100%;display:block;transform-origin:50% 50%;
  animation:fi-alb 1.8s ease-in-out infinite}
.alp{opacity:.16;animation:fi-alp 1.8s ease-in-out infinite}
.alp4{animation-delay:0s}
.alp1{animation-delay:.16s}
.alp3{animation-delay:.32s}
.alp2{animation-delay:.48s}
@keyframes fi-alp{0%,70%,100%{opacity:.16}18%,42%{opacity:1}}
@keyframes fi-alb{0%,100%{transform:scale(1)}30%{transform:scale(1.05)}}
/* The whole-mark fade used INSTEAD of the wave when motion is reduced. */
@keyframes fi-alf{0%,100%{opacity:1}50%{opacity:.45}}
/* Reduced motion still MOVES, and that is deliberate rather than sloppy.
   Freezing this to a static mark would recreate the exact ambiguity the
   loader exists to remove — a still logo cannot tell a user whether the
   app is working or hung. So the scale and the four-part wave both go (they
   are the parts that read as motion), and what remains is one slow opacity
   breath on the whole mark: unmistakably alive, nothing travelling across
   the screen, nothing that could trigger vestibular discomfort. */
@media (prefers-reduced-motion:reduce){
  .alp{animation:none;opacity:1}
  .auth-load svg{animation:fi-alf 2.6s ease-in-out infinite}
}
/* The sub-line carries live progress, so it must not shift the layout as the
   wording changes under it — two lines' worth of room, always. */
.auth-busy-sub{min-height:2.4em;margin-bottom:14px}
/* The slow-connection reassurance. Amber, like every other "this is taking
   longer than it should but nothing is wrong" surface in the app, and NOT
   red: nothing has failed. */
/* AMBER IS THE FRAME, NOT THE PROSE, and that is a contrast decision rather
   than a stylistic one. Amber-on-amber is how every other "taking longer
   than it should" surface in the app is drawn, but those are a word or two;
   this is three lines of reassurance, and at --am on --amd it measures about
   2.1:1 in the light theme — a caution nobody can read is worse than no
   caution at all. The palette is deliberate (see the light-theme notes) so
   it is left alone: the amber moves to the rule down the left edge, where it
   still says "attention, not alarm", and the sentence itself takes the
   ordinary muted text colour and becomes legible in both themes. */
.auth-slow{font-size:var(--fs-sm);line-height:1.5;color:var(--t2);
  background:var(--amd);border:1px solid color-mix(in srgb,var(--am) 30%,transparent);
  border-left:3px solid var(--am);border-radius:var(--rsm);
  padding:10px 12px;text-align:left}
/* THE QUIET LINK. Now used only by "Forgotten your password?". It carries a
   deliberately low weight, and that rule was learned the hard way: the
   offline/solo route used to live in this class as a full-width primary
   button beneath "Send link" labelled "Continue" — two controls of identical
   weight doing opposite things — and in acceptance testing the club owner
   pressed it and created a second, unwanted club. Anything added here is a way
   OUT of the obvious path and must never look like the obvious path. */
.auth-alt{margin-top:14px;text-align:center;font-size:var(--fs-sm);color:var(--t3)}
.auth-alt a{color:var(--t3);text-decoration:underline;cursor:pointer}
.auth-alt a:hover{color:var(--t2)}
/* The way back out of S2. Deliberately not a .auth-gb: a full-width bordered
   Back is louder than the thing it is going back from. Centred with everything
   else — hard left against a centred card reads as a stray element. */
.auth-crumb{align-self:center;background:none;border:0;padding:0 0 12px;margin:0;
  font-family:var(--sans);font-size:var(--fs-sm);color:var(--t3);cursor:pointer}
.auth-crumb:hover{color:var(--t2)}
.auth-foot{margin-top:16px;text-align:center;font-size:var(--fs-sm);color:var(--t3)}
.auth-foot a{color:var(--t2);text-decoration:underline;cursor:pointer}
/* ── THE TWO-FOLDER MAIL PROMPT ──────────────────────────────────────────
   Shown wherever the app has just caused an email to be sent: the sign-in
   overlay (#auth-mailtip, revealed by js/auth.js fiAuthMailTip) and the team
   roster (js/teamui.js). One class so the advice cannot be worded two ways.

   A PANEL, NOT A SENTENCE, and that is the point of it. "Check your email" is
   an instruction nobody can fail and therefore nobody acts on. Our mail is
   sent by Firebase from a domain the recipient has never corresponded with,
   which is exactly the profile a spam filter parks on first contact, so the
   message has to name BOTH folders and ask for the one tap ("Not spam") that
   fixes every message after it. Left-aligned inside a centred card because it
   is three lines of instruction, not a title. */
.fi-mailtip{display:flex;gap:10px;align-items:flex-start;text-align:left;
  background:var(--s2);border:1px solid var(--bm);border-radius:var(--rsm);
  padding:11px 12px;margin-top:12px;font-size:var(--fs-sm);line-height:1.6;color:var(--t2)}
.fi-mailtip[hidden]{display:none}
.fi-mailtip b{color:var(--t);font-weight:600}
.fi-mailtip-ic{flex:none;width:20px;height:20px;border-radius:50%;background:var(--acd);
  border:1px solid var(--acb);color:var(--ac);display:flex;align-items:center;
  justify-content:center;font-size:var(--fs-label);line-height:1}
.plan-cards{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin:14px 0}
.plan-card{padding:12px;border-radius:var(--rsm);border:1px solid var(--bm);background:var(--s2);cursor:pointer}
.plan-card.selected{border-color:var(--acb);background:var(--acd)}
.plan-name{font-size:var(--fs-sm);font-weight:600;margin-bottom:3px}
.plan-price{font-size:17px;font-weight:700;color:var(--ac)}
.plan-price span{font-size:var(--fs-label);color:var(--t3);font-weight:400}
.plan-feat{font-size:var(--fs-label);color:var(--t2);margin-top:5px;line-height:1.4}

/* THE AUTH ROLE PICKER (.role-cards / .role-card / .role-ic / .role-tx /
   .role-nm / .role-ds) was deleted here. It served two screens that no longer
   exist: the offline/solo sign-in screen, where a device named its own role,
   and S3's "start a club / just me" pair. A role now comes down from the team
   membership and the club question is two chips on setup step 1 (.su-who in
   css/setup.css). Nothing in public/ references these class names. */

/* ── RECORD CHOOSER (§1-1) — the three capture modes as big, calm cards ── */
.rec-choose{display:flex;flex-direction:column;gap:10px;margin-top:18px}
.rec-mode{display:flex;gap:14px;align-items:center;width:100%;text-align:left;padding:16px 18px;border-radius:var(--r);border:1px solid var(--bm);background:var(--s2);cursor:pointer;transition:border-color var(--dur),background var(--dur),transform var(--dur);font-family:var(--sans)}
.rec-mode:hover{border-color:var(--acb);background:var(--acd);transform:translateY(-1px)}
.rec-mode.locked{display:none}
.rec-mode-ic{width:44px;height:44px;border-radius:var(--r);background:var(--s3);border:1px solid var(--bm);display:flex;align-items:center;justify-content:center;flex-shrink:0}
.rec-mode:hover .rec-mode-ic{background:var(--ac);border-color:var(--ac)}
.rec-mode-ic svg{width:22px;height:22px;color:var(--t2)}
.rec-mode:hover .rec-mode-ic svg{color:var(--on-ac)}
.rec-mode-tx{flex:1;min-width:0}
.rec-mode-nm{display:block;font-size:var(--fs-h2);font-weight:600;color:var(--t)}
.rec-mode-ds{display:block;font-size:var(--fs-sm);color:var(--t2);margin-top:3px;line-height:1.5}
.rec-mode-go{color:var(--t3);flex-shrink:0;display:flex}
.rec-mode-go svg{width:20px;height:20px}
.rec-mode:hover .rec-mode-go{color:var(--ac)}

/* ── NAV ── */
#nav{position:fixed;top:0;left:0;right:0;z-index:100;height:var(--navtop);padding-top:var(--safetop);background:var(--nav-bg);backdrop-filter:blur(12px);border-bottom:1px solid var(--b);display:flex;align-items:center;padding-left:16px;padding-right:16px;gap:0}
.nav-logo{display:flex;align-items:center;gap:8px;margin-right:16px;flex-shrink:0}
.nav-lm{width:26px;height:26px;flex:none;display:block}
.nav-lm svg{width:100%;height:100%;display:block}
.nav-ln{font-size:var(--fs-body);font-weight:700;color:var(--t);letter-spacing:-0.3px;white-space:nowrap}
.nav-tabs{display:flex;gap:2px;overflow-x:auto;scrollbar-width:none}
.nav-tabs::-webkit-scrollbar{display:none}
.nav-tab{height:30px;padding:0 11px;border-radius:var(--rsm);border:none;background:transparent;font-family:var(--sans);font-size:var(--fs-sm);font-weight:500;color:var(--t3);cursor:pointer;transition:all var(--dur);display:flex;align-items:center;gap:5px;white-space:nowrap;flex-shrink:0}
.nav-tab:hover{color:var(--t2);background:var(--s2)}
.nav-tab.active{background:var(--acd);color:var(--t)}
.nav-right{margin-left:auto;display:flex;align-items:center;gap:8px;flex-shrink:0}
.nav-pb{font-size:9px;font-weight:700;letter-spacing:var(--ls-label);text-transform:uppercase;padding:3px 7px;border-radius:3px;background:var(--acd);border:1px solid var(--acb);color:var(--ac)}
/* One chip: initials + role, opens Settings. */
.nav-usr{display:flex;align-items:center;gap:7px;padding:3px 9px 3px 3px;border-radius:99px;background:var(--s2);border:1px solid var(--bm);cursor:pointer;font-family:var(--sans)}
.nav-usr:hover{border-color:var(--bs);background:var(--s3)}
.nav-usr-in{width:26px;height:26px;border-radius:50%;background:var(--s3);border:1px solid var(--bm);display:flex;align-items:center;justify-content:center;font-size:var(--fs-xs);font-weight:700;color:var(--t2);flex-shrink:0}
.nav-usr .nav-pb{background:none;border:none;padding:0}
.nav-tab.locked{display:none}

/* ── NAV COLLAPSE ──
   The tagger and the presentation editor are single-task screens where the
   tab strip is dead weight. Collapsing leaves a 30px lip that still gets you
   home, into Settings, and back out again — navigation is thinned, never
   removed. Desktop only: on a phone the bar is already just a burger, and a
   30px row is below the touch-target floor (see mobile.css). */

/* ── NAV AUTO-HIDE (desktop) ──
   Scrolling down slides the bar out of the way so the screen is all content;
   returning to the top, or putting the pointer in the top strip of the window,
   slides it straight back (see fiNavAutoHide in js/nav.js). Nothing reflows —
   the bar is fixed and .page already reserves --nh, so the content it uncovers
   was scrolled under it anyway. */
#nav{transition:transform .22s cubic-bezier(.22,.61,.36,1)}
html.nav-hidden #nav{transform:translateY(calc(-1 * var(--navtop)))}
@media(prefers-reduced-motion:reduce){#nav{transition:none}}

/* ── PAGES ── */
.page{display:none;padding-top:var(--nh);min-height:100vh}
/* Page switches used to hard-cut; a brief fade + rise makes navigation feel
   considered. The video pages (tagger/retag/review) fade only — a transform
   there would reparent their fixed fullscreen children mid-animation. */
/* NO `both`, AND THAT WORD WAS A BUG NOT A FLOURISH.
   pageIn animates `transform`, and an element with a transform animation IN
   EFFECT is a containing block for its position:fixed descendants — even
   when the value it has animated to is `none`. `animation-fill-mode:both`
   keeps the animation in effect for ever after it finishes, so every .page
   in this app was permanently a containing block, and every fixed overlay
   INSIDE a page was being laid out against the page instead of the screen.

   That is the whole of "the clip player does not open in the middle — it
   opens in weird places": #ce-player is `position:fixed; inset:0` inside
   #page-clipexplorer, so on a phone scrolled 261px down a 1105px-tall
   results page it was measured at top:-261 and 1105px TALL, and the box it
   centres landed wherever that put it. The further you had scrolled, the
   further out it went, which is what made it look random.

   It is also why js/fullscreen.js has to pin the page before it opens a
   tagging surface (_fsLockScroll, and the note beside it) — same cause,
   found from the other end when the Stand Tagger's Stop bar went missing.
   That workaround stays: it is still needed for iOS Safari's own
   layout-vs-visual viewport gap, which is a different problem with the same
   symptom.

   With the default fill mode the animation stops being in effect the moment
   it ends, the containing block goes with it, and the 6px rise is unchanged
   — measured at 390x844: `both` → top:-16, height:860; without → top:0,
   height:844. The 160ms while it runs is still a containing block; nothing
   opens an overlay inside a page transition. */
.page.active{display:block;animation:pageIn .16s ease}
#page-tagger.active,#page-retag.active,#page-review.active{display:flex;flex-direction:column;animation:fadeIn .16s ease both}
@keyframes pageIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
@keyframes fadeIn{from{opacity:0}to{opacity:1}}

/* Respect the OS "reduce motion" setting — collapse animations/transitions. */
@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important;scroll-behavior:auto!important}
}

/* ── DESKTOP NAV: the logo is the Home button, the tabs are centred ──
   The logo has called goHome() since it was added, but it was dressed as a
   wordmark — no hover, no focus ring, and no active state on Home — so it
   read as decoration and the Home TAB was the only thing that looked
   pressable. It now takes the same treatment the tabs have, and the tab it
   duplicates is dropped from the bar.

   Dropped from the BAR only. The <button> stays in the DOM and reappears
   below 921px, where .nav-tabs becomes the off-canvas drawer (css/mobile.css)
   — a drawer is a list of destinations and a list should name all of them,
   whereas a bar has a mark in the corner doing the same job.

   Centred by absolute positioning rather than auto margins: the logo and the
   account chip are different widths, and every flow-based centring splits the
   leftover space between the flanks instead of finding the middle of the bar,
   which leaves the strip visibly off-centre by half the difference. The
   max-width keeps it clear of both flanks at every width; .nav-tabs already
   carries overflow-x:auto, so a bar too narrow for the tabs scrolls them
   exactly as it does today rather than colliding. */
@media(min-width:921px){
  #navtab-home{display:none}
  .nav-logo{padding:4px 9px;margin-right:8px;border-radius:var(--rsm);transition:background var(--dur)}
  .nav-logo:hover{background:var(--s2)}
  .nav-logo:focus-visible{outline:2px solid var(--ac);outline-offset:2px}
  /* NO ACTIVE GROUND ON THE LOGO. It used to take .at-home{background:var(--acd)}
     so it wore the same selected state as the tab it replaces up here. On the
     mark specifically that reads as a highlight box drawn round the brand
     rather than as "you are on Home" — the wordmark is not one of the tabs and
     should not look picked. Hover and the focus ring stay: it is still a
     button, it just no longer claims to be the current one. The tabs keep
     their selected state, which is where that signal belongs. */
  .nav-tabs{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);max-width:calc(100% - 420px)}
}

/* Soft, themed scrollbars (WebKit) — the nav-tabs keep their own hidden bar. */
*::-webkit-scrollbar{width:10px;height:10px}
*::-webkit-scrollbar-thumb{background:var(--bm);border-radius:99px;border:2px solid transparent;background-clip:padding-box}
*::-webkit-scrollbar-thumb:hover{background:var(--bs)}
*::-webkit-scrollbar-track{background:transparent}

/* A clear focus affordance for text fields: the faint border shift alone was
   easy to miss. Soft accent ring on focus; the global :focus-visible outline
   would otherwise double up here, so suppress it for form fields. */
input:focus,select:focus,textarea:focus{box-shadow:0 0 0 3px var(--acd)}
input:focus-visible,select:focus-visible,textarea:focus-visible{outline:none}

/* ── INNER PAGES ── */
.page-inner{max-width:820px;margin:0 auto;padding:24px 18px 48px;display:flex;flex-direction:column;gap:13px}
.page-hdr{margin-bottom:4px}
.page-hdr h1{font-size:var(--fs-h1);font-weight:700;letter-spacing:-.4px;margin-bottom:5px}
.page-hdr p{font-size:var(--fs-sm);color:var(--t2);line-height:1.6}
.panel{background:var(--s1);border:1px solid var(--b);border-radius:var(--r);overflow:hidden}
.panel-hdr{padding:10px 15px;border-bottom:1px solid var(--b);display:flex;align-items:center;justify-content:space-between}
.panel-hdr h2{font-size:var(--fs-xs);font-weight:700;letter-spacing:var(--ls-label);text-transform:uppercase;color:var(--t3)}
.panel-body{padding:15px}

/* ── UNDO BAR (live cancel timer) ── */
/* D-03 — opacity:0 hides a thing from the EYE, not from the tab order.
   This bar carries a Cancel button, so on every page of the app a keyboard
   user tabbed to an invisible control that did something. pointer-events
   does not help: it only stops the mouse. visibility:hidden is what takes
   a subtree out of focus, and it is transitioned at 0s DELAYED by the fade
   so the bar still fades out instead of vanishing. */
.undo-bar{position:fixed;left:50%;bottom:18px;transform:translateX(-50%) translateY(20px);z-index:300;display:flex;align-items:center;gap:11px;padding:9px 11px 9px 14px;background:var(--s3);border:1px solid var(--bs);border-radius:99px;box-shadow:var(--shadowlg);opacity:0;visibility:hidden;pointer-events:none;transition:opacity .18s,transform .18s,visibility 0s linear .18s}
.undo-bar.show{opacity:1;visibility:visible;transform:translateX(-50%) translateY(0);pointer-events:auto;transition:opacity .18s,transform .18s,visibility 0s}
.undo-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
.undo-txt{font-size:var(--fs-sm);font-weight:600;color:var(--t);white-space:nowrap}
.undo-txt span{color:var(--t2);font-weight:500}
.undo-ring{position:relative;width:26px;height:26px;flex-shrink:0}
.undo-ring svg{transform:rotate(-90deg)}
.undo-ring .ur-num{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-family:var(--mono);font-size:var(--fs-label);font-weight:500;color:var(--t2)}
.undo-cancel{height:28px;padding:0 13px;border-radius:99px;border:1px solid var(--rdd);background:var(--rdd);color:var(--rd);font-family:var(--sans);font-size:var(--fs-sm);font-weight:700;cursor:pointer;white-space:nowrap}
.undo-cancel:hover{background:rgba(255,77,77,.2)}

@keyframes blink{0%,100%{opacity:1}50%{opacity:.15}}

/* ── HOME / LANDING ── */
.home-inner{max-width:880px;margin:0 auto;padding:38px 18px 60px}
.home-hero{margin-bottom:26px}
.home-greeting{font-family:var(--mono);font-size:var(--fs-label);letter-spacing:var(--ls-label);text-transform:uppercase;color:var(--ac);margin-bottom:10px}
/* What you are in this club. The club NAME is the <h1> below now, so .htc-nm
   is gone with it — this line carries the role and the ownership fact only,
   and stays small and muted because it is a standing condition rather than
   news. */
.home-team-chip{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap;margin:-4px 0 10px}
.home-team-chip .htc-rl{font-family:var(--mono);font-size:var(--fs-xs);letter-spacing:var(--ls-label);text-transform:uppercase;color:var(--t2)}
/* The club whose work is on screen (js/home.js fiRenderHomeTitle). A club
   name can be long and is user-typed, so it wraps rather than overflowing. */
.home-title{font-size:var(--fs-display);font-weight:700;letter-spacing:-.6px;margin-bottom:8px;overflow-wrap:anywhere}
.home-sub{font-size:var(--fs-body);color:var(--t2);line-height:1.6;max-width:560px}
/* Storage headroom banner (js/quota.js). Amber at 80/92%, red once full —
   the colour is the whole message at a glance, so the two levels must not
   look alike. */
.home-quota{display:flex;align-items:flex-start;gap:13px;padding:14px 16px;margin-bottom:22px;border-radius:var(--r);
  border:1px solid color-mix(in srgb,var(--am) 45%,transparent);background:color-mix(in srgb,var(--am) 10%,var(--s1))}
.home-quota.lv-full{border-color:color-mix(in srgb,var(--rd-str) 50%,transparent);background:color-mix(in srgb,var(--rd-str) 11%,var(--s1))}
.hq-ic{flex-shrink:0;width:26px;height:26px;border-radius:50%;display:flex;align-items:center;justify-content:center;
  font-family:var(--mono);font-size:var(--fs-sm);font-weight:500;color:var(--am);background:color-mix(in srgb,var(--am) 18%,transparent)}
.home-quota.lv-full .hq-ic{color:var(--rd-str);background:color-mix(in srgb,var(--rd-str) 18%,transparent)}
.hq-tx{flex:1;min-width:0}
.hq-nm{font-size:var(--fs-h3);font-weight:600;margin-bottom:3px}
.hq-ds{font-size:var(--fs-sm);color:var(--t2);line-height:1.55}
.hq-ax{display:flex;align-items:center;gap:7px;flex-shrink:0;flex-wrap:wrap}
.hq-up,.hq-mg,.hq-x{font-family:var(--sans);font-size:var(--fs-sm);font-weight:600;border-radius:var(--rsm);cursor:pointer;
  border:1px solid var(--bm);background:var(--s2);color:var(--t);height:30px;padding:0 11px}
.hq-up{background:var(--ac);color:var(--on-ac);border-color:transparent}
.hq-x{width:30px;padding:0;font-size:16px;font-weight:400;color:var(--t3)}
.hq-up:hover,.hq-mg:hover,.hq-x:hover{filter:brightness(1.08)}
@media(max-width:640px){.home-quota{flex-wrap:wrap}.hq-ax{width:100%;justify-content:flex-end}}
.home-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:12px}
.home-card{display:flex;align-items:center;gap:13px;text-align:left;padding:16px;background:var(--s1);border:1px solid var(--b);border-radius:var(--r);cursor:pointer;font-family:var(--sans);color:var(--t);transition:border-color var(--dur),background var(--dur),transform var(--dur)}
.home-card:hover{border-color:var(--acb);background:var(--s2);transform:translateY(-2px);box-shadow:var(--shadowmd)}
.home-card-ic{width:42px;height:42px;border-radius:9px;background:var(--acd);border:1px solid var(--acb);display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:background var(--dur),border-color var(--dur)}
.home-card:hover .home-card-ic{background:var(--ac);border-color:var(--ac)}
.home-card-ic svg{width:20px;height:20px;color:var(--ac);transition:color var(--dur)}
.home-card:hover .home-card-ic svg{color:var(--on-ac)}
.home-card-tx{flex:1;min-width:0}
.home-card-nm{font-size:var(--fs-h3);font-weight:600;margin-bottom:3px}
.home-card-ds{font-size:var(--fs-sm);color:var(--t2);line-height:1.45}
.home-card-go{color:var(--t3);flex-shrink:0;display:flex;transition:color var(--dur),transform var(--dur)}
.home-card:hover .home-card-go{color:var(--ac);transform:translateX(3px)}
.home-card-go svg{width:18px;height:18px}
.home-foot{display:flex;align-items:center;gap:9px;margin-top:26px;padding:13px 15px;background:var(--s1);border:1px solid var(--b);border-radius:var(--rsm);font-size:var(--fs-sm);color:var(--t2);line-height:1.5}
.home-foot svg{width:16px;height:16px;color:var(--ac);flex-shrink:0}

/* ── HOME: usage / impact tiles ("Your analysis so far") ── */
.home-usage{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px}
/* Chrome scales with the viewport so the label keeps its room on a narrow
   phone (see .hu-l). Upper bounds are the original desktop values, so nothing
   changes from tablet width up. */
/* CENTRED, and stacked: icon over figure over label. It used to be a row —
   icon left, text ragged left beside it — which made Home the only screen in
   the app that stated a number differently from every other. The KPI strip on
   Insights and in Match Stats (.fi-kpi / .dh-kpi below) has always been a
   centred column, and the note above those rules says the whole point is that
   a coach learns one visual grammar rather than three. This is Home joining
   it. Column also gives the label its full tile width instead of the ~84px
   left over beside a 38px icon, which is what the .hu-l note below is about. */
/* CENTRED ON BOTH AXES. Grid stretches every tile in the row to the height of
   the tallest — and the storage tile is always the tallest, because it carries
   a plan line or a usage bar the counting tiles do not. With the contents
   packed to the top, the four figures beside it sat high in their cards with a
   band of empty ground underneath, so a row that IS one measurement read as
   four tiles that had drifted. justify-content:center puts each tile's stack in
   the middle of whatever height the row settles on. */
.hu-tile{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:clamp(8px,2.2vw,10px);padding:clamp(13px,3vw,16px) clamp(10px,2.6vw,12px);background:var(--s1);border:1px solid var(--b);border-radius:var(--r);color:var(--t);font-family:var(--sans);width:100%}
button.hu-tile{cursor:pointer;transition:border-color var(--dur),background var(--dur),transform var(--dur)}
button.hu-tile:hover{border-color:var(--acb);background:var(--s2);transform:translateY(-2px);box-shadow:var(--shadowmd)}
.hu-ic{width:clamp(31px,8.5vw,38px);height:clamp(31px,8.5vw,38px);flex-shrink:0;border-radius:var(--r);background:var(--acd);border:1px solid var(--acb);display:flex;align-items:center;justify-content:center;color:var(--ac)}
.hu-ic svg{width:clamp(16px,4.3vw,19px);height:clamp(16px,4.3vw,19px)}
.hu-tx{width:100%;min-width:0}
/* Outfit + tabular-nums, per decision 8 — see the note in css/tagger.css. */
.hu-n{font-size:21px;font-weight:700;color:var(--t);line-height:1.1;letter-spacing:-.4px;white-space:nowrap;font-variant-numeric:tabular-nums}
/* Unit (GB / min): small, sans, muted — sits on the figure's baseline so the
   number reads as "1.2 GB" without the unit competing with the mono digits. */
.hu-u{font-family:var(--sans);font-size:var(--fs-sm);font-weight:600;color:var(--t3);letter-spacing:0;margin-left:3px}
/* "PRESENTATIONS" is thirteen characters with no place to break, and on a 375px
   phone the tile's own chrome (38px icon + 12px gap + 28px padding) left it 84px
   to live in when it needed 100 — so it ran out past the card's edge. The chrome
   now scales with the viewport (below) and the tracking is a shade tighter, which
   buys the room back; overflow-wrap is the last-resort guard so a longer word in
   a future label breaks rather than escapes. */
.hu-l{font-size:var(--fs-sm);color:var(--t2);margin-top:4px;text-transform:uppercase;letter-spacing:var(--ls-label);line-height:1.3;overflow-wrap:anywhere}
.hu-hint{font-size:var(--fs-xs);color:var(--ac);margin-top:4px;font-weight:600}

/* ── MATCH WORKSPACE (P0 #1) ── */
.mw-back{align-self:flex-start;background:none;border:none;color:var(--t2);font-family:var(--sans);font-size:var(--fs-body);font-weight:600;cursor:pointer;padding:2px 0;margin-bottom:6px}
.mw-back:hover{color:var(--t)}
.mw-head{display:flex;align-items:flex-start;gap:12px;margin-bottom:18px}
.mw-head-tx{flex:1;min-width:0}
.mw-title-row{display:flex;align-items:center;gap:9px;flex-wrap:wrap}
.mw-title{font-size:var(--fs-h1);font-weight:700;letter-spacing:-.4px}
/* Sans for the same reason as .match-meta above (decision 4). */
.mw-meta{font-size:var(--fs-sm);color:var(--t2);margin-top:5px}

/* ── HOME: MY MATCHES ── */
.home-matches-sec{margin-top:34px}
.home-sec-hdr{font-size:var(--fs-h2);font-weight:700;letter-spacing:.02em;margin-bottom:12px;display:flex;align-items:baseline;gap:9px;flex-wrap:wrap}
.home-sec-hdr::before{content:'';align-self:center;width:3px;height:13px;border-radius:2px;background:var(--ac);flex-shrink:0}
.home-sec-ttl{align-self:center}
.home-sec-sub{align-self:center;font-family:var(--mono);font-size:var(--fs-xs);font-weight:400;letter-spacing:var(--ls-label);text-transform:uppercase;color:var(--t3)}
/* §5-4: primary action + quiet icon buttons pinned to the right of the header */
.home-sec-actions{margin-left:auto;align-self:center;display:flex;align-items:center;gap:8px}
/* Use .set-btn.set-icon so the padding override beats .set-btn{padding:0 16px}
   (which is defined later in this file). Without the higher specificity the
   32px-wide icon button keeps 16px side padding → 0 content width → the SVG is
   squeezed to nothing and the icon vanishes. */
.set-btn.set-icon{width:32px;padding:0;justify-content:center}
.set-btn.set-icon svg{width:15px;height:15px}
.match-btn-lg{height:34px;padding:0 16px;font-size:var(--fs-sm)}
/* Search now lives on its own full-width row under the header (mobile-first). */
.home-match-search{display:block;width:100%;height:var(--ctl-h);margin:0 0 12px;padding:0 12px;border:1px solid var(--bm);border-radius:var(--rsm);background:var(--s2);color:var(--t);font-family:var(--sans);font-size:var(--fs-body);font-weight:400;outline:none}
.home-match-search:focus{border-color:var(--acb)}
.home-match-search::placeholder{color:var(--t3)}
/* Matches lay out 3-up so a growing season stays scannable without a long
   scroll. Each card becomes a compact tile: icon + name on top, meta, then a
   full-width actions row beneath. Collapses to 2 / 1 columns on small screens
   (css/responsive.css). */
/* Every tile is the same height (grid-auto-rows:1fr sizes all rows to the
   tallest card) so an extra line — the video badge — on one match doesn't
   leave a ragged grid. The actions row is pinned to the card's bottom edge so
   the buttons line up across the whole grid. */
.home-matches{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));grid-auto-rows:1fr;gap:12px;align-items:stretch}
.home-matches .home-empty{grid-column:1/-1}
.home-empty p{margin:0;max-width:360px}
.home-empty-ic{width:48px;height:48px;border-radius:13px;background:var(--acd);border:1px solid var(--acb);display:flex;align-items:center;justify-content:center;color:var(--ac);flex-shrink:0}
.home-empty-ic svg{width:23px;height:23px}
.match-card{display:grid;grid-template-columns:auto 1fr;grid-template-rows:auto 1fr;grid-template-areas:"ic tx" "act act";align-items:center;gap:11px 12px;padding:14px;background:var(--s1);border:1px solid var(--b);border-radius:var(--r);transition:border-color var(--dur),background var(--dur),box-shadow var(--dur)}
/* Centred, not top-aligned. The body is two lines now (the footage line moved
   to the action row) and the name is allowed to run to two of its own, so an
   icon pinned to the top of a variable-height block floats away from the
   thing it belongs to. Centred, the two always read as one unit. */
.match-card>.match-ic{grid-area:ic;align-self:center;margin-top:0}
.match-card>.match-tx{grid-area:tx}
.match-card>.match-actions{grid-area:act;align-self:end}
.match-card:hover{border-color:var(--acb);background:var(--s2);box-shadow:var(--shadowmd)}
.match-ic{width:38px;height:38px;border-radius:9px;background:var(--s3);border:1px solid var(--bm);display:flex;align-items:center;justify-content:center;flex-shrink:0}
.match-ic svg{width:16px;height:16px;color:var(--ac)}
.match-tx{flex:1;min-width:0}
/* TWO LINES, NOT ONE. This was a single nowrap line with an ellipsis, which
   is the harshest rule on the card applied to the one field a coach reads to
   pick the right game — "U15 vs Riverside FC (away, r…" is not a match name.
   The card has the height to spare now the Open button and the footage line
   have left the body, so it wraps once and clamps at two. */
.match-nm{font-size:var(--fs-h3);font-weight:600;line-height:1.3;
  display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;
  overflow:hidden;overflow-wrap:anywhere}
/* Sans, not mono: since decision 4 this line reads "13 tags · 20 Aug 2026",
   which is prose with figures in it rather than a column of figures. */
.match-meta{font-size:var(--fs-label);color:var(--t2);margin-top:2px}
/* Now a row item beside the card's two icon buttons, not a line in the body —
   which is how public/help.html has always drawn this class. It takes the
   rest of the row so it can shorten itself rather than pushing the row into
   a second line on a narrow card.
   display:block is still deliberate and still load-bearing: `text-overflow:
   ellipsis` only applies to a BLOCK container's inline content and is
   silently inert on a flex one. Without it "Video available · 2.1 GB" ended
   mid-character as "2.1 GE" on a 320px card with nothing to say so. The icon
   keeps its 5px gap through a margin and sits on the text baseline via
   vertical-align, for the same reason. */
.match-vid{display:block;flex:1 1 auto;min-width:0;margin:0;font-family:var(--mono);font-size:var(--fs-label);color:var(--ac);font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* No footage yet. Said in the same slot and the same words-per-glance, but
   in the muted colour — it is the absence of the thing the accent announces,
   and a card list where half the rows are accented says nothing. */
.match-vid.none{color:var(--t3);font-weight:400}
.match-vid svg{vertical-align:-2px;margin-right:5px}
.match-score{display:inline-block;margin-right:7px;padding:1px 7px;border-radius:var(--rsm);background:var(--acd);border:1px solid var(--acb);color:var(--ac);font-size:var(--fs-sm);font-weight:700;vertical-align:1px;font-variant-numeric:tabular-nums}
/* The workspace's score badge is a BUTTON (decision 3) — it reads as the
   badge it replaced, and says what it is when you reach for it. */
.mw-score-b{font-family:var(--sans);font-size:var(--fs-body);padding:2px 10px;cursor:pointer;line-height:1.5}
.mw-score-b:hover{background:color-mix(in srgb,var(--ac) 22%,transparent)}
.mw-score-b:focus-visible{outline:2px solid var(--ac);outline-offset:2px}
/* Nothing entered yet: an invitation, not a badge. */
.mw-score-b.empty{background:none;border-style:dashed;color:var(--t3);font-weight:600}
.mw-score-b.empty:hover{color:var(--ac);border-color:var(--acb);background:var(--acd)}
/* nowrap, not wrap: the row is now two small buttons and one line of text
   that shortens itself (.match-vid), so there is nothing left that needs a
   second line — and letting it wrap would put the footage line under the
   buttons, which is the layout this rework moved it out of. */
.match-actions{display:flex;align-items:center;flex-wrap:nowrap;gap:6px;border-top:1px solid var(--b);padding-top:11px}
.match-btn{height:28px;padding:0 13px;border-radius:var(--rsm);border:1px solid var(--acb);background:var(--acd);color:var(--ac);font-family:var(--sans);font-size:var(--fs-sm);font-weight:700;cursor:pointer}
.match-btn:hover{background:color-mix(in srgb, var(--ac) 22%, transparent)}
.match-ibtn{width:28px;height:28px;border-radius:var(--rsm);border:1px solid var(--bm);background:var(--s2);color:var(--t2);font-size:14px;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center}
/* Without this the inline SVGs render at the browser's 300×150 default and jam
   against the button edges — this is what made the stats / CSV / edit / delete
   icons look cramped. 15px in a 28px button leaves a comfortable margin. */
.match-ibtn svg{width:15px;height:15px}
.match-ibtn:hover{color:var(--t);border-color:var(--bs)}
/* NO LONGER ON A MATCH CARD. The whole card opens the match, so a button in
   the action row saying so was the widest thing in that row for no work — it
   is gone from js/sessions.js. This survives because public/help.html builds
   its nine question cards out of .match-card, where "Open" IS the action
   (they are links, and there is nothing else on them to press). Do not
   re-add it to a match card; the ⋯ menu carries "Open match" in words. */
.match-open{flex:1;min-width:84px;height:28px;padding:0 12px;border-radius:var(--rsm);
  border:1px solid var(--acb);background:var(--acd);color:var(--ac);
  font-family:var(--sans);font-size:var(--fs-sm);font-weight:700;cursor:pointer;
  display:inline-flex;align-items:center;justify-content:center;gap:6px}
.match-open svg{width:14px;height:14px}
.match-open:hover{background:color-mix(in srgb, var(--ac) 22%, transparent)}

/* ── NAV GEAR ── */
.nav-gear{width:30px;height:30px;border-radius:var(--rsm);border:1px solid transparent;background:transparent;color:var(--t2);cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0}
.nav-gear:hover{background:var(--s2);color:var(--t);border-color:var(--b)}
.nav-gear svg{width:16px;height:16px}

/* ── RECOVERY BAR ── */
/* Centred with left/right + margin auto, NOT left:50% + translateX(-50%).
   The old pair looks equivalent and is not: a shrink-to-fit fixed box laid out
   from left:50% can only use the space between that edge and the right of the
   viewport, so the bar could never be wider than HALF the screen however much
   room the page had. At 744px that gave it 372px for 406px of content and the
   sentence ellipsised with 337px going spare — which is why the iPad mini cut
   the text and the 834px iPad, one hair over twice the content width, did not.
   left/right/margin-inline:auto centres the same way and has the whole width. */
.recover-bar{position:fixed;top:calc(var(--nh) + 12px);left:12px;right:12px;margin-inline:auto;width:max-content;max-width:calc(100vw - 24px);z-index:400;display:flex;align-items:center;gap:11px;padding:10px 12px 10px 15px;background:var(--s3);border:1px solid var(--am);border-radius:99px;box-shadow:var(--shadowlg)}
.recover-bar svg{width:16px;height:16px;color:var(--am);flex-shrink:0}
.recover-bar span{font-size:var(--fs-sm);font-weight:600;color:var(--t);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.rec-restore{flex-shrink:0}
.rec-dismiss{height:30px;padding:0 12px;border-radius:99px;border:1px solid var(--bm);background:transparent;color:var(--t2);font-family:var(--sans);font-size:var(--fs-sm);font-weight:600;cursor:pointer;flex-shrink:0}
.rec-dismiss:hover{color:var(--t)}
/* Update-ready bar — same chrome as the recovery bar, accent (not amber) edge. */
.recover-bar.update-bar{border-color:var(--acb)}
.recover-bar.update-bar svg{color:var(--ac)}

/* ── THE PILL CANNOT HOLD A SENTENCE ON A PHONE ──
   One line, nowrap, two flex-shrink:0 buttons beside it: the icon, the two
   buttons, the gaps and the padding come to ~209px before the text gets a
   pixel, so on a 375px phone ~140px is left for a string that wants 195px.
   Measured: 28% of "Add Touchline to your home screen" cut on every phone AND
   the iPad mini, 56% on Retag where the bar is narrower still. Users read
   "Add Touchline to your ho…" — and this is the ANDROID install prompt
   (beforeinstallprompt is Chrome-only), so it is the home-screen flow on the
   platform that has one. The recovery and update bars share the class and so
   shared the bug.

   The fix is to stop pretending it is a pill. Below 620px the text takes its
   own line and the buttons sit under it, which is why the radius drops to a
   card — a 99px radius on a two-line box draws a lozenge. Above that the
   single-line pill is untouched, because there it fits. */
@media(max-width:620px){
  /* width:auto, so the bar spans left:12px to right:12px instead of sizing to
     its content — on a phone the sentence needs every pixel of that. */
  .recover-bar{flex-wrap:wrap;border-radius:14px;padding:12px 14px;gap:9px 11px;width:auto}
  /* flex:1 1 auto, NOT 1 1 100%. A 100% basis makes the span its own flex
     line, which pushes the 16px icon onto a line by itself — an icon alone
     above a sentence. With an auto basis the icon and the text share line one
     (the text wrapping inside its own box) and only the two buttons, which
     cannot shrink, drop to line two. */
  .recover-bar span{white-space:normal;overflow:visible;text-overflow:clip;
    flex:1 1 auto;line-height:1.35;min-width:0}
  /* Both buttons sit right on their own line; auto on the first moves the pair. */
  .recover-bar .rec-restore{margin-left:auto}
}
/* 30px is short of the touch floor by fourteen, and these two are the whole
   point of the bar. Height only — the pill's width is set by its label. */
@media(max-width:760px),(hover:none) and (pointer:coarse){
  .rec-restore,.rec-dismiss{height:44px}
}

/* While a fixed overlay owns the screen the page behind it must not scroll —
   otherwise a flick meant for the modal's own list scrolls the results table
   underneath, and the overlay's position is wrong again the moment it closes.
   Set and cleared by fiLockScroll / fiUnlockScroll (js/ui.js). */
body.fi-scroll-lock{overflow:hidden}

/* ── SETTINGS MODAL ── */
/* padding is --safe-pad, not 20px: this one selector is the scrim behind
   Settings, every fiNote / fiConfirm / fiPrompt / fiForm / fiPickList, the
   onboarding card, the camera help and every help chapter on help.html. The
   20px gutter it used to carry is smaller than an iPhone's 59px notch, so the
   header of a tall sheet — the row holding the × — sat behind the status bar
   with nothing to scroll it into view. See --safe-pad in the body block. */
.settings-overlay{position:fixed;inset:0;z-index:600;background:var(--scrim);backdrop-filter:blur(3px);display:none;align-items:center;justify-content:center;padding:var(--safe-pad)}
.settings-overlay.show{display:flex}
/* Cap the panel to the viewport and lay it out as a column so the header
   stays pinned while the body scrolls — otherwise a tall panel overflows
   off the top and bottom with no way to reach the cut-off rows. */
/* The ceiling is the SAFE height minus the scrim's own gutters, so the cap
   agrees with the padding above it. `calc(100dvh - 40px)` did not: on a
   notched phone the scrim was pushing the box down by 59px while the box was
   still sizing itself as though it had the whole screen, which is how a panel
   ended up taller than the space it had been given. */
.settings-box{width:100%;max-width:460px;
  max-height:calc(var(--safeh) - 2*var(--gutter));
  background:var(--s1);border:1px solid var(--bm);border-radius:14px;overflow:hidden;display:flex;flex-direction:column}
.settings-hdr{flex-shrink:0;display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid var(--b)}
.settings-hdr h2{font-size:var(--fs-h1);font-weight:700}
.settings-x{width:28px;height:28px;border:none;background:transparent;color:var(--t3);font-size:20px;line-height:1;cursor:pointer;border-radius:var(--rsm)}
.settings-x:hover{color:var(--t);background:var(--s2)}
/* Settings section bar. Sits between the header and the scrolling body and
   does not scroll with it — the point of the bar is to get you out of a long
   column, so it has to stay reachable from the bottom of one. Scrolls
   horizontally on a phone rather than wrapping to two rows, which would eat
   the height the panel is short of in the first place. */
/* ── C-08 · a strip that scrolls has to LOOK like it scrolls ──
   These two tab strips hide their scrollbar (right, on a phone) and then said
   nothing at all about the tabs past the edge: Settings showed five of seven
   and clipped "Preferences" mid-word, so Data and Help were invisible with
   nothing to suggest they existed.
   The shared class below is Roman Komarov's scroll-shadow trick, and it is
   CSS-only on purpose — no listener, no resize handling, nothing to keep in
   sync. Two `local` layers scroll WITH the content and mask the shadow when
   the strip is at that end; two `scroll` layers stay put and show it when it
   is not. `--sh-bg` is the colour the strip sits on, so each user sets it. */
/* Extended 2026-09-06: the affordance was on 8 of the 21 horizontal
   scrollers, so on the other 13 nothing said there was more sideways. The
   five added here are the ones whose ground is a plain token surface --
   the rest sit over video or already declare a `background:` shorthand,
   which cancels this rule outright (that is what these gradients ARE).
   --sh-bg names the surface behind each so the fade ends in the right
   colour rather than in the default card white. */
.dh-tabs,.lg-scroll{--sh-bg:var(--bg)}
.fi-scroll-x,.pos-tabs,.dh-tabs,.fi-grid-wrap,.dh-g-wrap,.lg-scroll{
  background:
    linear-gradient(to right,var(--sh-bg,var(--s1)) 40%,transparent) left  center / 26px 100% no-repeat local,
    linear-gradient(to left, var(--sh-bg,var(--s1)) 40%,transparent) right center / 26px 100% no-repeat local,
    radial-gradient(farthest-side at 0    50%,var(--sh-c,rgba(0,0,0,.16)),transparent) left  center / 13px 100% no-repeat scroll,
    radial-gradient(farthest-side at 100% 50%,var(--sh-c,rgba(0,0,0,.16)),transparent) right center / 13px 100% no-repeat scroll;
}
.set-nav{flex-shrink:0;display:flex;gap:4px;padding:8px 20px 0;overflow-x:auto;overflow-y:hidden;scrollbar-width:none;-webkit-overflow-scrolling:touch;border-bottom:1px solid var(--b)}
.set-nav::-webkit-scrollbar{display:none}
.set-nav button{flex-shrink:0;padding:7px 12px 8px;border:0;border-bottom:2px solid transparent;background:none;color:var(--t2);font-family:var(--sans);font-size:var(--fs-sm);font-weight:600;cursor:pointer;white-space:nowrap}
.set-nav button:hover{color:var(--t)}
.set-nav button.on{color:var(--ac);border-bottom-color:var(--ac)}
/* min-height:0 is the load-bearing half. .settings-box is a column flex with a
   max-height, and a flex item's default `min-height:auto` refuses to shrink
   below its content — so a body taller than the box did not scroll, it simply
   overflowed and was clipped by the box's own `overflow:hidden`. That is why
   the last rows of a long sheet were cut off mid-line with no way to reach
   them. With this, the body is the ONE scroller every dialog uses. */
.settings-body{padding:8px 20px 18px;flex:1 1 auto;min-height:0;overflow-y:auto;-webkit-overflow-scrolling:touch;overscroll-behavior:contain}
/* With a section bar above it, the group heading says exactly what the tab
   you just pressed says. Two labels for one thing is one too many, so the
   heading goes and the tab carries it. */
.set-nav+.settings-body .set-group{display:none}
.set-nav+.settings-body>.set-row:first-of-type{border-top:0}
.set-row{display:flex;align-items:center;gap:14px;padding:15px 0;border-bottom:1px solid var(--b)}
.set-row:last-child{border-bottom:none}
.set-tx{flex:1;min-width:0}
.set-nm{font-size:var(--fs-h3);font-weight:600;margin-bottom:3px}
.set-row.danger .set-nm{color:var(--rd)}
.set-ds{font-size:var(--fs-sm);color:var(--t2);line-height:1.5}
.set-ds a{color:var(--ac);text-decoration:none;font-weight:600;white-space:nowrap}
.set-ds a:hover{text-decoration:underline}
/* ── NO-JAVASCRIPT FALLBACK ──
   The app is 100% client-side, so with scripts off it would otherwise render
   as an empty dark page. Full-bleed so the (still-rendered) auth overlay
   behind it is covered; the text itself is capped for readability. */
.fi-noscript{position:fixed;inset:0;z-index:99999;display:flex;flex-direction:column;
  align-items:center;justify-content:center;gap:14px;padding:32px;
  background:var(--bg);color:var(--t);text-align:center;overflow:auto}
.fi-noscript>*{max-width:560px}
.fi-noscript h1{font-size:var(--fs-h1);font-weight:700;margin:0}
.fi-noscript p{font-size:var(--fs-body);line-height:1.65;color:var(--t2);margin:0}
.fi-noscript a{color:var(--ac);font-weight:600}

/* Storage-eviction state. Amber by default (action needed), green once the
   browser has granted persistence. */
.set-persist{margin-top:7px;font-size:var(--fs-label);line-height:1.5;color:var(--am)}
.set-persist.ok{color:var(--ac)}
/* .set-warn WAS DECLARED HERE and is gone with its only user (2026-09-05):
   the "match video is not included" caution under Download my data, which was
   a bordered block sitting outside the row's own chevron — so it stayed open
   while every other explanation collapsed, and read as an alarm on the one row
   people press when being careful. The sentence itself was not lost; it is a
   clause of that row's description now. .set-ds strong carries the emphasis. */
.set-ds strong{color:var(--t);font-weight:700}
.set-btn{height:32px;padding:0 16px;border-radius:var(--rsm);border:1px solid var(--bm);background:var(--s2);color:var(--t);font-family:var(--sans);font-size:var(--fs-sm);font-weight:700;cursor:pointer;flex-shrink:0;display:flex;align-items:center}
.set-btn:hover{border-color:var(--bs);background:var(--s3)}
/* The date-order toggle on Home and on Presentations (js/sessions.js
   fiRenderSortBtns). A .set-btn is a flex row, so the arrow and the label need
   a gap of their own; nowrap because "Newest first" turning into two lines
   changes the height of the row it shares with + New match. */
.fi-sort-btn{gap:6px;white-space:nowrap}
.set-btn.danger{border-color:var(--rdd);background:var(--rdd);color:var(--rd)}
.set-btn.danger:hover{background:rgba(255,77,77,.2)}
/* The one button in a row that is the point of the row — Save, next to a
   Cancel. Settings had no primary button until the account editor got a pair
   of them, and two identical grey buttons make the user read to find out
   which one commits. */
.set-btn.go{border-color:var(--ac);background:var(--ac);color:var(--on-ac)}
.set-btn.go:hover{border-color:var(--ac);background:var(--ac);filter:brightness(1.06)}

/* ── Account details, as a read-out (js/profile.js fiProfileDetailHTML) ──
   What Settings ▸ Account shows once the row is unfolded. A labelled list,
   not a sentence: the row's job is "what do you hold about me", and eight
   answers squeezed into one line answered four of them.

   PLAIN ROWS, NOT A CARD GRID (2026-09-06). This was
   `grid-template-columns:repeat(auto-fill,minmax(184px,1fr))` with the label
   stacked above its value — eight small tiles that reflowed into two columns
   or three depending on the width of the phone, so "Sport" sat beside "Age
   group" on one device and under "Club" on the next. It was the only block in
   Settings shaped like that, and it read as a dashboard rather than as the
   list of answers it is. Label left, value right, one per line: the same
   shape as every other stated fact in this panel, and — because the eight
   values total about twelve words — also the shorter of the two.

   .fig marks a value that is a COUNTED thing — a phone number, a headcount —
   which takes the mono face at weight 500, the type rule the whole app
   follows. Names, clubs and sports are words and stay in the sans. */
.acct-dl{margin:2px 0 0}
.acct-d{display:flex;align-items:baseline;justify-content:space-between;gap:16px;
  padding:9px 0;border-bottom:1px solid var(--b)}
.acct-d:last-child{border-bottom:0;padding-bottom:2px}
.acct-d dt{flex:0 0 auto;font-size:var(--fs-sm);color:var(--t3)}
.acct-d dd{margin:0;flex:1 1 auto;min-width:0;text-align:right;
  font-size:var(--fs-body);color:var(--t);word-break:break-word}
.acct-d dd.fig{font-family:var(--mono);font-weight:500}
.acct-note{margin-top:13px;font-size:var(--fs-xs);color:var(--t3);line-height:1.55}
/* A narrow phone stacks the pair instead of squeezing a wrapped email into
   half a line: below this width a right-aligned value shares the row with its
   label only by breaking mid-word, which is worse than one more line.

   360, not 420. At 420 this caught the 414px phones — iPhone Plus and Pro Max,
   among the commonest handsets a coach owns — and stacked every pair on a
   screen with room to spare, which is the layout the fold was meant to replace.
   Below 360 there genuinely is not room for a label, a gap and an email
   address on one line. */
@media(max-width:360px){
  .acct-d{flex-direction:column;align-items:stretch;gap:2px}
  .acct-d dd{text-align:left;font-size:var(--fs-sm)}
}
/* Appearance segmented toggle (Dark / Light) */
.set-seg{display:flex;gap:3px;background:var(--s2);border:1px solid var(--b);border-radius:var(--rsm);padding:3px;flex-shrink:0}
/* A settings control that needs a continuous value rather than on/off — it sits
   UNDER its description (not in the right-hand gutter like .set-seg) because a
   slider needs the full row width to be draggable with a thumb. */
.set-range-row{display:flex;align-items:center;gap:11px;margin-top:10px}
.set-range{flex:1;min-width:0;height:22px;accent-color:var(--ac);cursor:pointer;background:transparent}
.set-range-val{font-family:var(--mono);font-size:var(--fs-label);font-weight:500;color:var(--t2);min-width:38px;text-align:right;font-variant-numeric:tabular-nums}
.set-range-reset{height:28px;padding:0 11px;font-size:var(--fs-label);flex-shrink:0}
.set-seg button{height:28px;padding:0 13px;border:none;background:transparent;border-radius:calc(var(--rsm) - 2px);font-family:var(--sans);font-size:var(--fs-sm);font-weight:600;color:var(--t2);cursor:pointer;display:flex;align-items:center;gap:6px;transition:color var(--dur),background var(--dur)}
.set-seg button:hover{color:var(--t)}
.set-seg button.active{background:var(--s1);color:var(--t);box-shadow:0 1px 3px rgba(0,0,0,.16)}
/* Storage manager (per-match footage) */
/* Plan meter — same visual language as the Home storage tile's bar, so the
   two readings are recognisably the same number. */
/* ── Storage: two figures, as cards ──────────────────────────────────
   The Storage row used to say its numbers in sentences, stacked with three
   more sentences around them, and the one thing someone opened the row for
   was the hardest thing in it to find. Two cards side by side, each stating
   one figure, and only the metered one carries a bar — the device has no
   ceiling we are able to read (js/quota.js header).
   Figures are mono at weight 500 and units are not, which is the same type
   rule the Home tiles follow. */
.stor-cards{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:10px}
.stor-card{background:var(--s2);border:1px solid var(--b);border-radius:var(--r);padding:12px 13px;min-width:0}
.stor-k{font-size:var(--fs-label);font-weight:600;color:var(--t3);text-transform:uppercase;letter-spacing:var(--ls-label)}
.stor-v{margin-top:5px;font-family:var(--mono);font-weight:500;font-size:24px;line-height:1.1;
  color:var(--t);letter-spacing:-.5px;white-space:nowrap;font-variant-numeric:tabular-nums}
.stor-u{font-family:var(--sans);font-size:var(--fs-sm);font-weight:600;color:var(--t3);letter-spacing:0;margin-left:4px}
.stor-sub{margin-top:5px;font-size:var(--fs-label);color:var(--t2);line-height:1.35;overflow-wrap:anywhere}
.stor-bar{margin-top:8px;height:5px;border-radius:3px;background:var(--s3);overflow:hidden}
.stor-bar>span{display:block;height:100%;width:0;background:var(--ac);border-radius:3px;transition:width .3s}
.stor-bar.warn>span{background:var(--rd-str)}
/* Spans both cards: it is about the pair, not about either one. */
.stor-nudge{grid-column:1/-1;display:flex;align-items:center;gap:10px;flex-wrap:wrap;
  font-size:var(--fs-label);color:var(--t2);line-height:1.4}
.stor-nudge-b{flex-shrink:0;height:28px;padding:0 12px;border-radius:var(--rsm);border:1px solid var(--bm);
  background:var(--s2);color:var(--t);font-family:var(--sans);font-size:var(--fs-label);font-weight:700;cursor:pointer}
.stor-nudge-b:hover{border-color:var(--bs);background:var(--s3)}
/* A phone is not wide enough for two figures plus their units side by side —
   they wrapped mid-number, which is worse than stacking. */
@media(max-width:420px){.stor-cards{grid-template-columns:1fr}}
.storage-list{margin-top:9px;display:flex;flex-direction:column;gap:7px}
.storage-extra{margin-top:13px;display:flex;flex-direction:column;gap:7px}
.storage-head{font-size:var(--fs-label);font-weight:700;color:var(--t2);
  padding-top:9px;border-top:1px solid var(--b)}
.storage-note{font-size:var(--fs-label);color:var(--t3);line-height:1.45}
.storage-total{font-size:var(--fs-label);color:var(--t3);font-family:var(--mono);margin-bottom:2px}
.storage-item{display:flex;align-items:center;gap:9px;font-size:var(--fs-sm)}
.storage-nm{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--t)}
.storage-sz{font-family:var(--mono);font-size:var(--fs-label);color:var(--t3);flex-shrink:0}

/* ── Test harness (js/mockcloud.js) ─────────────────────────────────
   Deliberately ugly and deliberately fixed to the bottom of the screen: it
   must never be mistaken for product UI, and it has to stay reachable on a
   phone while you walk through the roles. */
.fi-mock{position:fixed;left:8px;right:8px;bottom:8px;z-index:2000;border:2px solid #c98a2e;border-radius:10px;background:#1b1a17;color:#f3efe6;font-family:var(--sans);font-size:var(--fs-sm);box-shadow:0 8px 28px rgba(0,0,0,.5);max-width:520px;margin:0 auto}
.fmk-hd{display:flex;align-items:center;gap:8px;padding:7px 10px;background:#c98a2e;color:#1b1a17;font-size:var(--fs-label);border-radius:7px 7px 0 0}
.fmk-hd b{font-weight:700}
.fmk-x{margin-left:auto;background:none;border:0;color:#1b1a17;font-size:15px;cursor:pointer;padding:0 4px;line-height:1}
.fmk-bd{padding:8px 10px;display:flex;flex-direction:column;gap:7px;max-height:52vh;overflow-y:auto}
.fmk-r{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.fmk-r>span{min-width:88px;color:#b9b2a4;font-size:var(--fs-label)}
.fmk-r>div{display:flex;gap:5px;flex-wrap:wrap}
.fmk-b{padding:4px 9px;border-radius:6px;border:1px solid #4a453c;background:#26241f;color:#f3efe6;font-size:var(--fs-label);cursor:pointer}
.fmk-b.on{background:#c98a2e;border-color:#c98a2e;color:#1b1a17;font-weight:700}
.fmk-n{color:#b9b2a4;font-size:var(--fs-xs);line-height:1.45}

/* ── Team roster (js/teamui.js) ─────────────────────────────────────
   Built around one idea: the FIGURES answer the question. Someone opening
   this is asking "have I got room for one more", so the used count carries
   the accent and the ceiling sits behind it in plain text — 3/10 reads as
   "three of my ten" at a glance, without a sentence. A tier with no ceiling
   says "Unlimited" in the same accent, replacing the number rather than
   adding words to it. */
.tm{margin-top:16px}
.tm-load{font-size:var(--fs-sm);color:var(--t2);padding:6px 0}

/* The club name is the heading. Everything that used to sit above it — a
   field label, a dropdown of one, a "signed in as" line — collapses into
   these two. */
/* ── The account bar (D-35) ───────────────────────────────────────
   Above the Settings section tabs, so it shows in every section. Its job
   is to answer "who am I, where am I, and how do I leave" in one glance —
   all three used to be three levels down inside the Team section. */
.acct-bar{display:flex;flex-direction:column;gap:11px;padding:14px 16px;margin-bottom:4px;
  background:var(--s1);border:1px solid var(--b);border-radius:var(--r)}
.ab-nm{font-family:var(--sans);font-size:var(--fs-h2);font-weight:700;color:var(--t);line-height:1.25}
.ab-sub{margin-top:2px;font-size:var(--fs-sm);color:var(--t3);word-break:break-word}
.ab-team{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap}
.ab-lb{font-size:var(--fs-xs);text-transform:uppercase;letter-spacing:var(--ls-label);color:var(--t3)}
.ab-tn{font-family:var(--sans);font-size:var(--fs-body);font-weight:600;color:var(--t)}
/* The plan is a figure-ish fact, so it gets the mono face like every other
   countable thing in the app (see the type rule in css/base.css). */
.ab-plan{font-family:var(--mono);font-size:var(--fs-xs);font-weight:500;color:var(--ac);
  padding:1px 7px;border:1px solid var(--bm);border-radius:99px}
.ab-sw{display:flex;flex-direction:column;gap:5px}
/* The switcher was a <select>; it is a button that opens the club picker
   (fiClubPicker in js/teamui.js). Both are full-width, so the rule covers
   both rather than being re-pointed at one of them. */
.ab-sw .ai,.ab-swb{width:100%}
.ab-swb{justify-content:center}
.ab-warn{font-size:var(--fs-xs);color:var(--t3);line-height:1.5}
.ab-act{display:flex;gap:16px;flex-wrap:wrap;padding-top:9px;border-top:1px solid var(--b)}

/* ── Linked recordings (js/linked.js) ─────────────────────────────
   The parts of one game that was filmed in pieces. A read-out first and a
   control second: the offsets are computed, so being able to SEE them is
   the point. */
.lk-panel{margin:0 0 18px;padding:13px 15px;background:var(--s1);border:1px solid var(--b);border-radius:var(--r)}
.lk-hd{font-family:var(--sans);font-size:var(--fs-sm);font-weight:600;color:var(--t);margin-bottom:9px}
.lk-row{display:grid;grid-template-columns:auto 1fr auto auto;align-items:center;gap:10px;
  padding:6px 0;border-top:1px solid var(--b)}
.lk-row.me{background:linear-gradient(90deg,var(--s2),transparent);margin:0 -6px;padding-left:6px;padding-right:6px;border-radius:var(--rsm)}
/* The part number is a figure, so it takes the mono face like every other
   countable thing in the app. */
.lk-ix{font-family:var(--mono);font-size:var(--fs-xs);font-weight:500;color:var(--ac);
  width:20px;text-align:center}
.lk-nm{font-size:var(--fs-sm);color:var(--t);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.lk-nm em{font-style:normal;opacity:.55}
.lk-off{font-family:var(--mono);font-size:var(--fs-xs);color:var(--t3);white-space:nowrap}
.lk-mv{display:flex;gap:2px}
.lk-b{width:26px;height:26px;border:1px solid var(--b);background:var(--s2);color:var(--t2);
  border-radius:var(--rsm);cursor:pointer;font-size:var(--fs-sm);line-height:1}
.lk-b:hover:not(:disabled){color:var(--t);border-color:var(--bm)}
.lk-b:disabled{opacity:.3;cursor:default}
.lk-note{margin-top:10px;font-size:var(--fs-xs);color:var(--t3);line-height:1.55}
.lk-act{margin-top:11px;padding-top:10px;border-top:1px solid var(--b);display:flex;gap:16px;flex-wrap:wrap}
/* Phone: the offset read-out drops under the name rather than squeezing it. */
@media (max-width:560px){
  .lk-row{grid-template-columns:auto 1fr auto;row-gap:2px}
  .lk-off{grid-column:2/4;font-size:var(--fs-xs)}
}

/* The staff half of the share picker (js/audience.js). Separated by a rule
   above it because it answers a second question, not a continuation of the
   first — who among the COACHES, rather than who among the players. */
.au-staff{margin-top:16px;padding-top:14px;border-top:1px solid var(--b)}
.au-row-t .au-nm i{display:block;font-style:normal;font-size:var(--fs-xs);color:var(--t3);margin-top:1px}

.tm-hd{margin-bottom:2px}
.tm-name{font-family:var(--sans);font-size:var(--fs-h2);font-weight:700;color:var(--t);line-height:1.25}
.tm-sub{margin-top:3px;font-size:var(--fs-sm);color:var(--t3);word-break:break-word}
.tm-switch{width:100%;margin-top:9px}

.tm-seats{display:grid;grid-template-columns:repeat(auto-fit,minmax(74px,1fr));gap:8px}
/* Four tiles across a phone leaves an orphan on the second row. Two by two
   is the shape that always lands square. */
@media(max-width:560px){.tm-seats{grid-template-columns:repeat(2,1fr)}}
.tm-seat{padding:9px 10px 8px;border:1px solid var(--b);border-radius:var(--rsm);background:var(--s2)}
.tm-fig{display:flex;align-items:baseline;gap:1px;font-family:var(--mono);line-height:1}
/* 500, not 600: this inherits DM Mono from .tm-fig, and 500 is the heaviest
   face that exists (css/fonts.css). */
.tm-used{font-size:17px;font-weight:500;color:var(--ac)}
.tm-max{font-size:var(--fs-body);color:var(--t2)}
/* "(unlimited)" sits in the LABEL now, not in the figure — see _seat() in
   js/teamui.js for why. Lower case and not tracked out, against a label that
   is uppercase and is: it is an aside about the tier, and it should not
   compete with the word naming the tile. */
.tm-inf{margin-left:5px;font-size:var(--fs-xs);font-weight:500;letter-spacing:0;text-transform:none;color:var(--t3)}
.tm-lbl{margin-top:5px;font-size:var(--fs-xs);letter-spacing:var(--ls-label);text-transform:uppercase;color:var(--t3)}
/* The one tile that has to say who it counts. Devices are a COACH allowance
   (two per coach seat on every tier), and a tile reading "3/20" beside three
   others about people invites exactly the wrong sum. */
.tm-seat-n{margin-top:3px;font-size:var(--fs-xs);letter-spacing:0;text-transform:none;color:var(--ac)}
.tm-seat.wide{grid-column:1/-1}
/* Over the limit is amber, never red: you are at a ceiling your plan sets,
   which is a fact to act on, not a mistake you have made. */
.tm-seat.over{border-color:var(--am)}
.tm-seat.over .tm-used{color:var(--am)}

/* Add someone. The role is three buttons rather than a dropdown of
   sentences — the descriptions moved to one line underneath that changes
   with the choice, which is where most of the old clutter went. */
.tm-add{margin-top:14px}
/* Equal columns rather than wrapping flex: three choices that reflow to
   2 + 1 read as a list someone gave up on formatting. A long label wraps
   inside its own column and the row stays square. */
.tm-roles{display:grid;grid-template-columns:repeat(auto-fit,minmax(0,1fr));gap:6px}
.tm-role{padding:8px 6px;border:1px solid var(--bm);border-radius:var(--rsm);background:transparent;color:var(--t2);font-family:var(--sans);font-size:var(--fs-sm);font-weight:600;line-height:1.3;cursor:pointer;transition:border-color var(--dur),color var(--dur)}
.tm-role:hover{border-color:var(--bs);color:var(--t)}
.tm-role.on{border-color:var(--ac);background:var(--acd);color:var(--t)}
.tm-role.full{opacity:.45}
.tm-role-ds{margin-top:7px;font-size:var(--fs-sm);color:var(--t3);line-height:1.45;min-height:1.45em}
/* TWO NUMBERED STEPS: who, then what they are. The numerals are the whole
   affordance — they say "there are two things here and this is the first" at a
   glance, which a pair of unlabelled controls cannot. */
.tm-step{margin-top:11px}
.tm-step-l{display:flex;align-items:center;gap:7px;margin-bottom:6px;
  font-size:var(--fs-xs);letter-spacing:var(--ls-label);text-transform:uppercase;color:var(--t3)}
.tm-step-l b{display:inline-flex;align-items:center;justify-content:center;
  width:16px;height:16px;border-radius:50%;background:var(--s3);
  font-family:var(--mono);font-size:9.5px;font-weight:500;color:var(--t2);letter-spacing:0}
.tm-em-in{width:100%}
/* Full width, under both steps, because it completes them rather than
   belonging to either. It was glued to the right of the email field, which
   read as "search" and put the commit action before the role had been set. */
.tm-addbtn{display:block;width:100%;margin-top:12px;padding:10px 20px;border:0;
  border-radius:var(--rsm);background:var(--ac);color:var(--on-ac);
  font-family:var(--sans);font-size:var(--fs-sm);font-weight:650;cursor:pointer}
.tm-addbtn:hover{filter:brightness(1.06)}
/* Full width is right on a phone and absurd on a laptop — a metre of solid
   accent for a two-word action. Above the phone breakpoint it sizes to its
   label and sits under the steps it completes. */
@media(min-width:561px){ .tm-addbtn{width:auto;min-width:190px} }
.tm-msg{margin-top:7px;font-size:var(--fs-sm);line-height:1.45;color:var(--t3)}
.tm-msg.ok{color:var(--ac)}
.tm-msg.bad{color:var(--am)}

/* Member rows. A two-column grid with the actions spanning both rows, so a
   long address wraps its chips underneath instead of shoving the buttons off
   the edge — which is what the old flex row did on a phone. */
.tm-mh{display:flex;align-items:center;gap:7px;margin:20px 0 0;font-size:var(--fs-xs);letter-spacing:var(--ls-label);text-transform:uppercase;color:var(--t3)}
.tm-mh span{padding:1px 7px;border-radius:99px;background:var(--s3);font-family:var(--mono);font-size:var(--fs-xs);color:var(--t2)}
.tm-list{list-style:none;margin:6px 0 0;padding:0}
.tm-mem{display:grid;grid-template-columns:1fr auto;gap:3px 10px;align-items:center;padding:10px 0;border-top:1px solid var(--b)}
.tm-em{grid-column:1;grid-row:1;font-size:var(--fs-sm);color:var(--t);word-break:break-all}
.tm-chips{grid-column:1;grid-row:2;display:flex;gap:4px;flex-wrap:wrap}
.tm-chip{padding:2px 7px;border-radius:99px;font-family:var(--sans);font-size:9px;font-style:normal;font-weight:700;letter-spacing:var(--ls-label);text-transform:uppercase;background:var(--s3);color:var(--t2)}
.tm-chip.own{background:var(--acd);color:var(--ac)}
.tm-chip.you{background:transparent;border:1px solid var(--bm);color:var(--t3)}
.tm-chip.pend{background:var(--amd);color:var(--am)}
/* Group membership. Outlined rather than filled: a group is a fact about
   where this player sits, not a status like Owner or Invited, and three
   filled chips on one row read as three warnings. */
.tm-chip.sq{background:transparent;border:1px solid var(--acb);color:var(--ac);text-transform:none;letter-spacing:0;font-weight:600}
/* Groups block in the team panel (js/teamui.js, js/audience.js). */
.tm-sq{margin-top:10px}
.tm-sq-list{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:9px}
.tm-sq-c{display:inline-flex;align-items:center;gap:6px;padding:5px 11px;border-radius:99px;
  border:1px solid var(--bm);background:var(--s2);font-family:var(--sans);
  font-size:var(--fs-sm);color:var(--t);cursor:pointer}
.tm-sq-c:hover{border-color:var(--ac);color:var(--t)}
.tm-sq-c i{font-style:normal;font-family:var(--mono);font-size:var(--fs-xs);color:var(--t3)}
.tm-sq-e{font-size:var(--fs-sm);color:var(--t2);line-height:1.6;margin-bottom:9px}
/* ONE BUTTON PER ROW. Up to three named buttons used to live here; on a phone
   they wrapped under the address and parked "Remove" a thumb-width from
   "Copy link". They are the same actions behind the ⋮ the rest of the app
   already uses for "what can I do to this thing" (fiTeamMemberMenu). */
.tm-ax{grid-column:2;grid-row:1/3;display:flex;align-self:center}
.tm-more{display:inline-flex;align-items:center;justify-content:center;
  width:34px;height:34px;padding:0;border:1px solid transparent;border-radius:var(--rsm);
  background:transparent;color:var(--t3);cursor:pointer;transition:border-color var(--dur),color var(--dur)}
.tm-more:hover{border-color:var(--bm);color:var(--t)}
.tm-more .fi-ic{width:17px;height:17px}
/* A 34px target is under the 44px touch floor, so the phone gets the floor —
   the roster is a list of small rows and this is the only control on each. */
@media (hover:none) and (pointer:coarse){ .tm-more{width:44px;height:44px} }

/* (The owner's toolkit switch was styled here. Removed with the control —
   see js/teamui.js.) */
/* Housekeeping actions — "Create a group", "Create another team", "Leave this
   team". They were 12px underlined text, which put the only way to make a
   group at the size the app uses for captions: read as a footnote, missed as
   a control. Now the same bordered pill as every other secondary button on
   this screen (.au-mini, .tm-more), at body size, so the two that create
   something are found by looking rather than by hunting. Still outline-only,
   because none of them competes with Add for the primary slot. */
.tm-foot{margin-top:18px;padding-top:12px;border-top:1px solid var(--b);display:flex;gap:10px;flex-wrap:wrap}
.tm-link{display:inline-flex;align-items:center;height:32px;padding:0 13px;border-radius:var(--rsm);
  border:1px solid var(--bm);background:var(--s2);color:var(--t2);
  font-family:var(--sans);font-size:var(--fs-body);font-weight:600;cursor:pointer}
.tm-link:hover{background:var(--s3);color:var(--t);border-color:var(--bs)}
.tm-link.danger:hover{color:var(--rd);border-color:var(--rd)}
/* First-run onboarding (reuses .settings-overlay / .settings-box) */
.onboard-lead{margin:0 0 14px;font-size:var(--fs-body);color:var(--t2);line-height:1.6}
.onboard-steps{margin:0;padding:0 0 0 1.2em;display:flex;flex-direction:column;gap:12px}
.onboard-steps li{font-size:var(--fs-body);color:var(--t2);line-height:1.6}
.onboard-steps li b{color:var(--t)}
.onboard-foot{display:flex;gap:8px;justify-content:flex-end;padding:14px 20px;border-top:1px solid var(--b)}
.onboard-go{padding:0 18px}
.storage-del{flex-shrink:0;height:28px;padding:0 11px;border-radius:var(--rsm);border:1px solid var(--rdd);background:var(--rdd);color:var(--rd);font-family:var(--sans);font-size:var(--fs-label);font-weight:700;cursor:pointer}
.storage-del:hover{background:rgba(255,77,77,.2)}
/* Keyboard shortcuts overlay (hotkey cheat sheet) */
.hk-note{font-size:var(--fs-sm);color:var(--t2);line-height:1.55;margin-bottom:13px}
.hk-note b{color:var(--t)}
.hk-sec{margin-bottom:15px}
.hk-sec-h{font-size:var(--fs-xs);font-weight:700;letter-spacing:var(--ls-label);text-transform:uppercase;color:var(--t3);margin-bottom:8px}
.hk-grid{display:grid;grid-template-columns:1fr 1fr;gap:7px}
.hk-row{display:flex;align-items:center;gap:8px;font-size:var(--fs-sm);min-width:0}
.hk-key{display:inline-flex;align-items:center;justify-content:center;min-width:22px;height:22px;padding:0 6px;border-radius:5px;background:var(--s2);border:1px solid var(--bm);font-family:var(--mono);font-size:var(--fs-label);font-weight:500;color:var(--t);flex-shrink:0}
.hk-lbl{color:var(--t2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}

/* secondary, muted line inside a page header description */
.hint-sub{display:inline-block;margin-top:4px;font-size:.92em;color:var(--t3)}

/* ── INLINE "?" HELP (fiHelpBtn → fiHelp) ──────────────────────────
   Sits beside a control's label and carries the explanation that used to be
   printed under it permanently. Deliberately quiet — it must not compete with
   the control it annotates — but a real 26px tap target on a phone.
   Its label is usually a flex row (.pp-hdr, .rq-sub, .tl-toggle-lbl), which
   centres it against the text. `vertical-align:middle` is the fallback for
   the inline cases — a "?" ending a sentence, as in .local-note. */
.fi-q{width:17px;height:17px;flex-shrink:0;padding:0;margin-left:6px;position:relative;
  border:1px solid var(--bm);border-radius:50%;background:var(--s2);color:var(--t3);
  font-family:var(--sans);font-size:var(--fs-xs);font-weight:700;line-height:1;cursor:pointer;
  display:inline-flex;align-items:center;justify-content:center;vertical-align:middle}
.fi-q::after{content:'';position:absolute;top:-5px;left:-5px;right:-5px;bottom:-5px}
.fi-q:hover,.fi-q:focus-visible{border-color:var(--acb);background:var(--acd);color:var(--ac)}

/* ── SHARED MODALS (fiConfirm / fiPrompt / fiPickList) ── */
.fi-modal{z-index:2100}             /* above settings (600), toast (700) and the
                                       recording card (.rec-saved, 2000). At 900 it
                                       sat UNDER that card, so the two warnings that
                                       matter most — "some footage is missing" and
                                       "this recording was NOT saved, download it
                                       now" — were painted behind the very screen
                                       they were warning about. */
.fi-modal-box{max-width:420px}
.fi-modal-body{padding:18px 20px}
.fi-modal-msg{display:block;font-size:var(--fs-body);color:var(--t2);line-height:1.6;margin:0 0 4px}
.fi-modal-input{width:100%;height:var(--ctl-h);margin-top:10px;padding:0 12px;background:var(--s2);border:1px solid var(--bm);border-radius:var(--rsm);font-family:var(--sans);font-size:var(--fs-body);color:var(--t);outline:none}
.fi-modal-input:focus{border-color:var(--acb)}
.fi-modal-foot{flex-shrink:0;display:flex;flex-wrap:wrap;align-items:center;gap:8px;justify-content:flex-end;padding:14px 20px;border-top:1px solid var(--b)}
/* text-decoration:none because a dialog's primary action is sometimes an <a>,
   not a <button> — "Compare plans" on the upgrade prompt (js/billing.js) is a
   real link to the pricing page. There is no global anchor reset in this app,
   so without it a filled accent button arrives wearing the browser's default
   underline. Set on both footer classes, so it stays true whichever element
   the next dialog reaches for. */
.fi-modal-go{padding:0 18px}
.fi-modal-go.danger{background:var(--rd);color:#fff}
/* THE WAY OUT OF A DIALOG, and until now it had no rule anywhere. Four modals
   ship it — Share (js/audience.js), the upgrade prompt (js/billing.js), the
   consent form, Contact support — and every one of them was painting a RAW
   NATIVE BUTTON: system font, white box, default border, next to a styled
   accent Save. It is the most-pressed control in a dialog after the primary
   one, and it looked like the page had failed to load.
   Ghost, not filled: leaving is always available and never the recommendation,
   so it takes the same box as .fi-modal-go and gives up the fill. Same
   language as .tm-link and .au-mini, which are the app's other secondary
   buttons — one outline treatment, not three. */
.fi-modal-no{height:36px;padding:0 16px;border:1px solid var(--bm);border-radius:var(--rsm);background:var(--s2);color:var(--t2);font-family:var(--sans);font-size:var(--fs-sm);font-weight:600;cursor:pointer;display:flex;align-items:center;text-decoration:none}
.fi-modal-no:hover{background:var(--s3);color:var(--t);border-color:var(--bs)}
/* ══════════════════════════════════════════════════════════════════
   SELECTION — one language for every "choose something" surface
   ══════════════════════════════════════════════════════════════════
   Insights grew the good version of this first: a chip bar for the coarse
   cut, then rows you tick, each with a colour dot, a name, and a figure on
   the right in mono. It reads as one control rather than as a list of
   widgets, and it survives a 24-name squad panel next to a five-unit one.

   Every other picker in the app was its own shape — stacked cards here,
   11px pills there, a bare <select> somewhere else — so choosing a
   presentation felt like a different app from choosing a tag. This block is
   that language, promoted out of datahub.css so there is ONE definition.

   The `.dh-*` names are in the selector lists on purpose: Insights keeps
   its markup, and the rules cannot drift apart because there is only one
   copy of them. Do not re-add a .dh-pk-* or .dh-sp-* declaration to
   datahub.css — put it here, against both names.

   Two variants share it:
     .fi-sel-row   — MULTI-select. Carries a checkbox; ticking is the action.
     .fi-pick-row  — SINGLE-select. A <button>; clicking picks and closes.
   They look alike because they are the same decision at different arity.  */

/* ── the trigger: a button that says what is currently chosen ──────── */
.fi-sel-btn,.dh-pickbtn{display:inline-flex;align-items:center;gap:7px;height:30px;padding:0 11px;
  border:1px solid var(--bm);border-radius:99px;background:var(--s2);color:var(--t2);
  font-family:var(--sans);font-size:var(--fs-sm);font-weight:600;cursor:pointer;max-width:230px}
.fi-sel-btn:hover,.dh-pickbtn:hover{color:var(--t);border-color:var(--bs)}
.fi-sel-btn svg,.dh-pickbtn svg{width:14px;height:14px;flex-shrink:0}
.fi-sel-btn span,.dh-pickbtn span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.fi-sel-btn.on,.dh-pickbtn.on{background:var(--acd);border-color:var(--acb);color:var(--ac)}
.fi-sel-clear,.dh-pickclear{border:none;background:none;color:var(--t3);font-family:var(--sans);
  font-size:var(--fs-sm);font-weight:600;cursor:pointer;padding:0}
.fi-sel-clear:hover,.dh-pickclear:hover{color:var(--t2)}

/* ── the body: chip bar, then rows ────────────────────────────────── */
.fi-sel,.dh-pk,.dh-sp{display:flex;flex-direction:column;gap:12px;min-height:220px}
.fi-sel-bar,.dh-pk-panels,.dh-sp-presets{display:flex;flex-wrap:wrap;gap:6px;
  padding-bottom:12px;border-bottom:1px solid var(--b)}
.fi-sel-chip,.dh-pk-panel{display:inline-flex;align-items:center;gap:6px;height:var(--ctl-h-sm);padding:0 12px;
  border-radius:99px;border:1px solid var(--bm);background:var(--s2);color:var(--t2);
  font-family:var(--sans);font-size:var(--fs-sm);font-weight:600;cursor:pointer;white-space:nowrap}
.fi-sel-chip:hover,.dh-pk-panel:hover{color:var(--t);border-color:var(--bs)}
.fi-sel-chip.active,.dh-pk-panel.active{background:var(--acd);border-color:var(--acb);color:var(--ac)}
.fi-sel-badge,.dh-pk-badge{font-family:var(--mono);font-size:var(--fs-xs);font-weight:500;
  background:var(--ac);color:var(--on-ac);border-radius:99px;padding:0 5px;line-height:15px}

/* Search. Appears only past FI_SEL_SEARCH_AT options — below that it is a
   box asking you to type the name of something already on screen. */
.fi-sel-search{width:100%;height:var(--ctl-h);padding:0 11px;background:var(--s2);border:1px solid var(--bm);
  border-radius:var(--rsm);font-family:var(--sans);font-size:var(--fs-body);color:var(--t);outline:none}
.fi-sel-search:focus{border-color:var(--acb);box-shadow:0 0 0 3px var(--acd)}

/* Two columns, and the gap BETWEEN them has to beat the gap inside a row or
   the columns stop reading as columns. A row is checkbox · dot · name ·
   figure with the name stretching, so the space inside one row is ~100px
   while the space between two was 14px — which put every count nearer the
   next column's name than its own. 30px, and a 2px right inset so the
   scrollbar lands on padding rather than on the right column's figures. */
.fi-sel-list,.dh-pk-tags{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:2px 30px;
  overflow-y:auto;padding-right:2px}
.fi-sel-list.one,.dh-sp-list{display:block;overflow-y:auto}
/* The `max-height:46vh` these carried is gone — it was a second scroll area
   inside the scrolling dialog body, the same fault as .fi-pick-list below, and
   46vh on a landscape phone is under 200px. The height now comes from the
   card: body → wrapper → rows, each handing its spare height down, so the
   chip bar and the search box stay put and only the rows move. See the ONE
   SCROLLER note beside .fi-pick-list. */
.fi-pick-body .fi-sel-list,.fi-pick-body .dh-pk-tags,.fi-pick-body .dh-sp-list{flex:1 1 auto;min-height:0}
/* Aligned with the rows below it, not with the grid edge: the rows carry 8px
   of their own padding, so a flush-left "Tick all" sat 8px to their left. */
.fi-sel-all,.dh-pk-all{grid-column:1/-1;justify-self:start;border:none;background:none;color:var(--ac);
  font-family:var(--sans);font-size:var(--fs-sm);font-weight:600;cursor:pointer;padding:2px 0 6px 8px}

/* ── the row, multi-select ─────────────────────────────────────────── */
.fi-sel-row,.dh-pk-tag{display:flex;align-items:center;gap:9px;padding:7px 8px;
  border-radius:var(--rsm);cursor:pointer}
.fi-sel-row:hover,.dh-pk-tag:hover{background:var(--s2)}
.fi-sel-row input,.dh-pk-tag input,.dh-sp-row input{width:15px;height:15px;accent-color:var(--ac);
  cursor:pointer;flex-shrink:0;margin:0}
.fi-sel-dot,.dh-pk-dot{width:9px;height:9px;border-radius:50%;flex-shrink:0}
.fi-sel-lb,.dh-pk-lb{flex:1;min-width:0;font-size:var(--fs-body);color:var(--t);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.fi-sel-n,.dh-pk-n{font-family:var(--mono);font-size:var(--fs-label);color:var(--t3);flex-shrink:0;
  min-width:2.2em;text-align:right;font-variant-numeric:tabular-nums}
/* An option nobody has used still has to be pickable — the filter above may
   be what is hiding it — but it should not read as a live option. */
.fi-sel-row.empty .fi-sel-lb,.dh-pk-tag.empty .dh-pk-lb{color:var(--t3)}
.fi-sel-row.empty .fi-sel-dot,.dh-pk-tag.empty .dh-pk-dot{opacity:.4}
.fi-sel-count,.dh-pk-count{flex:1 1 150px;min-width:0;margin-right:auto;font-size:var(--fs-sm);color:var(--t3);
  line-height:1.4}

/* ── A PICKER IS NOT A SENTENCE ────────────────────────────────────
   .fi-modal-box caps at 420px, which is right for a confirm — 420px is about
   sixty characters, and a dialog that asks one question should not run wider
   than a line anyone wants to read. These are the other kind: two columns of
   tick-rows, each ending in a figure. At 420 the grid had 378px to place two
   columns of "Individual Moment 43" in, and the counts went off the right
   edge behind a sideways scrollbar — the one thing a picker must not do,
   since the figure is the reason you opened it.

   :has() rather than a class on the box, because the box is built by
   _fiBuildModal (js/ui.js) for every dialog in the app and threading an
   option through it for this would touch every caller. Where :has() is not
   supported the rule is simply dropped and these fall back to 420px — which
   is why the minmax(0,1fr) above is the real fix and this is the comfort:
   the grid fits either way, this just stops it fitting by truncation. */
.settings-box.fi-modal-box:has(.fi-sel,.dh-pk,.dh-sp){max-width:min(620px,92vw)}

/* Insights' span picker: same row, four columns instead of a flex line. */
.dh-sp-row{display:grid;grid-template-columns:auto minmax(0,1fr) auto auto;align-items:center;gap:10px;
  padding:8px;border-radius:var(--rsm);cursor:pointer}
.dh-sp-row:hover{background:var(--s2)}
.dh-sp-nm{min-width:0;font-size:var(--fs-body);color:var(--t);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.dh-sp-dt{font-family:var(--mono);font-size:var(--fs-xs);color:var(--t3);white-space:nowrap}
.dh-sp-n{font-family:var(--mono);font-size:var(--fs-xs);color:var(--t3);min-width:2.4em;text-align:right}

/* ── the row, single-select (js/ui.js → fiPickList) ─────────────────
   Nine call sites: pick a presentation, a folder, a match, a footage
   source, a CSV column, a 2nd level tag.

   Built on the SAME four columns as .dh-sp-row above — marker, name, then
   two right-aligned mono figures — because that row is the one people
   already read in Insights, and the first version of this got it wrong in
   a way worth writing down: the name and its sub-line were stacked in a
   shrink-to-fit cell, so every date started at a different x and no two
   figures lined up. Figures only align if they are in their OWN column,
   right-aligned, at a fixed minimum width. That is what the columns below
   are for; do not fold them back into the text cell.

   The sub-line survives for the two pickers that carry prose rather than
   figures (the footage source, the CSV column). Rows with one are laid out
   from the top so the name still shares a baseline with its figures. */
/* ── ONE SCROLLER, NOT TWO ──────────────────────────────────
   The list used to carry `max-height:52vh;overflow-y:auto` INSIDE the already
   scrolling .settings-body, so every picker was two nested scroll areas in one
   card: the presentation menu on a phone was reported as exactly that — a list
   you could drag inside a sheet you could also drag, with rows cut off at the
   bottom of both. On a phone in landscape 52vh is 224px, which is where the
   inner one came from, and 100% of the complaints.

   The card is a column: header, body, footer. Inside the body the chips, the
   note and the search box are FURNITURE — they must stay put, because a search
   field that scrolls away is no use in the list it filters — and only the rows
   scroll. So the body stops scrolling and hands its spare height to the list.
   .fi-pick-body is put on by fiPickList (js/ui.js) rather than inferred with
   :has(), so this is deterministic on every browser that renders the app. */
.fi-modal-body.fi-pick-body{display:flex;flex-direction:column;gap:0;overflow:hidden}
.fi-pick-list{display:flex;flex-direction:column;gap:1px}
/* Cards need air between them or the borders read as one table. */
.fi-pick-list.cards{gap:8px}
.fi-pick-body>.fi-pick-list{flex:1 1 auto;min-height:0;overflow-y:auto;-webkit-overflow-scrolling:touch;overscroll-behavior:contain}
/* fiPickList's rows are a direct child of the body; fiCheckPick's and the two
   Insights pickers' sit one level down, inside .fi-sel / .dh-pk / .dh-sp —
   which is already a column, so it only needs telling to take the height and
   pass it on.

   min-height:0 overrides the 220px floor those three carry, and it has to: a
   flex item cannot shrink below its min-height, so on a landscape phone — body
   183-238px, floor 220 — the wrapper stayed 220px tall and the body started
   scrolling again to reach it. Measured, both ways round. The floor is not
   missed: with the chip bar and search above it, a short list still opens at
   about 200px, and the alternative was the two-scroller card on exactly the
   screens this whole change exists to fix. */
.fi-pick-body>.fi-sel,.fi-pick-body>.dh-pk,.fi-pick-body>.dh-sp{flex:1 1 auto;min-height:0}
/* The furniture must not be shrunk to make room for the rows. The chip bar and
   the search box are flex items in a column that is now height-constrained, and
   a flex item shrinks by default — which squashed the chip bar to half a chip,
   labels and all, before this. They are fixed-size; the list is the elastic one. */
.fi-pick-body>.fi-sel-bar,.fi-pick-body>.fi-pick-note,.fi-pick-body>.fi-sel-search,
.fi-pick-body .fi-sel>.fi-sel-bar,.fi-pick-body .fi-sel>.fi-sel-search,
.fi-pick-body .dh-pk>.dh-pk-panels,.fi-pick-body .dh-sp>.dh-sp-presets{flex-shrink:0}

/* ── A PHONE ON ITS SIDE ──────────────────────────────────────
   Making the rows the only scroller is only half a fix if there is no room
   left to scroll them IN. Landscape is 375-430px tall, and on a 375px screen
   the card's own furniture — 20px gutters top and bottom, a 50px header, a
   64px footer, then a chip bar wrapping to two rows and a search box — ate
   everything: measured at 667x375, the list came out 22 pixels tall. One row.
   Nobody picks a tag out of a 22px window.

   So on a short viewport the dialog gives its chrome back to the list: thinner
   gutters (the notch is still cleared, via max()), a tighter header and foot,
   and the chip bar becomes ONE row that scrolls sideways instead of wrapping
   to three. Nothing moves, nothing is hidden — the same controls, drawn to
   the height there actually is. */
@media(max-height:560px){
  .settings-overlay.fi-modal{padding:max(8px,var(--sat)) max(12px,var(--sar)) max(8px,var(--sab)) max(12px,var(--sal))}
  .fi-modal .settings-box{max-height:calc(var(--safeh) - 16px)}
  .fi-modal .settings-hdr{padding:9px 16px}
  .fi-modal .settings-hdr h2{font-size:15px}
  .fi-modal .fi-modal-foot{padding:9px 16px}
  .fi-modal-body.fi-pick-body{padding:6px 16px 10px}
  .fi-pick-body>.fi-sel,.fi-pick-body>.dh-pk,.fi-pick-body>.dh-sp{gap:8px}
  .fi-pick-body .fi-sel-bar,.fi-pick-body .dh-pk-panels,.fi-pick-body .dh-sp-presets{
    flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none;padding-bottom:8px}
  .fi-pick-body .fi-sel-bar::-webkit-scrollbar,
  .fi-pick-body .dh-pk-panels::-webkit-scrollbar,
  .fi-pick-body .dh-sp-presets::-webkit-scrollbar{display:none}
  .fi-pick-body .fi-sel-chip,.fi-pick-body .dh-pk-panel{height:27px;flex-shrink:0}
  .fi-pick-body .fi-sel-search{height:30px}
  .fi-pick-body .fi-pick-note{margin-bottom:6px}
  /* Two pixels a side off every row is another whole row on screen, and the
     targets are still over the touch floor. */
  .fi-pick-body .fi-sel-row,.fi-pick-body .dh-pk-tag,.fi-pick-body .dh-sp-row{padding:5px 8px}
  .fi-pick-body .fi-sel-all,.fi-pick-body .dh-pk-all{padding:0 0 4px 8px}
}
/* ── A PHONE ON ITS SIDE, WITH THE KEYBOARD UP ─────────────────
   Reported 2026-09-15 from a new club: landscape phone, "Create your first
   match", and "no place to put the name". The dialog is fine until the field
   is tapped. Then, on a browser whose keyboard shrinks the layout viewport,
   the screen is 110-200px tall — and the card's two fixed rows, a 63px header
   (the 44px × plus padding) and a 63px footer (44px buttons), took all of it.
   The body is the one part allowed to shrink, so the name field was cut to a
   36px sliver at 160px and to half a box, with Create gone, at 110px.
   Measured, headless, at those heights.
   Below 300px there is no room to pin anything, so the CARD becomes the one
   scroller instead of the body: nothing shrinks, the focused field is
   scrolled into view like any other, and Create is one flick (or the
   keyboard's own Enter) away. Still one scroller per dialog, just a different
   one. 300px is under an installed PWA on any landscape phone with the
   keyboard down (those are 320-430px, the block above); a small Android in
   the browser, under its URL bar, can dip below it, and at that height the
   card scrolling whole is the better trade there too. */
@media(max-height:300px){
  .fi-modal .settings-box{overflow-y:auto;-webkit-overflow-scrolling:touch;overscroll-behavior:contain}
  .fi-modal .settings-box>*{flex-shrink:0}
  .fi-modal .fi-modal-body{overflow:visible;padding:8px 16px 12px}
}
.fi-pick-row{display:grid;grid-template-columns:auto minmax(0,1fr) auto auto;align-items:center;
  gap:10px;width:100%;padding:8px 9px;border:1px solid transparent;border-radius:var(--rsm);
  background:none;color:var(--t);font-family:var(--sans);cursor:pointer;text-align:left}
.fi-pick-row:hover{background:var(--s2)}
.fi-pick-row:focus-visible{outline:none;border-color:var(--acb);box-shadow:0 0 0 3px var(--acd)}
/* The one already in force. Tinted rather than ticked: there is no choice to
   make about it, it is a statement of where you are. */
.fi-pick-row.current{background:var(--acd);border-color:var(--acb)}
.fi-pick-row.current .fi-pick-lb{color:var(--ac)}
/* No row in this list has a dot or an icon, so the marker column goes
   entirely rather than sitting there as an empty 16px inset. It is removed
   from the template AND from flow together — leaving the cell in place with
   zero width still pays for the column gap. */
.fi-pick-row.flat{grid-template-columns:minmax(0,1fr) auto auto}
.fi-pick-row.flat .fi-pick-ic{display:none}
/* ── THE CARD VARIANT (fiPickList opts.cards) ──
   Every option gets the edge the selected one already had. The default row
   is transparent until hovered, which is right for a menu and wrong for a
   list of things you are comparing: in the "Which club?" question only the
   club you were already in had a box, so the other clubs read as inert
   text above a button rather than as the other answers to the question. */
.fi-pick-row.card{border-color:var(--bm);background:var(--s2);padding:11px 13px;
  transition:border-color var(--dur),background var(--dur)}
.fi-pick-row.card:hover{border-color:var(--bs);background:var(--s2)}
.fi-pick-row.card.current{background:var(--acd);border-color:var(--acb)}
/* A card is already tall enough to breathe; the sub-line takes the extra
   leading instead of the row taking more height. */
.fi-pick-row.card .fi-pick-sub{margin-top:3px}
/* No card list uses the second figure column, and a 4ch gutter reserved for
   nothing pushed the badge in off the card's edge. Collapsed only where it
   is empty, so a card list that one day carries a count still lines up. */
.fi-pick-row.card .fi-pick-n.count:empty{min-width:0}
.fi-pick-row.has-sub{align-items:start}
.fi-pick-row.has-sub .fi-pick-dot,
.fi-pick-row.has-sub .fi-pick-ic{margin-top:3px}
.fi-pick-row.has-sub .fi-pick-n{padding-top:1px}
.fi-pick-dot{width:9px;height:9px;border-radius:50%;flex-shrink:0}
.fi-pick-ic{display:flex;width:16px;height:16px;color:var(--t3);flex-shrink:0}
.fi-pick-ic svg{width:16px;height:16px}
.fi-pick-tx{min-width:0}
/* THE NAME IS THE ANSWER, and it is the one cell allowed a second line.
   It was nowrap + ellipsis, which is right for a cell that merely labels a
   row and wrong for the cell the whole list exists to let you read: on a
   phone the "Which club?" picker rendered "Manchester United Academy" as
   "Manchest…" while the plan column beside it sat at a fixed 14em holding
   the word "Club". Two lines, then ellipsis — a bounded wrap, so one
   absurdly long name still cannot push the rows below it off the screen. */
.fi-pick-lb{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;
  font-size:var(--fs-body);font-weight:600;
  overflow:hidden;white-space:normal;overflow-wrap:anywhere}
/* The figure columns, and the one thing that actually makes them columns.
   Each ROW is its own grid — the list is a flex column of buttons, not one
   big grid — so a column only lines up across rows if every cell in it is
   the same width. Right-aligning is not enough: it aligns the right edge
   and leaves the left edge wandering by however much the content differs,
   which is the ragged-date complaint in its quieter form.

   `ch` is the fix and it is exact: in a monospace face 1ch is one character,
   so 11ch always holds dd/mm/yyyy and never has to grow. Widen these if a
   longer format ever appears — do NOT switch them to em, which is a guess
   about the glyph, or the wobble comes straight back. */
.fi-pick-n{font-family:var(--mono);font-size:var(--fs-xs);color:var(--t3);
  white-space:nowrap;text-align:right}
.fi-pick-n.date{min-width:11ch}
.fi-pick-n.count{min-width:4ch}
/* ── A PILL, NOT A COLUMN ──
   For a meta that CLASSIFIES the row — the plan a club is on. Set as a
   14em left-aligned middle column (.text below) the word "Starter" landed
   10px to the right of the club's name with nothing between them and no
   edge of its own, so it read as part of the name and the whole dialog
   looked like a paragraph that had lost its formatting. A pill on the
   right edge has a shape, and where a row is only two facts wide that
   shape is all the separation the row needs.

   Outfit, because a plan name is a word — the type rule above .fi-g-mh
   applies here too, and this cell inherits DM Mono from .fi-pick-n. */
/* A width, so the pills share a right edge. Without one the auto column
   shrinks to each row's own word and "Basic" hung 14px inside "Starter" —
   three badges, three right edges, which is the ragged-column fault this
   file already fixes twice above. 6.5em holds the longest plan name. */
.fi-pick-n.badge{font-family:var(--sans);text-align:right;min-width:6.5em}
.fi-pick-n.badge>b{display:inline-block;padding:2px 9px;border-radius:999px;
  background:var(--s3);border:1px solid var(--b);color:var(--t2);
  font-size:var(--fs-label);font-weight:600;letter-spacing:.01em}
.fi-pick-row.current .fi-pick-n.badge>b{background:var(--acd);border-color:var(--acb);color:var(--ac)}
/* A middle column carrying words rather than a figure — which match a reel
   came from, say. Sans, because it is a name. Fixed width and LEFT-aligned,
   not right: names have no common right edge to hang from the way a date
   does, so left is what makes them a column, and the width keeps the one
   long entry from setting the width for everybody. */
.fi-pick-n.text{font-family:var(--sans);font-size:var(--fs-sm);text-align:left;
  width:14em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* …and on a phone the column loses its argument. 14em is most of a 360px
   row, so holding it costs the name the space it needs to be read, and a
   column that aligns perfectly beside an unreadable name has won nothing.
   Below this width the cell shrinks to its content and the name takes the
   rest — the alignment goes, the answer stays. */
@media (max-width:560px){
  .fi-pick-n.text{width:auto;max-width:8em}
}
/* The "＋ New folder" row. Dashed, so it reads as making something rather
   than choosing something, and pinned above the options. */
/* The destructive row in a menu. Colour only — same box, same focus ring, and
   whatever it calls still asks before it acts. */
.fi-pick-row.danger .fi-pick-lb{color:var(--rd)}
.fi-pick-row.danger .fi-pick-ic{color:var(--rd)}
.fi-pick-row.danger:hover{background:color-mix(in srgb, var(--rd) 12%, transparent)}
.fi-pick-extra{border-style:dashed;border-color:var(--acb);background:var(--acd);color:var(--ac);
  margin-bottom:6px}
.fi-pick-extra .fi-pick-lb{color:var(--ac)}
/* The sub-line under a picker option. This is PROSE — whole sentences of
   instruction ("A file already on this device — it saves to this match so you
   never load it twice") — and DM Mono is for figures only (see the type scale
   above). Set in mono it read as terminal output dropped into the middle of a
   dialog, which is exactly what it looked like.

   --fs-sm, not --fs-xs: 10.5px is calibrated for uppercase micro-labels, and a
   sentence that wraps to two lines needs the description size and a line-height
   that lets it breathe. */
.fi-pick-sub{display:block;margin-top:2px;font-size:var(--fs-sm);color:var(--t3);
  line-height:1.5;white-space:normal}
/* "Here is what you have now" above the options, for pickers that change an
   existing setting. Also prose ("A video file on this device · 4.3 GB"), so it
   matches the sub-line. It can carry a URL, which is why the wrap stays — but a
   URL reads perfectly well in Outfit, and mono for the whole line meant the
   sentence wrapped around it paid for the one case that needed it. */
/* ── fiForm: two or three short answers, one column of labels ── */
.fi-form{display:flex;flex-direction:column;gap:10px}
.fi-form-row{display:flex;align-items:center;gap:12px}
.fi-form-l{flex:0 0 74px;font-size:var(--fs-sm);font-weight:600;color:var(--t2)}
.fi-form-in{flex:1;min-width:0;width:auto;margin:0}
/* --fw lets a field say it holds a short value (a scoreline, not a name) so
   two of them sit side by side instead of each spanning the dialog. */
.fi-form-row[style*="--fw"] .fi-form-in{flex:0 0 var(--fw)}
.fi-pick-note{font-size:var(--fs-sm);color:var(--t3);line-height:1.5;
  padding:7px 9px;background:var(--s1);border:1px solid var(--b);border-radius:var(--rsm);
  margin-bottom:6px;overflow-wrap:anywhere}
/* .meta — an ATTRIBUTION, not a callout. The boxed default is right for a
   note that warns or explains; a line saying who made the thing you are
   looking at is a caption, and in a box on its own it reads as an empty
   text field the dialog forgot to fill in. Same words, no chrome, and the
   rule under it does the separating instead. */
.fi-pick-note.meta{padding:1px 2px 8px;background:none;border:0;border-bottom:1px solid var(--b);
  border-radius:0;margin-bottom:8px;font-family:var(--sans);color:var(--t2)}
/* ══════════════════════════════════════════════════════════════════
   THE TINTED COUNT ROW
   ══════════════════════════════════════════════════════════════════
   Insights' 2nd level profile card ("TAGGED AS") states a set of counts
   more quietly than a grid does: a label, a mono figure, and a wash of the
   action's own colour behind them whose WIDTH is that row's share of the
   biggest. No borders, no axis, no bar to read — the number is the reading
   and the tint only ranks the rows at a glance.

   Promoted out of datahub.css so Match stats leads with the same thing. The
   `.dh-*` names are in the selector lists; add rules here, never back there.

   Two columns by default (auto-fill at 180px), one on a phone. */
.fi-acts,.dh-acts{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:4px 14px}
.fi-act,.dh-act{position:relative;display:flex;align-items:center;gap:10px;padding:8px 10px;
  border-radius:var(--rsm);cursor:pointer;overflow:hidden;width:100%;
  border:0;background:none;font-family:var(--sans);text-align:left}
.fi-act:hover,.dh-act:hover{background:var(--s2)}
/* B-06 — the fill needs a track, or it isn't a bar.
   The tint WAS the whole row background, with nothing behind it, so a row
   with 5 tags next to one with 40 did not read as a short bar: it read as a
   narrower CARD in a grid where every other element is full width, and the
   eye compared box sizes instead of lengths. A faint full-width track in the
   same hue gives the fill something to be short against, and costs one span.
   6% is deliberately well below the 14% fill — present enough to bound the row,
   quiet enough that a full row still looks full. */
.fi-act-trk,.dh-act-trk{position:absolute;left:0;top:0;bottom:0;width:100%;opacity:.06;border-radius:var(--rsm)}
.fi-act-bar,.dh-act-bar{position:absolute;left:0;top:0;bottom:0;opacity:.14;border-radius:var(--rsm)}
.fi-act-lb,.dh-act-lb{position:relative;flex:1;min-width:0;font-size:var(--fs-body);color:var(--t);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.fi-act:hover .fi-act-lb,.dh-act:hover .dh-act-lb{color:var(--ac)}
.fi-act-n,.dh-act-n{position:relative;font-family:var(--mono);font-size:var(--fs-body);color:var(--t2)}
/* Card header: name, a line of facts under it, an action and a close. */
.fi-card-h,.dh-card-h{display:flex;align-items:flex-start;gap:10px;margin-bottom:14px}
.fi-card-tx,.dh-card-tx{flex:1;min-width:0}
.fi-card-t,.dh-card-t{font-size:var(--fs-h2);font-weight:700;color:var(--t);line-height:1.2;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.fi-card-s,.dh-card-s{font-size:var(--fs-sm);color:var(--t3);margin-top:3px}
.fi-card-x,.dh-card-x{flex-shrink:0;width:26px;height:26px;border:none;background:none;color:var(--t3);
  font-size:19px;line-height:1;cursor:pointer;border-radius:var(--rsm)}
.fi-card-x:hover,.dh-card-x:hover{color:var(--t);background:var(--s3)}
/* The small-caps section label, with an optional quiet hint beside it. */
.fi-sub2,.dh-sub2{font-size:var(--fs-label);font-weight:700;text-transform:uppercase;letter-spacing:var(--ls-label);
  color:var(--t3);margin-bottom:10px}
.fi-sub,.dh-sub{font-weight:400;text-transform:none;letter-spacing:0;color:var(--t3);font-size:var(--fs-label)}
.fi-sub2-sp,.dh-sub2-sp{margin-top:20px}
@media(max-width:560px){
  .fi-acts,.dh-acts{grid-template-columns:1fr}
}

/* ══════════════════════════════════════════════════════════════════
   FIGURES — the KPI strip and the heat grid
   ══════════════════════════════════════════════════════════════════
   Promoted out of datahub.css for the same reason the picker was: these
   are how this app states a number, and Insights was the only screen
   using them. Match stats and the match workspace now read the same way,
   so a coach learns one visual grammar instead of three.

   The `.dh-*` names are in the selector lists on purpose — Insights keeps
   its markup and there is one copy of the rules. Do NOT re-add a .dh-kpi*
   or .dh-g-* declaration to datahub.css.

   The grammar, in one line: figures are mono, labels are small-caps sans,
   a count sits on a heat patch whose opacity is its share of the row's
   maximum, and a direction of travel is an arrow plus the two numbers —
   never a percentage, which reads as precision the sample cannot carry. */

/* ── KPI strip ── */
.fi-kpis,.dh-kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(124px,1fr));gap:10px;margin-bottom:18px}
/* Same treatment as Home's usage tiles, and for the same reason: the strip is
   auto-fit, so the last row can hold fewer tiles than the first and a wrapped
   two-line label makes one tile taller than its neighbours. Flex-centred on
   both axes keeps every figure on the same optical line whatever the row is
   holding — it is one measurement, so it has to read as one row. */
.fi-kpi,.dh-kpi{display:flex;flex-direction:column;align-items:center;justify-content:center;
  background:var(--s1);border:1px solid var(--b);border-radius:var(--r);padding:14px 12px;text-align:center}
.fi-kpi>*,.dh-kpi>*{width:100%}
/* Outfit + tabular-nums, per decision 8 — see the note in css/tagger.css. */
.fi-kpi-n,.dh-kpi-n{font-family:var(--sans);font-size:clamp(19px,1.1vw + 14px,24px);font-weight:700;
  color:var(--ac);line-height:1;letter-spacing:-.6px;font-variant-numeric:tabular-nums}
/* The unit rides small and sans beside the mono figure — same treatment as
   Home's usage tiles, so "1h 40m" and "12 min" share one vertical rhythm. */
.fi-kpi-u,.dh-kpi-u{font-family:var(--sans);font-size:var(--fs-sm);font-weight:600;color:var(--t2);margin-left:2px}
/* WRAPS, it does not truncate. The tiles are auto-fit at a 92px minimum on a
   phone, and uppercase with letter-spacing "TAGS PER MATCH" has never once
   fitted on one line — every handset tested showed "TAGS PER M…", "ACTION
   TYP…", "2ND LEVEL T…". Worst on the LARGEST phone, because a wider viewport
   fits more tiles per row rather than making each one wider. A figure whose
   label has been cut off is not a figure. The two-line min-height keeps the
   row of tiles level whether a label wraps or not. */
/* CENTRED INSIDE THE RESERVE, not parked at the top of it. The min-height
   above holds two lines so a row of tiles stays level whether a label wraps or
   not — but a label that does NOT wrap was drawn at the top of that box, so
   half a line of empty space hung below it and the figure+label pair sat
   visibly high in the tile. Every tile was doing it, which is why the whole
   strip read as "not vertically middle aligned" rather than as one odd tile.
   A flex box centres the one line in the two-line reserve and the group lands
   where justify-content:center on the tile always meant to put it. */
.fi-kpi-l,.dh-kpi-l{font-size:var(--fs-xs);color:var(--t2);margin-top:6px;text-transform:uppercase;letter-spacing:var(--ls-label);
  line-height:1.25;min-height:2.5em;overflow-wrap:anywhere;
  display:flex;align-items:center;justify-content:center;text-align:center}
/* Tighter tiles for a modal or a sidebar, where the strip is a supporting
   fact rather than the headline it is on the Insights page. */
.fi-kpis.sm{grid-template-columns:repeat(auto-fit,minmax(84px,1fr));gap:8px;margin-bottom:14px}
.fi-kpis.sm .fi-kpi{padding:10px 8px;border-radius:var(--rsm)}
.fi-kpis.sm .fi-kpi-n{font-size:18px}
.fi-kpis.sm .fi-kpi-l{font-size:9.5px;margin-top:4px;min-height:2.4em}

/* ── the heat grid ── */
.fi-grid-wrap,.dh-g-wrap{overflow-x:auto;-webkit-overflow-scrolling:touch}
.fi-grid,.dh-grid{border-collapse:separate;border-spacing:0;font-size:var(--fs-sm);width:100%}
/* ══════════════════════════════════════════════════════════════════
   EQUAL MATCH COLUMNS  (the SEASON grid only — .dh-*)
   ══════════════════════════════════════════════════════════════════
   A heat grid is read across a row: "we built up less against Rovers than
   against City". Auto table layout sized each column to its own heading, so
   a five-letter opponent got a narrow column and a twelve-letter one got a
   wide one, and two identical figures sat on visibly different patches. The
   comparison the grid exists to make was being made against the length of a
   club's name.

   `table-layout:fixed` is what fixes it, and it needs a width for every
   column that is NOT meant to flex — that is the <colgroup> js/datahub.js
   emits. Columns with no width divide the remainder equally between
   themselves, which is exactly "available width ÷ number of matches".

   THE TWO FLOORS, and why min-width carries both:
     --dh-k  = matches / 10. At ten or fewer it is 1, the table is 100% wide
               and nothing scrolls. At fourteen it is 1.4, so the table is
               1.4× the free width — ten columns fill the screen at full
               width and the other four are scrolled to. A percentage inside
               calc() in min-width resolves against the wrapper, which is
               what makes this expressible in CSS at all.
     --dh-n  = the match count, against a per-column minimum. This is the
               narrow-screen floor: a portrait phone runs out of room at
               five columns, long before ten is in question, and without it
               fixed layout would happily draw a 22px column.
   max() takes whichever floor bites first. Both are only ever floors — the
   `width:100%` above still wins whenever neither applies.

   .fi-grid (Match Stats) is deliberately NOT included: its columns are
   PERIODS, there are two to four of them, and they have never been ragged. */
/* --dh-cfix is the sum of EVERY named column — the action column plus All,
   /match and Lately — not just the sticky one on the left. It was the action
   column alone at first, which under-counted the reserved width by 228px and
   put five match columns on a phone at 15px each: narrow enough that a
   two-digit figure did not fit, and no scrollbar, because by the table's own
   arithmetic it still fitted. Keep this in step with the three col widths
   below; they are the only four numbers it is made of. */
.dh-g-wrap{--dh-cfix:418px;--dh-cmin:56px;--dh-n:1;--dh-k:1}
.dh-grid{table-layout:fixed;
  min-width:max(calc(var(--dh-cfix) + var(--dh-n)*var(--dh-cmin)),
                calc(var(--dh-cfix) + (100% - var(--dh-cfix))*var(--dh-k)))}
/* The named columns state their width; .dh-c-m states none, on purpose. */
.dh-grid col.dh-c-a{width:190px}
.dh-grid col.dh-c-n{width:62px}
.dh-grid col.dh-c-t{width:104px}
/* Fixed layout stops a cell widening for its content, so the one cell whose
   content genuinely varies has to be told it may wrap. An action name is the
   answer to "which row am I on" and must never be clipped to nothing. */
.dh-grid .dh-g-a{white-space:normal;overflow-wrap:break-word;word-break:normal}
.fi-grid th,.fi-grid td,.dh-grid th,.dh-grid td{padding:0;border-bottom:1px solid var(--b)}
.fi-grid tbody tr:hover td,.fi-grid tbody tr:hover th,
.dh-grid tbody tr:hover td,.dh-grid tbody tr:hover th{background:var(--s2)}
.fi-g-a,.dh-g-a{position:sticky;left:0;z-index:2;background:var(--s1);text-align:left;font-weight:600;color:var(--t);
  padding:8px 14px 8px 2px!important;white-space:nowrap;max-width:190px;overflow:hidden;text-overflow:ellipsis;
  border-right:1px solid var(--b)}
.fi-grid tbody tr:hover .fi-g-a,.dh-grid tbody tr:hover .dh-g-a{background:var(--s2)}
.fi-g-an,.dh-g-an{border-bottom:1px dotted transparent}
th.fi-g-a[data-labels],th.dh-g-a[data-labels]{cursor:pointer}
th.fi-g-a[data-labels]:hover .fi-g-an,
th.dh-g-a[data-labels]:hover .dh-g-an{color:var(--ac);border-bottom-color:var(--acb)}
/* ── THE TYPE RULE, STATED ONCE FOR BOTH GRIDS ──
   Season grid, match grid and the Match-stats tables had drifted into a mix
   of faces: period names ("1st half") and column labels ("All", "/period")
   were set in DM Mono, which is for FIGURES, while the figures beside them
   were the same size in the same face at a different weight.

     DM Mono  counts, timecodes, dates, minute marks     -- a figure
     Outfit   everything that is a word                  -- a label

   And a <th> is font-weight:700 in the UA stylesheet, which is above DM
   Mono's ceiling (css/fonts.css: the family stops at 500, so 700 is
   synthesised by smearing the 500 -- at 9.5px that is what "a different
   font" looked like). Header cells are therefore Outfit 600, and the two
   mono spans inside a header state their own weight. */
.fi-grid th,.dh-grid th{font-family:var(--sans);font-weight:600}
.fi-g-corner,.dh-g-corner{color:var(--t3);font-size:var(--fs-xs);font-weight:700;text-transform:uppercase;letter-spacing:var(--ls-label)}
/* vertical-align:top, so every column's FIRST line starts on one baseline.
   Middled, a head with a W/L chip sat higher than one without and a
   date-only head (no opponent recorded) sat lower still — three baselines
   across one row of headers. */
.fi-g-mh,.dh-g-mh{cursor:pointer;vertical-align:top;padding:6px 5px!important;min-width:52px}
.fi-g-mh:hover .fi-g-d,.dh-g-mh:hover .dh-g-d,
.fi-g-mh:hover .fi-g-op,.dh-g-mh:hover .dh-g-op{color:var(--ac)}
/* The WORD slot in a column head: the opponent on the season grid, the
   period name ("1st half") on the match grid. Outfit, because it is a word.
   C-11 — a season grid is read as "we stopped building up against Rovers",
   and a date carries none of that. */
.fi-g-op,.dh-g-op{display:block;font-family:var(--sans);font-size:var(--fs-label);font-weight:600;color:var(--t2);
  white-space:nowrap;max-width:13ch;overflow:hidden;text-overflow:ellipsis}
/* The FIGURE slot in a column head: the date under an opponent, the minute
   mark on the timeline. Mono at 500 — the family's ceiling. */
.fi-g-d,.dh-g-d{display:block;font-family:var(--mono);font-weight:500;font-size:9.5px;color:var(--t3);white-space:nowrap;margin-top:1px}
/* A match with no opponent recorded shows its date ALONE, so the date takes
   the weight the missing name would have had rather than sitting small and
   orphaned at the top of an otherwise empty column head. */
.fi-g-d.lone,.dh-g-d.lone{font-size:var(--fs-label);color:var(--t2);margin-top:0}
/* W / L / D is a letter, not a figure. */
.fi-g-r,.dh-g-r{display:inline-block;margin-top:3px;font-family:var(--sans);font-size:9.5px;font-weight:700;
  letter-spacing:.04em;padding:0 5px;border-radius:3px;background:var(--s3);color:var(--t3)}
.fi-g-r.W,.dh-g-r.W{background:var(--acd);color:var(--ac)}
.fi-g-r.L,.dh-g-r.L{background:var(--rdd);color:var(--rd)}
/* A cell is a value on a heat patch. The patch is its own layer so the
   figure keeps full contrast at every intensity. */
.fi-g-c,.dh-g-c{position:relative;text-align:center;min-width:38px;padding:7px 4px!important;cursor:pointer}
.fi-g-c.zero,.dh-g-c.zero{cursor:default}
.fi-g-f,.dh-g-f{position:absolute;inset:3px;border-radius:5px;background:var(--ac);pointer-events:none}
.fi-g-v,.dh-g-v{position:relative;font-family:var(--mono);font-size:var(--fs-sm);color:var(--t)}
.fi-g-c.zero .fi-g-v,.dh-g-c.zero .dh-g-v{color:var(--t3);opacity:.5}
.fi-g-c:not(.zero):hover .fi-g-f,.dh-g-c:not(.zero):hover .dh-g-f{outline:1.5px solid var(--ac);outline-offset:-1px}
.fi-g-n,.dh-g-n{text-align:right;font-family:var(--mono);font-size:var(--fs-sm);color:var(--t2);padding:7px 8px!important;white-space:nowrap}
/* "All", "/period", "Lately", "Shift" are labels, not figures — so the
   header cells of the numeric columns take the word face, and only the
   cells under them stay mono. */
.fi-grid thead .fi-g-n,.dh-grid thead .dh-g-n,
.fi-grid thead .fi-g-tc,.dh-grid thead .dh-g-tc{font-family:var(--sans);font-size:var(--fs-label);font-weight:600;color:var(--t3)}
.fi-g-tc,.dh-g-tc{text-align:right;padding:7px 2px 7px 8px!important;white-space:nowrap}
.fi-g-t,.dh-g-t{font-family:var(--mono);font-weight:500;font-size:var(--fs-label);color:var(--t3)}
.fi-g-t.up,.dh-g-t.up{color:var(--ac)}
.fi-g-t.down,.dh-g-t.down{color:var(--am)}
.fi-grid tfoot th,.fi-grid tfoot td,
.dh-grid tfoot th,.dh-grid tfoot td{border-bottom:none;border-top:1px solid var(--bm);color:var(--t2);padding-top:9px!important}
.fi-grid tfoot .fi-g-a,.dh-grid tfoot .dh-g-a{font-family:var(--sans);font-size:var(--fs-label);text-transform:uppercase;letter-spacing:var(--ls-label);color:var(--t3);font-weight:700}
.fi-dot,.dh-dot{display:inline-block;width:9px;height:9px;border-radius:50%;margin-right:8px;vertical-align:middle}
/* A caption that says how to read the grid, on the section heading's line. */
.fi-g-legend{font-size:var(--fs-label);font-weight:400;color:var(--t3);text-transform:none;letter-spacing:0}

/* Single-column on a phone: two columns of tick-rows at 320px puts two
   14px labels side by side, and both truncate to nothing. And the KPI tiles
   go tighter, because five of them at 124px wrap to three rows. */
@media(max-width:560px){
  .fi-sel-list,.dh-pk-tags{grid-template-columns:minmax(0,1fr)}
  .fi-kpis,.dh-kpis{grid-template-columns:repeat(auto-fit,minmax(92px,1fr));gap:8px;margin-bottom:14px}
  .fi-kpi,.dh-kpi{padding:10px 8px}
  .fi-kpi-n,.dh-kpi-n{font-size:19px}
  .fi-kpi-l,.dh-kpi-l{font-size:9.5px;margin-top:4px;min-height:2.4em}

  /* ── THE ACTION COLUMN GIVES UP ITS WIDTH ──
     "Cannot even see 3 games because of the size." 190px of sticky first
     column out of a 390px screen leaves ~160px for the grid itself — two
     match columns and the edge of a third, before the All / per-match /
     Lately columns are even reached. And the column was that wide for a
     reason that does not survive a phone: `white-space:nowrap`, so it sized
     itself to the longest action name in the whole season.

     So it wraps instead of running on. A name is allowed as many lines as it
     needs at 108px — "Aggressive Movement" takes two — which costs a little
     row height and buys back 80px of grid on every row. Reading an action
     ACROSS the season is what this table is for, and you cannot do that
     through two matches. The cells and heads tighten with it. */
  /* A FIXED width, not a max-width, and break-word rather than anywhere.
     `max-width` is only a ceiling to a table cell: with `overflow-wrap:anywhere`
     the column's MIN-content width becomes one character, auto table layout
     hands the width to the numeric columns instead, and the first attempt at
     this rule produced a 24px column with every action name spelled downwards
     one letter per line. Pinning all three of width/min/max takes the column
     out of the auto-layout negotiation, and break-word only splits a word that
     genuinely cannot fit — so "Opp Ball Lost" takes two lines and nothing is
     ever set vertically. */
  /* The colgroup widths and the floor come down with everything else, so a
     portrait phone gets the same equal-columns rule at phone dimensions —
     the number of columns that fit simply falls out of the width available,
     which is why landscape shows more without a second rule. */
  .dh-g-wrap{--dh-cfix:284px;--dh-cmin:46px}   /* 104 + 46 + 46 + 88; the floor matches .dh-g-mh min-width */
  .dh-grid col.dh-c-a{width:104px}
  .dh-grid col.dh-c-n{width:46px}
  .dh-grid col.dh-c-t{width:88px}
  .fi-g-a,.dh-g-a{width:104px;min-width:104px;max-width:104px;
    font-size:var(--fs-label);line-height:1.25;padding:7px 8px 7px 2px!important;
    white-space:normal;overflow:visible;text-overflow:clip;
    overflow-wrap:break-word;word-break:normal;vertical-align:middle}
  .fi-g-corner,.dh-g-corner{font-size:9.5px}
  .fi-dot,.dh-dot{width:7px;height:7px;margin-right:6px}
  .fi-g-mh,.dh-g-mh{min-width:46px;padding:6px 4px!important}
  .fi-g-op,.dh-g-op{max-width:8ch;font-size:var(--fs-xs)}
  .fi-g-d,.dh-g-d{font-size:9px}
  .fi-g-c,.dh-g-c{min-width:34px;padding:6px 3px!important}
  .fi-g-v,.dh-g-v{font-size:var(--fs-label)}
  .fi-g-n,.dh-g-n{font-size:var(--fs-label);padding:6px 5px!important}
  .fi-g-tc,.dh-g-tc{padding:6px 2px 6px 5px!important}
  .fi-grid tfoot .fi-g-a,.dh-grid tfoot .dh-g-a{font-size:var(--fs-xs)}
}

/* ── CAMERA & MIC HELP (js/tagger.js → fiCameraHelp) ──
   Taller than the shared 420px modal because it carries two sets of steps,
   and the steps have to be readable at a ground with the phone in one hand. */
.fi-modal-box.cam-help{max-width:480px}
.cam-help-body{max-height:min(62vh,560px);overflow-y:auto}
.chelp-lead{font-size:var(--fs-body);color:var(--t2);line-height:1.6;margin:0 0 14px}
.chelp-sec{border:1px solid var(--b);border-radius:var(--rsm);padding:12px 14px;margin-bottom:10px;background:var(--s2)}
.chelp-h{display:flex;align-items:center;gap:8px;font-size:var(--fs-sm);font-weight:700;color:var(--t);margin-bottom:8px}
.chelp-h svg{width:16px;height:16px;flex-shrink:0;color:var(--ac)}
.chelp-sec ol{margin:0;padding-left:18px}
.chelp-sec li{font-size:var(--fs-sm);color:var(--t2);line-height:1.65;margin-bottom:5px}
.chelp-sec li:last-child{margin-bottom:0}
.chelp-sec b{color:var(--t);font-weight:600}
.chelp-note{font-size:var(--fs-sm);color:var(--t3);line-height:1.6;margin:12px 0 0;padding-top:12px;border-top:1px solid var(--b)}
.chelp-warn{display:flex;gap:9px;align-items:flex-start;background:var(--acd);border:1px solid var(--acb);
  border-radius:var(--rsm);padding:10px 12px;margin:0 0 14px;font-size:var(--fs-sm);color:var(--t2);line-height:1.55}
.chelp-warn svg{width:15px;height:15px;flex-shrink:0;margin-top:2px;color:var(--ac)}

/* ── TOAST ── */
/* Same as .undo-bar above: hidden means hidden, including from focus. */
.toast{position:fixed;left:50%;bottom:22px;transform:translateX(-50%) translateY(20px);z-index:700;max-width:min(calc(100vw - 32px),34rem);padding:11px 18px;background:var(--s3);border:1px solid var(--bs);border-radius:22px;box-shadow:var(--shadowlg);font-size:var(--fs-sm);font-weight:600;color:var(--t);opacity:0;visibility:hidden;pointer-events:none;transition:opacity var(--dur-slow),transform var(--dur-slow),visibility 0s linear var(--dur-slow);display:inline-flex;align-items:center;gap:9px;/* A TOAST MUST BE READABLE TO THE END. This was nowrap + ellipsis, which silently truncated every message longer than one line - including the orientation warning, whose whole value is the sentence it was cutting off. Radius 22px reads as the same pill on a single line (half of a ~40px row) and as a rounded card when it wraps. */white-space:normal;overflow-wrap:anywhere}
.toast::before{content:'';width:7px;height:7px;border-radius:50%;background:var(--ac);flex-shrink:0}
.toast.show{opacity:1;visibility:visible;transform:translateX(-50%) translateY(0);transition:opacity var(--dur-slow),transform var(--dur-slow),visibility 0s}

/* ── MATCH WORKSPACE: progress + grouped sections (first-run IA pass) ──
   The workspace used to be one flat grid of every tool, which told a coach
   nothing about where the match had got to or what to do next. */
.mw-steps{display:flex;gap:8px;flex-wrap:wrap;margin:0 0 22px}
.mw-step{flex:1 1 170px;display:flex;align-items:center;gap:10px;padding:11px 13px;background:var(--s1);border:1px solid var(--b);border-radius:var(--r)}
.mw-step-n{width:24px;height:24px;flex-shrink:0;border-radius:50%;display:flex;align-items:center;justify-content:center;background:var(--s3);border:1px solid var(--bm);color:var(--t3);font-family:var(--mono);font-size:var(--fs-label);font-weight:500}
.mw-step-n svg{width:13px;height:13px}
.mw-step.done{border-color:var(--acb);background:var(--acd)}
.mw-step.done .mw-step-n{background:var(--ac);border-color:var(--ac);color:var(--on-ac)}
.mw-step.done .mw-step-nm{color:var(--t)}
.mw-step-tx{min-width:0}
.mw-step-nm{display:block;font-size:var(--fs-sm);font-weight:600;color:var(--t2)}
.mw-step-vl{display:block;font-size:var(--fs-label);color:var(--t3);margin-top:1px}
.mw-sec{margin-bottom:24px}
.mw-sec-h{font-size:var(--fs-xs);font-weight:700;letter-spacing:var(--ls-label);text-transform:uppercase;color:var(--t3);margin-bottom:10px}
/* The single "do this next" card carries the accent so it reads as the one
   thing to press on this screen. */
#mw-next{grid-template-columns:1fr}   /* one card, full width — it IS the point of the screen */
.mw-card-primary{border-color:var(--acb);background:var(--acd);padding:20px}
.mw-card-primary .home-card-nm{font-size:15px}
.mw-card-primary:hover{background:var(--acd)}
.home-card.mw-card-off{opacity:.45;cursor:not-allowed}
.home-card.mw-card-off:hover{transform:none;border-color:var(--b);background:var(--s1);box-shadow:none}
.home-card.mw-card-off:hover .home-card-ic{background:var(--acd);border-color:var(--acb)}
.home-card.mw-card-off:hover .home-card-ic svg{color:var(--ac)}
.mw-share-note{margin-top:10px;padding:10px 12px;background:var(--s1);border:1px solid var(--b);border-radius:var(--rsm);font-size:var(--fs-label);line-height:1.6;color:var(--t2)}

/* "Recording for <match>" banner on the Record chooser. */
.rec-forwrap{display:flex;align-items:center;gap:9px;margin:0 0 16px;padding:10px 13px;background:var(--acd);border:1px solid var(--acb);border-radius:var(--r);font-size:var(--fs-sm);color:var(--t2)}
.rec-forwrap svg{width:17px;height:17px;flex-shrink:0;color:var(--ac)}
.rec-forwrap b{color:var(--t)}
.rec-for-clear{margin-left:auto;flex-shrink:0;background:none;border:1px solid var(--bm);border-radius:var(--rsm);padding:4px 10px;font-family:var(--sans);font-size:var(--fs-label);font-weight:600;color:var(--t2);cursor:pointer}
.rec-for-clear:hover{color:var(--t);border-color:var(--bs)}


/* ══════════════════════════════════════════════════════════════════
   D-08 · KEYBOARD SHORTCUT CHIPS ARE FOR KEYBOARDS
   ══════════════════════════════════════════════════════════════════
   Every tag button carried a numbered key chip and the Tag Panels preview
   was headed KEY 1 … KEY 0 — on a phone, which has no keys. In the live
   Stand Tagger it was worse than noise: the Build Up card showed a "1" as
   its shortcut and a "1" as its tag COUNT, side by side, two different
   meanings in the same place.

   `(hover:none) and (pointer:coarse)` is the touch signal — it catches a
   phone and a bare tablet, and it is right about both. What it cannot know
   is that a tablet has since been docked to a keyboard case, so js/kbd.js
   puts `fi-kbd` on <html> the first time a real key is pressed and the
   chips come back for good. Both directions, and neither guesses.

   TWO CHIPS, TWO CLASS NAMES. The tag buttons carry `.tsc`; the two-level
   chip grid (js/twolevel.js) and the tag board built out of it draw the same
   badge as `.kk`. Only the first was listed here, so a phone in two-level
   tagging kept a grid of keycaps for keys it does not have -- beside a tag
   COUNT, which is the exact collision this rule was written to end. */
@media (hover:none) and (pointer:coarse){
  html:not(.fi-kbd) .tag-btn .tsc,
  html:not(.fi-kbd) .tl-chip .kk,
  html:not(.fi-kbd) .prev-th{display:none}
}

/* ── PAID-FEATURE MARKERS (UX only — see fiCan() in js/settings.js) ──
   A locked control still looks like a control: it opens an explanation of what
   the feature is, rather than silently doing nothing.

   THE MARKER IS A PADLOCK AND THE ROW IS GREY (2026-08-26). The chip carried
   the word "Paid" and the row was dimmed to .72, which on a light theme is not
   a state — it is a slightly tired version of the normal one. Both halves are
   now unambiguous: the icon reads at a glance without being read, and
   `grayscale(1)` drains the colour a tag, a plan badge or an accent border was
   using to say "this is live", so a locked row cannot be mistaken for an
   available one whatever colour it started as.

   THE PADLOCK CANNOT OPT OUT OF THE GREY, so it is drawn to survive it. A
   `filter` establishes a containing block for its whole subtree: the chip is
   INSIDE the row, so `filter:none` on the chip does not undo the row's
   greyscale — it only stops the chip adding another. That is why the dimming
   stops at .68 rather than going as far as it wants to, and why the chip is a
   solid fill with a light glyph instead of the old tinted-amber-on-transparent:
   desaturated, a solid contrast pair is still a padlock, and a 10%-alpha amber
   wash is nothing at all. The colour is doing no work here; the shape is. */
.plan-chip{display:inline-flex;align-items:center;justify-content:center;
  margin-left:7px;width:18px;height:18px;border-radius:99px;
  background:var(--am);border:1px solid var(--am);color:var(--bg);flex-shrink:0}
.plan-chip .fi-ic{width:11px;height:11px;stroke-width:2.2;vertical-align:0}
.plan-locked{filter:grayscale(1);opacity:.68}
.plan-locked:hover{filter:grayscale(.6);opacity:.9}
/* In the Review source list the chip belongs beside the option title. */
.rev-src-opt.plan-locked .plan-chip{position:absolute;top:8px;right:32px}
.rev-src-opt.plan-locked{position:relative}

/* ── "still loading" spinner, for an empty list that is not empty yet ──
   Home and Presentations draw their EMPTY state the instant a list is short,
   which on a device that has just joined a club means telling a coach they have
   no games seconds before the season arrives (fiSyncWaiting, js/cloudsync.js).
   Those branches now draw a wait instead, and this is what it looks like.

   Same geometry and the same two tokens as .fi-tour-spin (css/tour.css), a size
   up: one spinner idiom, not two that drift. Centred as a block because it sits
   above a line of text inside .home-empty / .pr-empty rather than beside one. */
.fi-spin{width:20px;height:20px;margin:0 auto 10px;border-radius:50%;
  border:2px solid var(--bm);border-top-color:var(--ac);animation:fiSpin .7s linear infinite}
@keyframes fiSpin{to{transform:rotate(360deg)}}
@media (prefers-reduced-motion:reduce){.fi-spin{animation-duration:2.4s}}
