/* Generated from styles/app.scss by scripts/build-css.js - edit the .scss files, not this one. fingerprint 9dffb2e6c380c0c3 */
/* Wiener Dog Race Day - staff screens.
   Oktoberfest palette. Big touch targets: these get used standing at a
   check-in table on a tablet, not sitting at a desk.

   Everything below is built on the token layer in :root. Add a token before
   adding a literal - the file used to carry around forty hand-tuned pixel
   values and three different definitions of the same blue.

   The brand tokens marked SHARED are duplicated inline in public/display.html
   and public/announcer.html, because the kiosks must stay self-contained and
   cannot load this file. test/design-tokens.test.js asserts the three copies
   agree; change one and you must change all three. */
:root {
  color-scheme: light dark;
  /* ---------------------------------------------------- brand (SHARED) */
  /* Unified on the kiosk values. app.css had drifted to a lighter blue
     (#1462a5) and gold (#f2b546); #00529B was already the announcer's blue,
     and - by accident - the colour every focus ring on these screens was
     already drawn in. It also clears 7:1 on white where #1462a5 managed 5.6. */
  --bav-blue: #00529B;
  --bav-blue-dark: #0C4175;
  --gold: #E5A93B;
  --hops: #146449;
  --rustic: #563B29;
  --danger: #C13B35;
  /* ------------------------------------------------- semantic surfaces */
  --surface: #F6F8FB;
  --surface-raised: #FFFFFF;
  --surface-sunken: #EDF2F7;
  --surface-chrome: #0C4175;
  --border: #DCE3EA;
  --border-strong: #C3D0DC;
  /* Input outlines need 3:1 to be seen at all; --border-strong is also a fill. */
  --border-field: #8A99A9;
  --text: #17212B;
  --text-muted: #5A6875;
  --text-inverse: #FFFFFF;
  --text-on-chrome: #DCE9F6;
  --text-on-gold: #3A2A10;
  --accent: var(--bav-blue);
  --accent-hover: var(--bav-blue-dark);
  --success: var(--hops);
  --success-hover: #1D3D29;
  --gold-hover: #D3982F;
  /* What text may sit ON a filled accent, success or danger surface. These
     are not --text-inverse: in dark mode those fills become light, and white
     on the dark-mode accent measures 2.8:1. */
  --text-on-accent: #FFFFFF;
  --text-on-success: #FFFFFF;
  --text-on-danger: #FFFFFF;
  /* Soft tints and the text that sits on them, so notes, pills, round
     headings and drop targets all flip together in dark mode. */
  --tint-gold: #FFF3D6;
  --tint-gold-line: #E8CF96;
  --on-tint-gold: #5A4213;
  --tint-hops: #E4F4EC;
  --tint-hops-line: #BCD4C3;
  --on-tint-hops: #1D3D29;
  --tint-danger: #FFF0EE;
  --tint-danger-line: #E7C3BD;
  --on-tint-danger: #7A2418;
  --tint-blue: #EDF5FC;
  --tint-plum: #F3ECF9;
  --tint-plum-line: #8055A0;
  --tint-neutral: #EFE9E0;
  /* 2nd and 3rd place on Race Control's podium (race.js podiumLane) and
     its dialog's tags: silver and bronze beside the Champion's gold. */
  --silver: #C5CDD6;
  --text-on-silver: #17212B;
  --bronze: #95562A;
  --text-on-bronze: #FFFFFF;
  /* Your profile's badge (1.11.0, src/profiles.js COLOURS): the round
     initial badge's fill for each colour a person can pick, and the letter
     on it - white or ink, whichever clears 4.5:1 on the fill, in both
     schemes (test/design-tokens.test.js ON_PAIRS). Gold is the brand gold
     and its ink in both; the others are lifted in dark mode with the roles
     they follow. 1.12.0 (Ryan, FB-20260929-RY-19, "more color choices")
     adds six - red, orange, teal, pink, brown and slate - each far enough
     from the rest in both schemes to tell apart at a glance (no two closer
     than 23 in CIELAB), orange carrying a dark letter as gold does. */
  --badge-gold: var(--gold);
  --on-badge-gold: var(--text-on-gold);
  --badge-green: var(--hops);
  --on-badge-green: #FFFFFF;
  --badge-plum: #8055A0;
  --on-badge-plum: #FFFFFF;
  --badge-blue: var(--bav-blue);
  --on-badge-blue: #FFFFFF;
  --badge-red: #B83227;
  --on-badge-red: #FFFFFF;
  --badge-orange: #E06C2C;
  --on-badge-orange: #2B1203;
  --badge-teal: #0E7C86;
  --on-badge-teal: #FFFFFF;
  --badge-pink: #C2185B;
  --on-badge-pink: #FFFFFF;
  --badge-brown: #8D5524;
  --on-badge-brown: #FFFFFF;
  --badge-slate: #52606D;
  --on-badge-slate: #FFFFFF;
  /* Each race's colour (2.0.0, races.colour; src/race-days.js
     RACE_COLOURS): six of the badge colours above, so a race's chip in the
     race switch, its card's edge on a race day's page and its swatch on Set
     up a race follow the badges into dark mode with them. A rule names a
     race's colour only through these, never a hex. */
  --race-blue: var(--badge-blue);
  --race-orange: var(--badge-orange);
  --race-teal: var(--badge-teal);
  --race-plum: var(--badge-plum);
  --race-pink: var(--badge-pink);
  --race-green: var(--badge-green);
  /* --------------------------------------------------------- spacing */
  /* A 2px base, not 4px: the three most-used values in this file are odd
     multiples of 2, and snapping them to a 4px grid would re-flow the roster
     and race-control rows for no gain. */
  --space-2: 2px;
  --space-4: 4px;
  --space-6: 6px;
  --space-8: 8px;
  --space-10: 10px;
  --space-12: 12px;
  --space-14: 14px;
  --space-16: 16px;
  --space-20: 20px;
  --space-24: 24px;
  --space-28: 28px;
  /* ------------------------------------------------------------ type */
  --text-micro: 11px;
  --text-caption: 12px;
  --text-label: 13px;
  --text-sm: 14px;
  --text-base: 16px;
  --text-lg: 18px;
  --text-xl: 24px;
  --text-stat: 40px;
  --text-display: clamp(26px, 4vw, 34px);
  --leading-tight: 1.2;
  --leading-base: 1.45;
  --tracking-caps: 0.05em;
  /* The longest comfortable line of running text. What's new ran its notes
     the full card width, about 150 characters a line on a desktop (DS-90). */
  --measure: 72ch;
  /* No webfont. Inter was declared here for months and never loaded - there
     was no @font-face and no stylesheet link - so every screen has always
     rendered in the system font. These screens are used on the venue's own
     flaky network, the same one the kiosks are forbidden to depend on, so a
     webfont is a race-day risk for no functional gain. */
  --font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;
  /* ----------------------------------------------------------- radii */
  --radius-sm: 8px;
  --radius-md: 12px;
  --radius-lg: 18px;
  --radius-pill: 999px;
  /* ------------------------------------------------------- elevation */
  --shadow-tint: 24 42 58;
  --shadow-sm: 0 1px 3px rgb(var(--shadow-tint) / 0.08);
  --shadow-md: 0 2px 4px rgb(var(--shadow-tint) / 0.03), 0 12px 30px rgb(var(--shadow-tint) / 0.08);
  --shadow-lg: 0 7px 18px rgb(var(--shadow-tint) / 0.12);
  --shadow-up: 0 -8px 24px rgb(var(--shadow-tint) / 0.18);
  --shadow-modal: 0 18px 50px rgb(var(--shadow-tint) / 0.32);
  /* ---------------------------------------------------------- motion */
  --dur-fast: 150ms;
  --dur-base: 200ms;
  --ease: ease;
  /* The dog page's "tap the picture" nudge: a soft ring that spreads from a
     picture still to be added, a few times after the page opens and then
     never again. Only with prefers-reduced-motion: no-preference. */
  --dur-nudge: 1800ms;
  --nudge-delay: 600ms;
  --nudge-cycles: 3;
  /* ---------------------------------------------- touch and focus */
  /* 44px is the iOS minimum and the floor for every control here. 48 is the
     comfortable default; 58 is a full row. */
  --touch-min: 44px;
  --touch-base: 48px;
  --touch-row: 58px;
  /* Race Control's Break time field ("Set a time" / "3:00 PM"): wide
     enough for "12:45 PM" in the field's bold, as the rc2 pictures draw it. */
  --time-field: 128px;
  /* The Build dialog's minutes slider (Staff.askBuildStart): the track's
     thickness. Its thumb is --touch-min square, so the finger has 44px. */
  --range-track: 6px;
  /* The slot at the right of a Bracket Builder lane: the move arrow, or Place
     here / Swap here while a dog is picked up. One width for both, so picking
     a dog up never narrows the name beside it. Swap here and Place here at
     --text-caption bold measured 76px in Chrome on macOS (SF, as on the iPad);
     the rest is headroom, and the button may still grow past it. */
  --lane-slot: 80px;
  /* The blue lane number beside a dog's name in a heat. Named so a layout
     that sets it beside the 44px picture can centre it there: the picked
     card in Race Control's current heat floats it (_rounds.scss). */
  --lane-badge: 30px;
  /* One dog's card in Race Control's current heat on a phone, where the cards
     scroll sideways: the measured width _phone.scss explains. The picked card
     may take two of them and the gap between (_phone.scss). */
  --lane-card-phone: 140px;
  /* The least width of one dog's card in Race Control's current heat above a
     phone: what "Cinnamon" needs beside its picture (_rounds.scss). */
  --lane-card-min: 204px;
  /* Dog pictures. One size per place a dog is named: the Check-In card and a
     dialog heading, a lane on a heat card, a row inside a dialog, a chip in the
     slim pinned heat, and the dog's own page. */
  --avatar: 44px;
  --avatar-lane: 36px;
  --avatar-sm: 32px;
  /* Small enough that five chips plus Wiener still fit a 375px row at 44px
     tall, large enough to tell two brown dachshunds apart at arm's length. */
  --avatar-chip: 26px;
  --avatar-lg: 104px;
  /* The dog's own page on an iPad or a computer, where the picture sits beside
     the name, and on a tall phone once the dog has a photo. Ryan: "keep a
     larger dog image on the profile page, it sometimes is the best place to
     actually be able to see the dog's image on a device." An SE keeps
     --avatar-lg, so Race status still comes up there. */
  --avatar-xl: 144px;
  /* On the dog's page the picture is the photo button (Decision #11). The
     camera badge on its lower edge, the ring that keeps the badge off the
     photo, and the dashed edge of a picture still to be added. */
  --photo-badge: 36px;
  --photo-badge-xl: 40px;
  --photo-badge-ring: 3px;
  --photo-dash: 2px;
  /* My account's "Where you are logged in" (1.11.0): the tinted square each
     device's 22px icon sits on, about as tall as the device's two lines of
     words; and the check beside "Logged out 2 devices.", shorter than that
     12px line is tall (17.4px at --leading-base), so it never makes the
     line taller when it comes. */
  --device-icon: 40px;
  --icon-status: 14px;
  /* Your profile's round initial badge (1.11.0): in the account button,
     Event settings' notice and My account's preview; a little larger beside
     each colour in My account's choice, as the design drew it. */
  --person-badge: 24px;
  --person-badge-choice: 26px;
  /* My account's colour swatches (1.12.0): the narrowest tile, four across
     a 393px phone's card. */
  --swatch-tile: 74px;
  /* The send log's labels (1.12.0, Results emails' Send log): To, Went to,
     Dogs, By, Cards - one column wide enough for the longest, Went to. */
  --sendlog-label: 76px;
  /* How much of a dog's name Race Control's winner selector shows before the
     ellipsis. A cap in ch is a cap in characters of whatever type the name is
     set in, so this one value stops a name at the same letter on the 16px
     current-heat card, in the 12px slim bar and inside the Record Wiener button:
     "Doppelbock" fits all three and "Sausage Link" is cut in all three.
     Measured in Chrome on macOS (SF, as on the iPad): 98.6px on the card,
     73.9px in the slim bar, where nine was already the measured number.
     Staff screens only. The kiosks are read aloud from, so they shrink a name
     to fit rather than cut it. */
  --name-cap: 9ch;
  /* The owner line under a capped name, ending where that name ends: 11ch of
     the 14px owner type is 97.0px, against 98.6px for the name above it. */
  --owner-cap: 11ch;
  --focus-width: 3px;
  --focus-offset: 2px;
  /* The coloured edge on a checked-in Check-In row. Named so the row can take
     the extra width out of its own padding and line up with the rows around
     it (DS-59). */
  --status-edge: 4px;
  --focus-ring: 0 0 0 var(--focus-width) rgb(0 82 155 / 0.38);
  --focus-ring-inverse: 0 0 0 var(--focus-width) rgb(229 169 59 / 0.75);
  /* ------------------------------------------------------- stacking */
  --z-sticky: 15;
  /* A board lane's name tip (Staff.nameTip): over the lanes below it and
     over the sticky round headings (--z-sticky - 2), under the pinned current
     heat and the top bar. Under the headings, a tip in the last heat of a
     round grew beneath the next round's heading, and a tip on a lane half
     under a stuck heading hid the dog's name - the very words it opens to
     show. It closes on any scroll, so it is never seen sliding under one. */
  --z-tip: calc(var(--z-sticky) - 1);
  --z-toolbar: 40;
  --z-chrome: 50;
  /* The start's request for help (the draft for next year, .help-bar): over
     the top bar and everything pinned under it, under the connection strip
     and the toast. */
  --z-help: 60;
  --z-conn: 90;
  --z-toast: 100;
  --z-drag: 150;
  /* --------------------------------------------- the event's lettering */
  /* The title face (views/partials/print-face.ejs, inline data like the
     kiosks') with Devin's serif behind it, and the Big Screen's 3D offset:
     gold behind the letters. The print pages and the login page use them. */
  --title-font: "Grenze Gotisch", "Palatino Linotype", "Book Antiqua", Palatino, Georgia, serif;
  --title-3d: 0.03em 0.03em 0 var(--gold), 0.06em 0.06em 0 var(--gold);
  /* ------------------------------------------- the login page's dog show */
  /* The Big Screen's idle look on the login page (styles/_login.scss):
     its light blue diamond field, the white glow the dogs stand in, and
     "Who'll Be the Grand “Wiener”?" in --show-title-ink on a white ribbon
     with a gold edge, over the navy offset. The light values are the
     kiosks' own (--field, --lattice-idle, --idle-glow, --gold-ink, --ink in
     public/display.html). Dark mode has its own below: a navy field, the
     ribbon a dark card, the title in gold. */
  --show-field: #F3F9FD;
  --show-lattice: rgb(0 152 212 / 0.22);
  --show-glow: rgb(255 255 255 / 0.95);
  --show-ribbon: #FFFFFF;
  --show-title-ink: #8A5A00;
  --show-title-3d: 0.03em 0.03em 0 #0C2D4F;
  /* The diamonds' tile, the Big Screen's: in vmin, so every screen gets the
     same number of diamonds across its short side. */
  --show-tile: 12vmin;
  --show-shadow: 0 10px 24px rgb(12 45 79 / 0.24);
  /* One dog's turn is JavaScript's (public/js/login-show.js); these are the
     crossfade inside it and the fade the show comes in with. The global
     reduced-motion block turns both into a cut. */
  --dur-crossfade: 900ms;
  --dur-show-in: 600ms;
  /* The crossfade's glide: quick, then settling, never past its end. */
  --ease-crossfade: cubic-bezier(0.22, 0.61, 0.36, 1);
  /* ---------------------------------------------------- print pages */
  /* Bracket Builder's poster and a dog's keepsake (styles/_print.scss) print
     in the kiosks' light look, so these are the kiosks' own colours
     (agent/kiosk-screens.md, "The look"; test/print-exports.test.js holds
     them to the kiosk-shared block). They paint paper, which is white in
     either scheme, so they have no dark-mode values: in dark mode the page
     around a sheet goes dark and the sheet stays paper, as it will print. */
  --print-field: #F3F9FD;
  --print-paper: #FFFFFF;
  --print-ink: #0C2D4F;
  --print-ink-2: #4A5E72;
  --print-rule: #C9DDEC;
  --print-sky: #1C9BD7;
  --print-mark-bg: #EAF4FB;
  --print-mark: #7FA3C0;
  --print-lattice: rgb(0 152 212 / 0.10);
  --print-win-bg: #FFF3D6;
  --print-gold-ink: #8A5A00;
  /* Qualifiers, Second Chance, Semi-Finals, Championship: the round colours
     every heat card and zone title wears, as on the Big Screen. */
  --print-r1: #8A5A00;
  --print-r2: #146449;
  --print-r3: #00529B;
  --print-r4: #9C2F6B;
  /* The rosette's own colours on the Big Screen (display.html): a lighter
     sky for the participation ribbon, and a darker edge for each petal. */
  --print-sky-light: #7CC6EC;
  --print-sky-edge: #0C7DB5;
  --print-gold-edge: #C8902A;
  /* The title face (views/partials/print-face.ejs) with Devin's serif behind
     it, and the kiosks' 3D offset: gold behind blue, ink behind gold ink. */
  --print-title-font: var(--title-font);
  --print-title-3d: var(--title-3d);
  --print-title-3d-ink: 0.03em 0.03em 0 var(--print-ink);
  /* Names, owners and labels on paper: a face every device has under its own
     name. The staff sans is the system face, which Chrome on a Mac writes
     into a PDF as Type 3 glyphs - still lines, but what a print shop's
     preflight flags on a 24 x 36 file. */
  --print-font: "Helvetica Neue", Helvetica, Arial, "Noto Sans", sans-serif;
  /* Each sheet's size: the box inside its page's margins (src/print.js
     PAPERS, the @page rules in _print.scss), a hair shorter so rounding can
     never push a sheet onto a second page. A sheet is laid out at this size
     on screen too and only previewed smaller, so the type, the wrapping and
     the tree's lines on screen are the paper's own. The ratio is the same
     size as width over height, for the preview's box. "Turned" is the owner's
     bracket on a device that prints every page on one upright paper
     (public/js/print.js): drawn sideways, then turned to fill upright Letter
     - or A4, which is why it is a little shorter than Letter's own. */
  --paper-tabloid-w: 16.4in;
  --paper-tabloid-h: 10.36in;
  --paper-tabloid: 16.4 / 10.36;
  --paper-poster-w: 35.4in;
  --paper-poster-h: 23.36in;
  --paper-poster: 35.4 / 23.36;
  --paper-letter-w: 10.4in;
  --paper-letter-h: 7.86in;
  --paper-letter: 10.4 / 7.86;
  --paper-turned-w: 10.4in;
  --paper-turned-h: 7.6in;
  --paper-turned: 10.4 / 7.6;
  /* On paper a sheet is sized by its page, not in inches (_print.scss): as
     wide as the page, or as tall, whichever is less. The turned bracket is
     fitted upright, in this shape (7.6 wide, 10.4 tall). */
  --paper-turned-upright: 7.6 / 10.4;
  --paper-keepsake-w: 8.5in;
  --paper-keepsake-h: 10.96in;
  --paper-keepsake: 8.5 / 10.96;
  /* How wide the upright keepsake is previewed on a screen wider than it. */
  --print-preview-upright: 720px;
  /* A dog's card as its card link's preview picture (1.12.0,
     views/card-image.ejs, photographed by the card printer): the size every
     link preview reads (src/share-card.js), the keepsake as tall as fits
     between the two diamond strips, Devin's two dogs either side of it on
     the Big Screen's diamonds - a little stronger than paper's, as the app's
     other preview pictures have them, so they show at thumbnail size
     (scripts/share-image/share.html). Paper, so no dark-mode values. */
  --card-image-w: 1200px;
  --card-image-h: 630px;
  --card-image-strip: 22px;
  --card-image-sheet-h: 562px;
  --card-image-dog-h: 264px;
  --card-image-tile: 76px;
  --card-image-lattice: rgb(0 152 212 / 0.18);
  --card-image-dog-shadow: drop-shadow(0 8px 12px rgb(12 45 79 / 0.25));
  /* -------------------------------------------------------- layout */
  --layout-max: 1200px;
  /* The login card's width, and on an iPad on its side or a computer the
     width of its column beside the dog show. */
  --login-card: 390px;
  --layout-gutter: var(--space-20);
  /* Event settings (1.11.0, styles/_settings.scss): the side list of its six
     sections from 900px, beside the sections, and the edge a part reached
     by a link wears (:target) until another link is followed. */
  --settings-side: 248px;
  --target-edge: 3px;
  /* All three are re-measured from the real elements by public/js/staff.js.
     The values here are the no-JavaScript fallback, not the source of truth -
     the old code hard-coded a 72px clearance that never accounted for the
     home indicator and left the build stamp behind the bar.
     --staff-current-height is Race Control's current heat while it is pinned
     over the page - the full box while CSS has it sticky, the slim bar on a
     phone while it is showing - and 0 everywhere else. It changes as the screen
     re-renders rather than only when the window resizes: turning dog photos
     on, or recording a winner so Undo appears, both make the panel taller. */
  --staff-nav-height: 58px;
  --staff-tabbar-height: 0px;
  /* The connection strip's height while it shows, else 0 (staff.js). */
  --staff-conn-height: 0px;
  --staff-current-height: 0px;
  /* Whatever the browser scrolls into view - a control taking focus, a heat
     card a tap brings up - lands clear of the chrome instead of underneath
     it. Race Control's current heat is pinned under the top bar - the full box
     above 640px, the slim bar on a phone - and without this, tapping Correct
     result on a heat card scrolled that button beneath it, where the tap never
     reached it. That was half the reason 1.9.0 stopped pinning the full box on
     a phone. */
  scroll-padding-top: calc(var(--staff-nav-height, 58px) + var(--staff-current-height, 0px) + var(--space-8));
  scroll-padding-bottom: calc(var(--staff-tabbar-height, 0px) + var(--staff-conn-height, 0px) + var(--space-8));
}

@media (prefers-color-scheme: dark) {
  :root {
    --surface: #10171E;
    --surface-raised: #17212B;
    --surface-sunken: #0B1116;
    --surface-chrome: #0A2C4D;
    --border: #2A3844;
    --border-strong: #3C4C5A;
    --border-field: #55697C;
    --text: #E8EEF4;
    --text-muted: #9BAAB8;
    --text-on-chrome: #C6DCF0;
    /* Lift the brand colours off the dark surface: the light-mode values are
       tuned against white and go muddy on #10171E. */
    --accent: #4E9BE0;
    --accent-hover: #6DAFEA;
    --success: #3FA274;
    --success-hover: #4FB585;
    --danger: #E8766F;
    --text-on-accent: #06121C;
    --text-on-success: #04140D;
    --text-on-danger: #2A0A07;
    --tint-gold: #2E2510;
    --tint-gold-line: #5A4A22;
    --on-tint-gold: #F0D79B;
    --tint-hops: #102A1F;
    --tint-hops-line: #245740;
    --on-tint-hops: #8FD9B4;
    --tint-danger: #2E1714;
    --tint-danger-line: #5E2F29;
    --on-tint-danger: #F3ADA7;
    --tint-blue: #12263A;
    --tint-plum: #241B2E;
    --tint-plum-line: #A77FC4;
    --tint-neutral: #262019;
    --silver: #9AA6B2;
    --text-on-silver: #06121C;
    --bronze: #C98A55;
    --text-on-bronze: #06121C;
    --badge-green: #3FA274;
    --on-badge-green: #04140D;
    --badge-plum: #A77FC4;
    --on-badge-plum: #1A0F24;
    --badge-blue: #4E9BE0;
    --on-badge-blue: #06121C;
    --badge-red: #F07167;
    --on-badge-red: #2A0A07;
    --badge-orange: #F2853A;
    --on-badge-orange: #2A1300;
    --badge-teal: #3CBFCB;
    --on-badge-teal: #04201D;
    --badge-pink: #F48FB1;
    --on-badge-pink: #2A0614;
    --badge-brown: #C8935F;
    --on-badge-brown: #1F1004;
    --badge-slate: #9AA8B5;
    --on-badge-slate: #10161C;
    --shadow-tint: 0 0 0;
    --shadow-sm: 0 1px 3px rgb(0 0 0 / 0.4);
    --shadow-md: 0 2px 4px rgb(0 0 0 / 0.3), 0 12px 30px rgb(0 0 0 / 0.45);
    --shadow-lg: 0 7px 18px rgb(0 0 0 / 0.5);
    --shadow-up: 0 -8px 24px rgb(0 0 0 / 0.5);
    --shadow-modal: 0 18px 50px rgb(0 0 0 / 0.6);
    --focus-ring: 0 0 0 var(--focus-width) rgb(110 175 235 / 0.5);
    /* The login page's dog show at night: navy diamonds, a blue glow, the
       ribbon a dark card with its gold edge, the title in gold over black. */
    --show-field: #0A1F33;
    --show-lattice: rgb(78 155 224 / 0.12);
    --show-glow: rgb(78 155 224 / 0.20);
    --show-ribbon: #17212B;
    --show-title-ink: #E5A93B;
    --show-title-3d: 0.03em 0.03em 0 #000000;
    --show-shadow: 0 10px 24px rgb(0 0 0 / 0.55);
  }
}
* {
  box-sizing: border-box;
}

/* The page stops at its end. Ryan, on Bracket Builder on an iPhone: "Scroll
   should have stopped when reaching the end of the page and not have started
   moving the bottom nav bar up the screen" - he had dragged the fixed tab row
   nearly to the top of the window over bare background. That is iOS rubber
   band: the document overscrolls, and everything fixed to the viewport travels
   with it, because on iOS `position: fixed` is fixed to the page during the
   bounce and not to the screen. `overscroll-behavior-y: none` is what turns
   the bounce off, and it has to be on the scrolling element - the document -
   not on .wrap. The background goes on `html` as well as `body` so nothing can
   paint the light grey default behind the page if a bounce does get through on
   an older Safari. Every staff screen is one document scroll, so this covers
   all of them rather than the one page he happened to notice it on. */
html {
  -webkit-text-size-adjust: 100%;
  overscroll-behavior-y: none;
  background: var(--surface);
}

/* Scroll anchoring off, for the one moment race.js holdPage measures a pick
   that grew or shrank Race Control's pinned box and scrolls the page by what
   the page under it moved. Safari before 27.0 has no anchoring at all, so the
   measurement is what holds the page still there; switched off here so a
   browser that has it cannot hold the page as well and move it twice. */
html.page-held {
  overflow-anchor: none;
}

body {
  margin: 0;
  overscroll-behavior-y: none;
  background: var(--surface);
  color: var(--text);
  font: var(--text-base)/var(--leading-base) var(--font-sans);
}

/* ----------------------------------------------------------------- icons */
/* The sprite itself never renders; views/partials/icons.ejs holds the shapes. */
.icon-sprite {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}

.icon {
  width: 22px;
  height: 22px;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* --------------------------------------------------------------- chrome */
.topbar {
  position: sticky;
  top: 0;
  z-index: var(--z-chrome);
  background: var(--surface-chrome);
  color: var(--text-inverse);
  padding-top: env(safe-area-inset-top);
  box-shadow: var(--shadow-sm);
}

/* Matches .wrap exactly, so the brand and the h1 below it share one left
   edge. The old nav used its own centring expression and sat 20px out. */
.topbar__inner {
  max-width: var(--layout-max);
  margin: 0 auto;
  padding: 0 max(var(--layout-gutter), env(safe-area-inset-left)) 0 max(var(--layout-gutter), env(safe-area-inset-right));
  display: flex;
  align-items: center;
  gap: var(--space-8);
  min-height: var(--touch-row);
}

.topbar__brand {
  order: 1;
  display: inline-flex;
  align-items: center;
  gap: var(--space-8);
  color: var(--text-inverse);
  text-decoration: none;
  font-weight: 800;
  letter-spacing: -0.02em;
  white-space: nowrap;
  min-height: var(--touch-min);
  padding: var(--space-4) var(--space-2) var(--space-4) 0;
  margin-right: auto;
}

.topbar__mark {
  width: 28px;
  height: 28px;
  color: var(--gold);
}

.topbar__name span {
  color: var(--gold);
}

@media (hover: hover) {
  .topbar__brand:hover .topbar__name {
    text-decoration: underline;
  }
}
.tabs {
  order: 2;
  display: flex;
  align-items: stretch;
  gap: var(--space-2);
}

.tab {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-6);
  min-height: var(--touch-row);
  min-width: var(--touch-min);
  padding: var(--space-10) var(--space-12);
  color: var(--text-on-chrome);
  text-decoration: none;
  font-weight: 600;
  border-bottom: 3px solid transparent;
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
}

@media (hover: hover) {
  .tab:hover {
    color: var(--text-inverse);
    background: rgba(255, 255, 255, 0.08);
  }
}
.tab:active {
  background: rgba(255, 255, 255, 0.16);
}

.tab.is-active {
  color: var(--text-inverse);
  border-bottom-color: var(--gold);
  background: rgba(255, 255, 255, 0.1);
}

.tab__icon {
  width: 21px;
  height: 21px;
}

/* The one-row bar starts at 900px, where the brand, five tabs and the account
   control came to 906px with "ryan" and more with "devin": the page scrolled
   sideways and "Check-In" broke onto two lines. The upright iPad's narrower
   tab sides (styles/_tablet-portrait.scss) give back 60px until the bar has
   room of its own. */
@media (min-width: 900px) and (max-width: 1023px) {
  .tab {
    padding-left: var(--space-6);
    padding-right: var(--space-6);
  }
}
/* In the one-row bar a tab's label never breaks onto two lines: the account
   control gives way instead, its name cut with an ellipsis (_account.scss),
   however long the name in it (Your profile, 1.11.0: up to 20 characters -
   a 20-letter name broke "Check-In" at 900 and 960px, and "Bracket Builder"
   at 1024px). The tabs and the brand keep their width; the account control
   alone may shrink. */
@media (min-width: 900px) {
  .tab {
    white-space: nowrap;
  }
}
/* ------------------------------------------------------------- account */
/* The account control may give up room before anything else on the bar:
   a long name (Your profile, up to 20 characters) is cut with an ellipsis
   in the button - the menu names you whole - rather than push the bar into
   a sideways scroll on a phone or at 900px. Where the bar has the room (an
   iPad's top row, a wide window), the name shows whole: up to 20ch. */
.account {
  order: 3;
  position: relative;
  min-width: 0;
}

/* The race switch's button (2.0.0, styles/_race-switch.scss) is the same
   pill on the same bar. */
.account__toggle,
.race-switch__toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-6);
  min-height: var(--touch-min);
  padding: var(--space-6) var(--space-10);
  background: rgba(255, 255, 255, 0.1);
  border: 1px solid rgba(255, 255, 255, 0.28);
  border-radius: var(--radius-pill);
  color: var(--text-inverse);
  font: inherit;
  font-size: var(--text-sm);
  font-weight: 600;
  cursor: pointer;
  max-width: 100%;
}

@media (hover: hover) {
  .account__toggle:hover, .race-switch__toggle:hover {
    background: rgba(255, 255, 255, 0.18);
  }
}
.account__toggle:active, .race-switch__toggle:active {
  background: rgba(255, 255, 255, 0.26);
}

.account__name {
  flex: 0 1 auto;
  min-width: 0;
  max-width: 20ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* On the narrowest phones (320px) the button's own sides and gaps give back
   12px, so "Devin" shows whole beside the brand, where it was cut to "De…";
   the button stays 44px tall. */
@media (max-width: 359px) {
  .account__toggle {
    gap: var(--space-4);
    padding-left: var(--space-6);
    padding-right: var(--space-6);
  }
}
/* Your badge in the button wears a white ring, so every colour stands off the bar. */
.account__badge {
  box-shadow: 0 0 0 var(--space-2) var(--text-inverse);
}

.account__chevron {
  width: 16px;
  height: 16px;
  transition: transform var(--dur-fast) var(--ease);
}

.account__toggle[aria-expanded=true] .account__chevron {
  transform: rotate(180deg);
}

.account__menu {
  position: absolute;
  right: 0;
  top: calc(100% + var(--space-6));
  min-width: 232px;
  background: var(--surface-raised);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  padding: var(--space-6);
  z-index: var(--z-chrome);
}

.account__who {
  margin: 0;
  padding: var(--space-8) var(--space-10) var(--space-10);
  font-size: var(--text-caption);
  color: var(--text-muted);
  border-bottom: 1px solid var(--border);
}

/* The race switch's races (2.0.0) are the same rows in the same panel. */
.account__item,
.race-switch__item {
  display: flex;
  align-items: center;
  gap: var(--space-10);
  width: 100%;
  min-height: var(--touch-base);
  padding: var(--space-10) var(--space-10);
  margin-top: var(--space-4);
  background: none;
  border: 0;
  border-radius: var(--radius-sm);
  color: inherit;
  font: inherit;
  font-weight: 600;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}

@media (hover: hover) {
  .account__item:hover, .race-switch__item:hover {
    background: var(--surface-sunken);
  }
}
.account__item:active, .race-switch__item:active {
  background: var(--border);
}

.account__item.is-active, .race-switch__item.is-active {
  color: var(--accent);
}

.account__item--danger {
  color: var(--danger);
}

@media (hover: hover) {
  .account__item--danger:hover {
    background: var(--tint-danger);
  }
}
.account__menu form {
  margin: 0;
}

/* My account and Event settings (1.10.0) are two items with a line each
   under the name saying whose they are - "Yours alone" (1.11.0; it was "Your
   password" until the page held your devices too), "Shared by Ryan and
   Devin" - so nobody opens the wrong one. The line takes the item's colour
   when it is the page you are on. */
.account__words {
  display: grid;
  gap: var(--space-2);
}

.account__sub {
  font-size: var(--text-caption);
  font-weight: 400;
  color: var(--text-muted);
}

.account__item.is-active .account__sub {
  color: var(--accent);
}

/* My account's result line under Change password: the refusal in the
   danger note (role="alert"), or the success in the green one, never both
   and never twice (public/js/account.js). */
.password-result {
  margin: var(--space-14) 0 0;
}

/* Empty, a line is drawn as nothing but stays in the page - not `hidden`,
   which would take it out of what a screen reader knows - so its words are
   read out when they arrive. */
.password-result:empty {
  margin: 0;
  padding: 0;
  border: 0;
}

/* My account's four cards (1.12.0, FB-20260929-RY-18): one under another on
   a phone; from 900px wide two by two - Your profile beside Change my
   password, then the devices' line beside How you write - each its own
   height, so Change my password is at the top of the screen. */
.account-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-20);
}

.account-grid > .card {
  margin-bottom: 0;
}

@media (min-width: 900px) {
  .account-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    align-items: start;
  }
}
/* My account's one line of where you are logged in (1.12.0): the line, and
   the button to the list on its own page. */
.devices-summary {
  margin: 0 0 var(--space-14);
}

/* Where you are logged in, on its own page (1.12.0): the way back to My
   account under the heading. */
.devices-back {
  margin: var(--space-8) 0 var(--space-16);
}

/* My account's "Where you are logged in" (1.11.0): one row per login of this
   account (public/js/account.js draws them), each at least a --touch-row
   tall, ruled off from the next, as the design drew it: the device's icon on
   a tinted square, its name at 16px bold with "This device" in a blue pill
   after it on the device in your hand, when it was last used under that in
   14px muted, and a ghost 44px Log out. Beside the words from 641px up; on a
   phone under them (styles/_phone.scss). A long name wraps; it is never cut,
   since two devices can differ only in the words at the end. */
.devices-lead {
  margin: 0 0 var(--space-16);
}

.device-list {
  list-style: none;
  margin: 0;
  padding: 0;
  border-bottom: 1px solid var(--border);
}

.device-row {
  display: flex;
  align-items: center;
  gap: var(--space-14);
  min-height: var(--touch-row);
  padding: var(--space-10) 0;
  border-top: 1px solid var(--border);
}

.device-row__icon {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--device-icon);
  height: var(--device-icon);
  border-radius: var(--radius-md);
  background: var(--surface-sunken);
  color: var(--text);
}

.device-row__body {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: var(--space-8) var(--space-12);
}

.device-row__words {
  flex: 1 1 auto;
  min-width: 0;
}

.device-row__name {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-4) var(--space-8);
  margin: 0;
  font-size: var(--text-base);
  font-weight: 700;
  overflow-wrap: anywhere;
}

.device-row__seen {
  margin: 0;
  font-size: var(--text-sm);
  color: var(--text-muted);
}

/* The blue of a thing that is yours, not the gold of the event's own pills;
   scoped to the row so it wins over .pill's dark-mode colour, which comes
   later in the cascade. */
.device-row .pill--this {
  background: var(--tint-blue);
  color: var(--accent);
}

.device-row__form {
  flex: none;
  margin: 0;
}

.device-row__out {
  flex: none;
}

/* The foot: Log out my other devices (greyed while there is none), then the
   status line. The line keeps its one line of room while it is empty, so
   "Logged out 2 devices." arriving moves nothing (the no-grow rule); it is
   muted for "No other device is logged in." and green, with the check,
   for what a Log out did (.is-done). A refusal is said once, in the danger
   note under it, drawn as nothing while it is empty, as the password's is. */
.devices-foot {
  margin-top: var(--space-16);
}

.devices-status {
  display: flex;
  align-items: center;
  gap: var(--space-6);
  min-height: calc(1em * var(--leading-base));
  margin: var(--space-8) 0 0;
  font-size: var(--text-caption);
  font-weight: 700;
  color: var(--text-muted);
}

.devices-status__mark {
  display: none;
  width: var(--icon-status);
  height: var(--icon-status);
}

.devices-status.is-done {
  color: var(--success);
}

.devices-status.is-done .devices-status__mark {
  display: block;
}

.note.devices-error {
  margin: var(--space-14) 0 0;
}

.note.devices-error:empty {
  margin: 0;
  padding: 0;
  border: 0;
}

/* Your profile's round initial badge (1.11.0, src/profiles.js; Staff.badge
   builds it in a page's script): the first letter of the name the app shows
   for a person, in the colour they picked, wherever the app names them - the
   account button, Event settings' notice (#toast.has-badge) and My account's
   colour choice and preview. Decorative: the name is always beside it, so it
   is aria-hidden. A badge with no colour of its own is blue. */
.person-badge {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--person-badge);
  height: var(--person-badge);
  border-radius: 50%;
  background: var(--badge-blue);
  color: var(--on-badge-blue);
  font-size: var(--text-micro);
  font-weight: 800;
  line-height: 1;
}

.person-badge--gold {
  background: var(--badge-gold);
  color: var(--on-badge-gold);
}

.person-badge--green {
  background: var(--badge-green);
  color: var(--on-badge-green);
}

.person-badge--plum {
  background: var(--badge-plum);
  color: var(--on-badge-plum);
}

.person-badge--blue {
  background: var(--badge-blue);
  color: var(--on-badge-blue);
}

.person-badge--red {
  background: var(--badge-red);
  color: var(--on-badge-red);
}

.person-badge--orange {
  background: var(--badge-orange);
  color: var(--on-badge-orange);
}

.person-badge--teal {
  background: var(--badge-teal);
  color: var(--on-badge-teal);
}

.person-badge--pink {
  background: var(--badge-pink);
  color: var(--on-badge-pink);
}

.person-badge--brown {
  background: var(--badge-brown);
  color: var(--on-badge-brown);
}

.person-badge--slate {
  background: var(--badge-slate);
  color: var(--on-badge-slate);
}

.person-badge--choice {
  width: var(--person-badge-choice);
  height: var(--person-badge-choice);
  font-size: var(--text-caption);
}

/* My account's Your profile (1.11.0, decision 15): the name box in the
   password form's narrow column (.stack-form), the colours as Event
   settings' choice list with each one's badge, and under them what the
   other person sees - the notice drawn in the toast's own colours - redrawn
   as the name or the colour changes (public/js/account.js). Save profile's
   status line keeps its room while it is empty (.devices-status, the no-grow
   rule); a refusal is said once, in the danger note, drawn as nothing until
   it has words. */
.profile-lead {
  margin: 0 0 var(--space-16);
}

/* The ten colours (1.12.0, FB-20260929-RY-19): swatch tiles in a grid - four
   across on a phone, five or more beside them - each its radio and your
   badge on one line, its name under them, and in caption type who else
   wears it. A long name wraps inside its tile; nothing widens the page. */
.profile-colours__opts {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(var(--swatch-tile), 1fr));
  gap: var(--space-8);
}

.choice-list__opt.profile-swatch {
  display: grid;
  grid-template-columns: auto auto;
  justify-content: start;
  align-content: start;
  gap: var(--space-4) var(--space-6);
  padding: var(--space-8) var(--space-10);
  min-width: 0;
}

.profile-swatch__words {
  grid-column: 1/-1;
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.profile-swatch__worn {
  font-size: var(--text-caption);
  font-weight: 600;
  color: var(--text-muted);
  overflow-wrap: anywhere;
}

.profile-colours__hint {
  margin: var(--space-8) 0 0;
}

.profile-form .field {
  margin-bottom: var(--space-16);
}

.profile-sees {
  margin: var(--space-16) 0 var(--space-6);
  font-size: var(--text-caption);
  font-weight: 700;
  color: var(--text-muted);
}

.profile-preview {
  display: flex;
  align-items: center;
  gap: var(--space-10);
  width: max-content;
  max-width: 100%;
  margin: 0 0 var(--space-16);
  padding: var(--space-10) var(--space-16);
  border-radius: var(--radius-md);
  background: var(--text);
  color: var(--surface);
  font-size: var(--text-sm);
  font-weight: 600;
}

.profile-preview > span:last-child {
  min-width: 0;
  overflow-wrap: anywhere;
}

.note.profile-error {
  margin: var(--space-14) 0 0;
}

.note.profile-error:empty {
  margin: 0;
  padding: 0;
  border: 0;
}

/* My account's How you write (1.11.0, decision 3): Event settings' choice
   list, and its hint under it. */
.writing-hint {
  margin: var(--space-6) 0 0;
}

.profile-name-fixed {
  margin: 0 0 var(--space-14);
}

/* --------------------------------------------------------- race switch */
/* The race a staff page is about (2.0.0, views/partials/header.ejs), in
   the brand's place on the top bar: drawn only while the database holds
   more than one race. The brand keeps its dachshund, its link home and its
   44px; its words make room for the race, whose name is cut with an
   ellipsis where the bar is short, as the account control's is, so a long
   race name never scrolls the bar or pushes a tab onto two lines. */
.topbar__brand[data-race-shown] {
  margin-right: 0;
  min-width: var(--touch-min);
  justify-content: center;
}

.topbar__brand[data-race-shown] .topbar__name {
  display: none;
}

/* It gives way before the account control does (styles/_account.scss): on
   the narrowest phones the race's name is cut first, so the person's name
   still shows whole beside it. */
.race-switch {
  order: 1;
  position: relative;
  display: flex;
  flex: 0 1000 auto;
  min-width: 0;
  margin-right: auto;
}

/* The pill itself is the account control's (styles/_account.scss); a login
   on one race gets the same pill as plain words, with nothing to tap. */
.race-switch__label {
  display: inline-flex;
  align-items: center;
  gap: var(--space-6);
  min-width: 0;
  max-width: 100%;
  padding: var(--space-6) var(--space-10);
  color: var(--text-inverse);
  font-size: var(--text-sm);
  font-weight: 600;
}

.race-switch__name {
  flex: 0 1 auto;
  min-width: 0;
  max-width: 16ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.race-switch__chevron {
  width: var(--space-16);
  height: var(--space-16);
  transition: transform var(--dur-fast) var(--ease);
}

.race-switch__toggle[aria-expanded=true] .race-switch__chevron {
  transform: rotate(180deg);
}

/* A race's colour (races.colour: one of six, each one of the badge colour
   tokens, so each has its dark-scheme counterpart): a dot. */
.race-chip {
  flex: none;
  width: var(--space-12);
  height: var(--space-12);
  border-radius: 50%;
  background: var(--race-blue);
}

/* On the top bar it sits in a white disc: the bar is the blue race's own
   colour, and a blue dot ringed in white straight on it read as an empty
   ring. */
.race-switch__toggle .race-chip,
.race-switch__label .race-chip {
  box-shadow: 0 0 0 var(--space-2) var(--text-inverse), inset 0 0 0 var(--space-2) var(--text-inverse);
}

.race-chip--blue {
  background: var(--race-blue);
}

.race-chip--orange {
  background: var(--race-orange);
}

.race-chip--teal {
  background: var(--race-teal);
}

.race-chip--plum {
  background: var(--race-plum);
}

.race-chip--pink {
  background: var(--race-pink);
}

.race-chip--green {
  background: var(--race-green);
}

/* The races to switch to, by race day: the account menu's panel and rows,
   opening under the switch from its left edge, six touch rows wide. On a
   tablet or a computer that always fits: the switch starts 72px into the
   bar (the gutter, the brand's 44px and the gap), so the menu ends by
   360px, inside the narrowest of them (641px). On a phone it hangs from
   the top bar instead (styles/_phone.scss). */
.race-switch__menu {
  position: absolute;
  left: 0;
  top: calc(100% + var(--space-6));
  width: calc(var(--touch-base) * 6);
  max-height: 70vh;
  overflow-y: auto;
  background: var(--surface-raised);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  padding: var(--space-6);
  z-index: var(--z-chrome);
}

.race-switch__day {
  margin: 0;
  padding: var(--space-8) var(--space-10) var(--space-4);
  font-size: var(--text-caption);
  font-weight: 700;
  color: var(--text-muted);
}

.race-switch__day + .race-switch__item {
  margin-top: 0;
}

.race-switch__item + .race-switch__day {
  margin-top: var(--space-6);
  border-top: 1px solid var(--border);
}

.race-switch__kept {
  font-weight: 400;
}

.race-switch__item-name {
  min-width: 0;
  overflow-wrap: anywhere;
}

/* The menu's last row (2.0.0, slice S10): every race day, on its own page. */
.race-switch__all {
  margin-top: var(--space-6);
  border-top: 1px solid var(--border);
}

.race-switch__all-icon {
  flex: none;
}

/* The "not on this race" page (views/not-on-race.ejs): the login's own
   races, each a row to tap. */
.not-on-race__mine {
  margin-top: var(--space-20);
  font-size: var(--text-base);
}

.not-on-race__list {
  list-style: none;
  margin: var(--space-8) 0 0;
  padding: 0;
  display: grid;
  gap: var(--space-4);
}

.not-on-race__race {
  display: flex;
  align-items: center;
  gap: var(--space-10);
  min-height: var(--touch-base);
  padding: var(--space-10);
  border-radius: var(--radius-sm);
  color: var(--accent);
  font-weight: 600;
  text-decoration: none;
}

@media (hover: hover) {
  .not-on-race__race:hover {
    background: var(--surface-sunken);
  }
}
.not-on-race__race:active {
  background: var(--border);
}

.not-on-race__day {
  color: var(--text-muted);
  font-weight: 400;
}

/* --------------------------------------------------------------- layout */
.wrap {
  max-width: var(--layout-max);
  margin: 0 auto;
  padding: var(--space-28) var(--layout-gutter) calc(var(--space-28) + var(--staff-conn-height, 0px));
}

h1 {
  font-size: var(--text-display);
  letter-spacing: -0.035em;
  margin: var(--space-4) 0;
  line-height: var(--leading-tight);
}

h2 {
  font-size: var(--text-lg);
  margin: 0 0 var(--space-12);
}

.sub {
  color: var(--text-muted);
  margin: 0 0 var(--space-20);
}

/* Race Control's pace line (1.11.0): the heading's sentence and the pace
   sentence in one cell, the one not shown kept unseen, so the line is as tall
   as the longer and a change of words moves nothing under it. */
.pace-line {
  display: grid;
}

.pace-line__words {
  grid-area: 1/1;
}

.pace-line__words.is-other {
  visibility: hidden;
}

a {
  color: var(--accent);
}

.card {
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
  padding: var(--space-24);
  margin-bottom: var(--space-20);
}

.card__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-12);
  flex-wrap: wrap;
  margin-bottom: var(--space-14);
}

.card__head h2 {
  margin: 0;
}

.grid2 {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: var(--space-20);
  align-items: stretch;
}

.grid2 > .card {
  display: flex;
  flex-direction: column;
  margin-bottom: 0;
}

/* A card after a row of cards gets the gap a card's own margin gives
   everywhere else; the cards inside the grid gave theirs up to the grid gap,
   so Screens' Compare card sat flush against the cards above it (DS-77). */
.grid2 + .card {
  margin-top: var(--space-20);
}

/* Push an address and the buttons that use it to the bottom together, so
   side-by-side cards agree on both, whatever length their descriptions
   happen to be (DS-79). Only the buttons were pushed before, so a longer
   description put one card's address lower than its neighbour's. */
.grid2 > .card > .card__foot {
  margin-top: auto;
  padding-top: var(--space-12);
}

.card__foot > p {
  margin: 0 0 var(--space-12);
}

/* Three cards as a set: Screens' Big Screen, Announcer and Lineup. Three
   across from 1024px, and one above the other below it, the phone included.
   Not three from 900: a third of 900-980px is narrower than
   "https://stage.wienerracing.app", so the real hosts broke inside the host
   name, "stage.wienerracing | .app" (DS-79) - and a third of an upright iPad
   is narrower still. Two across with the Lineup alone underneath, which is
   what an upright iPad had, broke the set two and one. */
.grid2--three {
  grid-template-columns: minmax(0, 1fr);
}

@media (min-width: 1024px) {
  .grid2--three {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}
/* The addresses start level whatever the host is. Pushed to the bottom on its
   own, a foot whose address took two lines started a line higher than its
   neighbours': the stage address wraps at a third of an iPad held sideways,
   the localhost one never does, so only the real hosts showed it (DS-79).
   Each card shares two rows with the cards beside it - its text, then its
   foot - so every foot starts on one line and ends on another: the
   addresses start level and the buttons end level, however many lines any
   address takes. Where subgrid is missing, the bottom-pushed foot above
   still keeps the buttons level. */
@supports (grid-template-rows: subgrid) {
  .grid2--three > .card {
    display: grid;
    grid-row: span 2;
    grid-template-rows: subgrid;
    row-gap: 0;
  }
  .grid2--three > .card > .card__foot {
    margin-top: 0;
    display: flex;
    flex-direction: column;
  }
  .grid2--three > .card > .card__foot > .btnrow {
    margin-top: auto;
  }
}
/* --------------------------------------------------------------- inputs */
label {
  display: block;
  font-weight: 600;
  font-size: var(--text-label);
  margin-bottom: var(--space-4);
  color: var(--text);
}

input[type=text], input[type=email], input[type=password], input[type=search], input[type=file], select, textarea {
  width: 100%;
  min-height: var(--touch-base);
  padding: var(--space-10) var(--space-14);
  border: 1px solid var(--border-field);
  border-radius: var(--radius-md);
  font: inherit;
  background: var(--surface-raised);
  color: var(--text);
}

textarea {
  resize: vertical;
  min-height: 68px;
}

/* The box a refusal is about - My account marks it aria-invalid beside the
   danger note that says why - wears the same danger edge, so the note and
   the box read as one error - focused too, where the focus ring goes round
   it (the :focus rule in _hover.scss would otherwise take the edge back). */
input[aria-invalid=true], select[aria-invalid=true], textarea[aria-invalid=true],
input[aria-invalid=true]:focus, select[aria-invalid=true]:focus, textarea[aria-invalid=true]:focus {
  border-color: var(--danger);
}

.field {
  margin-bottom: var(--space-12);
}

/* Event settings' "Race starts at" (1.11.0): a time field dressed as the
   boxes above, as wide as the time it holds rather than the card. */
.schedule__time {
  min-height: var(--touch-base);
  padding: var(--space-10) var(--space-14);
  border: 1px solid var(--border-field);
  border-radius: var(--radius-md);
  font: inherit;
  background: var(--surface-raised);
  color: var(--text);
}

.field__hint {
  font-size: var(--text-caption);
  color: var(--text-muted);
  font-weight: 400;
  margin-top: var(--space-4);
}

/* A live count under a box (the Champion's notes, a quick message): over its
   limit it turns the danger colour, the warning before a save is refused -
   the Rich tab has no maxlength to stop the typing (CN-R6). */
.field__count--over {
  color: var(--danger);
  font-weight: 700;
}

.search {
  font-size: var(--text-lg);
  padding: var(--space-14) var(--space-16);
}

/* Two px top and bottom, not six: the 44px Choose File button inside plus six
   each way made a 58px box beside a 48px Preview import (DS-75). The six at
   the sides keep Choose File off the input's edge. */
input[type=file] {
  padding: var(--space-2) var(--space-6);
}

input[type=file]::file-selector-button {
  min-height: var(--touch-min);
  margin-right: var(--space-12);
  padding: var(--space-8) var(--space-14);
  border: 1px solid var(--border-field);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--accent);
  font: inherit;
  font-weight: 700;
  cursor: pointer;
}

@media (hover: hover) {
  input[type=file]::file-selector-button:hover {
    background: var(--tint-blue);
  }
}
/* --------------------------------------------------------------- hover */
/* Every :hover in the staff stylesheet sits inside @media (hover: hover), and that is
   not a nicety. On iOS a tap leaves the element hovered until something else
   is tapped, so the Winner button stayed gold after recording a result and
   the picked dog's card kept its pressed border - Ryan, 2026-09-22: "Winner
   button was still colored after selecting a winner and approving it with
   winner button... Tapping somewhere else on screen fixes it though, like
   it's a leftover tap action hanging or something." The old rule here was
   "every :hover needs an :active answer", which fixes what the press looks
   like and not what is left behind afterwards. A phone and an iPad match
   (hover: none), so they now get the :active treatment and nothing sticky;
   a mouse still gets both. Anything added here follows the same shape:
   `X:active { ... }` in the open, `@media (hover: hover) { X:hover { ... } }`
   beside it. */
/* One focus treatment, on everything focusable. .btn used to have a :hover
   and no focus style at all, so keyboard focus vanished on the primary
   control of every screen. */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: var(--focus-width) solid var(--accent);
  outline-offset: var(--focus-offset);
  border-radius: var(--radius-sm);
}

.topbar :where(a, button):focus-visible {
  outline-color: var(--gold);
}

input:focus, select:focus, textarea:focus {
  border-color: var(--accent);
}

/* -------------------------------------------------------------- buttons */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-8);
  padding: var(--space-10) var(--space-16);
  border-radius: var(--radius-md);
  border: 1px solid transparent;
  background: var(--accent);
  color: var(--text-on-accent);
  font: inherit;
  font-weight: 700;
  cursor: pointer;
  min-height: var(--touch-base);
  text-decoration: none;
  white-space: nowrap;
  transition: background var(--dur-fast) var(--ease);
}

@media (hover: hover) {
  .btn:hover {
    background: var(--accent-hover);
  }
}
.btn:active {
  background: var(--accent-hover);
  transform: translateY(1px);
}

/* A real disabled treatment rather than 45% opacity, which left white text on
   a washed-out fill and made the label harder to read than the state. */
.btn:disabled {
  background: var(--surface-sunken);
  color: var(--text-muted);
  border-color: var(--border);
  cursor: not-allowed;
}

.btn:disabled:active {
  transform: none;
}

.btn--ghost {
  background: var(--surface-raised);
  color: var(--accent);
  border-color: var(--border-strong);
}

.btn--ghost:active {
  background: var(--tint-blue);
}

@media (hover: hover) {
  .btn--ghost:hover {
    background: var(--tint-blue);
  }
}
/* A ghost button that opens a panel says so while the panel is open, from
   its aria-expanded (Registration's Add a dog and Import spreadsheet). On a
   touch screen there is no hover, so nothing showed which one had been
   tapped (DS-70). Tinted and edged in the accent rather than filled: a filled
   toggle beside a form reads as that form's submit button. The inset line
   doubles the 1px edge so the state reads by eye, not only by a hairline. */
.btn--ghost[aria-expanded=true] {
  background: var(--tint-blue);
  border-color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent);
}

.btn--gold {
  background: var(--gold);
  color: var(--text-on-gold);
}

.btn--gold:active {
  background: var(--gold-hover);
}

@media (hover: hover) {
  .btn--gold:hover {
    background: var(--gold-hover);
  }
}
/* Green is for the two buttons that put one dog on the roster by hand, there
   and then - Add to roster and Restore - the same green as a checked-in dog
   (DS-73, as ruled). Every other primary action is the default blue, the
   approval queue's Approve and Import's "Import N dogs" included: Change
   password and Open Announcer were green for no reason. */
.btn--hops {
  background: var(--success);
  color: var(--text-on-success);
}

.btn--hops:active {
  background: var(--success-hover);
}

@media (hover: hover) {
  .btn--hops:hover {
    background: var(--success-hover);
  }
}
.btn--danger {
  background: var(--surface-raised);
  color: var(--danger);
  border-color: var(--tint-danger-line);
}

.btn--danger:active {
  background: var(--tint-danger);
}

@media (hover: hover) {
  .btn--danger:hover {
    background: var(--tint-danger);
  }
}
/* Was 40px. These are Check-In's Photo, Edit and Restore and every button in
   the action row under Race Control's current heat, Record Wiener included -
   the buttons pressed most often, standing up, under time. One height across
   that row, so nothing sits proud of it; the gold is what keeps Record Wiener
   the main button (DS-02, DS-99). */
.btn--sm {
  padding: var(--space-8) var(--space-12);
  min-height: var(--touch-min);
  font-size: var(--text-sm);
}

.btn--big {
  font-size: var(--text-lg);
  padding: var(--space-16) var(--space-24);
  min-height: 60px;
}

.btn--block {
  width: 100%;
}

/* A button whose words change with a pick (Held dogs' Place: "Place in this
   heat", "Add heat and place"; staff.js facedButton). Every face sits in one
   cell and the ones not in force are unseen, so the button is as wide as its
   widest words from the start and a pick moves neither it nor Cancel beside
   it (Ryan, 2026-09-25: a pick never moves a button). */
.btn--faces {
  display: inline-grid;
  justify-items: center;
}

.btn__face {
  grid-area: 1/1;
}

.btn__face.is-other {
  visibility: hidden;
}

.btn .icon {
  width: 18px;
  height: 18px;
}

.btnrow {
  display: flex;
  align-items: stretch;
  gap: var(--space-10);
  flex-wrap: wrap;
}

/* --------------------------------------------------------------- tables */
.tablewrap {
  overflow-x: auto;
}

table {
  width: 100%;
  border-collapse: collapse;
}

th {
  text-align: left;
  font-size: var(--text-caption);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--text-muted);
  padding: var(--space-8) var(--space-10);
  border-bottom: 2px solid var(--border);
  white-space: nowrap;
}

td {
  padding: var(--space-10) var(--space-10);
  border-bottom: 1px solid var(--border);
  vertical-align: middle;
}

tr.is-in td {
  background: var(--tint-hops);
}

tr.is-out td {
  color: var(--text-muted);
}

/* All three break inside a word if they have to. Real entries include a
   240-character fun fact typed without a single space, and an owner who
   pasted an address - either one takes the whole document into horizontal
   scroll without this. */
.dogname {
  font-weight: 700;
  font-size: var(--text-base);
  overflow-wrap: anywhere;
}

/* On Check-In the dog's name is the way to its own page, now that Photo takes
   the picture on the row instead of sending anyone there. A link in a list is
   a control, so it gets a control's target: the 18px one this replaces is what
   failed the harness in 1.5.0 and is why the name was plain text until now.
   Inline-flex rather than block, so the target is the name's own width and not
   the whole column. */
a.dogname {
  display: inline-flex;
  align-items: center;
  /* Both directions. A short name - Otto, Heidi - measured 35x44 and the
     screenshot harness failed it, rightly: a 44px floor is a floor on the
     target, not on the line height it happens to sit in. */
  min-width: var(--touch-min);
  min-height: var(--touch-min);
  color: var(--text);
  text-decoration: none;
}

a.dogname:active {
  color: var(--accent);
}

@media (hover: hover) {
  a.dogname:hover {
    color: var(--accent);
    text-decoration: underline;
  }
}
.dogowner {
  color: var(--text-muted);
  font-size: var(--text-sm);
  overflow-wrap: anywhere;
}

.funfact {
  color: var(--text-muted);
  font-size: var(--text-label);
  max-width: 34ch;
  overflow-wrap: anywhere;
}

/* One long fun fact must not stretch a lane row out of shape. */
.lane__who .funfact {
  max-width: 48ch;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* ---------------------------------------------------------------- toggle */
.toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-8);
  cursor: pointer;
  user-select: none;
  font-weight: 700;
  font-size: var(--text-sm);
  min-height: var(--touch-base);
  padding: var(--space-8) var(--space-6);
}

/* A switch that starts a line of card text lines up with it (Event settings). Its
   side padding put the switch 6px in from the words above it (DS-82);
   Check-In's row switch keeps it. */
.toggle--flush {
  padding-left: 0;
}

.toggle input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
}

.toggle__box {
  width: 52px;
  height: 30px;
  border-radius: var(--radius-pill);
  background: var(--border-strong);
  position: relative;
  transition: background var(--dur-fast) var(--ease);
  flex: none;
}

.toggle__box::after {
  content: "";
  position: absolute;
  top: 3px;
  left: 3px;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: #fff;
  transition: transform var(--dur-fast) var(--ease);
  box-shadow: var(--shadow-sm);
}

.toggle input:checked + .toggle__box {
  background: var(--success);
}

.toggle input:checked + .toggle__box::after {
  transform: translateX(22px);
}

.toggle input:focus-visible + .toggle__box {
  outline: var(--focus-width) solid var(--accent);
  outline-offset: var(--focus-offset);
}

/* ----------------------------------------------------------- choice list */
/* One choice of several, saved as it is picked (Event settings' "Introduce
   each heat"): a row of radios, each a whole 44px target with its words,
   the way the print pages choose their paper (.paper-choice). The picked one
   is told by more than the dot - the ghost buttons' open state - and the
   legend sits where a switch's words would, so the part reads like the
   switches around it. */
.choice-list {
  border: 0;
  padding: 0;
  margin: 0;
  min-width: 0;
}

.choice-list__legend {
  padding: 0;
  margin: 0 0 var(--space-8);
  font-weight: 700;
  font-size: var(--text-sm);
}

.choice-list__opts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-10);
}

.choice-list__opt {
  display: inline-flex;
  align-items: center;
  gap: var(--space-10);
  min-height: var(--touch-min);
  padding: var(--space-8) var(--space-16);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  background: var(--surface-raised);
  font-weight: 700;
  font-size: var(--text-sm);
  cursor: pointer;
  user-select: none;
}

.choice-list__opt:active {
  background: var(--tint-blue);
}

@media (hover: hover) {
  .choice-list__opt:hover {
    background: var(--tint-blue);
  }
}
.choice-list__opt:has(input:checked) {
  background: var(--tint-blue);
  border-color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent);
}

.choice-list__opt:has(input:focus-visible) {
  outline: var(--focus-width) solid var(--accent);
  outline-offset: var(--focus-offset);
}

.choice-list__opt input {
  width: var(--space-20);
  height: var(--space-20);
  margin: 0;
  flex: none;
  accent-color: var(--accent);
}

/* While a pick is being saved: nothing to press until the server has answered. */
.choice-list__opt:has(input:disabled) {
  cursor: default;
}

/* ---------------------------------------------------------------- misc */
.pill {
  display: inline-block;
  padding: var(--space-4) var(--space-10);
  border-radius: var(--radius-pill);
  font-size: var(--text-caption);
  font-weight: 700;
  background: var(--tint-neutral);
  color: var(--rustic);
  white-space: nowrap;
}

@media (prefers-color-scheme: dark) {
  .pill {
    color: var(--on-tint-gold);
  }
}
.pill--live {
  background: var(--danger);
  color: var(--text-on-danger);
}

.pill--done {
  background: var(--success);
  color: var(--text-on-success);
}

.pill--gold {
  background: var(--gold);
  color: var(--text-on-gold);
}

/* The Champion, and the dog that only moved on. Ryan's six-finalist event put
   the same gold Winner pill on all three Championship heats, so the Champion
   looked like nothing in particular: the gold now belongs to the final's
   winner alone, with the trophy on it, and a stage winner gets a deliberately
   quiet pill saying where they went instead. */
.pill--champion {
  display: inline-flex;
  align-items: center;
  gap: var(--space-4);
}

.pill--advance {
  background: var(--tint-blue);
  color: var(--accent);
  /* "Through to the Final" is longer than a lane is wide on a phone. */
  white-space: normal;
  max-width: 14ch;
  text-align: center;
}

/* The heat to get ready next, told apart from every later heat by more than
   its two words: the accent fill the lane numbers already wear, so it reads as
   the queue, quieter than the red At the line, and never the tinted chip a
   stage winner's "Through to the Final" wears (DS-49). After the dark-mode
   .pill colour rule above, which would otherwise win. */
.pill--next {
  background: var(--accent);
  color: var(--text-on-accent);
}

.pill .icon {
  width: 15px;
  height: 15px;
  vertical-align: -2px;
}

.stat {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 120px), 1fr));
  gap: var(--space-16) var(--space-28);
}

/* Registration puts its counters inside .card__head beside the action row.
   Without a basis to grow from, the grid sizes to its content and collapses
   to a single column on a phone. */
.card__head > .stat {
  flex: 1 1 240px;
}

.stat__n {
  font-size: var(--text-stat);
  font-weight: 800;
  line-height: 1;
  color: var(--accent);
}

.stat__l {
  font-size: var(--text-label);
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
}

.empty {
  color: var(--text-muted);
  padding: var(--space-24) var(--space-4);
  text-align: center;
}

/* Small layout helpers that replace inline style attributes in the views.
   A token cannot reach a style="margin-bottom:18px", so those values sat
   outside the scale no matter what :root said. */
.filter-row {
  margin-bottom: var(--space-12);
}

/* A short form reads better in one column than in a two-column grid that
   leaves a quarter of the card empty. */
.stack-form {
  max-width: 440px;
}

/* The Announcer's notes for the Champion on Event settings: lines the width of a
   comfortable sentence, wider than a password form, so a sponsor's name
   reads on one line as it will on the Announcer. */
.notes-form {
  max-width: var(--measure);
}

/* A panel disclosed inside a card gets a rule above it, so it reads as a new
   section rather than more of whatever it opened underneath. */
.disclosed {
  margin-top: var(--space-20);
  padding-top: var(--space-20);
  border-top: 1px solid var(--border);
}

.stat--spaced {
  margin-bottom: var(--space-20);
}

.sub--tight {
  margin-bottom: var(--space-14);
}

.field--bottom {
  align-self: end;
}

.steps {
  line-height: 1.9;
  margin: 0;
  padding-left: var(--space-20);
}

.dog-detail th {
  width: 120px;
}

.dog-fact {
  margin: var(--space-16) 0 0;
}

/* The dog page's Race status card: the pills, then one line saying where the
   dog is racing, the same size and colour whatever that line says (DS-84). The
   pills' paragraph gives up its own margins so the line sits one step under
   them, not two margins apart. */
.dog-pills {
  margin: 0;
}

.dog-status {
  margin: var(--space-10) 0 0;
}

/* About, when no details were sent: a sentence at the heading's left edge, not
   a table cell inset on a table rule (DS-88). */
.dog-empty {
  margin: 0;
  color: var(--text-muted);
  font-size: var(--text-sm);
}

/* Registration's import preview, which builds its own DOM in JavaScript. */
.import-note {
  margin: var(--space-8) 0;
}

.dupe-pair {
  margin-bottom: var(--space-10);
}

.dupe-row {
  display: flex;
  gap: var(--space-20);
  flex-wrap: wrap;
  padding: var(--space-8) 0;
}

.dupe-side {
  flex: 1 1 220px;
  min-width: 0;
}

.commit-row {
  margin-top: var(--space-14);
}

/* An address breaks where Screens allows it, between the host and the path
   (a <wbr>), and inside a word only when a part cannot fit a line by itself.
   break-all split "announc|er" on the stage host (DS-79). */
.url {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  overflow-wrap: anywhere;
}

/* Screens' Announcer card, on a race day of two races or more (2.0.0, S41): the race day's own Announcer's address under the race's own. */
.card__foot > .screens-day-announcer {
  margin-top: var(--space-16);
}

/* Each part of a path its own box (2.0.0, S40: /events, /fall-fun-day,
   /races, /all-dogs-race, /display): a race's own address breaks between its
   parts, never after a hyphen inside a race's or a race day's name, unless
   that part alone is longer than a line. */
.url__part {
  display: inline-block;
}

/* The Big Screen layout choices. One row per layout so the name, what it does
   and its two actions stay together when the row wraps on a phone. */
.layouts {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--space-10);
}

.layout {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-10);
  padding: var(--space-12) 0;
  border-top: 1px solid var(--border);
}

.layout:first-child {
  border-top: 0;
  padding-top: 0;
}

.layout > div:first-child {
  flex: 1 1 260px;
  min-width: 0;
}

.layout__name {
  margin: 0 0 var(--space-2);
  font-size: var(--text-base);
  font-weight: 800;
}

/* Screens at each box (2.0.0, S34): a box's name and lanes, then a row a
   screen - its name and its two actions - the rows one under another across
   the card's width, so a phone keeps each screen's buttons beside its name. */
.box-screens__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--space-8);
  flex: 1 1 100%;
}

.box-screens__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-8);
}

.box-screens__name {
  font-weight: 700;
}

.box-screens__turns {
  flex: 1 1 100%;
  margin: var(--space-4) 0 0;
}

/* Which build is serving this page. Deliberately faint: it is there to be
   looked for after a deploy, not to be read the rest of the time. */
.buildstamp {
  margin: var(--space-24) 0 0;
  text-align: center;
  font-size: var(--text-micro);
  letter-spacing: 0.03em;
  color: var(--text-muted);
}

/* The stamp is faint on purpose; its link still has to be a real target, so it
   takes the full touch height rather than only the height of micro text. It
   gets height, not side padding: padding set the second dot twice as far from
   the link as the first (DS-96), and the link's words are already far wider
   than 44px. */
.buildstamp a {
  display: inline-flex;
  align-items: center;
  min-height: var(--touch-min);
  color: inherit;
}

/* Release notes. One card per version, newest first, with the running build
   called out - the point is to make a build reviewable by reading it. */
.release__head {
  align-items: baseline;
}

/* The date is last in the head and pushed to the right, so every release's
   date lines up down the page. The Running now pill sits beside the version,
   before it: after the date, it pushed that one date out of line (DS-93). */
.release__date {
  margin-left: auto;
  font-size: var(--text-sm);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

/* Each release's list of changes folds behind one row, open only for the
   release that is running (DS-90). The row is a full touch target; its
   chevron points right while closed and down while open. */
.release__more > summary {
  list-style: none;
  cursor: pointer;
  color: var(--accent);
  font-size: var(--text-sm);
  font-weight: 700;
  border-radius: var(--radius-sm);
}

.release__more > summary::-webkit-details-marker {
  display: none;
}

.release__toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-6);
  min-height: var(--touch-min);
}

.release__more > summary:active {
  color: var(--accent-hover);
}

@media (hover: hover) {
  .release__more > summary:hover {
    text-decoration: underline;
  }
}
.release__chevron {
  flex: none;
  transform: rotate(-90deg);
  transition: transform var(--dur-fast) var(--ease);
}

/* Each fold's own chevron only: a screen's row inside an open release keeps
   pointing right until that screen is opened too. */
.release__more[open] > summary .release__chevron,
.release__group[open] > summary .release__chevron {
  transform: none;
}

.release__more[open] > summary {
  margin-bottom: var(--space-8);
}

/* A closed <details> hides its list but Chrome still lays it out: 1.9.0's
   folded list kept a 19,000px box below the page, which anything measuring
   the page - the ui-check harness's content-under-nav audit among them -
   read as content. Out of layout while closed. */
.release__more:not([open]) > .release__groups,
.release__group:not([open]) > .release__list {
  display: none;
}

/* A release's screens, one row each, ruled apart: the screen's name, then
   how many changes it holds. */
.release__groups {
  border-top: 1px solid var(--border);
}

.release__group {
  border-bottom: 1px solid var(--border);
}

.release__group > summary {
  list-style: none;
  cursor: pointer;
  border-radius: var(--radius-sm);
}

.release__group > summary::-webkit-details-marker {
  display: none;
}

.release__group > summary .release__toggle {
  display: flex;
  align-items: center;
  padding: var(--space-4) 0;
}

/* The screen's name and its count run as one line of text beside the
   chevron, so a long name wraps under itself, never under the chevron. */
.release__group-name {
  min-width: 0;
}

.release__group > summary .release__where {
  display: inline;
  margin: 0 var(--space-4) 0 0;
}

.release__group > summary:active {
  background: var(--tint-blue);
}

@media (hover: hover) {
  .release__group > summary:hover .release__where {
    text-decoration: underline;
  }
}
.release__count {
  font-size: var(--text-sm);
  color: var(--text-muted);
}

.release__group[open] > .release__list {
  padding: var(--space-4) 0 var(--space-14);
}

.release__list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: var(--space-12);
}

.release__change {
  max-width: var(--measure);
  border-left: 3px solid var(--border-strong);
  padding: var(--space-4) 0 var(--space-4) var(--space-14);
}

[data-release-current] .release__change {
  border-left-color: var(--accent);
}

/* Not uppercased: a `where` may carry a URL Devin has to type, and
   text-transform would turn /display?variant=heat-focus into a query string
   that is wrong in a place where case matters. */
.release__where {
  margin: 0 0 var(--space-4);
  font-size: var(--text-sm);
  font-weight: 800;
  letter-spacing: 0.02em;
  color: var(--accent);
}

.release__what {
  margin: 0 0 var(--space-6);
}

.release__check {
  margin: 0;
  font-size: var(--text-sm);
  color: var(--text-muted);
}

.release__check strong {
  color: var(--text);
}

.note {
  background: var(--tint-gold);
  border: 1px solid var(--tint-gold-line);
  border-radius: var(--radius-sm);
  padding: var(--space-12) var(--space-14);
  font-size: var(--text-sm);
  color: var(--on-tint-gold);
  margin-bottom: var(--space-14);
}

.note--danger {
  background: var(--tint-danger);
  border-color: var(--tint-danger-line);
  color: var(--on-tint-danger);
}

.note--ok {
  background: var(--tint-hops);
  border-color: var(--tint-hops-line);
  color: var(--on-tint-hops);
}

/* An approval queue entry's details: each one whole, moving to the next line
   as a unit, so a phone number or a submitted time is never split across two
   lines (public/js/registration.js renderQueue). One wider than the whole
   line, such as a long email address, still breaks rather than running out
   of the card. */
.queue-fact {
  margin-bottom: var(--space-4);
}

.queue-details {
  margin-top: 0;
}

.queue-detail {
  display: inline-block;
  max-width: 100%;
  overflow-wrap: anywhere;
}

/* Registration's Google Sheet (2.0.0, PLAN S15): the line saying which
   sheet the race day reads, then its link's box and Save sheet. On a race
   day of two races or more a New dog card approves into a race: each button
   its race's dot, the filled one's dot in a disc of the button's own text
   colour (a ring outside and in, as the race switch draws it), so a blue
   race's dot still shows on the blue button. */
.sheet-line {
  margin: 0 0 var(--space-12);
}

.sheet-form {
  margin: 0 0 var(--space-16);
}

.sheet-form .field {
  max-width: var(--measure);
}

.queue-approve:not(.btn--ghost) .race-chip {
  box-shadow: 0 0 0 var(--space-2) var(--text-on-accent), inset 0 0 0 var(--space-2) var(--text-on-accent);
}

/* "Capitalized: dog name, trainer" under a row the server capitalized, in the
   import preview's Dog cell and on an approval queue card (public/js/
   registration.js capitalizedNote, src/name-style.js): a remark about the
   values above it, small and muted, not a warning. */
.caps-note {
  font-size: var(--text-caption);
  color: var(--text-muted);
  margin: var(--space-4) 0 0;
}

/* Words for a screen reader only: in the page, and read out, but drawn
   nowhere and taking no room. Race Control's "Saving result…" is one: as a
   line over the heat it pushed the dogs and every button under them down
   while a result saved, and the heading's pill now shows it instead
   (race.js renderCurrent). Clipped to nothing rather than display: none or
   zero-sized, which some screen readers skip. */
.visually-hidden {
  position: absolute;
  width: var(--space-2);
  height: var(--space-2);
  margin: 0;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* -------------------------------------------------------------- heatcard */
.heat {
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: var(--space-16);
  background: var(--surface-raised);
}

.heat--live {
  border-color: var(--danger);
  border-width: 2px;
}

/* The heat that ends the event, set apart from the heats that feed it. On a
   board of identical cards the Final was findable only by counting, which is
   the reading that turned one Champion into three winners. Gold, from the same
   token the Championship round heading already uses. */
.heat--final {
  border-color: var(--gold);
  border-width: 2px;
  background: var(--tint-gold);
}

/* At the line it is the race, and the race is red like every other heat at
   the line; the gold wash still says which heat it is. It was gold-bordered
   with only a small red pill, because this rule came first (DS-13). */
.heat--final.heat--live {
  border-color: var(--danger);
}

/* The trophy is a flex item, so a title too long for the card wraps under its
   own first word rather than under the trophy (DS-30). The pinned current heat
   gives the Final the same mark, at the same size (DS-14). */
.heat--final .heat__title {
  color: var(--on-tint-gold);
  display: flex;
  align-items: center;
  gap: var(--space-6);
}

.heat--final .heat__title .icon, .current-panel--final .card__head h2 .icon {
  width: 20px;
  height: 20px;
  color: var(--gold);
  flex: none;
}

.heat__head {
  display: flex;
  /* The pill drops under the title when both do not fit, instead of squeezing
     "Championship Final" onto two lines with "Final" alone under the trophy
     (DS-30). */
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-10);
  margin-bottom: var(--space-10);
}

.heat__title {
  font-weight: 800;
  font-size: var(--text-base);
  margin: 0;
}

.lane {
  display: flex;
  align-items: center;
  gap: var(--space-12);
  min-height: var(--touch-row);
  padding: var(--space-10) 0;
  border-top: 1px solid var(--border);
}

.lane__n {
  flex: none;
  width: var(--lane-badge);
  height: var(--lane-badge);
  border-radius: var(--radius-sm);
  background: var(--accent);
  color: var(--text-on-accent);
  display: grid;
  place-items: center;
  font-weight: 800;
  font-size: var(--text-sm);
}

.lane__who {
  flex: 1 1 auto;
  min-width: 0;
}

/* A heat lane is something you act on, not prose. Resting a finger on a dog's
   name on an iPad otherwise selects the word and raises the copy/lookup callout
   - which reads as the app doing something, on a screen where the next tap
   records a race result. Left selectable everywhere else, including the Roster,
   so a name or a fun fact can still be copied. */
.current-lane, .bracket-lane {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}

.lane__acts {
  display: flex;
  gap: var(--space-6);
  flex: none;
  flex-wrap: wrap;
  justify-content: flex-end;
}

.lane--winner .lane__n {
  background: var(--gold);
  color: var(--text-on-gold);
}

/* The name is a bare <strong> on both boards: .lane--winner .dogname never
   matched it, so a winner's name stayed near-black (DS-33). */
.lane--winner .lane__who strong {
  color: var(--success);
}

/* ---------------------------------------------------------------- toast */
#toast {
  position: fixed;
  left: 50%;
  bottom: calc(var(--staff-tabbar-height, 0px) + var(--staff-conn-height, 0px) + var(--space-20));
  /* The hidden position has to clear the whole offset, not a guess at it.
     `translateY(140%)` was 140% of the TOAST'S OWN height, while the distance it
     needs to fall is its height plus however far `bottom` holds it off the
     viewport edge - and on a phone that includes the tab bar. Measured at 393px
     wide on Race Control: a 70px toast, a 57px tab bar, so it had to travel
     147px and moved 99, parking 49px of itself on top of the bottom row of
     buttons and staying there until the screen was left. The two numbers could
     never agree, because one scaled with the message length and the other with
     the chrome.

     `opacity` and `pointer-events` are the belt to that braces: a toast is never
     something to tap, so it may not intercept one even mid-transition, with
     motion reduced, or if a transition is interrupted. */
  transform: translateX(-50%) translateY(calc(100% + var(--staff-tabbar-height, 0px) + var(--staff-conn-height, 0px) + var(--space-20)));
  opacity: 0;
  pointer-events: none;
  background: var(--text);
  color: var(--surface);
  padding: var(--space-12) var(--space-20);
  border-radius: var(--radius-md);
  font-weight: 600;
  box-shadow: var(--shadow-modal);
  transition: transform var(--dur-base) var(--ease), opacity var(--dur-base) var(--ease);
  z-index: var(--z-toast);
  /* As wide as its words, up to the cap. A fixed box at left: 50% may only be
     as wide as the other half of the screen unless it is told otherwise, so
     on a 393px phone every toast was held to about 240px and a two-sentence
     one - "2 new registrations queued. 1 dog already on the roster gained
     details from the sheet..." - stood six lines tall over the page. */
  width: max-content;
  max-width: min(92vw, 560px);
  text-align: center;
}

#toast.is-up {
  transform: translateX(-50%) translateY(0);
  opacity: 1;
}

/* A notice that names who made a change (1.11.0, Event settings' "Devin
   turned off Take a break between rounds."): their round initial badge,
   then the words, on one row. */
#toast.has-badge {
  display: flex;
  align-items: center;
  gap: var(--space-10);
  text-align: left;
}

#toast.is-bad {
  background: var(--danger);
  color: var(--text-on-danger);
}

#toast.is-good {
  background: var(--success);
  color: var(--text-on-success);
}

/* A toast that carries an Undo is the one exception to "a toast is never
   something to tap" (R-08). It takes pointer events back while it is up, lays
   its message and its button out on one row, and keeps the button at the full
   44x44 - it is the fastest way to take a move or a result back, which is the
   whole point of it being here rather than at the top of the page.

   ONE row, not a paragraph with a button under it. Ryan, watching one during
   the race-day walkthrough: "if we can distribute it more horizontally than
   vertically than it is. It took up a lot of vertical screen realestate of
   useful details." So the message is held to a single line at --text-sm and
   cut with an ellipsis if it will not fit: it confirms something just done -
   "Placement saved", "Result saved: Bailey wins" - it is not something to
   read. So what was done comes first and the dog's name after it: a long
   name then cuts itself, never the words that say the save happened (TN-04). */
#toast.has-undo {
  display: flex;
  align-items: center;
  gap: var(--space-12);
  text-align: left;
  padding: var(--space-8) var(--space-10) var(--space-8) var(--space-20);
  /* Gap and padding are not in the list: they no longer change (is-slim below). */
  transition: transform var(--dur-base) var(--ease), opacity var(--dur-base) var(--ease), background-color var(--dur-base) var(--ease), box-shadow var(--dur-base) var(--ease);
}

#toast.has-undo.is-up {
  pointer-events: auto;
}

.toast__text {
  flex: 1 1 auto;
  min-width: 0;
}

#toast.has-undo .toast__text {
  font-size: var(--text-sm);
  max-width: 100%;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  transition: opacity var(--dur-base) var(--ease);
}

/* Its second life (public/js/staff.js TOAST_MS): three seconds in, the message
   fades and the dark bar goes with it, but the box keeps its width and
   padding, so the button is left exactly where the thumb last saw it - still
   a full 44x44 - for another three seconds. "The undo gets smaller and more
   out of the way while still being there." Collapsing the message used to
   re-centre the toast around the button alone, which moved Undo about 68px
   left and 8px down on a phone (DS-36). The empty box takes no taps; only the
   button does. Two classes beat #toast.is-good's background whatever the
   order, so a good or bad toast fades out from under the button too. */
#toast.has-undo.is-slim {
  background: transparent;
  box-shadow: none;
}

#toast.has-undo.is-slim .toast__text {
  opacity: 0;
}

.toast__undo {
  flex: 0 0 auto;
  min-height: var(--touch-min);
  background: var(--surface-raised);
  color: var(--text);
  border-color: var(--border-strong);
  /* Its own elevation, because for half its life there is no bar behind it. */
  box-shadow: var(--shadow-lg);
}

.toast__undo:active {
  background: var(--surface-sunken);
}

@media (hover: hover) {
  .toast__undo:hover {
    background: var(--surface-sunken);
  }
}
/* The slim toast's empty box takes no taps; only its button does (DS-36).
   After the base .toast__undo rule, which test/staff-nav.test.js reads first. */
#toast.has-undo.is-up.is-slim {
  pointer-events: none;
}

#toast.has-undo.is-up.is-slim .toast__undo {
  pointer-events: auto;
}

/* A toast whose button does something other than Undo (Staff.toast's icon
   option): Race Control's "Big Screen: Break" with Set times
   (Ryan, 23:42). Its message is not a confirmation to glance at but the name
   of what the TV now shows, so it is never cut: from 375px up it takes two
   lines beside the button, which is 44px tall anyway, so the toast is no
   taller than an Undo toast; on a 320px phone it takes three (66px), which
   is accepted rather than drop the button's word. */
#toast.has-undo.is-action .toast__text {
  white-space: normal;
  text-overflow: clip;
  text-wrap: balance;
  line-height: var(--leading-tight);
}

.conn {
  /* A strip across the bottom of the screen, directly over the phone's tab
     row, in the body's small size rather than a caption in a corner. It was
     a 12px pill in the bottom-right corner, over whatever was there - on an
     iPhone the bottom of a dog's row, just above the tabs - and the paused
     one was white on the near-white page, easy to miss. The page reserves
     its height while it shows (--staff-conn-height, measured by staff.js
     like the tab row's), so the end of the page scrolls clear of it and a
     toast rises above it. */
  position: fixed;
  left: 0;
  right: 0;
  bottom: var(--staff-tabbar-height, 0px);
  padding: var(--space-8) max(var(--space-16), env(safe-area-inset-right)) calc(var(--space-8) + env(safe-area-inset-bottom)) max(var(--space-16), env(safe-area-inset-left));
  font-size: var(--text-sm);
  font-weight: 700;
  line-height: var(--leading-tight);
  text-align: center;
  background: var(--danger);
  color: var(--text-on-danger);
  box-shadow: var(--shadow-up);
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease);
  pointer-events: none;
  z-index: var(--z-conn);
}

.conn.is-shown {
  opacity: 1;
}

/* The same strip, but not an alarm: the screen paused itself because nobody
   was looking, and any tap brings it back. The accent fill, which reads
   against the page in light and dark alike (white on the brand blue is about
   7.5:1), where the page's own white vanished into it. */
.conn.is-paused {
  background: var(--accent);
  color: var(--text-on-accent);
}

/* --------------------------------------------------------------- login */
/* The login page carries a little of the Big Screen: its light blue diamond
   field, and the dogs that have checked in coming up one at a time in a gold
   ring under "Who'll Be the Grand “Wiener”?" - "Thanks for Coming Out!" once
   the Final is over - between Devin's two mascots (Ryan, 2026-09-24: "Can we
   get these dogs and styling mixed into the login page as well?").
   views/partials/login-show.ejs is the show, public/js/login-show.js runs it.

   The form is still the page's job. It comes first - first in the page, at
   the top when the two are stacked and on the left when they stand side by
   side - and it is laid out before the show and without it: the show is a
   size container, so nothing in it can make its track bigger, and it takes
   only the room the form leaves. On a phone that is a band under the form;
   where the form leaves too little (a phone held sideways), it is nothing. */
.login {
  /* 100dvh, with a 100vh fallback for browsers that do not know it. Both
     kiosk files already do this; the login card used to shift as Safari's
     toolbars collapsed. */
  min-height: 100vh;
  min-height: 100dvh;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: auto minmax(0, 1fr);
  justify-items: center;
  align-items: start;
  row-gap: var(--space-12);
  background: conic-gradient(from 45deg at 50% 50%, var(--show-lattice) 25%, transparent 0 50%, var(--show-lattice) 0 75%, transparent 0) 0 0/var(--show-tile) var(--show-tile), var(--show-field);
  padding: max(var(--space-20), env(safe-area-inset-top)) max(var(--space-20), env(safe-area-inset-right)) max(var(--space-20), env(safe-area-inset-bottom)) max(var(--space-20), env(safe-area-inset-left));
}

.login__card {
  background: var(--surface-raised);
  color: var(--text);
  border-radius: var(--radius-lg);
  padding: var(--space-28) var(--space-28);
  width: min(100%, var(--login-card));
  box-shadow: var(--shadow-modal);
}

/* The event's lettering, as on the Big Screen's masthead: the title face,
   blue over the gold offset. The same height as the sans title it replaces
   on a phone, so the card is no taller there. One line always: laid out in
   the fallback serif for the moment before the inline face is ready, the
   title would wrap and the fields under it jump when the face arrived. */
.login__title {
  font-family: var(--title-font);
  font-size: var(--text-display);
  font-weight: 800;
  line-height: 1.1;
  letter-spacing: 0.01em;
  color: var(--accent);
  text-shadow: var(--title-3d);
  white-space: nowrap;
  margin: 0 0 var(--space-2);
}

.login__sub {
  color: var(--text-muted);
  font-size: var(--text-sm);
  margin: 0 0 var(--space-20);
}

.login__mark {
  width: 52px;
  height: 52px;
  color: var(--bav-blue);
  margin-bottom: var(--space-4);
}

@media (prefers-color-scheme: dark) {
  .login__mark {
    color: var(--accent);
  }
}
/* An iPad on its side and a computer: the form on the left, in the middle
   of the height, and the show in the rest of the width. Upright, and on a
   phone, the show is under the form. */
@media (min-width: 900px) and (min-aspect-ratio: 1/1) {
  .login {
    grid-template-columns: minmax(0, var(--login-card)) minmax(0, 1fr);
    grid-template-rows: minmax(0, 1fr);
    align-items: center;
    column-gap: var(--space-28);
    padding-inline-start: max(var(--space-28) * 2, env(safe-area-inset-left));
  }
  .login-show {
    align-self: stretch;
  }
}
/* ---------------------------------------------------- the dog show */
/* A size container: its track is sized by the page, never by what is in it,
   and everything in it is sized from its box (cq units). It is decoration -
   aria-hidden, nothing in it can be focused or tapped - and it is not drawn
   until the script knows what to show (data-state), then fades in once. With
   no script it is never drawn at all.
   Its room is declared here, as the kiosks declare theirs (--cqh, --cqw), and
   everything in it is sized from those, never from a raw cq unit: the helper
   tablets may be the 2015 iPad Pro, whose Safari 16 measures cq units to the
   outside of a container's edge (agent/kiosk-screens.md, "Safari 16";
   test/kiosk-safari16.test.js holds this sheet to the kiosks' rules). This
   box has no padding and no edge, so on every engine its room is all of it. */
.login-show {
  container: login-show/size;
  --cqh: 100cqh;
  --cqw: 100cqw;
  justify-self: stretch;
  align-self: stretch;
  display: grid;
  opacity: 0;
  transition: opacity var(--dur-show-in) ease-out;
  pointer-events: none;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}

.login-show[data-state=dogs],
.login-show[data-state=mascots] {
  opacity: 1;
}

/* Before it changes what it is (dogs to mascots and back, the ribbon's
   words) the show fades out just as it is drawn - its layout left alone
   until it is out of sight - and the script makes the change then. */
.login-show[data-fading] {
  opacity: 0;
}

/* Upright (a tall box: under the form on a phone or an upright iPad, and
   beside it on a computer): the title, the ring, the name, and the mascots
   side by side under them, facing each other - the Big Screen held upright.
   Every size is a share of --u, one hundredth of the box's height unless its
   width is the tighter: 97.7 of them tall, and 1.28 of the width at most, so
   the wider mascot (1.3675 times its height, 0.6 of the ring's) never
   passes its half of the row. */
.login-show__stage {
  --u: min(calc(var(--cqh) / 100), calc(var(--cqw) * 0.0128));
  --title-size: calc(var(--u) * 4.6);
  --ring: calc(var(--u) * 46);
  --name-size: calc(var(--u) * 4.6);
  --mascot: calc(var(--ring) * 0.6);
  width: 100%;
  height: 100%;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  grid-template-rows: auto auto calc(var(--name-size) * 2.4) auto;
  grid-template-areas: "title title" "ring ring" "caption caption" "left right";
  align-content: center;
  justify-items: center;
  column-gap: calc(var(--u) * 0.8);
  row-gap: calc(var(--u) * 2);
  background: radial-gradient(58% 52% at 50% 50%, var(--show-glow), transparent 75%);
}

/* The title: one of two constants, so its size is worked out rather than
   fitted - "Who'll Be the Grand “Wiener”?", the longer, makes the ribbon
   13.2 of its font sizes wide, and --title-size never lets that pass
   the box. --show-title-ink over the navy offset, on a white ribbon with a
   gold edge, as on the Big Screen. Once the Final has crowned the Grand
   Wiener it thanks everyone instead (data-final, from the list). */
.login-show__title {
  grid-area: title;
  margin: 0;
  padding: 0.2em 0.6em;
  font-family: var(--title-font);
  font-size: var(--title-size);
  font-weight: 800;
  line-height: 1.1;
  white-space: nowrap;
  color: var(--show-title-ink);
  text-shadow: var(--show-title-3d);
  background: var(--show-ribbon);
  border: max(var(--space-2), 0.06em) solid var(--gold);
  border-radius: var(--radius-pill);
  box-shadow: var(--show-shadow);
}

.login-show__thanks {
  display: none;
}

.login-show[data-final] .login-show__ask {
  display: none;
}

.login-show[data-final] .login-show__thanks {
  display: inline;
}

/* The winner cards' ring: gold, a pale gold halo, lifted off the diamonds.
   One ring, which never moves; only what is in its window crosses. Its size
   is fixed before any picture arrives, so a picture landing moves nothing.
   Its height is set outright, the same as its width, never through
   aspect-ratio: Safari 16 lays out the rows of a grid whose height comes
   from aspect-ratio as if its border took no room, so the window would
   stand out of the ring by the border, top and bottom (the Big Screen's
   idle ring, 2026-09-24). */
.login-show__ring {
  grid-area: ring;
  display: grid;
  width: var(--ring);
  height: var(--ring);
  border: max(var(--space-2), var(--ring) * 0.016) solid var(--gold);
  border-radius: var(--radius-pill);
  background: var(--tint-blue);
  box-shadow: 0 0 0 max(var(--space-2), var(--ring) * 0.012) var(--tint-gold), var(--show-shadow);
}

.login-show__window {
  display: grid;
  grid-template: minmax(0, 1fr)/minmax(0, 1fr);
  overflow: hidden;
  border-radius: var(--radius-pill);
  clip-path: circle(50%);
}

.login-show__picture {
  display: grid;
  grid-template: minmax(0, 1fr)/minmax(0, 1fr);
}

.login-show__photo {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* A dog with no picture, or with photos switched off: the app's mark on the
   tint, the stand-in every staff screen draws. */
.login-show__picture--mark {
  place-items: center;
  background: var(--tint-blue);
  color: var(--accent);
}

.login-show__picture--mark .icon {
  width: 62%;
  height: 62%;
}

/* The name, whole: never an owner, never cut. Two lines' room at full size;
   a longer name steps its type down (data-fit, the script) and only at the
   last step may break a word. */
.login-show__captions {
  grid-area: caption;
  align-self: stretch;
  justify-self: stretch;
  display: grid;
  grid-template: minmax(0, 1fr)/minmax(0, 1fr);
  padding: 0 calc(var(--u) * 3);
}

.login-show__caption {
  display: block;
  overflow: hidden;
  margin: 0;
  font-size: var(--name-size);
  font-weight: 800;
  line-height: 1.2;
  text-align: center;
  text-wrap: balance;
  color: var(--text);
}

.login-show__caption[data-fit="1"] {
  font-size: calc(var(--name-size) * 0.85);
}

.login-show__caption[data-fit="2"] {
  font-size: calc(var(--name-size) * 0.72);
}

.login-show__caption[data-fit="3"],
.login-show__caption[data-fit="4"] {
  font-size: calc(var(--name-size) * 0.6);
}

.login-show__caption[data-fit="4"] {
  overflow-wrap: anywhere;
}

/* Devin's mascots, lederhosen on the left looking right and dirndl on the
   right looking left. Each has its drawing's shape before it loads, and one
   that will not load keeps its place empty, so nothing moves either way.
   Each is a box holding its picture (1.12.0), which _mascot-rig.scss makes
   wag and pant with Play animations on. */
.login-show__mascot {
  display: block;
  grid-area: left;
  justify-self: end;
  align-self: end;
  height: var(--mascot);
  width: auto;
  aspect-ratio: 1094/800;
}

.login-show__mascot--dirndl {
  grid-area: right;
  justify-self: start;
  aspect-ratio: 945/800;
}

.login-show__mascot.is-gone {
  visibility: hidden;
}

/* The crossfade, the Big Screen's: the next dog's picture fades in from a
   little to the right and a little larger and settles, while the last one
   fades out to the left - always covering the window, so no edge shows.
   The names take turns rather than cross, so two are never read at once. */
.login-show__slide {
  grid-area: 1/1;
  opacity: 0;
  transition: opacity var(--dur-crossfade) ease-out, transform var(--dur-crossfade) var(--ease-crossfade);
}

.login-show__slide.is-on {
  opacity: 1;
  transform: none;
}

.login-show__slide.is-off {
  opacity: 0;
  transition: opacity var(--dur-crossfade) ease-in, transform var(--dur-crossfade) var(--ease-crossfade);
}

.login-show__picture {
  transform: translateX(3%) scale(1.08);
}

.login-show__picture.is-off {
  transform: translateX(-3%) scale(1.08);
}

.login-show__caption {
  transform: translateX(2%) scale(0.96);
}

.login-show__caption.is-on {
  transition: opacity calc(var(--dur-crossfade) / 2) ease-out calc(var(--dur-crossfade) / 2), transform var(--dur-crossfade) var(--ease-crossfade);
}

.login-show__caption.is-off {
  transform: translateX(-2%) scale(0.96);
  transition: opacity calc(var(--dur-crossfade) / 2) ease-in, transform var(--dur-crossfade) var(--ease-crossfade);
}

/* With nobody to show - nobody has checked in yet, or the list could not be
   had - the two mascots alone under the title, facing each other in the
   middle, a little bigger. The script decides this before the show is
   drawn, and a later change to the dogs or back is made while the show is
   faded out (data-fading), so it is never seen. */
.login-show[data-state=mascots] .login-show__stage {
  --mascot: min(calc(var(--u) * 40), calc(var(--cqw) * 0.35));
  grid-template-rows: auto auto;
  grid-template-areas: "title title" "left right";
}

.login-show[data-state=mascots] .login-show__ring,
.login-show[data-state=mascots] .login-show__captions {
  display: none;
}

/* A wide, short box - the room a small phone leaves under the form: one row,
   the ring between the mascots, and the name under all three. The ring is
   bigger this way once the box is wider than 4:3. The three stand together
   in the middle however wide the box is (a desktop window under the form),
   rather than the mascots going off to its edges. */
@container login-show (min-aspect-ratio: 4/3) {
  .login-show__stage {
    --u: min(calc(var(--cqh) / 100), calc(var(--cqw) * 0.0062));
    justify-content: center;
    --title-size: calc(var(--u) * 8.5);
    --ring: calc(var(--u) * 56);
    --name-size: calc(var(--u) * 8.5);
    --mascot: calc(var(--ring) * 0.66);
    grid-template-columns: calc(var(--mascot) * 1.3675) auto calc(var(--mascot) * 1.3675);
    grid-template-rows: auto auto calc(var(--name-size) * 2.4);
    grid-template-areas: "title title title" "left ring right" "caption caption caption";
    row-gap: calc(var(--u) * 3);
    column-gap: calc(var(--u) * 2);
  }
  .login-show__mascot--dirndl {
    justify-self: start;
  }
  .login-show[data-state=mascots] .login-show__stage {
    --mascot: min(calc(var(--u) * 62), calc(var(--cqw) * 0.38));
    grid-template-columns: auto auto;
    grid-template-rows: auto auto;
    grid-template-areas: "title title" "left right";
  }
}
/* Too little room for a ring anyone could see - a phone in Safari with the
   error note showing, an old SE: no show, and the script asks for no picture
   while the stage has no box. A phone held sideways has no show at all, and
   its form is in the middle as before (_short-viewport.scss). */
@container login-show (max-height: 150px) {
  .login-show__stage {
    display: none;
  }
}
@container login-show (max-width: 240px) {
  .login-show__stage {
    display: none;
  }
}
/* For anyone who has asked for less motion the dogs change with a cut (the
   global block ends every transition at once) and more slowly (the
   script), and nothing slides. */
@media (prefers-reduced-motion: reduce) {
  .login-show__slide,
  .login-show__slide.is-off,
  .login-show__picture.is-off,
  .login-show__caption.is-off {
    transform: none;
  }
}
/* Event settings' Animations switch off (data-motion="cut", from the list):
   the same cut for everyone - the show comes on, and each dog comes on,
   without a fade or a glide. */
.login-show[data-motion=cut],
.login-show[data-motion=cut] .login-show__slide,
.login-show[data-motion=cut] .login-show__slide.is-on,
.login-show[data-motion=cut] .login-show__slide.is-off,
.login-show[data-motion=cut] .login-show__caption.is-on,
.login-show[data-motion=cut] .login-show__caption.is-off {
  transition: none;
}

.login-show[data-motion=cut] .login-show__slide,
.login-show[data-motion=cut] .login-show__picture.is-off,
.login-show[data-motion=cut] .login-show__caption.is-off {
  transform: none;
}

/* mascot-rig:start - written by scripts/mascots/make-css.py from public/img/mascot-masks/,
   rig.json and motion.json; change those and run it again, never this by hand. Devin's mascots wag
   and pant (1.12.0, Ryan, FB-20260930-RY-01). Each mascot is a box (div.mascot.mascot--X) that keeps
   the rules that size and place it, and its picture (.mascot__pic) fills it. With Play animations on
   (data-motion="on" on the page or the show), the first time only, the script adds the moving parts:
   small boxes (.mascot__part) holding the SAME picture again, cut out by the masks below. They show,
   and the picture takes its own mask, only once every part has loaded (data-rig="ready"), so a dog is
   never drawn without its tail. Off, or for Reduce Motion, nothing is masked and no part is drawn:
   the picture in a plain box, pixel for pixel. The parts move by transform only, inward from where
   Devin drew them, inside the mascot's box. */
.mascot__pic {
  display: block;
  width: 100%;
  height: 100%;
}

.mascot__part {
  position: absolute;
  display: none;
  overflow: hidden;
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
}

.mascot__part > img {
  position: absolute;
  display: block;
  max-width: none;
}

.mascot--lederhosen {
  --mask-base:url(/img/mascot-masks/lederhosen-base-mask.png);
  --mask-tongue:url(/img/mascot-masks/lederhosen-tongue-mask.png);
  --mask-tail:url(/img/mascot-masks/lederhosen-tail-mask.png);
}

.mascot--lederhosen > .mascot__tongue {
  left: 91.5905%;
  top: 28.25%;
  width: 6.0329%;
  height: 7.75%;
  transform-origin: 10.6061% 41.9355%;
  -webkit-mask-image: var(--mask-tongue);
  mask-image: var(--mask-tongue);
}

.mascot--lederhosen > .mascot__tongue > img {
  left: -1518.1818%;
  top: -364.5161%;
  width: 1657.5758%;
  height: 1290.3226%;
}

.mascot--lederhosen > .mascot__tail {
  left: 0%;
  top: 23%;
  width: 24.223%;
  height: 37.375%;
  transform-origin: 59.6226% 87.6254%;
  -webkit-mask-image: var(--mask-tail);
  mask-image: var(--mask-tail);
}

.mascot--lederhosen > .mascot__tail > img {
  left: 0%;
  top: -61.5385%;
  width: 412.8302%;
  height: 267.5585%;
}

.mascot--dirndl {
  --mask-base:url(/img/mascot-masks/dirndl-base-mask.png);
  --mask-tongue:url(/img/mascot-masks/dirndl-tongue-mask.png);
  --mask-fringe:url(/img/mascot-masks/dirndl-fringe-mask.png);
  --mask-tail:url(/img/mascot-masks/dirndl-tail-mask.png);
}

.mascot--dirndl > .mascot__tongue {
  left: 1.9048%;
  top: 27.125%;
  width: 5.3968%;
  height: 6.375%;
  transform-origin: 88.2353% 47.0588%;
  -webkit-mask-image: var(--mask-tongue);
  mask-image: var(--mask-tongue);
}

.mascot--dirndl > .mascot__tongue > img {
  left: -35.2941%;
  top: -425.4902%;
  width: 1852.9412%;
  height: 1568.6275%;
}

.mascot--dirndl > .mascot__fringe {
  left: 83.8095%;
  top: 49.375%;
  width: 15.6614%;
  height: 26.5%;
  transform-origin: 10.8108% 26.8868%;
  -webkit-mask-image: var(--mask-fringe);
  mask-image: var(--mask-fringe);
}

.mascot--dirndl > .mascot__fringe > img {
  left: -535.1351%;
  top: -186.3208%;
  width: 638.5135%;
  height: 377.3585%;
}

.mascot--dirndl > .mascot__tail {
  left: 79.8942%;
  top: 23.25%;
  width: 20.1058%;
  height: 34%;
  transform-origin: 27.8947% 94.8529%;
  -webkit-mask-image: var(--mask-tail);
  mask-image: var(--mask-tail);
}

.mascot--dirndl > .mascot__tail > img {
  left: -397.3684%;
  top: -68.3824%;
  width: 497.3684%;
  height: 294.1176%;
}

/* The tempo, by the box's data-tempo: Event settings' level, bar Still, which draws no rig. */
.mascot--lederhosen[data-tempo=relaxed] {
  --wag:5deg;
  --wag-t:1.2s;
  --wag-o:-.45s;
  --wag-e:cubic-bezier(.42,0,.58,1);
  --pant-skew:8deg;
  --pant-sx:1.1;
  --pant-t:.6s;
  --pant-o:-.2s;
  --pant-e:cubic-bezier(.45,0,.55,1);
}

.mascot--dirndl[data-tempo=relaxed] {
  --wag:-4.5deg;
  --wag-t:1s;
  --wag-o:-.7s;
  --wag-e:cubic-bezier(.42,0,.58,1);
  --pant-skew:-9deg;
  --pant-sx:1.11;
  --pant-t:.5s;
  --pant-o:-.35s;
  --pant-e:cubic-bezier(.45,0,.55,1);
  --fringe:-1.8deg;
  --fringe-lag:.1s;
}

.mascot--lederhosen[data-tempo=cheerful] {
  --wag:6deg;
  --wag-t:.7s;
  --wag-o:-.25s;
  --wag-e:cubic-bezier(.42,0,.58,1);
  --pant-skew:8.5deg;
  --pant-sx:1.1;
  --pant-t:.42s;
  --pant-o:-.1s;
  --pant-e:cubic-bezier(.45,0,.55,1);
}

.mascot--dirndl[data-tempo=cheerful] {
  --wag:-5deg;
  --wag-t:.6s;
  --wag-o:-.4s;
  --wag-e:cubic-bezier(.42,0,.58,1);
  --pant-skew:-9.5deg;
  --pant-sx:1.11;
  --pant-t:.36s;
  --pant-o:-.2s;
  --pant-e:cubic-bezier(.45,0,.55,1);
  --fringe:-2deg;
  --fringe-lag:.07s;
}

.mascot--lederhosen[data-tempo=happy] {
  --wag:7deg;
  --wag-t:.42s;
  --wag-o:-.15s;
  --wag-e:cubic-bezier(.45,0,.55,1);
  --pant-skew:9deg;
  --pant-sx:1.11;
  --pant-t:.3s;
  --pant-o:-.1s;
  --pant-e:cubic-bezier(.45,0,.55,1);
}

.mascot--dirndl[data-tempo=happy] {
  --wag:-5.5deg;
  --wag-t:.36s;
  --wag-o:-.2s;
  --wag-e:cubic-bezier(.45,0,.55,1);
  --pant-skew:-10deg;
  --pant-sx:1.12;
  --pant-t:.24s;
  --pant-o:-.05s;
  --pant-e:cubic-bezier(.45,0,.55,1);
  --fringe:-2.2deg;
  --fringe-lag:.05s;
}

/* Drawn: with the switch on, once every part has loaded, and never for Reduce Motion. Only then is the
   box positioned and clipped: otherwise it is nothing but a box around the picture. */
@media (prefers-reduced-motion: no-preference) {
  [data-motion=on] .mascot[data-rig=ready] {
    position: relative;
    overflow: hidden;
  }
  [data-motion=on] .mascot[data-rig=ready] > .mascot__part {
    display: block;
  }
  [data-motion=on] .mascot[data-rig=ready] > .mascot__pic {
    -webkit-mask-image: var(--mask-base);
    mask-image: var(--mask-base);
    -webkit-mask-size: 100% 100%;
    mask-size: 100% 100%;
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
  }
}
/* The moves: the confetti's ease-in-out ping-pong, each part on the screen's clock (--beat, placeMascot). */
[data-motion=on] .mascot[data-rig=ready] > .mascot__tail {
  animation: mascot-wag var(--wag-t) var(--wag-e) calc(var(--beat, 0s) + var(--wag-o)) infinite alternate;
}

[data-motion=on] .mascot[data-rig=ready] > .mascot__fringe {
  animation: mascot-fringe var(--wag-t) var(--wag-e) calc(var(--beat, 0s) + var(--wag-o) + var(--fringe-lag)) infinite alternate;
}

[data-motion=on] .mascot[data-rig=ready] > .mascot__tongue {
  animation: mascot-pant var(--pant-t) var(--pant-e) calc(var(--beat, 0s) + var(--pant-o)) infinite alternate;
}

@keyframes mascot-wag {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(var(--wag));
  }
}
@keyframes mascot-fringe {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(var(--fringe));
  }
}
@keyframes mascot-pant {
  from {
    transform: skewY(0deg) scaleX(1);
  }
  to {
    transform: skewY(var(--pant-skew)) scaleX(var(--pant-sx));
  }
}
/* Stopped: Animations off, nothing moves; Reduce Motion wins whatever the switch says (and the draw
   rules above never apply to it). */
body[data-motion=off] .mascot__tail, body[data-motion=off] .mascot__fringe, body[data-motion=off] .mascot__tongue {
  animation: none !important;
}

@media (prefers-reduced-motion: reduce) {
  .mascot__tail, .mascot__fringe, .mascot__tongue {
    animation: none !important;
  }
}
/* mascot-rig:end */
/* ---------------------------------------------------------------- roster */
/* The roster is the busy race-day screen. Cards keep each dog's status and
   action in one easy-to-scan, thumb-friendly unit. */
/* Check-In's three columns, written once. The list shares them with every
   row where the fun fact sits beside the buttons (the block after
   .roster-item:active below); a row falls back to them as a grid of its own
   where subgrid is unsupported. The upright iPad and the phone set their own
   tracks. */
.roster-list {
  display: grid;
  gap: var(--space-10);
  --roster-columns: minmax(190px, 1fr) minmax(170px, 1.2fr) auto;
}

.roster-item {
  display: grid;
  /* Three named tracks, and row() always renders three children - it used to
     return null for a dog with no fun fact, which dropped the actions into
     column 2 and left rows with and without a fact visibly misaligned. */
  grid-template-columns: var(--roster-columns);
  align-items: center;
  gap: var(--space-20);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: var(--space-14) var(--space-16);
  background: var(--surface-raised);
  transition: border-color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}

@media (hover: hover) {
  .roster-item:hover {
    border-color: var(--border-strong);
    box-shadow: var(--shadow-lg);
    transform: translateY(-1px);
  }
}
.roster-item:active {
  transform: none;
  box-shadow: none;
}

/* One set of columns for the whole list, not one per row. Each row used to be
   a grid of its own, so its last column was only as wide as that row's own
   buttons: a dog with a photo (no Photo button), an Out switch or an archived
   row's Restore moved the fun-fact column 15-50px against its neighbours
   (DS-56). subgrid sizes that column to the widest row's actions and every
   row shares it. A browser without subgrid ignores the declaration and keeps
   the per-row grid above. This query is exactly the complement of the
   upright-iPad and phone content ranges, so it adds no breakpoint; the
   upright iPad keeps its own per-row tracks (styles/_tablet-portrait.scss). */
@media (min-width: 900px), (min-width: 641px) and (max-height: 599px) {
  .roster-list {
    grid-template-columns: var(--roster-columns);
    column-gap: var(--space-20);
  }
  .roster-item {
    grid-column: 1/-1;
    grid-template-columns: subgrid;
  }
}
/* A wash that fades out, not a flat fill. On race morning almost every row
   is checked in, and a solid tint turns the whole list one colour - the left
   edge is what actually carries the status. */
.roster-item.is-in {
  background: linear-gradient(100deg, var(--tint-hops), var(--surface-raised) 55%);
  border-left: var(--status-edge) solid var(--success);
  /* The wider edge comes out of the padding, so the picture and the name start
     where they do on a row with the ordinary 1px border (the 1px here is that
     border, not a new measurement). They sat 3px further right than a row not
     checked in on a phone, and 1px further left here (DS-59). */
  padding-left: calc(var(--space-16) + 1px - var(--status-edge));
}

.roster-item.is-out {
  opacity: 0.68;
}

/* Without min-width: 0 a long dog or owner name blows this track out and
   takes the whole document into horizontal scroll. */
.roster-item__identity {
  min-width: 0;
  overflow-wrap: anywhere;
  display: flex;
  align-items: flex-start;
  gap: var(--space-12);
}

/* A dog's picture, and the stand-in for a dog without one. Both are the same
   box in the same place: photos can be switched on before every owner has sent
   a picture, and a list of some photos and some neutral marks reads as
   deliberate where a list of some photos and some holes does not. */
.dogphoto {
  flex: none;
  width: var(--avatar);
  height: var(--avatar);
  border-radius: var(--radius-pill);
  object-fit: cover;
  background: var(--tint-blue);
  border: 1px solid var(--border);
}

.dogphoto--none {
  display: grid;
  place-items: center;
  color: var(--accent);
}

/* The mark fills the same share of the disc at every avatar size, so one rule
   covers all four sizes instead of a hand-tuned pixel value per size. */
.dogphoto--none .icon {
  width: 60%;
  height: 60%;
}

.dogphoto--lg {
  width: var(--avatar-lg);
  height: var(--avatar-lg);
}

/* A lane on a heat card, and a row inside a dialog: smaller, so a five-dog
   heat still reads as five short rows and the names keep their room. */
.dogphoto--lane {
  width: var(--avatar-lane);
  height: var(--avatar-lane);
}

.dogphoto--sm {
  width: var(--avatar-sm);
  height: var(--avatar-sm);
}

.dogphoto--chip {
  width: var(--avatar-chip);
  height: var(--avatar-chip);
}

/* A dialog headed by a dog: picture beside the title, on one line. */
.dialog-dog {
  display: flex;
  align-items: center;
  gap: var(--space-12);
}

.dialog-dog h2 {
  margin-top: 0;
}

/* The picture - a plain one on the file side, a link to the dog's page on
   the roster side (CH-06) - beside one block holding the name, the owner, the
   traits and the fun fact. It used to span only the name's and the owner's
   grid rows, so the traits ran on under the picture with the fun fact beside
   them and the two halves of a pair no longer lined up. */
.dupe-side--pictured {
  display: flex;
  align-items: flex-start;
  gap: var(--space-10);
}

.dupe-side__text {
  flex: 1 1 0;
  min-width: 0;
}

/* The dog's own page. On an iPad or a computer the picture sits beside the
   name and owners, centred on them rather than hung from their top line, so a
   one-line name is not a small block in the picture's top corner (Decision
   #11, "balance out the dogs name and picture"). A phone stacks them, centred
   (the phone block). The block's own margins go, so the space between the
   name, the owners and what follows is the three set here. */
.dog-head {
  display: flex;
  align-items: center;
  gap: var(--space-24);
  flex-wrap: wrap;
  margin-bottom: var(--space-20);
}

.dog-head > div {
  flex: 1 1 260px;
  min-width: 0;
}

.dog-head h1 {
  margin: 0;
}

.dog-head .sub {
  margin: var(--space-4) 0 0;
}

.dog-head .field__hint {
  margin: var(--space-10) 0 0;
  max-width: var(--measure);
}

/* Print for the owner (PDF) and Remove photo, one row: the same top edge and
   the same height, whichever of the two a dog has. */
.dog-head__actions {
  margin-top: var(--space-12);
}

/* The picture is the photo button. Bare: no button fill or edge of its own,
   so it reads as the dog's picture with a camera on it, not as a button with a
   picture in it. Rounded like what it holds, so the focus ring follows the
   circle; a picture still to be added also holds "Tap to add a photo", so it
   rounds its corners instead. Safari's own tap flash stays, as on every other
   button: iOS may not apply :active below on a tap, and without the flash a
   tap on the picture could show nothing until the photo menu opens. */
.dog-head__photo {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-8);
  flex: none;
  padding: 0;
  border: 0;
  border-radius: var(--radius-pill);
  background: none;
  color: var(--accent);
  font: inherit;
  cursor: pointer;
}

.dog-head__photo--empty {
  border-radius: var(--radius-md);
}

.dog-head__frame {
  position: relative;
  display: block;
  border-radius: var(--radius-pill);
}

/* Positioned, with no offset, only to fix what paints over what: the nudge's
   ring (the frame's ::before), then the picture, then the camera badge, then
   "Tap to add a photo", each over the one before. So the ring spreads out from
   behind the picture, under the badge and under the caption's letters, and no
   z-index is needed. */
.dog-head__frame > .dogphoto {
  position: relative;
  display: block;
  transition: transform var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}

.dog-head__frame > .dogphoto--none {
  display: grid;
}

.dog-head__photo--empty .dogphoto {
  border: var(--photo-dash) dashed var(--accent);
}

/* The camera sits on the picture's lower edge, where a phone's own contacts
   and profile screens put it, ringed in the page colour so it never merges
   into a dark photo. Its centre is on the circle's rim at half past four at
   any picture size: 14.645% is (1 - cos 45deg) / 2 of the frame. */
.dog-head__badge {
  position: absolute;
  right: calc(14.645% - var(--photo-badge) / 2);
  bottom: calc(14.645% - var(--photo-badge) / 2);
  display: grid;
  place-items: center;
  width: var(--photo-badge);
  height: var(--photo-badge);
  border-radius: var(--radius-pill);
  background: var(--accent);
  color: var(--text-on-accent);
  box-shadow: 0 0 0 var(--photo-badge-ring) var(--surface);
  transition: background var(--dur-fast) var(--ease);
}

.dog-head__badge .icon {
  width: 60%;
  height: 60%;
}

.dog-head__cta {
  position: relative;
  font-size: var(--text-sm);
  font-weight: 700;
  line-height: var(--leading-tight);
}

.dog-head__photo:active .dogphoto {
  transform: scale(0.97);
  border-color: var(--accent);
}

.dog-head__photo:active .dog-head__badge {
  background: var(--accent-hover);
}

@media (hover: hover) {
  .dog-head__photo:hover .dogphoto {
    border-color: var(--accent);
  }
  .dog-head__photo:hover .dog-head__badge {
    background: var(--accent-hover);
  }
}
/* The nudge: a ring that spreads from the picture and fades, a few times as
   the page opens. At rest it is invisible, and the animation exists only for
   a viewer who has not asked for reduced motion - declared here, not merely
   shortened by the global reduced-motion block, which would still flash it
   once. ::before, not ::after: drawn last, the ring cut across the camera
   badge on every cycle. */
.dog-head__photo--nudge .dog-head__frame::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: var(--radius-pill);
  border: var(--photo-dash) solid var(--accent);
  opacity: 0;
  pointer-events: none;
}

@media (prefers-reduced-motion: no-preference) {
  .dog-head__photo--nudge .dog-head__frame::before {
    animation: dog-photo-nudge var(--dur-nudge) ease-out var(--nudge-delay) var(--nudge-cycles);
  }
  .dog-head__photo--nudge .dog-head__badge {
    animation: dog-photo-badge var(--dur-nudge) var(--ease) var(--nudge-delay) var(--nudge-cycles);
  }
}
@keyframes dog-photo-nudge {
  0% {
    transform: scale(1);
    opacity: 0.7;
  }
  100% {
    transform: scale(1.3);
    opacity: 0;
  }
}
@keyframes dog-photo-badge {
  0%, 30%, 100% {
    transform: scale(1);
  }
  12% {
    transform: scale(1.15);
  }
}
/* Bigger where the picture sits beside the name: an iPad either way up and a
   computer. Not a phone on its side, which is short enough without it. */
@media (min-width: 641px) and (min-height: 600px) {
  .dog-head {
    --photo-badge: var(--photo-badge-xl);
  }
  .dog-head__frame > .dogphoto--lg {
    width: var(--avatar-xl);
    height: var(--avatar-xl);
  }
}
/* And on a tall phone once the dog has a photo: the phone block. */
/* Each owner is kept whole where the line allows, so the break goes before
   "& <second owner>" before it goes inside a surname (DS-89). An owner wider
   than the line still wraps inside itself: it is never cut, because this page
   is where the whole name is always reachable. */
.dog-head__owner {
  display: inline-block;
  max-width: 100%;
  overflow-wrap: anywhere;
}

/* The picture is the way into a dog's page from every staff screen (CH-06).
   On Check-In the avatar is already exactly --touch-min square; in a heat lane
   it is 36px and on a current-heat card 32px, so the link keeps the 44px
   minimum around it and the picture stays its own size inside. It is the only
   part of a row that is a link, so the extra height is never a second meaning
   for a tap - it is the same meaning, reachable. */
a.dogphoto-link {
  display: grid;
  place-items: center;
  flex: none;
  min-width: var(--touch-min);
  min-height: var(--touch-min);
  border-radius: var(--radius-pill);
}

a.dogphoto-link:focus-visible {
  outline: var(--focus-width) solid var(--gold);
  outline-offset: var(--focus-offset);
}

a.dogphoto-link:active {
  background: var(--tint-blue);
}

@media (hover: hover) {
  a.dogphoto-link:hover {
    background: var(--tint-blue);
  }
}
.roster-item__fact {
  min-width: 0;
}

/* Four lines, then stop. A fun fact may be 240 characters and the column is
   narrow; unclamped, one entry makes its row twice the height of its
   neighbours and the list stops being scannable. The whole text is still on
   the dog's Edit dialog and on the announcer screen, which is what reads it. */
.roster-item__fact .funfact {
  max-width: none;
  display: -webkit-box;
  -webkit-line-clamp: 4;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* A placement pill is wider than the identity track and must wrap inside it
   rather than run under the fun fact beside it. */
.roster-item__identity .pill {
  white-space: normal;
  max-width: 100%;
}

/* The owner's number is a tel: link, so it is a control and takes the 44px
   minimum like every other one. Inline-flex keeps it the width of the number
   rather than the whole track, so a stray tap beside it does not dial. */
.dogphone {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-6);
  min-height: var(--touch-min);
  min-width: var(--touch-min);
  padding: 0 var(--space-6) 0 var(--space-2);
  margin-left: calc(-1 * var(--space-2));
  color: var(--accent);
  font-weight: 700;
  font-size: var(--text-sm);
  text-decoration: none;
  border-radius: var(--radius-sm);
}

.dogphone:active {
  background: var(--tint-blue);
}

@media (hover: hover) {
  .dogphone:hover {
    background: var(--tint-blue);
  }
}
/* A phone number is read and dialled as one thing, so one number stays on
   one line: on an upright iPad the identity track is narrower than the icon
   and the number together, and "(816) 555-0142" broke at its hyphen into
   "(816) 555-" over "0142". roster.js phoneText draws the hyphens and spaces
   inside a number as non-breaking ones, and the wrap above puts the number
   under the icon, whole, where the two do not fit side by side. The field is
   free text, though ("(816) 555-0142 cell / (913) 555-9876"), so the text as
   a whole still wraps at its other spaces; nowrap here pushed a long one out
   of the track and over the fun fact. A run with no break in it at all, wider
   than the track, breaks anywhere rather than leave it. Only the number: how
   a dog or owner name wraps on Check-In is untouched. */
.dogphone > span {
  overflow-wrap: anywhere;
}

.roster-item__eyebrow {
  display: block;
  margin-bottom: var(--space-2);
  color: var(--text-muted);
  font-size: var(--text-micro);
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.roster-item__actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-14);
}

.roster-item__actions .btnrow {
  flex-wrap: nowrap;
}

/* The switch is a <label>, and every label carries a 4px bottom margin for the
   field under it. Centred in this row, that margin lifted the switch 2px above
   the Photo and Edit buttons beside it (DS-61). Scoped to Check-In: Profile's
   switches sit in a stacked card where the margin does its usual job. */
.roster-item__actions .toggle {
  margin-bottom: 0;
}

/* "In" and "Out" take the same room, three characters, so the switch and the
   Photo and Edit buttons beside it stand in one column down a list that mixes
   the two. "Out" is the wider word, and it moved them 12px left of every
   checked-in row's (DS-56, DS-61). */
.roster-item__actions .toggle__word {
  min-width: 3ch;
}

/* A name too long for its column gets the whole width of its row (DS-54).
   In the first column the fixture "Baroness Wilhelmina Pumpernickel von
   Schnitzelhausen III" and its two owners broke a word or two per line, and
   on an upright iPad its row was 415px against 94px for the rows around it.
   Ryan, 2026-09-24, choosing this: "a lot less wasted screen space". The
   name, owners, number, details and pills go across the top; the fun fact
   and the buttons share the line under them, the buttons in the same last
   column as every other row's, so the switches still stand in one line down
   the list. The name is drawn exactly as before - it wraps, it is never cut -
   and only has more room.
   Which rows: roster.js placeLongNames measures, because no stylesheet can
   ask whether a text fits. A row whose name wraps in the first column is
   marked when the whole width splits fewer of its words mid-word - a name
   left as "Frankfurterwurs / t" always gets the room, even when its row
   grows - or when the row comes out at least half a line of the name
   shorter. Measured on the sandbox, everyone checked in: the fixture's row
   goes from 415px to 281px at 768, 322px to 281px at 834 and 281px to 262px
   at 1024x768. A name that wraps at spaces usually keeps its columns when
   it takes two lines (the buttons' own line costs more than its second line
   saves: 139px would become 180px), and so does the fixture from 1194px up,
   where it takes two lines and its row would grow from 219px to 244px. A
   saving of a few pixels keeps the columns too, so a row does not change
   shape when it gains a pill. A row whose name fits never gets the class,
   at any width, and no other row moves when one does.
   The spans below work on both three-track layouts: the shared subgrid
   above and the upright iPad's own tracks (styles/_tablet-portrait.scss).
   Not on a phone, where the name already has the whole width over its
   buttons and the fun fact is hidden; this query is exactly the complement
   of the phone content block. The block comes after the rules above so a
   search for the first .roster-item__identity rule still finds the row's
   own. */
@media (min-width: 641px) {
  .roster-item.roster-item--long-name {
    row-gap: var(--space-12);
  }
  .roster-item--long-name > .roster-item__identity {
    grid-column: 1/-1;
  }
  .roster-item--long-name > .roster-item__fact {
    grid-column: 1/-2;
  }
  .roster-item--long-name > .roster-item__actions {
    grid-column: -2/-1;
  }
  /* Its details line runs the width of the row with the name. The 34ch that
     keeps a fun fact to a readable measure held it to the old narrow column,
     so on an upright iPad "Female · 7 years · Chocolate and tan double /
     dapple · Wire-haired · 14 lb" wrapped under a name that ran across. */
  .roster-item--long-name > .roster-item__identity .funfact {
    max-width: none;
  }
}
[hidden] {
  display: none !important;
}

/* Make every trainer link (2.0.0, S18): under Check-In's counts, an Event manager's. */
.trainer-links {
  margin-top: var(--space-16);
}

/* Trainer link on a dog's card (2.0.0, S18; public/js/roster.js): from
   900px, with its words beside Edit, where the row has room for them. Below
   900px - an upright iPad, a phone either way up - the row's buttons and
   switch already fill the actions column, so it is the link mark alone, 44
   square, at the end of the dog's name line: the row keeps Photo, Edit and
   its switch on one line, and the name column keeps its width (review
   S18-RV-9: beside Edit on an upright iPad it squeezed every row's heat pill
   onto a third line). One of the two is shown at a time. */
.roster-item__name-line {
  display: flex;
  align-items: flex-start;
  gap: var(--space-8);
}

.roster-item__name-line .dogname {
  flex: 0 1 auto;
  min-width: var(--touch-min);
}

.roster-item__trainer--name {
  display: none;
}

@media (max-width: 899px) {
  .roster-item__trainer {
    min-width: var(--touch-min);
    padding: var(--space-6);
  }
  .roster-item__trainer--row {
    display: none;
  }
  .roster-item__trainer--name {
    display: inline-flex;
    flex: none;
    margin-left: auto;
  }
}
/* ------------------------------------------------ rounds and heat board */
/* Each card as tall as its own content. Stretched, the heat at the line took
   the height of a raced neighbour's result and Correct result, and ended in a
   red box of nothing; the live Final stood about 180px taller than its dogs
   (DS-34). */
.heat-grid {
  display: grid;
  gap: var(--space-14);
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 290px), 1fr));
  align-items: start;
}

.bracket-round {
  margin: var(--space-24) 0;
}

/* Sticky, so a long bracket says which round you are in while you scroll.
   Ryan: "just feels difficult to know where I am in the tournament scrolling
   down if I don't slow down to see the section headers between them on the
   way." It sticks BELOW whatever else is pinned - the top bar always, and Race
   Control's current heat when that is stuck too, whether that is the full box
   at iPad width or the slim bar on a phone - which is what
   --staff-current-height is measured for. It sits under the current panel in
   the z ladder so the panel always wins where they meet, and under an open
   name tip (--z-tip), which must show the whole name even beside a heading.

   On a phone this used to stand down on Race Control while the slim bar was
   up, on the reasoning that the bar already carried the round's name. It does
   not: the bar names the heat AT THE LINE, so while you are scrolled into
   Second Chance it still reads "Q · Heat 4 of 6" and answers the wrong
   question. Standing the heading down left it scrolling away under a fixed bar
   - Ryan: "they're hidden behind the race control sticky pannel on the race
   view". Three strips is the cost; the phone block below spends one line on the
   heading instead of two to pay some of it back. */
.round-heading {
  position: sticky;
  top: calc(var(--staff-nav-height, 58px) + var(--staff-current-height, 0px));
  z-index: calc(var(--z-sticky) - 2);
  margin: 0 0 var(--space-14);
  padding: var(--space-10) var(--space-12);
  border-left: 5px solid var(--accent);
  border-radius: var(--radius-sm);
  background: var(--tint-blue);
  flex-wrap: wrap;
  /* A band of the card's own background under the heading, so what scrolls
     beneath it is cut off a few pixels short instead of touching it. Without
     it the gold Championship heading sat on the gold Final card as one block
     with sliced text, and on Race Control its gold edge met the card's gold
     border as a notch (DS-39). At rest the band lies inside the heading's own
     bottom margin, over the card's background, and cannot be seen. */
  box-shadow: 0 var(--space-6) 0 0 var(--surface-raised);
}

.round-heading h2 {
  margin: 0;
  color: var(--accent);
}

.round-heading__range {
  color: var(--text-muted);
  font-size: var(--text-sm);
  font-weight: 700;
  flex-basis: 100%;
}

[data-round="2"] > .round-heading {
  border-color: var(--success);
  background: var(--tint-hops);
}

[data-round="2"] > .round-heading h2 {
  color: var(--on-tint-hops);
}

[data-round="3"] > .round-heading {
  border-color: var(--tint-plum-line);
  background: var(--tint-plum);
}

[data-round="3"] > .round-heading h2 {
  color: var(--tint-plum-line);
}

[data-round="4"] > .round-heading {
  border-color: var(--gold);
  background: var(--tint-gold);
}

[data-round="4"] > .round-heading h2 {
  color: var(--on-tint-gold);
}

/* Both boards' pinned headings - Race Control's (#raceBracket) and Bracket
   Builder's (#roundsBox) - fill their own rounded corners with the card's
   background as well. Pinned, a heat card scrolls under the heading, and the
   red edge of the heat at the line showed through the top-left corner's
   curve as a sliver, and a raced heat's grey edge through the top-right.
   A spread of the corner's own radius covers all four corners (a shadow is
   drawn only outside the curve, never on the heading); at rest it lies on
   the card's own background, in the space round the heading, and cannot be
   seen. The band under it stays as it is. Race Control's board had the same
   sliver as Bracket Builder's, which the every-screen pass fixed first. */
#raceBracket .round-heading, #roundsBox .round-heading {
  box-shadow: 0 var(--space-6) 0 0 var(--surface-raised), 0 0 0 var(--radius-sm) var(--surface-raised);
}

/* position: relative holds a lane's name tip (TN-B, below). */
.bracket-lane {
  position: relative;
  gap: var(--space-6);
  min-height: 64px;
  padding: var(--space-8) var(--space-4);
}

/* One line each for the name and the owner, cut at the same --name-cap and
   --owner-cap as Race Control's current heat, so a long name can no longer
   make its lane taller than the rest of the heat. It used to wrap anywhere,
   and beside a "Through to the Final" or "Champion" pill the longest name the
   add-dog form allows ran to six lines, broken mid-word ("Schnitzelhaus /
   en III"), in a lane three times the height of its neighbours. Ryan: the
   lanes should be evenly spaced. Both boards are private staff screens - the
   full name is on the dog's page and in the lane's tooltip. Check-In and the
   kiosks always show the whole name. */
/* At least 44px tall, because a tap on a cut name opens its tip (TN-B); 44px
   sits inside the 64px lane's 48px of content, so no lane grows. Every named
   lane listens for that tap, so Safari on the iPad would flash its grey tap
   highlight over every name tapped - a name that fits, or one tapped while a
   dog is picked up, where nothing happens - and read as "something happened".
   The tip appearing is the only answer a tap gets. */
.bracket-lane .lane__who {
  flex: 1;
  min-width: 0;
  min-height: var(--touch-min);
  display: flex;
  flex-direction: column;
  justify-content: center;
  -webkit-tap-highlight-color: transparent;
}

.bracket-lane .lane__who strong, .bracket-lane .lane__who .dogowner {
  display: block;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.bracket-lane .lane__who strong {
  max-width: var(--name-cap);
}

.bracket-lane .lane__who .dogowner {
  max-width: var(--owner-cap);
}

/* Both boards (Race Control's #raceBracket, Bracket Builder's #roundsBox)
   cut a name only where its lane has no room for it. At the shared cap an
   ordinary 12-letter name was cut in every lane at every size, on a computer
   with room to spare beside it: "Sausage L...", beside a Winner pill on Race
   Control too. Here the name and the owner take the whole column and are
   cut where the column ends, so an ordinary name is whole on an iPad and a
   computer, and only one wider than its lane - the long fixture anywhere, a
   long name on a phone - is cut, with its name tip (Staff.nameTip opens only
   on a name that really is cut). Still one line each, so no name makes its
   lane taller. The current heat's cards keep the cap: there it is the
   picked card that shows a name whole (fitLanes). */
#raceBracket .bracket-lane .lane__who strong,
#raceBracket .bracket-lane .lane__who .dogowner,
#roundsBox .bracket-lane .lane__who strong,
#roundsBox .bracket-lane .lane__who .dogowner {
  max-width: none;
}

/* The name tip (TN-B, Staff.nameTip): a clean tap on a cut name, on either
   board, lays the whole name and owner over that lane - its own left edge and
   top, at least its height, growing down over the lanes below without moving
   them. Near the bottom of the screen that would run it under the phone's tab
   row or past the window's edge, and a scroll closes it, so it moves up over
   the lanes above by just enough to end there (--tip-shift, the distance
   Staff's placeNameTip measures as it opens; 0 wherever it fits). Safari on
   the iPad never shows the title tooltip, so this is how a finger reads a
   cut name. Where the lane has a move arrow the tip stops short of it
   (--touch-min, the lane's gap and its right padding), so one tap on the
   arrow still picks the dog up, and closes the tip. The words wrap at spaces
   and a word is only broken if it is wider than the whole tip. A tap on the
   tip puts it away: it has its own click listener and the pointer cursor,
   because Safari on the iPad sends no click for a tap on something that does
   not respond to one, so the page-wide listener alone would never hear it. */
.name-tip {
  position: absolute;
  inset: var(--tip-shift, 0) 0 auto;
  min-height: 100%;
  z-index: var(--z-tip);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  display: flex;
  align-items: flex-start;
  gap: var(--space-6);
  padding: var(--space-8) var(--space-4);
  background: var(--surface-raised);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
}

.name-tip--handle {
  right: calc(var(--touch-min) + var(--space-6) + var(--space-4));
}

.name-tip .lane__n {
  margin-top: var(--space-8);
}

.name-tip__who {
  flex: 1;
  min-width: 0;
}

.name-tip strong, .name-tip .dogowner {
  display: block;
  white-space: normal;
  overflow-wrap: break-word;
}

.name-tip strong {
  font-size: var(--text-base);
  font-weight: 800;
  color: var(--text);
}

.name-tip .dogowner {
  font-size: var(--text-sm);
  color: var(--text-muted);
}

/* touch-action: manipulation, never none. A finger never drags a handle; with
   `none` a scroll that started on one did not scroll the board at all, which on
   an iPad held in the right hand - thumb resting on the right edge, exactly
   where these are - is what "dogs constantly moving around when she's scrolling"
   was. `manipulation` only removes double-tap zoom, so two taps on one handle
   (pick up, put back) can never zoom the page. test/staff-nav.test.js fails if
   `none` returns. */
.drag-handle {
  touch-action: manipulation;
  user-select: none;
  -webkit-user-select: none;
  cursor: grab;
  flex: none;
  display: grid;
  place-items: center;
  min-width: var(--touch-min);
  min-height: var(--touch-min);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--accent);
}

.drag-handle:focus-visible {
  outline: var(--focus-width) solid var(--gold);
  outline-offset: var(--focus-offset);
}

/* Picked up: gold, the same gold as a picked dog on Race Control, and its own
   lane washed the same way, so the dog is findable after scrolling away. */
.drag-handle.is-selected {
  outline: var(--focus-width) solid var(--gold);
  outline-offset: var(--focus-offset);
  background: var(--tint-gold);
  border-color: var(--gold);
}

.bracket-lane.is-source {
  background: var(--tint-gold);
}

/* One slot at the right of the lane, the same width whether it holds the
   arrow or Place here / Swap here, so picking a dog up never narrows the
   name column and moves where a name is cut (DS-35). The arrow keeps its
   44px box at the right of the slot; the space beside it is not part of the
   button, so a thumb resting there still touches nothing. Hold out is the
   exception, on the picked-up dog's own row only - that dog is named in
   full in the status line above the board. These come after the base
   .drag-handle rule on purpose: test/staff-nav.test.js reads the first one. */
.bracket-lane .drag-handle {
  margin-left: calc(var(--lane-slot) - var(--touch-min));
}

.bracket-lane .hold-out:not([hidden]) + .drag-handle {
  margin-left: 0;
}

.hold-out {
  padding: var(--space-6);
  font-size: var(--text-caption);
  min-height: var(--touch-min);
  flex: none;
}

.is-dragging, .is-dragging .drag-handle {
  cursor: grabbing;
  user-select: none;
  -webkit-user-select: none;
}

.drag-ghost {
  position: fixed;
  z-index: var(--z-drag);
  pointer-events: none;
  display: flex;
  align-items: center;
  gap: var(--space-8);
  padding: var(--space-10) var(--space-14);
  border-radius: var(--radius-md);
  background: var(--bav-blue-dark);
  color: #fff;
  box-shadow: var(--shadow-md);
  font-weight: 800;
  max-width: 240px;
}

.drop-valid {
  outline: 1px dashed var(--accent);
  outline-offset: -1px;
  background: var(--tint-blue);
}

.drop-over {
  outline: var(--focus-width) solid var(--success);
  background: var(--tint-hops);
}

.drop-unavailable {
  outline: var(--focus-width) solid var(--danger);
  background: var(--tint-danger);
}

/* Rounded only while washed. A radius on every lane bent each 1px divider
   (the .lane border-top) down into a hook at both ends, on both boards
   (DS-51); a washed or lit lane reads as a box, so there it stays. */
.bracket-lane.is-source, .drop-valid, .drop-over, .drop-unavailable {
  border-radius: var(--radius-sm);
}

/* Place here takes the handle's slot at the right edge of an eligible lane,
   and the slot is --lane-slot wide either way (DS-35); bracket.js hides the
   handle while the button shows, so the two never share a row. (Done in
   script rather than with :has(), which needed iPadOS 15.4 and could not be
   asserted in the test fake.) */
.place-target {
  padding: var(--space-6);
  font-size: var(--text-caption);
  min-height: var(--touch-min);
  min-width: var(--lane-slot);
  flex: none;
}

/* The status line above the board. In flow, at a fixed height: a mouse drag
   rewrites it on every move, and a box that changed height would slide the
   lanes under the pointer. It replaced a fixed overlay that floated over the
   board with a Cancel button, which on the iPad was the "stupid pop up" that
   had to be found and tapped to undo an accidental selection.
   Two lines at every width. 2.9em was less than one line in border-box, so it
   reserved nothing: a picked-up line on a phone, and the longest name on an
   iPad in portrait, grew the box and slid the board under the finger (DS-43).
   "No dog picked up." is one line in it; a picked-up dog is two, unless the
   name alone needs more - the line names that dog in full on purpose. The
   words sit in the middle of the box, so the one line at rest does not read
   as a box with its second line missing; the box itself never moves. */
.move-status {
  min-height: calc(2em * var(--leading-base) + 2 * var(--space-12) + var(--space-2));
  margin: 0 0 var(--space-12);
  display: flex;
  align-items: center;
}

/* Undo last change, under the status line here and above the bracket on Race
   Control. The height is reserved like .move-status above it, so the row
   appearing cannot slide the board out from under a finger.
   The line naming the change sits beside Undo at every width, two lines at
   most, so the row is the button's height whether or not there is a change
   to take back. Wrapping under the button made the row about 89px on a phone
   and slid the board 45px the moment the first change of the day landed
   (DS-43). The Undo confirmation names the change in full, and so does the
   button's aria-label. It is the body's small size, not the 12px caption,
   because it is read, not glanced at (DS-45), and it keeps a lone last word
   ("Heats 2 and / 3") off a line of its own where the browser can; its own
   top margin would sit it low beside the button. */
.undo-row {
  align-items: center;
  margin: 0 0 var(--space-12);
  min-height: var(--touch-min);
  flex-wrap: nowrap;
}

/* A phone's short word for "Last change it can take back:" (renderUndo), shown only there (_phone.scss). */
.undo-row__earlier {
  display: none;
}

.undo-row .field__hint {
  margin-top: 0;
  flex: 1 1 0;
  min-width: 0;
  font-size: var(--text-sm);
  text-wrap: pretty;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}

.held-panel {
  padding: var(--space-14);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface);
}

.held-panel h2 {
  margin-bottom: var(--space-2);
}

.held-dog {
  display: flex;
  align-items: center;
  gap: var(--space-16);
  padding: var(--space-10) var(--space-12);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface-raised);
}

/* Race Control's Held dogs, under the late-arrival picker (HD-01): Bracket
   Builder's own panel and cards, so it reads as the same list of the same
   dogs. The first version gave each dog a full-width row, which Ryan found
   took "way too much space per racer dog". */
.late-held {
  margin-top: var(--space-16);
}

.late-held__title {
  margin: 0 0 var(--space-2);
  font-size: var(--text-lg);
}

.late-held__list {
  margin-top: var(--space-10);
}

/* A word too long for the line is the only thing broken; a name with spaces
   wraps at them. */
.held-dog > div {
  min-width: 0;
  overflow-wrap: break-word;
}

/* Choose a heat is wider than Bracket Builder's arrow, so where the card is
   narrow it drops under the name instead of squeezing it - on a 393px phone
   it left the Baroness about 95px and broke her name mid-word. */
.late-held .held-dog {
  flex-wrap: wrap;
}

.late-held .held-dog > div {
  flex: 1 1 16ch;
}

.heat-actions {
  margin-top: var(--space-12);
}

.result-summary {
  color: var(--success);
  font-weight: 700;
  font-size: var(--text-sm);
  margin-bottom: var(--space-8);
}

.champion-banner:not(:empty) {
  margin-bottom: var(--space-16);
}

/* "Champion: Pretzel", the owner, and the dog's picture - the same line at the
   top of Race Control, in its current-heat box once the event is over, on its
   Championship card, and above Bracket Builder's Championship. Four places,
   one element, because "who won?" is one question. */
.champion-line {
  display: flex;
  align-items: center;
  gap: var(--space-10);
  min-height: var(--touch-min);
}

.champion-line > .icon {
  width: 28px;
  height: 28px;
  color: var(--gold);
  flex: none;
}

.champion-line__who {
  min-width: 0;
}

.champion-line__who strong {
  display: block;
  font-size: var(--text-lg);
  font-weight: 800;
  color: var(--success);
}

/* The podium on the Championship card (race.js podiumPicks, Ryan
   2026-09-26: "basically its a 3 lane heat card"): a heat card of three
   lanes - the board's own lane, its disc saying the place in gold, silver
   and bronze - and one button under them that opens the podium dialog. A
   name here is never cut: the lane has the card's whole width for it and
   wraps at its spaces, where the board's lanes cut at one line. */
.podium-card {
  margin-block: var(--space-12);
  padding-block: var(--space-12) var(--space-4);
}

.podium-card .heat__head {
  margin-bottom: var(--space-6);
}

.podium-lane .podium-lane__n {
  width: auto;
  min-width: var(--lane-badge);
  padding-inline: var(--space-6);
  font-size: var(--text-caption);
}

.podium-lane--1st .podium-lane__n {
  background: var(--gold);
  color: var(--text-on-gold);
}

.podium-lane--2nd .podium-lane__n {
  background: var(--silver);
  color: var(--text-on-silver);
}

.podium-lane--3rd .podium-lane__n {
  background: var(--bronze);
  color: var(--text-on-bronze);
}

.podium-lane .lane__who strong, .podium-lane .lane__who .dogowner {
  max-width: none;
  overflow: visible;
  white-space: normal;
  text-overflow: clip;
  overflow-wrap: break-word;
}

.podium-lane--open .lane__who strong {
  color: var(--text-muted);
  font-weight: 700;
}

/* A long name wraps at its spaces beside its picture, and the Champion tag
   drops under it, at the lane's end, rather than squeezing the name into a
   column that breaks inside its words on a phone. */
.podium-lane {
  flex-wrap: wrap;
  row-gap: var(--space-4);
}

.podium-lane .lane__who {
  flex: 1 1 calc(var(--name-cap) * 1.5);
}

.podium-lane > .pill {
  flex: none;
  margin-left: auto;
}

.podium-card__actions {
  margin-top: var(--space-8);
}

.current-panel {
  position: sticky;
  top: var(--staff-nav-height, 58px);
  z-index: var(--z-sticky);
  border: 2px solid var(--accent);
  padding: var(--space-14) var(--space-20);
  background: var(--surface-raised);
}

.current-panel .card__head {
  margin-bottom: var(--space-8);
}

/* Before the draw (1.12.0, race.js scheduleLine and autoMissNote): the
   evening's schedule under the count, and - when the start came and built
   nothing - why, with Build now beside its hint. */
#currentBox > .schedule-line {
  margin: var(--space-6) 0 0;
}

.auto-miss {
  margin: var(--space-8) 0 0;
}

.auto-miss > p {
  margin: 0;
}

.auto-miss__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-8) var(--space-12);
  margin-top: var(--space-8);
}

.auto-miss__row > .btn {
  flex: none;
}

.auto-miss__hint {
  flex: 1 1 0;
  min-width: 0;
  margin: 0;
}

/* The heat that crowns the Champion, marked the way its card, the Big Screen
   and the Announcer mark it: a gold edge and the trophy by its name. No gold
   wash, so a picked dog's gold card still stands out inside it (DS-14). The
   trophy's size is shared with the Final's card (_heatcard.scss). */
.current-panel--final {
  border-color: var(--gold);
}

.current-panel--final .card__head h2 {
  display: flex;
  align-items: center;
  gap: var(--space-6);
  color: var(--on-tint-gold);
}

/* Past the Big Screen controls (race.js watchScroll) the pinned box stands
   over the bracket rather than introducing the heat, so what only explains
   it steps aside - "Tap the dog that won, then Record Wiener." and the
   next-round line - and the four mode icons join the right-hand end of its
   button row (.mode-icons), not the heading row and not a row of their own.
   With all of it, an upright iPad was more than half pinned chrome (DS-05);
   the Championship's size has since moved out of this box altogether, to
   above its heats (DS-27). What says what recording a winner does in the
   Championship stays, and so does a warning; everything is back at the top
   of the page. */
.current-panel.is-past-controls .current-panel__aside {
  display: none;
}

/* Where the pinned row, measured, cannot hold every word with the Big Screen
   icons at its end, race.js fitActions marks it compact and three buttons
   give words up so the icons need no row of their own. With their words the
   four buttons take 620-715px and the icons 204px more, in a row 684px wide
   at 768, 750 at 834 and 660 on an iPad mini. Measured rather than keyed to
   a width, because the words, the font and a classic scrollbar all move the
   answer: at 1024px in Chrome the longest row had 10px to spare. Compact, Too
   Close to Call and Move a dog are their icons, named on the same buttons at
   the top of the page (DS-01); Undo is the slim bar's arrow and the word
   "Undo", so the arrow is learnt where it is shown; each keeps its whole
   line in its aria-label and title. Only where even that wraps - a window
   under about 665px with a long name picked - is Undo its arrow alone
   (tight), so the icons still never take a row of their own from 641px.
   The result button keeps its words: its picked name is never cut further.
   Undo's arrow shows only here. */
.current-actions__glyph {
  display: none;
}

.current-actions--compact .current-actions__words {
  display: none;
}

.current-actions--compact .current-actions__glyph {
  display: block;
}

.current-actions--tight .current-actions__verb {
  display: none;
}

/* The narrowest a card may be is what one needs to hold the picture, the lane
   number and a name at --name-cap side by side, so no card ever has to wrap:
   2px of border, 16px of padding, the 44px picture link, 6px, the 30px lane
   badge, 6px and the 98.6px name come to 202.6px. At 170px an iPad in portrait
   (820 and 834 wide) laid a five-dog heat out four across at 176-180px, where
   even "Cinnamon" could not fit beside its picture. At 204px that is three
   across and two below - the same two rows - and a desktop still takes all
   five on one row at 215px. Re-measure before changing it.
   Aligned to their tops: on a phone the picked card grows to show its
   whole name, and the cards beside it keep their own height rather than
   stretching to it. Above 640px it widens in its row instead (fitLanes,
   .current-lanes--flow below), and every card keeps its row's height. */
.current-lanes {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(var(--lane-card-min), 1fr));
  align-items: start;
  gap: var(--space-10);
  margin-bottom: var(--space-10);
}

/* Six dogs, and seven or eight (2.0.0: a track of 2 to 8 lanes). Wherever
   five cards sit across, six went five and one - a lone card under a full
   row, as four once went three and one (DS-07) - and eight five and three
   (ui-check at six and eight lanes, 2026-10-08). So six sit at most three
   across (three and three) and seven or eight at most four (four and three,
   four and four): a card's least width is the larger of its own and what
   one card more than that in a row would leave each, so the row never takes
   it. Narrower, the card's own width decides, as for five: three across on
   an upright iPad. The phone's sideways strip replaces the grid entirely. */
.current-lanes--six {
  grid-template-columns: repeat(auto-fit, minmax(max(var(--lane-card-min), (100% - 2 * var(--space-10)) / 4), 1fr));
}

.current-lanes--eight {
  grid-template-columns: repeat(auto-fit, minmax(max(var(--lane-card-min), (100% - 3 * var(--space-10)) / 5), 1fr));
}

/* Four dogs go two and two wherever four cards cannot sit across: the strip
   needs 846px for four 204px cards and their gaps, which it only has from
   about 930px wide, and auto-fit made the width in between three and one - a
   lone card under two empty slots on an upright iPad (DS-07). 899px is the
   tablet boundary the chrome already uses; a 900-929px window still goes three
   and one. Two rows either way, so the pinned box is no taller. Five dogs stay
   three and two, and the phone's sideways strip replaces the grid entirely. */
@media (max-width: 899px) {
  .current-lanes--four {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}
/* Each dog in the current heat is one card. For a heat of two or more the card
   is a button: tap the dog that won, then the one Record Wiener button below
   the strip. Five one-tap Winner buttons side by side were too easy to catch on a
   strip that scrolls under a thumb, which is what Devin asked to stop. */
.current-lane {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-6);
  min-height: var(--touch-min);
  padding: var(--space-8);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
}

/* The card used to BE the button. It is a row now, holding the picture - which
   is a link to the dog's page, and a link cannot live inside a button - beside
   a button carrying the lane number and the name. What picks the dog is the
   whole card except the picture, which is what CH-06 asks for: a tap on the
   picture must not also pick. The card itself takes the tap (race.js
   pickFromCard), so its edge, the gap by the picture and, on a phone, the
   row beside the picture pick as the name does; only the button did, and
   all of those were dead (Ryan, 2026-09-27). While a result saves, the card
   shows the not-allowed pointer its disabled button shows.
   And the button itself reaches over the whole card, under everything drawn
   in it (its ::after, below): a tap on the card's padding, beside the
   picture or in the square corners round the round picture lands on the
   button, not on the card. A tap on the card was not enough on a touch
   screen: iPad Safari's engine and Chrome both move a tap near a link onto
   it when all that is under the finger is the link's own card, so a tap
   up to 11px from the picture opened the dog's page (TAP-2). With the button
   under the finger they weigh the button against the link, and the button,
   which the finger is inside, wins. The card is its own stacking layer
   (isolation), so the cover sits over the card's background and under the
   picture, the lane number and the name: those keep their own taps and
   the name its tooltip. It covers the card's 1px border as well, with the
   card's own corners, so the cover and the drawn card are one shape. */
.current-lane--pick {
  position: relative;
  isolation: isolate;
  background: var(--surface-raised);
  cursor: pointer;
}

.current-lane__pick::after {
  content: "";
  position: absolute;
  inset: -1px;
  z-index: -1;
  border-radius: var(--radius-md);
}

@media (hover: hover) {
  .current-lane--pick:hover {
    border-color: var(--border-strong);
  }
}
.current-lane--pick:active {
  background: var(--tint-blue);
}

.current-lane--pick:has(> .current-lane__pick:disabled) {
  cursor: not-allowed;
}

/* Basis 0, not auto. With auto the button asked for the full unbroken width
   of its dog's name before the card wrapped, so a long name - or a long owner
   - pushed it onto a row of its own under the picture while its neighbours
   kept the picture beside them: five cards, two layouts, names at different
   heights (WS-01). From 0 it never wraps past the picture and grows into the
   room the grid above guarantees. On a phone the pictured card still puts it
   on its own row, on every card alike, by flex-basis: 100% below. */
.current-lane__pick {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: var(--space-6);
  min-height: var(--touch-min);
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.current-lane__pick:focus-visible {
  outline: var(--focus-width) solid var(--gold);
  outline-offset: var(--focus-offset);
}

.current-lane__pick:disabled {
  cursor: not-allowed;
}

/* Picked is unmistakable from across a table: gold ring and gold wash. */
.current-lane.is-picked {
  border-color: var(--gold);
  background: var(--tint-gold);
  box-shadow: 0 0 0 var(--focus-width) var(--gold);
}

/* The picked card widens in its row (Ryan, 2026-09-24 23:34: "this should
   slide horizontally to expand and take space from the other dogs areas
   instead of sliding the winner button down"; the rc2 notes, §10). Above
   640px race.js fitLanes lays the heat's cards out in the grid's own rows,
   read with nothing whole: each card a width it measured (--lane-w), each
   card its row's height (--lane-h), so the box never changes height and the
   buttons under it stay where they were to the pixel. A picked card whose
   words fit the width the grid gave it keeps it; otherwise it takes the
   rest of its row and shows the whole name and owner, set close
   (--leading-tight, as the phone's picked chip is), each on one line
   (is-one-line). Where even the widest card cannot hold them so at full
   size they are set smaller together (--pick-scale), still on one line,
   down to the readable floor (the name at --text-caption, the owner never
   under it); only then does the name take a second line, and the owner
   is cut (is-owner-cut) only where the whole name leaves it no room (race.js
   fitPickWords; Ryan, 2026-09-25). The name is never cut, and a word is
   never broken to fit. The others in its row give it room: as they are, then cut
   shorter (is-short: the ellipsis falls where their narrower column ends),
   then the lane number and the picture alone (is-bare: the picture link and
   the pick button, 44x44 each, still tappable). Every other row is as the
   grid drew it. The width eases over --dur-base, and _reduced-motion.scss
   makes it a change, as does Event settings' Animations off, the default
   (.current-lanes--still, set by fitLanes, as the phone bar's row has
   .current-bar--still); while the picked card
   widens its words are already at their final width (--who-w) and the card
   reveals them (is-growing) rather than reflowing them every frame.
   Replaces the 1.10.0 build that grew the picked card down, its words
   flowing round the picture: that made the pinned box 110px taller with the
   harness's long name and slid Wiener down under the finger. Until fitLanes
   has laid the row out (.current-lanes--flow) the picked card is cut like
   the rest, so it never grows the box. On a phone the cards are a sideways
   strip, and the picked card there widens as built (_phone.scss). */
@media (min-width: 641px) {
  .current-lanes--flow {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
  }
  .current-lanes--flow > .current-lane {
    flex: 0 0 auto;
    flex-wrap: nowrap;
    width: var(--lane-w);
    height: var(--lane-h);
    transition: width var(--dur-base) ease-out;
  }
  /* While fitLanes measures, nothing in the strip eases - not even with
     Reduce Motion, whose rule gives every property of every element a
     0.01ms transition (_reduced-motion.scss), so a measurement taken as a
     class changes would read the old value of a min-width or a font size. */
  .current-lanes.is-measuring, .current-lanes.is-measuring * {
    transition: none !important;
  }
  .current-lanes--flow.current-lanes--still > .current-lane {
    transition: none;
  }
  .current-lanes--flow > .current-lane.is-picked .lane__who strong,
  .current-lanes--flow > .current-lane.is-picked .dogowner {
    max-width: none;
    overflow: visible;
    white-space: normal;
    overflow-wrap: normal;
    line-height: var(--leading-tight);
  }
  .current-lanes--flow > .current-lane.is-picked .lane__who strong {
    font-size: calc(var(--text-base) * var(--pick-scale, 1));
  }
  .current-lanes--flow > .current-lane.is-picked .dogowner {
    font-size: max(var(--text-caption), var(--text-sm) * var(--pick-scale, 1));
  }
  .current-lanes--flow > .current-lane.is-picked.is-one-line .lane__who strong,
  .current-lanes--flow > .current-lane.is-picked.is-one-line .dogowner,
  .current-lanes--flow > .current-lane.is-picked.is-owner-cut .dogowner {
    white-space: nowrap;
  }
  .current-lanes--flow > .current-lane.is-picked.is-owner-cut .dogowner {
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .current-lanes--flow > .current-lane.is-growing {
    overflow: hidden;
  }
  .current-lanes--flow > .current-lane.is-growing .lane__who {
    flex: 0 0 var(--who-w);
  }
  .current-lanes--flow > .current-lane.is-bare .lane__who {
    display: none;
  }
  .current-lanes--flow > .current-lane.is-bare > .current-lane__pick {
    flex: 0 0 auto;
    min-width: var(--touch-min);
    justify-content: center;
  }
}
/* Was 42px. This is Advance, the one-tap result for a heat of one; it drops
   onto its own full-width row under the dog. */
.current-lane .btn {
  flex: 1 1 100%;
  min-height: var(--touch-min);
  padding: var(--space-6) var(--space-12);
}

.current-actions {
  align-items: center;
}

/* The hints under the buttons read as one caption block: the paragraph's own
   bottom margin had set them 4px and 16px apart (DS-29). */
.current-actions ~ .field__hint {
  margin: var(--space-4) 0 0;
}

/* What happens after this race, at a round boundary: a statement about the
   event, not the routine hint above it, so it is set as one (DS-15). */
.current-next {
  margin: var(--space-8) 0 0;
  font-size: var(--text-sm);
  font-weight: 700;
  color: var(--text);
}

/* "Wiener:" and the picked name are two flex items - the name has to be a box
   of its own to be cut at --name-cap - so what stands between them is the
   button's 8px gap, not the space after the colon, which a line drops at its
   end. 8px read as a double space: 10px of white between the colon and the B
   at 768, against 5px for a word space in the same button. 4px is a word space
   at the button's 14px (S6). */
.current-actions__win {
  gap: var(--space-4);
}

/* The picked dog's name inside "Wiener: ...", cut at the card's --name-cap.
   A .btn never wraps, so the whole name made the button as long as the name:
   553px beside Too Close to Call on a desktop, and on a 393px phone it took
   the page 577px wide into sideways scroll. The button's aria-label still
   names the dog in full. */
.current-actions__name {
  min-width: 0;
  max-width: var(--name-cap);
  overflow: hidden;
  text-overflow: ellipsis;
}

/* One width, whatever it says: its width at rest, "Record Wiener"'s, at
   every size. "Wiener: Lightning McQ..." was 43px wider, so a pick slid Too
   Close to Call, Undo and Move a dog along the row, and on a phone wrapped
   it onto a third line (Ryan, 2026-09-25: a pick never moves a button). So
   the stylesheet sets the button's width by "Record Wiener" drawn unseen
   (::after, the same words race.js writes; test/race-control-no-grow.test.js),
   exactly as wide as the words it had at rest, and the words it shows are a
   face laid over that width inside its padding: "Record Wiener" at rest, and
   picked, "Wiener:" whole with the dog's name cut to what is left - "Wiener:
   Lightn..." - since the card and the chip above show the whole name, and
   the button's aria-label names the dog in full. The rest of the row is as
   it was before a pick was ever made. */
.current-actions__win {
  position: relative;
}

.current-actions__win::after {
  content: "Record Wiener";
  visibility: hidden;
}

.current-actions__face {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-4);
  padding: inherit;
  overflow: hidden;
}

/* Where "Wiener: <name>" would cut the name on one line - "Wiener: Bisc..."
   on an upright iPad (PH-2) - fitWinName stacks the face: "Wiener:" small
   over the name, both inside the height the button already has (the face
   is laid over the button, so nothing it does can size it), and the name
   shrinks by --win-scale to the readable floor before its end is ever cut.
   12px and 14px at the tight leading are 31px, in the 36px the face has
   with 4px above and below. */
.current-actions__win.is-stacked .current-actions__face {
  flex-direction: column;
  gap: 0;
  padding-block: var(--space-4);
  line-height: var(--leading-tight);
}

.current-actions__win.is-stacked .current-actions__label {
  font-size: var(--text-caption);
}

.current-actions__win.is-stacked .current-actions__name {
  max-width: 100%;
  font-size: calc(var(--text-sm) * var(--win-scale, 1));
}

/* -------------------------------------------------- the slim pinned bar */
/* Race Control's current heat, small, for a phone: the heat's short name
   and its dogs on one row; Wiener, Too Close to Call, Undo, Message and the
   Big Screen button on the next; and under them, folded behind the Big
   Screen button, the five Big Screen modes (decision #10). The full box is a
   third of that screen with photos on, so it scrolls away there and this
   takes its place under the top bar for the rest of the page: Ryan's "a
   slim version is right - what's wrong is that it disappears". Hidden
   entirely above 640px, where the full box is pinned itself and there is
   nothing to stand in for. Fixed rather than sticky because it has no place
   in the flow to stick to. The rules are in the phone block (_phone.scss). */
.current-bar {
  display: none;
}

.current-bar[hidden] {
  display: none;
}

/* The five Big Screen modes as icons, in whichever current heat is pinned:
   in the full box above 640px, once the page has scrolled past their own
   controls, at the right-hand end of its button row after the Announcer's
   megaphone (_screen-tools.scss) - Devin: "right aligned on same row as
   other buttons to allow more screen real estate to see other races". They
   rode in the full box's heading row before, which they made 17px taller
   (DS-05). On a phone they are the slim bar's folded row, where they fill
   the line with a word under each (.mode-icons--fill, in the phone block).
   A target is never made smaller than 44x44. Each says what it does to a
   screen reader; the active one is filled gold the way the full row fills
   it, and each wears the icon its full button wears. */
.mode-icons {
  display: flex;
  flex: 0 0 auto;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--space-6);
  max-width: 100%;
  margin-left: auto;
}

.mode-icons__btn {
  flex: 0 0 auto;
  min-width: var(--touch-min);
  min-height: var(--touch-min);
  padding: var(--space-6);
}

.mode-icons__btn .icon {
  width: 22px;
  height: 22px;
}

/* The slim bar's buttons carry each mode's one word, shown only where the
   five fill a line of their own (.mode-icons--fill, in the phone block). */
.mode-icons__word {
  display: none;
}

/* The full Big Screen mode buttons, on a phone and an upright iPad: 44px
   tall, --text-sm, like the current heat's buttons above them, with 8px each
   side of the icon and the words. Each wears its mode's icon before its words
   now, so staff learn the icons the pinned heat shows (Devin), and at full
   size that cost the card a row: two rows where one had held the four at 641,
   700 and 744 (an iPad mini), three where two had on a 360px phone. At this
   size the four are one row of 44px from 641 to 899 and two rows at 360 to
   430; only a 320px phone takes three. From 900px they keep the full size,
   one row, as before. */
@media (max-width: 899px) {
  .mode-buttons > .btn {
    padding: var(--space-8);
    min-height: var(--touch-min);
    font-size: var(--text-sm);
  }
}
/* Cut at a fixed number of characters, not at whatever width the card
   happens to be. Where the ellipsis landed used to move with the heat size
   and the screen - "Baroness Wilhelmi..." at 1440, "Baroness Wilh..." at 834 -
   so the same dog read differently on each device. This is a private screen:
   the lane number and the picture say which dog it is, the card's label
   still carries the whole name for a screen reader, and the dog's own page
   has it in full. */
.current-lane .lane__who strong, .current-lane .dogowner {
  display: block;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.current-lane .lane__who strong {
  max-width: var(--name-cap);
}

.current-lane .dogowner {
  max-width: var(--owner-cap);
}

/* Except the picked dog's card, which shows its whole name and owner
   until it is unpicked - tapped again, recorded, or another dog picked - and
   is cut again. Ryan, on the phone's slim bar: "expanding it when selected
   to display the whole name and then if unselected it shrinks back down". A
   tap on a card already picks the dog for Wiener, so here it cannot also
   open a name tip the way a tap on a lane does (Staff.nameTip). Every other
   card keeps its cap. Where it is uncapped depends on the width: on a phone
   the card widens and keeps the height it had cut (_phone.scss); above
   640px it widens in its row once race.js fitLanes has laid the row out
   (_rounds.scss). Either way a pick never makes a row taller. */
.late-controls {
  display: flex;
  gap: var(--space-12);
  align-items: center;
}

.late-controls select {
  flex: 1;
}

/* One group, last in the row: the auto left margin takes whatever the
   buttons before it leave, so it ends at the row's right edge, and it never
   wraps inside itself. Where the row cannot hold it the buttons before it
   give up their words (race.js fitActions). Icons alone, as the pinned row
   was before 1.10.0: the result buttons keep their words (the rc2 pinned
   row, Ryan's default 31). */
.screen-tools {
  display: flex;
  flex: 0 0 auto;
  align-items: stretch;
  gap: var(--space-6);
  max-width: 100%;
  margin-left: auto;
}

.screen-tools > .mode-icons {
  margin-left: 0;
  flex-wrap: nowrap;
}

/* A hairline between the Announcer and the Big Screen: two different
   screens, so the megaphone does not read as a sixth mode. */
.screen-tools__rule {
  flex: none;
  width: 1px;
  margin-block: var(--space-6);
  background: var(--border-strong);
}

/* The hint under the Big Screen controls names the three screens as small
   links in its sentence, the one kind of control allowed under 44x44
   (decision #6). It sits 16px under the mode buttons (the rc2 notes,
   Usability 8), so a thumb that misses a mode lands in the gap rather than
   on a link, and one that misses a link does not switch the TV. It holds
   the link tip, below. */
#screenControls > p.field__hint {
  position: relative;
  margin-top: var(--space-16);
}

/* By touch each link takes two taps (Staff.linkTip): the first shows this
   tip under the sentence - under it, never over the link, so it can never
   cover a mode button - saying what the second will do, and marks the link
   it is for. A second tap on the link, or a tap on the tip, opens the
   screen; a tap anywhere else, a scroll or Escape puts it away. At least
   44px tall, as the whole tip is a target. --tip-x and --caret-x are the
   distances Staff measures as it opens (placeLinkTip), like the name tip's
   --tip-shift. Hidden, not removed, when it is put away (the global [hidden]
   rule), so a tap on it still opens the screen. */
.hint-link.is-armed {
  background: var(--tint-blue);
  border-radius: var(--radius-sm);
  box-shadow: 0 0 0 var(--space-2) var(--tint-blue);
}

.link-tip {
  position: absolute;
  top: calc(100% + var(--space-10));
  left: var(--tip-x, 0);
  z-index: var(--z-tip);
  display: grid;
  gap: var(--space-2);
  align-content: center;
  width: max-content;
  max-width: 100%;
  min-height: var(--touch-min);
  padding: var(--space-8) var(--space-12);
  background: var(--surface-raised);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  color: inherit;
  text-decoration: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.link-tip__what {
  font-size: var(--text-sm);
  font-weight: 700;
  color: var(--text);
}

.link-tip__how {
  font-size: var(--text-caption);
  font-weight: 700;
  color: var(--accent);
}

/* The caret, pointing up at the link: a square turned on its corner, its
   two top edges the tip's own border. */
.link-tip::after {
  content: "";
  position: absolute;
  left: var(--caret-x, var(--space-16));
  top: calc(-1 * var(--space-6));
  width: var(--space-12);
  height: var(--space-12);
  background: var(--surface-raised);
  border-left: 1px solid var(--border-strong);
  border-top: 1px solid var(--border-strong);
  transform: rotate(45deg);
}

/* ------------------------------------------------------ where you were */
/* Three things for getting about a long staff page (1.10.0, Ryan
   2026-09-25 13:26: "with easy scroll to top icon (maybe quick jump option
   to a round as well?)"), all drawn by public/js/staff.js: the button back
   to the top, the round names on both boards as buttons, and the "Jump to"
   list they open. Where each page was left is kept by the same script and
   needs no style of its own. */
/* Back to the top (staff.js watchTopButton): one round button, the sprite's
   arrow up, at the foot of every staff page, shown once the page is about a
   screen down. It sits above the phone's tab row and the connection strip
   the way the toast does, and staff.js lifts it (--to-top-lift, measured)
   clear of the toast and the Announcer's notice while they are up, and
   just above a control or an open tip that is under it; only with nothing
   clear within a few rows does it step aside (is-tucked): a tap can never
   land on it meant for what is under it.

   Hidden, not just transparent, while it is down: nothing to tap by
   accident, nothing for a screen reader to reach, and the harness's
   tap-target audit passes it over rather than measure a button nobody can
   see. The delay on visibility lets the fade play before it goes. */
.to-top {
  position: fixed;
  right: max(var(--space-16), env(safe-area-inset-right));
  bottom: calc(var(--staff-tabbar-height, 0px) + var(--staff-conn-height, 0px) + var(--space-16) + var(--to-top-lift, 0px));
  z-index: var(--z-conn);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--touch-min);
  height: var(--touch-min);
  padding: 0;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-pill);
  background: var(--surface-raised);
  color: var(--accent);
  box-shadow: var(--shadow-lg);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transform: translateY(var(--space-12));
  transition: opacity var(--dur-base) var(--ease), transform var(--dur-base) var(--ease), visibility 0s linear var(--dur-base);
}

.to-top.is-up {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transform: none;
  transition: opacity var(--dur-base) var(--ease), transform var(--dur-base) var(--ease), visibility 0s;
}

.to-top.is-up.is-tucked {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transform: translateY(var(--space-12));
  transition: opacity var(--dur-base) var(--ease), transform var(--dur-base) var(--ease), visibility 0s linear var(--dur-base);
}

.to-top:active {
  background: var(--surface-sunken);
}

/* On a phone, and a phone on its side, it sits nearer the edge: in
   styles/_phone.scss, with the phone's other rules (review PM-R1). */
@media (hover: hover) {
  .to-top:hover {
    background: var(--surface-sunken);
  }
}
/* Jump to a round (staff.js roundTitle, toggleJump): on Bracket Builder and
   on Race Control's "Bracket and results" the round's name in its pinned
   heading is a button with a chevron after it. It keeps the heading's own
   height: its 44px box overlaps the heading's padding above and below the
   name, so the pinned strip on a phone costs no extra line, and it wears
   the heading's font and colour, which each round tints its own way. */
.round-title {
  margin: 0;
}

.round-jump {
  display: inline-flex;
  align-items: center;
  gap: var(--space-4);
  min-height: var(--touch-min);
  margin: calc(-1 * var(--space-10)) 0;
  padding: 0 var(--space-4) 0 0;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.round-jump__chevron {
  width: 1em;
  height: 1em;
  transition: transform var(--dur-fast) var(--ease);
}

.round-jump[aria-expanded=true] .round-jump__chevron {
  transform: rotate(180deg);
}

/* The list itself: one for the page, on the body, so a poll drawing the
   board again under it does not take it away. staff.js places it under the
   heading it came from (--jump-top, --jump-left) inside the screen's
   gutters, and gives it the room above the phone's tab row (--jump-room); a
   list taller than that scrolls inside itself. Above the pinned headings and
   Race Control's pinned heat (--z-toolbar), under the top bar. */
.jump-menu {
  position: fixed;
  top: var(--jump-top, 0px);
  left: var(--jump-left, 0px);
  z-index: var(--z-toolbar);
  display: grid;
  gap: var(--space-2);
  width: max-content;
  min-width: calc(var(--touch-min) * 4);
  max-width: calc(100% - 2 * var(--space-12));
  max-height: var(--jump-room, calc(var(--touch-min) * 6));
  overflow: auto;
  padding: var(--space-8);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  background: var(--surface-raised);
  color: var(--text);
  box-shadow: var(--shadow-modal);
}

.jump-menu__title {
  margin: 0;
  padding: var(--space-4) var(--space-12) var(--space-6);
  color: var(--text-muted);
  font-size: var(--text-caption);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

/* Each round a row: its name, and at the right its heats and how many are
   raced, the words its heading already carries. The round the list was
   opened from is marked. */
.jump-menu__item {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-12);
  width: 100%;
  min-height: var(--touch-min);
  padding: var(--space-8) var(--space-12);
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: inherit;
  font: inherit;
  font-weight: 700;
  text-align: left;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.jump-menu__item[aria-current] {
  background: var(--tint-blue);
  color: var(--accent);
}

.jump-menu__item:active {
  background: var(--surface-sunken);
}

@media (hover: hover) {
  .jump-menu__item:hover {
    background: var(--surface-sunken);
  }
}
.jump-menu__range {
  color: var(--text-muted);
  font-size: var(--text-sm);
  font-weight: 600;
  white-space: nowrap;
}

/* Neither belongs on paper. */
@media print {
  .to-top, .jump-menu {
    display: none;
  }
}
/* ------------------------------------------------ after the Final */
/* One note: what the Big Screen is doing, with the switch at its end, so the
   card is barely taller than the note it replaced and the Championship card's
   buttons stay on the first screen of an iPad on its side. */
.note--end {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-16);
  padding-block: var(--space-4);
}

.note--end > .note__text {
  flex: 1 1 0;
  min-width: 0;
  margin: 0;
}

.note--end > .toggle {
  flex: none;
  padding-block: 0;
}

/* "Keep the Grand Wiener up" as three choices since 2nd and 3rd place
   (2026-09-26): Keep rotating, Grand Wiener only, Top three - the one in
   force gold, as a lit Big Screen mode is. */
.keep-choices {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-8);
  flex: 0 1 auto;
  min-width: 0;
}

/* Top three before a place is picked: tappable, but grey like a button
   that is not ready - its tap sends nothing to the Big Screen and opens the
   podium dialog (race.js editPodium) - with the line under the choices
   saying 2nd and 3rd come first. */
.keep-choices .btn.is-waiting {
  background: var(--surface-sunken);
  color: var(--text-muted);
  border-color: var(--border);
}

.keep-choices__hint {
  flex-basis: 100%;
  margin: 0;
}

/* ------------------------------------------ Break's options */
/* A sunken panel with the gold edge of the mode that opened it, straight
   under the mode buttons: the label in one column, its controls and hint in
   the other, so the two rows line up. */
.break {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  gap: var(--space-10) var(--space-16);
  margin-top: var(--space-14);
  padding: var(--space-14) var(--space-16);
  border: 1px solid var(--border);
  border-left: var(--status-edge) solid var(--gold);
  border-radius: var(--radius-md);
  background: var(--surface-sunken);
}

.break__now {
  grid-column: 1/-1;
  display: flex;
  align-items: flex-start;
  gap: var(--space-8);
  margin: 0;
  font-size: var(--text-sm);
  line-height: var(--leading-base);
}

.break__now .icon {
  flex: none;
  color: var(--accent);
}

/* Its words, and unseen in the same cell the words it would say were the
   running countdown switched to the other kind (race.js otherShows), so a
   tap on the switch never rewraps the line and moves the row under it. */
.break__now-words {
  display: grid;
  min-width: 0;
}

.break__now-face {
  grid-area: 1/1;
}

.break__now-face.is-other {
  visibility: hidden;
}

/* A Break between rounds (1.10.2): one line under "On the Big Screen:" saying
   the break started by itself when the round ended and racing starts at zero. */
.break__auto {
  grid-column: 1/-1;
  display: flex;
  align-items: flex-start;
  gap: var(--space-8);
  margin: 0;
  font-size: var(--text-sm);
  font-weight: 700;
  line-height: var(--leading-base);
}

.break__auto .icon {
  flex: none;
  color: var(--accent);
}

/* Its words while the break counts, and once No timer has stopped it
   (1.11.0), in one cell, the other unseen, so the line never changes height
   and the Countdown row under it never moves as the tap lands. */
.break__auto-words {
  display: grid;
}

.break__auto-words > * {
  grid-area: 1/1;
}

.break__auto-off, .break__auto.is-stopped .break__auto-on {
  visibility: hidden;
}

.break__auto.is-stopped .break__auto-off {
  visibility: visible;
  font-weight: 400;
  color: var(--text-muted);
}

/* Paused by Stop timer (1.12.0): "Break between rounds paused at 0:21", a third face in the same cell. */
.break__auto-paused, .break__auto.is-paused .break__auto-on {
  visibility: hidden;
}

.break__auto.is-paused .break__auto-paused {
  visibility: visible;
}

.break__row {
  grid-column: 1/-1;
  display: grid;
  grid-template-columns: subgrid;
  align-items: center;
  row-gap: var(--space-2);
}

.break__label {
  grid-column: 1;
  margin: 0;
  font-size: var(--text-sm);
  font-weight: 700;
}

.break__controls {
  grid-column: 2;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-8);
  min-width: 0;
}

.break__in {
  min-width: 9ch;
  font-size: var(--text-sm);
  font-weight: 700;
  color: var(--text-muted);
  white-space: nowrap;
}

/* With no countdown set its room is kept, unseen, beside the countdown's
   type (1.11.0), so starting or stopping one moves no button: its width,
   and a control's height, which it holds empty too - on a 320px phone,
   where it takes a line of its own under the type, an empty line of text
   had no height and the type jumped 21px as a countdown started. */
/* white-space: pre keeps the space in "in 12:47": as a flex container its
   "in " is an item of its own, whose last space would otherwise go (review NT-3). */
.break__in {
  display: inline-flex;
  align-items: center;
  min-height: var(--touch-min);
  white-space: pre;
}

.break__in.is-unset {
  visibility: hidden;
}

/* The countdown's digits, in "On the Big Screen:" and beside the presets
   ("in 12:47"): figures of one width, in room for the widest they will read
   before zero (data-room, "00:00", set unseen in the same cell), so as they
   tick every second nothing beside or after them moves. */
.break__count {
  display: inline-grid;
  font-variant-numeric: tabular-nums;
}

.break__count::before {
  content: attr(data-room);
  grid-area: 1/1;
  visibility: hidden;
}

.break__n {
  grid-area: 1/1;
}

.break__clear {
  min-width: var(--touch-min);
  padding: var(--space-6);
}

/* "At 6:00 PM (in 12 min)" (1.11.0): "Building bracket in" to the race's
   scheduled start. Its words sit on room held for the longest they read
   (.break__at-room, unseen, in the same cell), so the minutes going down
   never move its edge. */
.break__at {
  flex: none;
}

.break__at-face {
  display: inline-grid;
}

.break__at-words, .break__at-room {
  grid-area: 1/1;
  white-space: nowrap;
}

.break__at-room {
  visibility: hidden;
}

.break__hint {
  grid-column: 2;
  margin: 0;
}

/* The row's hint over the kind's own (1.11.0): the timer is optional. */
.break__hints {
  display: grid;
  gap: var(--space-2);
}

.break__hints > * {
  grid-column: 1;
  margin: 0;
}

/* The countdown's row (1.11.0, race.js timerRow): No timer, 1, 2, 3, 5 and
   Other…, on a line of its own under the countdown's type, each at least
   44x44 and never narrower than its words. The one in force - No timer
   while none is set - is tinted and edged in the accent as the type switch
   marks its own, so a pick changes a colour and never a size. Where the row,
   measured, would run past its line (race.js fitTimerRows) it wraps under
   No timer, which takes a line of its own, rather than shrink a button. */
.timer-row {
  display: flex;
  flex: 1 1 100%;
  flex-wrap: nowrap;
  gap: var(--space-6);
  min-width: 0;
}

.timer-row__opt {
  flex: 0 0 auto;
  min-width: var(--touch-min);
  padding-inline: var(--space-12);
}

.timer-row__opt[aria-pressed=true] {
  background: var(--tint-blue);
  color: var(--accent);
  border-color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent);
}

.timer-row--stacked {
  flex-wrap: wrap;
}

.timer-row.timer-row--stacked > .timer-row__none {
  flex-basis: 100%;
}

/* Its first button reads "No timer" while none is set and "Stop timer", with
   the close mark, while one is (Ryan, 2026-09-27). Both faces sit in one
   cell, the one not in force unseen, so the button is as wide as the wider
   and nothing in the row moves as the words change. */
.timer-row__opt.timer-row__none {
  display: inline-grid;
  justify-items: center;
}

.timer-row__face {
  grid-area: 1/1;
  display: inline-flex;
  align-items: center;
  gap: var(--space-6);
}

.timer-row__face.is-other {
  visibility: hidden;
}

/* "Resume 0:21" (1.12.0), while Stop timer has paused a Break between rounds:
   the third face, its figures of one width, edged and lettered in the accent
   so the way on is found - a colour, never a size, changes. */
.timer-row__resume {
  font-variant-numeric: tabular-nums;
}

.timer-row__none.is-resume {
  color: var(--accent);
  border-color: var(--accent);
}

/* The countdown's type (Ryan, 2026-09-25 11:55-11:57): "Race starts in" or
   "Line up in", two buttons joined into one control beside the presets (in
   the pinned panel, _break-times.scss, before them). The kind in force is
   tinted and edged in the accent, as an open toggle is (_buttons.scss),
   rather than filled, which reads as a submit button. While a countdown is
   set the switch is locked: the other kind greys, the one counting keeps
   its look, and both keep their size, so nothing moves as it locks. */
.kind-switch {
  display: inline-flex;
  flex: none;
}

.kind-switch__opt {
  background: var(--surface-raised);
  color: var(--text);
  border-color: var(--border-strong);
  border-radius: 0;
}

.kind-switch__opt:first-child {
  border-radius: var(--radius-md) 0 0 var(--radius-md);
}

.kind-switch__opt:last-child {
  margin-left: -1px;
  border-radius: 0 var(--radius-md) var(--radius-md) 0;
}

/* Before the bracket is built the switch is one button, "Building bracket in". */
.kind-switch__opt:only-child {
  margin-left: 0;
  border-radius: var(--radius-md);
}

@media (hover: hover) {
  .kind-switch__opt:hover {
    background: var(--tint-blue);
  }
}
.kind-switch__opt:active {
  background: var(--tint-blue);
}

.kind-switch__opt[aria-pressed=true] {
  position: relative;
  background: var(--tint-blue);
  color: var(--accent);
  border-color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent);
}

.kind-switch.is-locked .kind-switch__opt {
  cursor: not-allowed;
}

.kind-switch.is-locked .kind-switch__opt:active {
  transform: none;
}

.kind-switch.is-locked .kind-switch__opt[aria-pressed=false] {
  background: var(--surface-sunken);
  color: var(--text-muted);
  border-color: var(--border);
}

/* The hint under it: one for each kind in the same cell, the other unseen,
   so the switch changes the words and never the card's height. */
.kind-hint {
  display: grid;
}

.kind-hint__words {
  grid-area: 1/1;
}

.kind-hint__words.is-other {
  visibility: hidden;
}

/* The hint under the Countdown row: with no timer, and while one runs (what
   Stop timer does), in one cell, the other unseen, for the same reason. */
.stop-hint {
  display: grid;
}

.stop-hint__words {
  grid-area: 1/1;
}

.stop-hint__words.is-other {
  visibility: hidden;
}

.break__foot {
  grid-column: 1/-1;
  margin: 0;
}

/* Why a "Building bracket in" zero built nothing: the gold note, across the card. */
.break__miss {
  grid-column: 1/-1;
  margin: 0;
}

/* "Racing resumes at": the time as a field of its own, "Set a time" until
   one is set. The real time input lies over it, see-through, so a tap opens
   the device's own time wheel (a phone) or picker (a computer), and the face
   under it says the time the way every screen prints it. */
.break__time {
  position: relative;
  display: inline-grid;
  place-items: center;
  flex: none;
  width: var(--time-field);
  min-height: var(--touch-base);
  padding: var(--space-8);
  border: 1px solid var(--border-field);
  border-radius: var(--radius-md);
  background: var(--surface-raised);
  font-weight: 700;
}

.break__face.is-empty {
  color: var(--text-muted);
}

.break__picker {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  opacity: 0;
  cursor: pointer;
  font: inherit;
}

/* The input is see-through, so its focus ring is drawn on the field around it. */
.break__time:focus-within {
  outline: var(--focus-width) solid var(--accent);
  outline-offset: var(--focus-offset);
}

/* On a phone: the phone block in _phone.scss (the five modes in two columns,
   the options in one). */
/* -------------------------------------------------------------- dialogs */
.placement-dialog {
  width: min(700px, 94vw);
  max-height: 88vh;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  /* The side padding, named once: the pinned Save row reaches out through it. */
  --dialog-pad: var(--space-24);
  padding: var(--space-24) var(--dialog-pad) 0;
  font: inherit;
  background: var(--surface-raised);
  color: var(--text);
  box-shadow: var(--shadow-modal);
}

/* Pinned, so a dialog taller than a phone still shows how to leave it. It
   runs edge to edge - out through the dialog's side padding and back in by
   the same amount, so the buttons stay where they were - so its rule and the
   "more below" shadow read as a bar across the dialog, not a box floating
   inside it (DS-03). The dialog's own overflow clips the shadow at its edges;
   the buttons' focus rings stay inside the row's padding. */
.placement-dialog > .btnrow:last-child {
  position: sticky;
  bottom: 0;
  margin-top: var(--space-16);
  margin-inline: calc(-1 * var(--dialog-pad));
  padding: var(--space-12) var(--dialog-pad) var(--space-16);
  background: var(--surface-raised);
  border-top: 1px solid var(--border);
}

/* ...and while content carries on under that row, the row says so: a shadow
   lifts it off what is underneath and the edge of that content fades into
   it. The 1px rule was the only cue, so a list that happened to end exactly
   at the row looked complete (DS-03). staff.js markDialogOverflow sets the
   class and takes it off once the end of the dialog is in view. The fade
   ends on the row's padding edge, so its last pixel lay over the row's own
   rule and the rule vanished while there was more; the fade draws that rule
   again as its bottom border, the same 1px in the same colour. */
.placement-dialog--more > .btnrow:last-child {
  box-shadow: var(--shadow-up);
}

.placement-dialog--more > .btnrow:last-child::before {
  content: "";
  position: absolute;
  inset: auto 0 100%;
  height: var(--space-28);
  background: linear-gradient(to bottom, transparent, var(--surface-raised));
  border-bottom: 1px solid var(--border);
  pointer-events: none;
}

.placement-dialog::backdrop {
  background: rgba(4, 25, 47, 0.6);
}

.placement-dialog h2 {
  font-size: var(--text-xl);
}

.placement-dialog .btnrow {
  margin-top: var(--space-16);
}

/* A label wider than the dialog wraps, in balanced lines, instead of running
   out through the side padding: at 375px the Big Screen question's "Switch to
   Race presentation and do it" (a .btn is nowrap) ended 19px past the padding,
   at the dialog's edge. A row of buttons still wraps button by button first -
   a button only shrinks when it is wider than the whole row on its own - and
   a two-line button is taller than 44px, never shorter. */
.placement-dialog .btnrow > .btn {
  white-space: normal;
  text-wrap: balance;
  text-align: center;
}

/* A note in the pinned row - the late-arrival picker's "Frieda joins
   Qualifiers · Heat 4 of 6." - takes a line of its own above the buttons,
   so a pick is confirmed where the eye already is. */
.placement-dialog > .btnrow:last-child > .note {
  flex: 1 1 100%;
  margin: 0;
}

.placement-dialog .lane {
  font-size: var(--text-base);
  color: var(--text);
  font-weight: 400;
  margin-bottom: 0;
}

.placement-dialog .lane input {
  width: 26px;
  height: 26px;
  flex: none;
  accent-color: var(--accent);
}

/* Correct result's rows: the name over its owner, as on the heat cards, and
   whole - a word too long for the line breaks rather than running out of the
   dialog, and nothing is cut (DS-19). */
.placement-dialog .lane .lane__who {
  overflow-wrap: break-word;
}

.placement-dialog .lane .lane__who > * {
  display: block;
}

/* The podium dialog (race.js askPodium, Ryan 2026-09-26: "the too close to
   call window with the already selected champion locked"): Too Close to
   Call's rows, the Champion's first and locked with a gold tag, the dog in
   2nd locked under it on the second step with a silver one; the row picked
   washed gold, as a picked lane is. The heading over the dogs from the heats
   before the Final is the dialogs' own. */
/* One column no wider than the dialog: a grid track's default minimum is its
   longest word, and a long Champion with the tag beside it made a row wider
   than a phone's dialog, which then scrolled sideways (PCT-1). */
.podium-dialog__rows {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-4);
  margin-top: var(--space-12);
}

.podium-dialog__step {
  margin: 0;
}

.podium-row {
  border-radius: var(--radius-md);
  padding-inline: var(--space-8);
  min-height: var(--touch-min);
}

.podium-row.is-picked {
  background: var(--tint-gold);
  box-shadow: inset 0 0 0 1px var(--tint-gold-line);
}

.podium-row--locked {
  background: var(--surface-sunken);
}

/* A locked row wraps: the Champion tag drops under the name, at the row's
   end, when the name would otherwise have to break inside its words. */
.podium-row--locked {
  flex-wrap: wrap;
  row-gap: var(--space-4);
}

.podium-row .lane__who {
  flex: 1 1 auto;
  min-width: 0;
}

/* After the rule above, which it must beat: a basis of its own, not its whole
   name's width, so the name stays beside its picture and wraps there. */
.podium-row--locked .lane__who {
  flex: 1 1 calc(var(--name-cap) * 1.5);
}

.podium-row__tag {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: var(--space-4);
  margin-left: auto;
}

.podium-dialog__heat {
  margin: var(--space-6) 0 0;
  font-size: var(--text-sm);
  font-weight: 700;
  color: var(--text-muted);
}

.podium-row__tag--gold {
  background: var(--gold);
  color: var(--text-on-gold);
}

.podium-dialog__from {
  margin: var(--space-12) 0 var(--space-4);
}

/* The photo cropper. A dog's picture is a circle everywhere it appears, so the
   dialog masks the square to a circle and saves exactly that - what staff frame
   here is what the roster, the dog page and every kiosk show. */
.cropper__frame {
  position: relative;
  /* As big as the room allows: framing a dog's face is fiddly, and this is the
     only thing in the dialog worth space. Bounded by height as well as width so
     a tablet held sideways does not push the buttons out of the dialog. */
  width: min(420px, 72vw, 46vh);
  aspect-ratio: 1;
  margin: var(--space-16) auto 0;
  overflow: hidden;
  border-radius: var(--radius-md);
  background: var(--tint-blue);
  /* The gesture is the whole control, so the browser must not claim it for
     scrolling or its own pinch-zoom first. */
  touch-action: none;
  cursor: grab;
}

.cropper__frame:active {
  cursor: grabbing;
}

.cropper__canvas {
  display: block;
  width: 100%;
  height: 100%;
}

/* One element does both jobs: the ring marks what is kept, and the spread
   shadow dims everything outside it, clipped by the frame. */
.cropper__mask {
  position: absolute;
  inset: 0;
  border-radius: var(--radius-pill);
  box-shadow: inset 0 0 0 2px var(--gold), 0 0 0 9999px rgba(4, 25, 47, 0.55);
  pointer-events: none;
}

.cropper__tools {
  display: flex;
  align-items: center;
  gap: var(--space-12);
  margin-top: var(--space-16);
}

.cropper__zoom {
  flex: 1 1 auto;
  min-width: 0;
  height: var(--touch-min);
  accent-color: var(--accent);
}

.placement-dog {
  font-weight: 700;
  color: var(--accent);
}

/* A confirmation's second line is text to read, not the accent colour links,
   ghost buttons and a dog's owner line (.placement-dog) use (DS-20). */
.dialog-detail {
  font-weight: 700;
  color: var(--text);
}

.placement-heading {
  margin: var(--space-20) 0 var(--space-10);
  font-size: var(--text-base);
}

.placement-choices {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: var(--space-10);
  margin-bottom: var(--space-20);
}

/* Take back's three answers, one under another: two across left the third
   alone on a row of its own (DS-22). The dogs to pick from too (Take back and
   Change the Champion), one full-width row each, as Correct result lists
   them: two across left a fifth dog alone in half a row, its long name five
   lines tall beside an empty half (DS-04, DS-22). */
.placement-choices--options,
.placement-choices--dogs {
  grid-template-columns: 1fr;
}

.placement-choice {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  text-align: left;
  background: var(--surface-raised);
  color: var(--text);
  border: 2px solid var(--border);
  border-radius: var(--radius-md);
  padding: var(--space-12);
  font: inherit;
  cursor: pointer;
  min-height: var(--touch-min);
}

.placement-choice:focus-visible {
  border-color: var(--accent);
}

@media (hover: hover) {
  .placement-choice:hover {
    border-color: var(--accent);
  }
}
.placement-choice:active {
  background: var(--tint-blue);
}

/* A choice shown for completeness but not available - a full heat when a held
   dog is being put back. It has to read as unavailable at a glance, not only
   refuse the tap. */
.placement-choice:disabled {
  cursor: not-allowed;
  background: var(--surface-sunken);
  color: var(--text-muted);
  border-style: dashed;
}

@media (hover: hover) {
  .placement-choice:disabled:hover {
    border-color: var(--border);
  }
}
/* Both, because a card is a toggle in one dialog and a radio in another, and
   a rule that matches neither is invisible until somebody looks at the screen.
   R-06's three options and Change the Champion's dogs are radios. Gold, the
   colour of a picked dog on the page behind; green read as "saved" (DS-17).
   The gold edge alone is about 2:1 against white and the tint barely differs
   from it, so a dark gold line inside the edge is what reaches 3:1 for a
   selected state (WCAG 1.4.11). */
.placement-choice[aria-pressed=true],
.placement-choice[aria-checked=true] {
  border-color: var(--gold);
  background: var(--tint-gold);
  box-shadow: inset 0 0 0 1px var(--on-tint-gold);
}

.placement-choice span {
  font-size: var(--text-sm);
}

/* A dog to pick, as a row: its picture beside its name and owner, the shape
   Correct result lists them in. As a card per dog - picture above name above
   owner - five dogs were about 550px of a phone, and Take back showed one
   picture and nothing else above the Save row (DS-03, DS-04). The name wraps
   at its spaces, whole, never cut. */
.placement-choices--dogs .placement-choice {
  flex-direction: row;
  align-items: center;
  gap: var(--space-10);
  padding: var(--space-8) var(--space-12);
}

.placement-choice__who {
  display: grid;
  gap: var(--space-2);
  min-width: 0;
  overflow-wrap: break-word;
}

/* A card's markers - where the heat stands in the running order, Full, the
   soonest open heat (Choose a heat, 1.11.0) - on a line of their own under
   its name, at the pills' own size rather than the card's small text. */
.placement-marks {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
}

.placement-marks .pill {
  font-size: var(--text-caption);
}

.pill--soonest {
  background: var(--tint-gold);
  color: var(--on-tint-gold);
}

/* A full heat in Held dogs' Choose a heat: its name, then one row per lane
   across the dialog's whole width, however many columns the heats take. */
.placement-group {
  grid-column: 1/-1;
  display: grid;
  gap: var(--space-6);
}

.placement-group > .placement-choices {
  margin-bottom: 0;
}

/* Race Control's Move a dog (race.js moveDialog, 1.11.0). Its full height
   from the start, with the Save row at its foot, so going from the dogs to
   the heats, or picking a lane, never moves Save or Cancel (Ryan,
   2026-09-25: a pick never moves a button). The heats scroll under the
   pinned row, which keeps its "more below" mark (markDialogOverflow). */
/* Race Control's Undo dialog keeps its height the same way: an answer that
   hides "Which dog won?" left a shorter dialog that centred itself again, and
   the answer just tapped slid up from under the finger (DRUI-T1). */
.move-dialog[open], .undo-result-dialog[open] {
  display: flex;
  flex-direction: column;
  height: 88vh;
}

.move-dialog > .btnrow:last-child, .undo-result-dialog > .btnrow:last-child {
  margin-top: auto;
}

.undo-result-dialog > * {
  flex: none;
}

.move-dialog__body {
  padding-bottom: var(--space-16);
}

.move-dialog__back {
  margin-top: var(--space-8);
}

.move-heat {
  margin-bottom: var(--space-16);
}

.move-heat__title {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-8);
  margin: var(--space-16) 0 var(--space-8);
  font-size: var(--text-base);
}

.move-heat > .placement-choices {
  margin-bottom: 0;
}

/* The pinned line says one thing at a time, over room kept for the longest
   thing it can say: the words laid unseen in the same cell (race.js). */
.move-note {
  display: grid;
}

.move-note > * {
  grid-area: 1/1;
}

.move-note__room {
  visibility: hidden;
}

/* The edit form and the confirm prompt reuse .placement-dialog's shell. */
.dialog-fields {
  display: grid;
  gap: var(--space-4);
  margin-top: var(--space-16);
}

.dialog-note {
  color: var(--text-muted);
  font-size: var(--text-sm);
  margin: var(--space-4) 0 0;
}

/* An action that belongs with the record but is not "save": Check-In's Archive.
   Ruled off from the fields and kept out of the sticky Save/Cancel row, so
   leaving the dialog never means reaching past a destructive button. */
.dialog-extra {
  margin-top: var(--space-16);
  padding-top: var(--space-12);
  border-top: 1px solid var(--border);
  display: grid;
  gap: var(--space-6);
  justify-items: start;
}

/* "Bracket Builder" only spells itself out where there is room for it. */
@media (max-width: 1023px) {
  .tab__label-rest {
    display: none;
  }
}
/* ------------------------------------------------ the Announcer's words */
/* The marks the Announcer's words may carry (src/rich-text.js: **bold**,
   _italic_, "- " bullets, [words](https://…) links), drawn on a staff screen
   by Staff.noteWords through the one renderer: Event settings' preview,
   Race Control's copy of a message, the quick-message rows. The same blocks
   the Announcer draws - .rich-line, .rich-head, .rich-list of .rich-item,
   .rich-gap on a block a blank line came before - and the marks as
   themselves. Every word is set as text; nothing here is ever markup. */
.rich-line, .rich-head, .rich-list, .rich-blank {
  margin: 0;
}

/* A word longer than the line - a pasted web address - breaks rather than
   widening the page (CN-R2: 574px wide at 393 on a phone). */
.rich-line, .rich-head, .rich-item {
  overflow-wrap: anywhere;
  min-width: 0;
}

.rich-head {
  font-weight: 800;
}

.rich-gap {
  margin-top: var(--space-8);
}

.rich-list {
  display: grid;
  gap: var(--space-2);
  padding: 0;
  list-style: none;
}

/* The diamond sits in the item's padding, out of the flow, so the words after
   it - text, bold, italic and a link in one line - wrap as one line. */
.rich-item {
  position: relative;
  padding-left: calc(var(--space-6) + var(--space-8));
}

.rich-item::before {
  content: "";
  position: absolute;
  left: 0;
  top: calc(0.5lh - var(--space-4));
  width: var(--space-6);
  height: var(--space-6);
  background: var(--gold);
  transform: rotate(45deg);
}

/* A link is underlined words and no more: on the Announcer nothing can be
   tapped, and here it previews that rather than leading off the page. */
.rich-link {
  text-decoration: underline;
  text-decoration-thickness: from-font;
  text-underline-offset: 0.12em;
}

/* ---------------------------------- the Rich / Markdown editor (1.10.0) */
/* Two tabs over one box (public/js/rich-editor.js; Ryan, 2026-09-26: "Devin
   doesn't like markdown, I prefer markdown"): Rich, the default, is a
   toolbar of four 44px icon buttons over an editing area drawn like the box
   under it; Markdown is the box itself. Every control is the touch minimum. */
/* min-width 0 on the grid and its panes, so a long unbroken word in the
   editing area wraps inside the column instead of stretching it (CN-R2). */
.rich {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-8);
  min-width: 0;
}

.rich__tabs {
  display: flex;
  gap: var(--space-4);
}

.rich__tab {
  min-height: var(--touch-min);
  padding: var(--space-8) var(--space-14);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-pill);
  background: var(--surface-raised);
  color: var(--accent);
  font: inherit;
  font-size: var(--text-sm);
  font-weight: 700;
  cursor: pointer;
}

.rich__tab[aria-selected=true] {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--text-on-accent);
}

/* The one focus treatment (_hover.scss) rounds a focused control to --radius-sm; a pill keeps its own. */
.rich__tab:focus-visible {
  border-radius: var(--radius-pill);
}

.rich__pane {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-6);
  min-width: 0;
}

.rich__pane[hidden] {
  display: none;
}

.rich__toolbar {
  display: flex;
  gap: var(--space-4);
}

.rich__tool {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--touch-min);
  height: var(--touch-min);
  padding: 0;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface-raised);
  color: var(--accent);
  cursor: pointer;
}

.rich__tool .icon {
  width: 1.25em;
  height: 1.25em;
}

.rich__tool:active {
  background: var(--tint-blue);
}

/* Pressed: the words at the cursor already carry the mark - the same look
   as a ghost button that is open (.btn--ghost[aria-expanded="true"]). */
.rich__tool[aria-pressed=true] {
  background: var(--tint-blue);
  border-color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent);
}

/* The editing area, drawn as a textarea is (_inputs.scss): three lines of
   room for a message, the notes' twelve (.rich__editor--tall), growing with
   what is typed. */
.rich__editor {
  min-height: calc(3 * var(--leading-base) * 1em + 2 * var(--space-10));
  padding: var(--space-10) var(--space-14);
  border: 1px solid var(--border-field);
  border-radius: var(--radius-md);
  background: var(--surface-raised);
  color: var(--text);
  font: inherit;
  line-height: var(--leading-base);
  overflow-wrap: anywhere;
  min-width: 0;
}

/* Over the limit (Staff.editFields' live count): the same danger edge a
   refused box wears (_inputs.scss). */
.rich__editor[aria-invalid=true], .rich__editor[aria-invalid=true]:focus {
  border-color: var(--danger);
}

.rich__editor--tall {
  min-height: calc(12 * var(--leading-base) * 1em + 2 * var(--space-10));
}

.rich__editor:focus {
  border-color: var(--accent);
}

.rich__editor:focus-visible {
  outline: var(--focus-width) solid var(--accent);
  outline-offset: var(--focus-offset);
}

/* Nothing typed yet: the box's own placeholder, until the first character. */
.rich__editor:empty::before {
  content: attr(data-placeholder);
  color: var(--text-muted);
  white-space: pre-line;
}

/* What is typed, as the browser shapes it: a paragraph or a div per line, a
   real bullet on a list item (the list is the browser's own, so Enter and
   Backspace behave as they do everywhere), bold and italic as themselves,
   a link in the accent - words to edit, never a way off the page. */
.rich__editor p, .rich__editor div, .rich__editor ul {
  margin: 0;
}

.rich__editor ul {
  padding-left: var(--space-24);
  list-style: disc;
}

.rich__editor a {
  color: var(--accent);
  text-decoration: underline;
  cursor: text;
}

/* --------------------------------------------------------- short viewport */
/* A phone in landscape is 393px tall. A pinned current-heat panel plus the
   top bar leaves nothing to scroll through, and controls below the panel end
   up underneath it - Correct result becomes unclickable. Below this height
   the panel scrolls with the page instead; it is still the first thing on it. */
@media (max-height: 520px) {
  .current-panel {
    position: static;
  }
  /* A dialog's title takes the phone's size here too. A phone on its side is
     too wide for the phone block, so Check-In's "Edit <dog name>" stayed at
     24px and a long name's title filled the top of a 393px-tall screen
     (DS-66). The upright phone rule is in _phone.scss. */
  .placement-dialog h2 {
    font-size: var(--text-lg);
    text-wrap: balance;
  }
}
/* The login page on a phone held sideways: the form alone, in the middle,
   as before the dog show - there is no room under it or beside it
   (_login.scss). Only a phone on its side: short, wider than tall, and
   narrower than the biggest phone's 932px. A page made short by an
   on-screen keyboard (a browser whose keyboard shrinks the page) is not
   one - an upright phone keeps its form at the top, and an iPad or a
   computer keeps the form on the left - so the form never jumps to the
   middle as the keyboard comes up. */
@media (max-height: 520px) and (orientation: landscape) and (max-width: 959px) {
  .login {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto;
    align-items: center;
  }
  .login-show {
    display: none;
  }
}
/* ------------------------------------------------------- tablet portrait */
/* 768 and 834 CSS px are the iPad sizes staff actually hold. Below 900 the
   brand, five tabs and the account control do not fit on one line, so the
   tabs get a deliberate second row instead of wrapping into an accidental
   one - which is what the single 640px breakpoint used to produce. */
@media (min-width: 641px) and (max-width: 899px) and (min-height: 600px) {
  /* A row has at most two action buttons: Photo and Edit beside the check-in
     switch, or Edit and Restore on an archived row, which has no switch. These
     tracks were narrowed when a row still had three
     - Archive has since moved into the Edit dialog - because at 768px the
     roster took the document to 795px and into horizontal scroll. An extra
     line inside a card still costs nothing next to a sideways-scrolling page,
     so the action row keeps wrapping. */
  /* Shared by the whole list, as from 900px up (DS-56): each row used to be a
     grid of its own, so its last column was only as wide as that row's own
     buttons, and the fun fact started in a different place on almost every
     row - further right where a dog already had a photo and so no Photo
     button. The list's columns are these tracks, every row takes them as a
     subgrid, and the buttons' column is the widest row's. A browser without
     subgrid keeps a grid per row on the same tracks. */
  .roster-list {
    --roster-columns: minmax(150px, 1fr) minmax(130px, 1fr) auto;
    grid-template-columns: var(--roster-columns);
    column-gap: var(--space-10);
  }
  .roster-item {
    grid-column: 1/-1;
    grid-template-columns: subgrid;
    gap: var(--space-10);
  }
  /* The name column is the one that must not lose width here. At 768px it is
     about 185px, and the long-name fixture's "Schnitzelhausen" and
     "Featherstonehaugh" need all of it: DS-61 gave "In" the three
     characters "Out" takes, which took 15px from the actions' track and
     broke both words mid-way ("Schnitzelhause / n III") - and an Out row had
     always broken them. The switch's side padding (the label itself is still
     44px tall and wider than 44px) and a narrower gap beside it give that
     width back to every row, In or Out, without touching the tracks above
     or DS-61's one column of switches. The fixture itself no longer sits in
     that column here: a name too long for it takes the whole row, spanning
     these tracks rather than changing them (DS-54, styles/_roster.scss). */
  .roster-item__actions {
    gap: var(--space-6);
  }
  .roster-item__actions .toggle {
    padding-left: 0;
    padding-right: 0;
  }
  .roster-item__actions .btnrow {
    flex-wrap: wrap;
    justify-content: flex-end;
  }
  .topbar__inner {
    flex-wrap: wrap;
    padding-bottom: var(--space-6);
  }
  .topbar__brand {
    margin-right: auto;
  }
  /* Brand and account share the first row; the tabs take the second. Without
     moving the account ahead of the tabs it wraps onto a third row of its own. */
  .account {
    order: 2;
  }
  .tabs {
    order: 3;
    flex: 1 0 100%;
    display: grid;
    grid-template-columns: repeat(5, minmax(0, 1fr));
    gap: var(--space-4);
  }
  /* A Volunteer's two tabs (1.12.0): each half the row. */
  .tabs--two {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  /* An Event host's three (2.0.0, S13): each a third of the row. */
  .tabs--three {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
  .tab {
    padding: var(--space-8) var(--space-6);
    border-radius: var(--radius-sm);
    border-bottom-width: 3px;
  }
}
/* ------------------------------------------------------------- phone */
/* A phone, and a phone turned sideways. 852x393 is the same device as
   393x852: a single-row top bar needs about 900px for the wordmark, five tabs
   and the account control, so at 852 it overflowed into horizontal scroll.
   Keeping the bottom tab row there is both what fits and what the device does
   everywhere else. */
@media (max-width: 640px), (max-width: 899px) and (max-height: 599px) {
  /* The five destinations move to a fixed bottom row. The brand and the
     account control stay in the sticky top bar - the account control is the
     only way to sign out on a phone, so it is never hidden. */
  .tabs {
    position: fixed;
    inset: auto 0 0;
    z-index: var(--z-chrome);
    display: grid;
    /* Five columns for five destinations. test/staff-nav.test.js asserts this
       count matches the number of data-nav-tab links in header.ejs: the old
       six-column grid plus a link that never hid is exactly how Profile ended
       up on a second row covering the roster. */
    grid-template-columns: repeat(5, minmax(0, 1fr));
    gap: var(--space-2);
    background: var(--surface-chrome);
    box-shadow: var(--shadow-up);
    padding: var(--space-4) max(var(--space-6), env(safe-area-inset-right)) calc(var(--space-4) + env(safe-area-inset-bottom)) max(var(--space-6), env(safe-area-inset-left));
  }
  /* The row's colour carries on below its bottom edge, as far again as the
     row is tall. Where the row sits on the screen's edge that is off the
     screen and nothing sees it. It is for the moment it does not: an iPhone
     running the app from the home screen can bring it back from another app
     with the row drawn above the edge - by the home indicator's inset, or by
     the toolbar a home-screen app does not have - and the page showed through
     under it until the next scroll (public/js/staff.js nudgeFixedBars puts
     the row back). With this the gap is the row, so the row looks taller for
     a moment rather than afloat. Sized by the row itself, never by the window:
     the window's height is the number that is wrong at that moment. */
  /* A Volunteer opens two tabs, Check-In and Screens (1.12.0): two columns,
     each half the row, so neither is left a fifth of it wide. */
  .tabs--two {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  /* An Event host's three (2.0.0, S13): each a third of the row. */
  .tabs--three {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
  .tabs::after {
    content: "";
    position: absolute;
    inset: 100% 0 auto;
    height: 100%;
    background: inherit;
  }
  .tab {
    flex-direction: column;
    justify-content: center;
    gap: var(--space-2);
    min-height: var(--touch-min);
    padding: var(--space-6) var(--space-2);
    border-bottom: 0;
    border-radius: var(--radius-sm);
    font-size: var(--text-micro);
    line-height: var(--leading-tight);
    text-align: center;
  }
  .tab.is-active {
    border-bottom: 0;
  }
  /* The gold marker the tab row carries on an iPad and a desktop is an
     underline, and a bottom row has no room for one, so here it is the icon's
     colour. Without it the screen you are on was only a faint wash (DS-94).
     The icon, not the label: gold on the wash measures about 3.8:1, enough
     for an icon (3:1) but not for 11px text (4.5:1). */
  .tab.is-active .tab__icon {
    color: var(--gold);
  }
  /* The button back to the top (styles/_place-memory.scss) sits a
     --space-4 in from the edge rather than a --space-16: over the page's
     margin and the card's own padding, where nothing takes a tap. At a
     --space-16 in it stood over Bracket Builder's lane handles and the end
     of Check-In's In/Out switch all the way down both pages (review PM-R1).
     The same on a phone on its side, where the rows run wider and their
     ends sit under the --space-16 place just the same. */
  .to-top {
    right: max(var(--space-4), env(safe-area-inset-right));
  }
  .tab__icon {
    width: 22px;
    height: 22px;
  }
  .topbar__inner {
    min-height: var(--touch-min);
    padding-left: max(var(--space-12), env(safe-area-inset-left));
    padding-right: max(var(--space-12), env(safe-area-inset-right));
  }
  .topbar__mark {
    width: 24px;
    height: 24px;
  }
  .topbar__name {
    font-size: var(--text-sm);
  }
  .account__menu {
    min-width: 0;
    width: min(264px, 100vw - var(--space-24));
  }
  /* The race switch's menu (2.0.0, styles/_race-switch.scss) hangs from
     the top bar itself, from its left gutter, and is never wider than the
     bar less both gutters. Hung from the switch, 64px in, its 288px ran 32px
     past a 320px screen's edge, and the page scrolled sideways while it was
     open. The bar is one row here, so its bottom is the switch's. Where
     :has() is not known the menu hangs from the sticky bar around it
     instead, which on a phone is the same place. */
  .topbar__inner:has(.race-switch) {
    position: relative;
  }
  .race-switch {
    position: static;
  }
  .race-switch__menu {
    left: max(var(--space-12), env(safe-area-inset-left));
    width: min(var(--touch-base) * 6, 100% - max(var(--space-12), env(safe-area-inset-left)) - max(var(--space-12), env(safe-area-inset-right)));
  }
  /* Clearance for the tab row is derived from the measured bar, not a magic
     number, and it covers the home indicator because the bar's own padding
     does. It belongs on .wrap rather than body: only a staff page has a .wrap,
     and putting it on body reserved space for a tab bar on /login too, which
     pushed that page into a 12px scroll. */
  .wrap {
    padding: var(--space-16) var(--space-12) calc(var(--staff-tabbar-height, 68px) + var(--staff-conn-height, 0px) + var(--space-16));
  }
  /* The tab row already covers the home indicator under the strip. */
  .conn {
    padding-bottom: var(--space-8);
  }
}
/* Content, keyed on width alone. The chrome block above also catches a phone
   turned sideways, where the viewport is 852px WIDE - stretching the phone
   roster card across that leaves Photo/Edit at one edge and the toggle at
   the other with a hand's width of nothing between them. */
@media (max-width: 640px) {
  .card {
    padding: var(--space-16);
  }
  h1 {
    font-size: var(--text-xl);
  }
  /* A roster row's details line, on Check-In. Only there: unscoped, this
     also hid the import preview's Fun fact cells, so every row of that table
     lost a cell and "New dog" stood under the Fun fact heading, and it took
     the traits and the fun fact off a duplicate pair's card. */
  .roster-item .funfact {
    display: none;
  }
  /* The import preview, a card per row: the dog, its fun fact and the note
     down the left, every row starting at the same edge, and the action's pill
     at the top right like a heat card's status. As four columns in a phone's
     width, the Dog column was squeezed until names broke mid-word
     ("Gum/mibä/r", "Imoge/n Fairw/eathe/r"). The headings go, because each
     line says what it is: the action is a pill, the note reads as one. */
  .import-preview thead {
    display: none;
  }
  .import-preview, .import-preview tbody {
    display: block;
  }
  .import-preview tr {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    gap: var(--space-4) var(--space-10);
    padding: var(--space-10) 0;
    border-bottom: 1px solid var(--border);
  }
  .import-preview td {
    display: block;
    grid-column: 1;
    padding: 0;
    border: 0;
  }
  .import-preview td:first-child {
    grid-column: 2;
    grid-row: 1;
  }
  .roster-item {
    grid-template-columns: minmax(0, 1fr) auto;
    gap: var(--space-10);
    padding: var(--space-12);
  }
  /* The checked-in row's wider edge comes out of this padding too (DS-59). The
     base .roster-item.is-in rule outranks the one-class rule above, so without
     this a checked-in row would sit 4px out the other way on a phone. */
  .roster-item.is-in {
    padding-left: calc(var(--space-12) + 1px - var(--status-edge));
  }
  .roster-item__fact {
    display: none;
  }
  .roster-item__actions {
    grid-column: 1/-1;
    justify-content: space-between;
    border-top: 1px solid var(--border);
    padding-top: var(--space-8);
    flex-wrap: wrap;
  }
  /* Must re-allow wrapping: the base rule sets nowrap. When a row still had
     three buttons, an archived row - which then showed a wide status pill
     instead of the check-in switch - took a 393px phone to 407px and into
     horizontal scroll. Archive has since moved into the Edit dialog and the
     pill has gone (DS-60), but a long label must still wrap, not scroll. The
     buttons come first in the row and the switch last, so space-between
     above puts the switch at the right edge on its own; an archived row has
     no switch, so its Edit and Restore sit at the left. */
  .roster-item__actions .btnrow {
    flex-wrap: wrap;
  }
  /* Four filters, two by two, with the same words as on the iPad. They need
     about 400px to share a row and a phone gives them 319-374px, so they
     wrapped 3 + 1 and left Archived alone on a line of its own (DS-58).
     Equal halves keep every label whole and every target the same size; the
     gap is .btnrow's own, which works for a grid too. */
  .filter-row {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .grid2 {
    grid-template-columns: 1fr;
    gap: var(--space-12);
  }
  /* The dog page's name and owners go under the picture on every phone, as
     they already did at 393: beside it, a 430px phone left them 282px, which
     is narrower than one long owner, so each owner wrapped inside itself and
     "&" stood alone (DS-89). */
  .dog-head > div {
    flex-basis: 100%;
  }
  /* Check-In's In/Out switch keeps its place on the row but gives up the
     --space-6 of its label to the right of the word: the part of it the
     button back to the top would still have met (review PM-R1). */
  .roster-item__actions .toggle {
    padding-right: 0;
    margin-right: var(--space-6);
  }
  /* And centred, picture over name, so the picture is not a small block in the
     top corner with a hand's width of nothing beside it (Decision #11). */
  .dog-head {
    justify-content: center;
    gap: var(--space-12);
    text-align: center;
  }
  /* Centred lines of even length, so a long name is not a full line over two
     words. Where Safari has no text-wrap it wraps as before. */
  .dog-head h1 {
    text-wrap: balance;
  }
  .dog-head .field__hint {
    text-wrap: pretty;
  }
  /* Its buttons centred under the name like the rest of the head. */
  .dog-head__actions {
    justify-content: center;
  }
  /* Every counter on one row, however many a screen has: Check-In's three,
     Bracket Builder's three, Registration's two. auto-fit at 120px needs
     416px for three and a phone card gives 319-374px, so Check-In's third
     counter sat alone on a second row and cost the card about 75px (DS-55).
     A label may take two lines instead. Registration's two keep their row
     and move a little closer together (12px apart, not 28px). */
  .stat {
    grid-template-columns: none;
    grid-auto-flow: column;
    grid-auto-columns: minmax(0, 1fr);
    column-gap: var(--space-12);
  }
  /* Buttons that each take their own line on a phone take the whole line, so
     a stack of them ends at the card's edge rather than at two different text
     widths short of it (DS-53). Their words may wrap, in balanced lines: at
     375px "Close check-in and build bracket" is 14px wider than the card, and
     a .btn's nowrap ran it into the card's padding. */
  .btnrow--stack > .btn {
    flex: 1 1 100%;
    white-space: normal;
    text-wrap: balance;
  }
  /* Beside Undo on a phone the change has two lines of about 25 characters,
     and "Last change:" - the button's own words again - spent half of the
     first, so the part that says which change was cut: "Last change: You
     swapped Zeppelin and...". The Undo question names it in full (DS-45). */
  .undo-row__lead {
    display: none;
  }
  .undo-row__earlier {
    display: inline;
  }
  .current-panel {
    padding: var(--space-10);
  }
  .current-panel h2 {
    font-size: 15px;
  }
  .current-panel .pill {
    font-size: var(--text-micro);
  }
  /* The dog cards scroll sideways rather than stack, so the sticky panel stays
     short enough to see the bracket underneath it. These numbers are the whole
     point and were measured, not guessed: at 165px with a 10px gap two cards
     filled the strip exactly and lanes 3-5 came out at 0px visible, so a five-dog
     heat looked like a two-dog heat. At 140px with a 6px gap the third card starts
     at 292px, leaving 35px at 375px wide and 53px at 393px. The lane badge is
     trimmed to 26px so it sits entirely inside that 35px: a numbered blue chip at
     the edge is what tells you a lane 3 exists. The padding comes off the card
     rather than the dog's name, which is the thing being read. With photos on,
     the 32px picture takes its room from the name too, which is already
     ellipsised. Re-measured at 375px in 1.9.0 when Winner left the cards for
     one button under the strip. Re-measure again before changing any of them. */
  .current-lanes {
    display: flex;
    align-items: flex-start;
    overflow-x: auto;
    gap: var(--space-6);
    padding-bottom: var(--space-4);
    scroll-snap-type: x mandatory;
  }
  .current-lane {
    flex: 0 0 var(--lane-card-phone);
    scroll-snap-align: start;
    padding: var(--space-6);
    gap: var(--space-4);
    align-content: flex-start;
  }
  .current-lane .lane__n {
    width: 26px;
    height: 26px;
  }
  /* With a picture in the card: the picture takes the first row and the lane
     number and the name the second, on every pictured card alike. A 140px card
     cannot hold all three side by side, and a strip where some names wrapped
     and some did not was harder to scan. Without pictures the card stays one
     row, as it always was.
     Direct children only: the name now lives inside the pick button, and a
     descendant rule pushed it onto a third row of its own. */
  .current-lane--pictured > .lane__who {
    flex-basis: 100%;
  }
  .current-lane--pictured > .current-lane__pick {
    flex-basis: 100%;
  }
  /* The picked card shows its whole name and owner (_slim-bar.scss), and on
     a strip that scrolls sideways width is what can be spared, not height: it
     widens to what they need, from its own 140px to two cards and the gap
     between them, and only past that are its words fitted into the height it
     had cut (below). At two cards the next
     one still peeks in 292px along the strip, where the third card does at
     rest. The other cards keep their size and their tops; the strip keeps
     the picked card in view (race.js showPicked). A grid, not the card's
     flex row: sized to its content, a wrapping flex row asks for the picture
     and the name side by side, which widened a picked "Wiggles" that had
     nothing more to show. Never wider than the strip, though: below about
     334px (a 320px phone, or a wider one under Safari's page zoom) two cards
     are wider than it - 286px in a 272px strip at 320 - and wherever the
     strip rested, one end of the card was behind its edge: its border and
     half its lane number, or the end of its name. There it takes the strip's
     width and wraps a little sooner. */
  .current-lane.is-picked {
    display: grid;
    justify-items: start;
    flex: 0 0 auto;
    width: max-content;
    min-width: var(--lane-card-phone);
    max-width: min(2 * var(--lane-card-phone) + var(--space-6), 100%);
    scroll-snap-align: start;
  }
  /* Its end is a resting place too: one picked at the strip's right edge
     and grown past it is moved left only until its end shows, so the
     finger that picked it is still on it (race.js revealPick). That end
     is its button's, which spans the card, carried out to the card's own
     edge by the card's padding and border. It was "scroll-snap-align:
     start end" on the card, but a second value is the INLINE axis, so
     the picked card rested only at its end and its start was no resting
     place at all: a strip moved to lane 2 and a tap there slid the strip
     to lane 1 in Chrome and lane 3 in Safari, with another dog under the
     finger (TAP-1). */
  .current-lane.is-picked > .current-lane__pick {
    justify-self: stretch;
    scroll-snap-align: none end;
    scroll-margin-inline-end: calc(var(--space-6) + 1px);
  }
  /* Its whole name and owner, and never a line taller than the card was
     cut (Ryan, 2026-09-25, picking "Lightning McQueenie the Weenie": "it
     slightly increases the dogs name row height ... their next target button
     constantly shifting on them - don't want that"). race.js fitPhonePick
     reads the card's height cut and keeps it (--lane-h), and fitPickWords
     fits the words into it, as above 640px (_rounds.scss): one line each at
     full size; then smaller together on one line, down to the name at
     --text-caption and the owner never under it; only then the name on a
     second line, set close; the owner cut at the end of its line only
     where the whole name leaves it no room. A word is never broken inside
     itself. It was 23px taller with that name, 66 with the harness's, and
     Record Wiener slid down by as much. */
  .current-lane.is-picked {
    height: var(--lane-h, auto);
  }
  .current-lane.is-picked .lane__who strong, .current-lane.is-picked .dogowner {
    max-width: none;
    overflow: visible;
    white-space: normal;
    overflow-wrap: normal;
    line-height: var(--leading-tight);
  }
  .current-lane.is-picked .lane__who strong {
    font-size: calc(var(--text-base) * var(--pick-scale, 1));
  }
  .current-lane.is-picked .dogowner {
    font-size: max(var(--text-caption), var(--text-sm) * var(--pick-scale, 1));
  }
  .current-lane.is-picked.is-one-line .lane__who strong,
  .current-lane.is-picked.is-one-line .dogowner,
  .current-lane.is-picked.is-owner-cut .dogowner {
    white-space: nowrap;
  }
  .current-lane.is-picked.is-owner-cut .dogowner {
    overflow: hidden;
    text-overflow: ellipsis;
  }
  /* The full box is not pinned here: it scrolls away, and .current-bar below
     takes over once it has gone above the top bar. Pinned on a phone it took
     209px of a 375px screen bare and 299px with Undo; the slim bar is what
     answered Ryan's "sticky winner header is working on desktop but not mobile
     still" (2026-09-22): "a slim version is right - what's wrong is that it
     disappears." It is still the first thing on the page, so a result is one
     tap from the top; from there down the slim bar records one. Landscape was
     already unpinned by the max-height rule. */
  .current-panel {
    position: static;
  }
  /* The round's name at the left, its heats at the right, on one line. Ryan:
     "the heats should be on the right side and the round name on the left ...
     same line so that they take up less vertical realestate and maximize
     horizontal usage." Balance, on the round that has one, drops to its own
     row underneath rather than squeezing the two things that say which section
     you are in. An empty button row is hidden, or its gap alone would cost the
     line this saves. */
  /* The name comes down a step and the heats two, and the heats take whatever
     width the name leaves, wrapping inside it rather than dropping to a row of
     their own. The rule before repeated the base h2 size (--text-lg), so it
     changed nothing: on a 393px phone the line is 305px, and "Championship"
     (123px) beside "2 heats, then a Final · 1 raced" (199px) needed 334px, so
     the heats wrapped under the name every time (DS-06). At --text-base and
     --text-caption the pair is about 292px and holds at 393; at 375 the heats
     take two short lines beside the name instead of a row below it. The name
     is still the largest thing in the strip. The cost is real and taken
     knowingly: the heats are 12px on every round heading on a phone, both
     boards, where they were 14px - for a sticky strip one row shorter. */
  .round-heading h2 {
    font-size: var(--text-base);
  }
  .round-heading__range {
    flex: 1 1 0;
    text-align: right;
    font-size: var(--text-caption);
  }
  .round-heading .btnrow {
    order: 1;
    flex-basis: 100%;
  }
  .round-heading .btnrow:empty {
    display: none;
  }
  /* Two or more buttons share that row, half each, with their labels wrapping,
     rather than a full-width row apiece: the Championship's two stage Balance
     buttons made the sticky strip 185px over the Final card, and a held dog's
     link can add a third. One button keeps its own width. */
  .round-heading .btnrow > .btn:not(:only-child) {
    flex: 1 1 calc(50% - var(--space-10));
    white-space: normal;
    text-align: center;
  }
  /* On Bracket Builder they share the row only when both fit whole, and each
     takes a row of its own when they do not. At half each, "Balance eligible
     heats" beside "1 held dog" wrapped onto two lines on every phone, though
     the two fit side by side from 375px. A label still wraps, in balanced
     lines, only when it is wider than the row on its own. */
  #roundsBox .round-heading .btnrow > .btn:not(:only-child) {
    flex-basis: auto;
    text-wrap: balance;
  }
  /* 8px between every line of the bar and 8px inside its top and bottom
     edges (Ryan, 2026-09-24 ~22:33, on the rows "too close stacked on top of
     one another"): the dogs' strip keeps 2px above and below its chips for
     the picked chip's ring, so the padding above it, and the gap under it,
     are 2px short of 8. 114px closed and 170 open from 320 to 430px, with
     any dog picked or none (race.js fitChipName). It was 162 with the Big
     Screen row always showing. */
  .current-bar {
    display: block;
    position: fixed;
    inset: var(--staff-nav-height, 58px) 0 auto;
    z-index: var(--z-sticky);
    padding: calc(var(--space-8) - var(--space-2)) max(var(--space-8), env(safe-area-inset-left)) var(--space-8) max(var(--space-8), env(safe-area-inset-right));
    background: var(--surface-raised);
    border-bottom: 2px solid var(--accent);
    box-shadow: var(--shadow-sm);
  }
  .current-bar[hidden] {
    display: none;
  }
  /* Two rows and a third that folds (decision #10, the rc2 pictures): the
     heat's short name and its dogs; Wiener, Too Close to Call, Undo, Message
     and the Big Screen button; and the five modes the Big Screen button
     opens and closes (.current-bar__tray). Each row is one line: the dogs
     scroll sideways, and what the second row cannot hold at 44x44 gives up
     its words, measured (race.js fitBar), never its line. 6px along them. */
  .current-bar__row {
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
    gap: var(--space-6);
  }
  .current-bar__row--act {
    margin-top: calc(var(--space-8) - var(--space-2));
  }
  .current-bar__row--act > * {
    flex: 0 0 auto;
  }
  /* Only where the row cannot hold Wiener's word even with Message and the
     Big Screen button as icons - a window under about 290px, which Safari's
     page zoom can make of a small phone - it wraps, a line taller, so no
     control is pushed past the screen's edge (race.js fitBar). */
  .current-bar__row--act.is-crowded {
    flex-wrap: wrap;
    row-gap: calc(var(--space-8) - var(--space-2));
  }
  /* The round's short name, R-09's section indicator for the heat at the
     line, beside the dogs it names, in two short lines ("Q · Heat 1" over
     "of 5"), so the second row has room for Message and the Big Screen
     button at every width. Never shrinks: it gave its space up first once
     and came back as "Q · He…", which answers nothing. While the picked
     dog's name would need a third line beside it, it steps out altogether
     (race.js fitChipName, .is-picked-long) and the dogs take the whole row,
     so the bar keeps its height. */
  .current-bar__where {
    flex: 0 0 auto;
    white-space: nowrap;
    font-size: var(--text-micro);
    font-weight: 800;
    color: var(--accent);
  }
  .current-bar__where--two {
    display: grid;
    line-height: var(--leading-tight);
  }
  .current-bar__row--heat.is-picked-long > .current-bar__where {
    display: none;
  }
  /* The dogs scroll sideways rather than shrink, the same decision the full
     strip above makes for the same reason: a chip narrower than the lane badge
     is not a target. */
  .current-bar__dogs {
    display: flex;
    /* The rest of the first row after the heat's name. It scrolls sideways,
       because five named chips are wider than any phone. */
    flex: 1 1 0;
    min-width: 0;
    gap: var(--space-4);
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    padding: var(--space-2) 0;
  }
  .current-bar__dog {
    flex: 0 0 auto;
    scroll-snap-align: start;
    display: flex;
    align-items: center;
    gap: var(--space-4);
    min-width: var(--touch-min);
    min-height: var(--touch-min);
    padding: var(--space-4);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface);
    color: inherit;
    font: inherit;
    cursor: pointer;
  }
  .current-bar__dog.is-picked {
    border-color: var(--gold);
    background: var(--tint-gold);
    box-shadow: 0 0 0 var(--focus-width) var(--gold);
  }
  .current-bar__dog:disabled {
    opacity: 1;
    background: var(--surface-sunken);
    cursor: not-allowed;
  }
  .current-bar__dog .lane__n {
    width: 24px;
    height: 24px;
    font-size: var(--text-micro);
  }
  /* Long enough to tell two dogs apart at a glance and short enough that five
     of them do not push Wiener off the row; the strip scrolls for the rest.
     The same --name-cap as the full cards, so a name stops at the same letter
     in the bar as it does in the box it stands in for. */
  .current-bar__name {
    max-width: var(--name-cap);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: var(--text-caption);
    font-weight: 700;
  }
  /* Except the picked dog's chip: Ryan, on this bar, "expanding it when
     selected to display the whole name and then if unselected it shrinks back
     down". It widens to its whole name, up to its strip's width less half a
     card, so a neighbour still peeks in beside it and the other dogs are
     still seen to be there, and wraps at spaces past that. Set close, so two
     lines still fit the 44px chip, and from the left, as a name reads, not
     centred the way a button sets its words. Wrapped "pretty", not
     balanced: balanced lines are half the chip wide, and the chip cannot
     narrow to them, so it carried 85 to 140px of empty gold beside the
     name; pretty fills the first line and still keeps a word like "III" off
     a line of its own. Never a third line, and never a word broken inside
     itself: a pick never makes the row taller (Ryan, 2026-09-25). The name
     is at the readable floor already (--text-caption), so where it would
     need a third line the heat's name steps out of the row, then the chip
     takes the strip's whole width (.is-wide, the neighbour no longer
     peeking), and only past that - a name near the form's 80-character
     limit on a narrow phone - is it set smaller (--pick-scale; race.js
     fitChipName). The harness's 56-character fixture took three lines at
     320 and 360px and made the bar 9px taller; it is two lines at the
     strip's width there now. Its end is a resting place of the strip's
     too (revealPick). Every other chip keeps the cap. */
  .current-bar__dog.is-picked {
    max-width: calc(100% - var(--lane-card-phone) / 2);
  }
  /* The picked chip's end as a resting place is its name's, carried out to
     the chip's edge by its padding and border: "start end" on the chip
     made its END its only one (the second value is the inline axis), so a
     tap on a chip the strip rested on slid it away (TAP-1). */
  .current-bar__dog.is-picked > .current-bar__name {
    scroll-snap-align: none end;
    scroll-margin-inline-end: calc(var(--space-4) + 1px);
  }
  .current-bar__dog.is-picked.is-wide {
    max-width: 100%;
  }
  .current-bar__dog.is-picked .current-bar__name {
    max-width: none;
    white-space: normal;
    overflow-wrap: normal;
    text-wrap: pretty;
    line-height: var(--leading-tight);
    text-align: left;
    font-size: calc(var(--text-caption) * var(--pick-scale, 1));
  }
  /* Takes the rest of the row: the control the bar exists for, not a stub at
     the left with half the phone empty beside it (DS-28). Its word is always
     whole: where the row is too narrow for it, Message and then the Big
     Screen button give theirs up first (race.js fitBar). */
  .current-bar__win {
    flex: 1 1 auto;
    min-width: 0;
    min-height: var(--touch-min);
  }
  /* Too Close to Call and Undo, as icons. They are one tap away in the bar,
     not two: a "more" control would have cost the tap it saved. */
  .current-bar__icon {
    flex: 0 0 auto;
    min-width: var(--touch-min);
    min-height: var(--touch-min);
    padding: var(--space-6);
  }
  /* Message and the Big Screen button: an icon over one word, 44px tall
     like Wiener beside them. Where Wiener would be too narrow for its own
     word, Message's word goes first and then the Big Screen button's
     (.bar-tool--bare, race.js fitBar): at 320px Message is the megaphone
     alone, 44x44. */
  .bar-tool {
    flex-direction: column;
    gap: 0;
    padding: var(--space-2) var(--space-4);
    min-width: var(--touch-min);
    min-height: var(--touch-min);
    line-height: var(--leading-tight);
  }
  .bar-tool__word {
    font-size: var(--text-caption);
    white-space: nowrap;
  }
  .bar-tool--bare .bar-tool__word {
    display: none;
  }
  /* The Big Screen button (decision #10): the Screens tab's monitor and a
     chevron over the live mode's word, underlined in the gold that means
     "on the TV now", so it reads as the TV's state rather than a sixth
     mode. Every mode's word sits in one grid cell with only the live one
     shown, so the button is as wide as "Upcoming" whatever the mode (71px)
     and Wiener beside it never moves when the mode changes. Open, it wears
     the app's expanded-toggle look (.btn--ghost[aria-expanded="true"]) and
     the chevron points up. */
  .screen-fold__top {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
  }
  .screen-fold__chev {
    transition: transform var(--dur-base) ease-out;
  }
  .screen-fold[aria-expanded=true] .screen-fold__chev {
    transform: rotate(180deg);
  }
  .screen-fold__words {
    display: grid;
    justify-items: center;
  }
  .screen-fold__words > .bar-tool__word {
    grid-area: 1/1;
    visibility: hidden;
    padding-bottom: var(--space-2);
  }
  .screen-fold__words > .bar-tool__word.is-live {
    visibility: visible;
    background: linear-gradient(var(--gold), var(--gold)) center bottom/100% var(--space-2) no-repeat;
  }
  /* The row the Big Screen button opens: the five modes under Wiener's row,
     8px below it, so a low tap on Wiener lands in the gap and not on the TV.
     Only a tap on the Big Screen button opens or closes it, and it slides
     down from under that row in --dur-base, ease-out, no bounce, and back
     up the same way; the bar grows down over the bracket and the page never
     scrolls (race.js toggleScreens). Drawn in its state everywhere else, so
     it never slides on a redraw. At once with Reduce Motion
     (_reduced-motion.scss zeroes the duration and the closing delay) and
     with Event settings' Animations off (.current-bar--still). Closed, its
     buttons are hidden, not just clipped, so neither a finger nor a screen
     reader reaches them. */
  .current-bar__tray {
    display: grid;
    grid-template-rows: 0fr;
    transition: grid-template-rows var(--dur-base) ease-out;
  }
  .current-bar__tray.is-open {
    grid-template-rows: 1fr;
  }
  .current-bar__tray-clip {
    min-height: 0;
    overflow: hidden;
  }
  .current-bar__tray-inner {
    padding-top: var(--space-8);
    transform: translateY(-100%);
    transition: transform var(--dur-base) ease-out;
  }
  .current-bar__tray.is-open .current-bar__tray-inner {
    transform: none;
  }
  .current-bar__tray:not(.is-open) .current-bar__tray-inner {
    visibility: hidden;
    transition: transform var(--dur-base) ease-out, visibility 0s linear var(--dur-base);
  }
  .current-bar--still .current-bar__tray,
  .current-bar.current-bar--still .current-bar__tray .current-bar__tray-inner,
  .current-bar--still .screen-fold__chev {
    transition: none;
  }
  /* The five, filling the row: equal buttons, each with its mode's one word
     under its icon - Race, Upcoming, Bracket, Winner, Break - while the
     aria-label and title keep the full name ("Big Screen: Break
     (Break)"), and the one on the TV gold. Ryan, on a 393px phone: "can we
     space this out better somehow ... make the buttons slightly bigger".
     Under the icon, not beside it: beside, "Upcoming" and its icon need
     about 89px and a fifth of a 375px phone's row is 69. Under, "Upcoming"
     (61px) has 3.7px to spare at 375 and 7.7 at 393. Narrower - a 320px
     phone, or Safari's page zoom - a word that would run past its button's
     border takes all five words away and the five fill the row as icons
     (.mode-icons--bare, race.js fitBar): a word is whole or not there, never
     cut. 48px tall, a step up from 44. */
  .mode-icons--fill {
    flex: 1 1 100%;
    flex-wrap: nowrap;
    margin-left: 0;
  }
  .mode-icons--fill .mode-icons__btn {
    flex: 1 1 0;
    flex-direction: column;
    gap: var(--space-2);
    min-height: var(--touch-base);
    padding: var(--space-2);
  }
  .mode-icons--fill .mode-icons__word {
    display: block;
    font-size: var(--text-caption);
    line-height: var(--leading-tight);
    white-space: nowrap;
  }
  .mode-icons--bare .mode-icons__word {
    display: none;
  }
  .late-controls {
    flex-wrap: wrap;
  }
  .late-controls select {
    flex-basis: 100%;
  }
  .placement-dialog {
    --dialog-pad: var(--space-16);
    padding: var(--space-16) var(--dialog-pad) 0;
    max-height: 92dvh;
  }
  .move-dialog[open], .undo-result-dialog[open] {
    height: 92dvh;
  }
  /* A step down from the page's phone h1, and balanced, so "Heat 1 of 5" is
     not broken across lines and no title leaves one word alone on its second
     line (DS-21). */
  .placement-dialog h2 {
    font-size: var(--text-lg);
    text-wrap: balance;
  }
  .placement-choices {
    grid-template-columns: 1fr;
  }
  /* Race Control's Big Screen card (_break.scss): the five modes in
     two columns of equal buttons, Break across the last row directly
     over its options, and the options in one column (the rc2 phone
     pictures). */
  .mode-buttons {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .mode-buttons > .btn {
    justify-content: center;
  }
  .mode-buttons > .btn:last-child:nth-child(odd) {
    grid-column: 1/-1;
  }
  .break {
    grid-template-columns: minmax(0, 1fr);
    padding: var(--space-12);
    gap: var(--space-12);
  }
  .break__row {
    grid-template-columns: minmax(0, 1fr);
    row-gap: var(--space-6);
  }
  .break__label, .break__controls, .break__hint {
    grid-column: 1;
  }
  /* The countdown's type across the top, its two halves equal, what is
     left beside it in its kept room, and the row (No timer, 1, 2, 3, 5,
     Other…) under them, its buttons sharing the line and never narrower
     than their words (race.js fitTimerRows). */
  .break__controls > .kind-switch {
    flex: 1 1 auto;
  }
  .break__controls > .kind-switch > .kind-switch__opt {
    flex: 1 1 0;
    padding-inline: var(--space-4);
  }
  .break__in {
    flex: none;
    text-align: end;
  }
  .timer-row__opt {
    flex: 1 0 auto;
    padding-inline: var(--space-8);
  }
  .break__time {
    order: 0;
  }
  .break__clear {
    order: 2;
  }
  .break__controls > .break__at {
    flex: 1 1 100%;
    order: 4;
    justify-content: center;
  }
  .note--end {
    padding-block: var(--space-10) var(--space-2);
  }
  .note--end > .note__text {
    flex-basis: 100%;
  }
  /* The Build dialog's start picks (_start-picker.scss): three to a row,
     sharing it evenly, so none is left alone at a different width. */
  .start-picker__pick {
    flex: 1 1 30%;
    padding-inline: var(--space-4);
  }
  /* A round's own buttons (Balance eligible heats, a held dog's link) under
     its heading rather than in it, on Race Control (race.js renderRounds):
     they scroll with the round, and the heading pinned under the slim bar
     is one line, 56px of bracket back. 8px under the heading, and the
     heading's own 14px over the first heat. */
  .bracket-round > .round-actions {
    margin: calc(-1 * var(--space-6)) 0 var(--space-14);
  }
  /* My account's "Where you are logged in" (1.11.0): a row's Log out goes
     under the device's words, at their left edge, as the design drew it at
     iPhone width. Beside them, "iPhone · Home Screen app" and its "This
     device" pill would be squeezed into three lines by the button. */
  .device-row {
    align-items: flex-start;
    gap: var(--space-12);
    padding-block: var(--space-12);
  }
  .device-row__body {
    flex-direction: column;
    align-items: flex-start;
  }
}
/* The dog's page on a tall phone once the dog has a photo: 144px, as on an
   iPad. The photo is what Ryan opens this page to see ("it sometimes is the
   best place to actually be able to see the dog's image on a device"), and
   with the note gone there is room for the 40px. An SE, and any dog still
   without a photo, keep --avatar-lg, so Race status still comes up under the
   picture there (Decision #11). After the phone block, not inside it, so it
   wins over nothing but the 104px it replaces. */
@media (max-width: 640px) and (min-height: 740px) {
  .dog-head__photo:not(.dog-head__photo--empty) {
    --photo-badge: var(--photo-badge-xl);
  }
  .dog-head__photo:not(.dog-head__photo--empty) .dog-head__frame > .dogphoto--lg {
    width: var(--avatar-xl);
    height: var(--avatar-xl);
  }
}
/* ------------------------------------ the Big Screen's heat introduction */
/* Race Control while the Big Screen introduces the heat at the line (Ryan,
   2026-09-25 00:28; drawn as the rc2-intro pictures). It plays by itself
   after a result when Event settings' "Introduce each heat" is on; these
   say what it is showing and let staff stop it. public/js/race.js draws them
   from the staff snapshot's `intro`, the object the Big Screen plays. */
/* Two words in one place, the one not in use hidden: the place is as wide
   and as tall as the longer, so nothing moves when Pause becomes Start again
   or the strip's playing lines become its paused ones. */
.intro-swap {
  display: inline-grid;
  justify-items: center;
  align-items: center;
}

.intro-swap > * {
  grid-area: 1/1;
}

.intro-swap > .is-off {
  visibility: hidden;
}

.intro-swap--start {
  justify-items: start;
}

/* The progress: a grey line with the introduction's share of it in gold,
   brought up to date four times a second (race.js tickIntro), each step
   eased over the tick so it runs rather than jumps. Paused, the line keeps
   its room and is not drawn: there is nothing to go through. */
.intro-track {
  display: block;
  height: var(--space-4);
  border-radius: var(--radius-pill);
  background: var(--border);
  overflow: hidden;
}

.intro-track__fill {
  display: block;
  height: 100%;
  background: var(--gold);
  transform: scaleX(var(--intro-progress, 0));
  transform-origin: left center;
  transition: transform var(--dur-base) linear;
}

.is-paused .intro-track {
  visibility: hidden;
}

/* The strip under the Big Screen mode buttons, only while an introduction
   plays or is paused, and one height in both: its first line holds the
   playing words and the paused words in one place, its second line is one
   line (the step, cut on this private screen if a dog's name is that long),
   and the track keeps its room. So the mode buttons above it never move and
   neither does the card while the steps go by: 82px on an iPad on its side.
   Gold edge while it plays, grey paused, the card's status edge. */
.intro-strip {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-10) var(--space-12);
  margin-top: var(--space-14);
  padding: var(--space-10) var(--space-12) var(--space-10) var(--space-16);
  border: 1px solid var(--border);
  border-left: var(--status-edge) solid var(--gold);
  border-radius: var(--radius-md);
  background: var(--surface-sunken);
}

.intro-strip.is-paused {
  border-left-color: var(--border-strong);
}

.intro-strip > .icon {
  flex: none;
  align-self: flex-start;
  color: var(--accent);
}

.intro-strip__words {
  flex: 1 1 16rem;
  min-width: 0;
  display: grid;
  gap: var(--space-2);
}

.intro-strip__now,
.intro-strip__step {
  font-size: var(--text-sm);
  line-height: var(--leading-tight);
}

/* Stretched, not started: a step as wide as its words would run past the
   line instead of ending in an ellipsis. */
.intro-strip__step {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  justify-items: stretch;
  font-weight: 700;
}

.intro-strip__step > span {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.intro-strip.is-paused .intro-strip__step {
  font-weight: 400;
}

.intro-strip__track {
  margin-top: var(--space-4);
}

/* Never wider than the strip: under about 390px its two buttons (288px
   side by side) take a line each, the width of the strip, rather than run
   the page past the screen's edge, which widened the whole page (337px at
   320). Side by side they keep their own widths. */
.intro-strip__actions {
  flex: 0 1 auto;
  max-width: 100%;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-8);
  margin-left: auto;
}

.intro-strip__actions > .btn {
  flex: 1 1 auto;
}

.intro-swap--icon .icon {
  display: block;
}

/* The pinned row, past the Big Screen controls (above 640px): Pause (or
   Start again) and Skip to line-up as icons in the row's Big Screen tools
   (.screen-tools, race.js screenTools), between the Announcer's megaphone
   and the modes with a hairline either side, and the progress as a slim
   line under the pair (the rc2-intro pinned picture). */
.intro-tools {
  position: relative;
  display: flex;
  flex: 0 0 auto;
  gap: var(--space-6);
}

.intro-tools__track {
  position: absolute;
  left: var(--space-4);
  right: var(--space-4);
  bottom: calc(-1 * var(--space-6));
  height: var(--space-2);
}

/* The phone's bar (640px and under): in the row the Big Screen button
   opens (race.js screensTray), a line of its own over the five modes - what
   the Big Screen is showing and the track beside Pause (or Again) and
   Line-up, each an icon over its word like Message - so the open bar is
   52px taller only while an introduction is up (222px at 393 where it is
   170), and the closed bar never is. Closed, the Big Screen button says so:
   its word is "Intro" while one plays, its gold underline filling as it
   goes (.screen-fold__intro); paused, it is Race again. */
.bar-intro {
  display: flex;
  align-items: center;
  gap: var(--space-6);
  min-width: 0;
  margin-bottom: var(--space-8);
}

.bar-intro__words {
  flex: 1 1 auto;
  min-width: 0;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-2);
  font-size: var(--text-caption);
  line-height: var(--leading-tight);
}

.bar-intro__now {
  font-weight: 800;
  color: var(--accent);
}

.bar-intro__step {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  justify-items: stretch;
  font-weight: 700;
}

.bar-intro__step > span {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.bar-intro.is-paused .bar-intro__step {
  font-weight: 400;
}

.bar-intro__track {
  margin-top: var(--space-2);
}

/* Message's size and shape, beside it in the bar: 44px tall, the icon
   over the word. */
.bar-intro__btn {
  flex: 0 0 auto;
  flex-direction: column;
  gap: 0;
  min-width: var(--touch-min);
  min-height: var(--touch-min);
  padding: var(--space-2) var(--space-4);
}

.bar-intro__word {
  font-size: var(--text-caption);
  line-height: var(--leading-tight);
  white-space: nowrap;
}

/* The Big Screen button's "Intro": the underline every live word wears, as
   a grey line with the introduction's share of it in gold, brought up to
   date by the tick (--intro-progress, race.js tickIntro). */
.screen-fold__words > .screen-fold__intro.is-live {
  background: linear-gradient(var(--gold), var(--gold)) left bottom/calc(var(--intro-progress, 0) * 100%) var(--space-2) no-repeat, linear-gradient(var(--border), var(--border)) left bottom/100% var(--space-2) no-repeat;
  transition: background-size var(--dur-base) linear;
}

/* ---------------------------------------------------------- the button */
/* The stopwatch and what is set in the fewest words, 44px tall. Above
   640px the words are two short lines where both times are set ("Line up in
   12:47" over "Resumes 3:00 PM"), with no chevron: the row it shares with
   the result buttons has no room for one on a sideways iPad, and the
   expanded look (.btn--ghost[aria-expanded="true"]) says the panel is open.
   Where the row is measured too narrow (race.js fitActions) the words say
   only the times ("12:47" over "3:00 PM", --times-short), and past Undo's
   last step the stopwatch is alone (--times-bare), its name still whole.
   Its figures are all one width, and each time sits on room for every form
   it can take until it is set or cleared - the countdown's down to "Line up
   now", the resume time's and "Resumes shortly" (.break-times__room, race.js
   timesParts) - so the button's width never changes on the second's tick,
   at zero or as the resume time passes. It is first of the row's tools,
   which sit at the row's right-hand end, so it grows and goes on its left
   and the megaphone and the modes never move (race.js screenTools). Short,
   it keeps a little less room round its words, which is the 2px an upright
   iPad's row with a long name picked and Undo was short of. */
.break-times__btn {
  flex: 0 0 auto;
  gap: var(--space-6);
  min-width: var(--touch-min);
  padding: var(--space-2) var(--space-8);
}

.current-actions--times-short .break-times__btn {
  gap: var(--space-4);
  padding-inline: var(--space-6);
}

.break-times__btn > .icon:first-child {
  width: var(--space-20);
  height: var(--space-20);
}

.break-times__sum {
  display: grid;
  justify-items: start;
  font-size: var(--text-caption);
  line-height: var(--leading-tight);
  font-variant-numeric: tabular-nums;
  text-align: left;
  white-space: nowrap;
}

.break-times__form {
  display: inline-grid;
}

.break-times__form > * {
  grid-area: 1/1;
}

.break-times__form--short {
  display: none;
}

.current-actions--times-short .break-times__form--words {
  display: none;
}

.current-actions--times-short .break-times__form--short {
  display: inline-grid;
}

.current-actions--times-bare .break-times__sum {
  display: none;
}

.break-times__room {
  visibility: hidden;
}

.break-times__sep {
  display: none;
}

.break-times__chev {
  width: var(--space-14);
  height: var(--space-14);
  transition: transform var(--dur-base) ease-out;
}

.break-times__btn[aria-expanded=true] .break-times__chev {
  transform: rotate(180deg);
}

.break-times__btn--still .break-times__chev {
  transition: none;
}

/* The phone bar's line (race.js barTimes), under the five modes, 8px below
   them as the modes are below Wiener's row: the button across the bar, the
   times on one line with a dot between them, and at its end a chevron that
   turns over while the panel is open, as the Big Screen button's does. So
   the open bar is 52px taller only while the Big Screen is on
   Break, as it is during a heat introduction, and the closed bar
   never is. */
.bar-times {
  display: flex;
  margin-top: var(--space-8);
}

.break-times__btn--bar {
  flex: 1 1 auto;
  justify-content: flex-start;
  min-width: 0;
}

.break-times__btn--bar .break-times__sum {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  column-gap: var(--space-4);
  min-width: 0;
}

.break-times__btn--bar .break-times__sep {
  display: inline;
}

.break-times__btn--bar .break-times__chev {
  margin-left: auto;
}

/* ----------------------------------------------------------- the panel */
/* It folds open under the row the way the phone bar's Big Screen row does
   (_phone.scss .current-bar__tray): the rows' height eases from nothing in
   --dur-base and the panel slides down from under the row - only on a tap,
   and not at all with Event settings' Animations off, the default
   (.break-fold--still), or with Reduce Motion (_reduced-motion.scss).
   Closed, its controls are hidden, not just clipped, so neither a finger
   nor a screen reader reaches them. */
.break-fold {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows var(--dur-base) ease-out;
}

.break-fold.is-open {
  grid-template-rows: 1fr;
}

.break-fold__clip {
  min-height: 0;
  overflow: hidden;
}

.break-fold__inner {
  padding-top: var(--space-8);
  transform: translateY(-100%);
  transition: transform var(--dur-base) ease-out;
}

.break-fold.is-open .break-fold__inner {
  transform: none;
}

.break-fold:not(.is-open) .break-fold__inner {
  visibility: hidden;
  transition: transform var(--dur-base) ease-out, visibility 0s linear var(--dur-base);
}

.break-fold--still,
.break-fold.break-fold--still .break-fold__inner {
  transition: none;
}

/* The card's two times without its hints (_break.scss): "Line up
   in", its three presets and its reset; "Resumes at", the time and its
   clear. Above 640px, in the full box under its row, one line - the sunken
   panel with the gold edge of the mode that opened it, as in the card. The
   reset and the clear keep their room while there is nothing to reset or
   clear (.is-unset), so nothing moves under a finger as one appears. */
.break-times {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-8) var(--space-24);
  padding: var(--space-4) var(--space-12);
  border: 1px solid var(--border);
  border-left: var(--status-edge) solid var(--gold);
  border-radius: var(--radius-md);
  background: var(--surface-sunken);
}

.break-times__row {
  display: flex;
  align-items: center;
  gap: var(--space-6);
}

.break-times__label {
  margin: 0 var(--space-4) 0 0;
  font-size: var(--text-sm);
  font-weight: 700;
  white-space: nowrap;
}

/* The countdown's type and its row (1.11.0): side by side where the panel's
   line holds them, the row under the type where it does not. */
.break-times__row--count {
  flex-wrap: wrap;
}

.break-times .timer-row {
  flex: 0 1 auto;
}

.break-times .timer-row__opt {
  padding-inline: var(--space-10);
}

.break-times .kind-switch__opt {
  padding-inline: var(--space-10);
}

/* The time a control high, and 2px more for its 1px border above and
   below: the see-through field that takes the tap fills the inside of the
   border, and 44px is the least it may be (the harness measured 126x42 at
   --touch-min alone). */
.break-times .break__time {
  min-height: calc(var(--touch-min) + var(--space-2));
  padding-block: var(--space-4);
}

.break-times .break__clear.is-unset {
  visibility: hidden;
}

/* What Stop timer does, as under the card's row (Ryan, 2026-09-27: the
   same in every place the Countdown row appears): across the panel, after
   its rows. */
.break-times__hint {
  flex: 1 0 100%;
}

.break-times__hint .field__hint {
  margin: 0;
}

/* On a phone, in the slim bar under its Big Screen row: two rows, the
   labels in one column, the controls in the next and the reset and the
   clear at the end, lined up, with no box of its own - the bar is the box. */
.current-bar .break-times {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr) auto;
  gap: var(--space-8) var(--space-6);
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
}

.current-bar .break-times__row {
  grid-column: 1/-1;
  display: grid;
  grid-template-columns: subgrid;
  align-items: center;
}

.current-bar .break-times__label {
  margin: 0;
  font-size: var(--text-caption);
}

/* The countdown's type on a line of its own across the bar, its two halves
   equal, and under it the row across the bar, its buttons sharing the line
   and never narrower than their words (race.js fitTimerRows). */
.current-bar .break-times__row--count > .kind-switch {
  grid-column: 1/-1;
  display: flex;
}

.current-bar .break-times__row--count > .kind-switch > .kind-switch__opt {
  flex: 1 1 0;
}

.current-bar .break-times__row--count > .timer-row {
  grid-column: 1/-1;
}

.current-bar .timer-row__opt {
  flex: 1 0 auto;
  padding-inline: var(--space-8);
}

.current-bar .break-times__hint {
  grid-column: 1/-1;
}

/* A short window - a sideways iPad at Safari's 200% zoom is 597x417 - has
   no height for the panel's two rows under the bar's own two: the clear's
   foot sat 2px under the tab row. There the rows flow on one line where
   they fit (597 holds both), and a narrow short window (320x480) wraps to
   two as before. */
@media (max-width: 640px) and (max-height: 480px) {
  .current-bar .break-times {
    display: flex;
    flex-wrap: wrap;
    column-gap: var(--space-16);
  }
  .current-bar .break-times__row {
    display: flex;
  }
  /* No height for a third line here: the toast after the tap says it. */
  .current-bar .break-times__hint {
    display: none;
  }
}
.start-picker {
  display: grid;
  gap: var(--space-10);
  margin-block: var(--space-14) var(--space-6);
}

.start-picker__label {
  margin: 0;
  font-size: var(--text-sm);
  font-weight: 700;
}

.start-picker__picks {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-6);
}

.start-picker__pick {
  min-width: var(--touch-min);
}

/* The pick in force: tinted and edged in the accent, as the countdown's type
   switch on Race Control marks its own (_break.scss), not filled. */
.start-picker__pick[aria-pressed=true] {
  background: var(--tint-blue);
  color: var(--accent);
  border-color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent);
}

/* The slider: a 44px thumb on a thin track, the input itself 44px tall so
   the whole row is a target. Its value sits beside it in room for "10 min",
   so dragging never moves the slider's own length. */
.start-picker__slide {
  display: flex;
  align-items: center;
  gap: var(--space-12);
}

.start-picker__range {
  flex: 1 1 auto;
  min-width: 0;
  height: var(--touch-min);
  margin: 0;
  background: transparent;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
}

.start-picker__range::-webkit-slider-runnable-track {
  height: var(--range-track);
  border-radius: var(--radius-pill);
  background: var(--border-strong);
}

.start-picker__range::-moz-range-track {
  height: var(--range-track);
  border-radius: var(--radius-pill);
  background: var(--border-strong);
}

.start-picker__range::-webkit-slider-thumb {
  width: var(--touch-min);
  height: var(--touch-min);
  margin-top: calc((var(--range-track) - var(--touch-min)) / 2);
  border: var(--focus-width) solid var(--surface-raised);
  border-radius: var(--radius-pill);
  background: var(--accent);
  box-shadow: var(--shadow-sm);
  -webkit-appearance: none;
  appearance: none;
}

.start-picker__range::-moz-range-thumb {
  width: var(--touch-min);
  height: var(--touch-min);
  box-sizing: border-box;
  border: var(--focus-width) solid var(--surface-raised);
  border-radius: var(--radius-pill);
  background: var(--accent);
  box-shadow: var(--shadow-sm);
}

.start-picker__range:focus-visible {
  outline: none;
}

.start-picker__range:focus-visible::-webkit-slider-thumb {
  box-shadow: var(--focus-ring);
}

.start-picker__range:focus-visible::-moz-range-thumb {
  box-shadow: var(--focus-ring);
}

.start-picker__range.is-off::-webkit-slider-thumb {
  background: var(--text-muted);
}

.start-picker__range.is-off::-moz-range-thumb {
  background: var(--text-muted);
}

.start-picker__value {
  display: inline-grid;
  flex: none;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.start-picker__value::before {
  content: attr(data-room);
  grid-area: 1/1;
  visibility: hidden;
}

.start-picker__n {
  grid-area: 1/1;
  justify-self: end;
}

.start-picker__hint {
  margin: 0;
}

/* ------------------------------------------------------- reduced motion */
/* The kiosks have honoured this since they were written; the staff screens
   never did, despite the roster lift, the toast slide and the move toolbar. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
  /* The phone bar's Big Screen row hides its buttons after it has slid shut,
     with a delay as long as the slide; the durations above are zeroed, but a
     delay is not, so it is zeroed here and the row goes at once. */
  /* The same for the pinned Break times panel (_break-times.scss). */
  /* The dog page's picture button keeps its darker badge on a press, not the
     shrink. */
}
@media (prefers-reduced-motion: reduce) and (hover: hover) {
  .roster-item:hover {
    transform: none;
  }
}
@media (prefers-reduced-motion: reduce) {
  .btn:active {
    transform: none;
  }
  .current-bar__tray-inner {
    transition-delay: 0s !important;
  }
  .break-fold__inner {
    transition-delay: 0s !important;
  }
  .dog-head__photo:active .dogphoto {
    transform: none;
  }
}
/* ------------------------------------------- a round that is not drawn yet */
/* Who has reached the next round so far, in place of heats that would be
   redrawn from scratch on every result (public/js/staff.js waitingRound). */
.waiting-round {
  margin-bottom: 0;
}

/* The note is a <p>, and .note sets only its bottom margin, so the browser's
   own 1em top margin put 14px more above it than at the sides (DS-50). With
   nobody in the round yet - every later round, from the build until the first
   result (DS-40) - the note is the whole card, so its bottom margin goes too
   and the card is the short line it says. */
.waiting-round > .note {
  margin-top: 0;
}

.waiting-round > .note:last-child {
  margin-bottom: 0;
}

.waiting-round__dogs {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 220px), 1fr));
  gap: var(--space-8);
  margin-top: var(--space-12);
  /* One long name makes one tall chip, not a row of tall empty ones. The
     names stay whole here: nothing in this list is cut (DS-34). */
  align-items: start;
}

.waiting-round__dog {
  display: flex;
  align-items: center;
  gap: var(--space-8);
  min-height: var(--touch-min);
  padding: var(--space-6) var(--space-8);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface);
}

/* Whole names, wrapped at their spaces; only a single word too long for the
   chip breaks, rather than running out of it (DS-34). */
.waiting-round__dog .lane__who {
  overflow-wrap: break-word;
}

/* ---------------------------------------------------------- print pages */
/* Bracket Builder's poster (/print/bracket) and a dog's print for its owner
   (/print/dogs/:id), agent/ui-standards.md "Print pages". On screen: a bar
   that says what the page is, the paper, and Print; under it the sheets as
   they will print. On paper: the sheets and nothing else.

   A sheet is laid out at its paper's own size, on screen as on paper, and
   the screen only shows it smaller (the box around it scales it down). So
   public/js/print.js fits the type and draws the tree's lines on the very
   layout that prints: a name wraps where it will wrap on paper, whatever
   device prints it. Every size inside a sheet is in units of its width (cqw,
   or em of a cqw font size), so the drawing is the same shape on every
   paper. No blur and no gradient reaches paper: a PDF keeps lines, fills and
   type as shapes, and a shadow or a gradient is where a browser falls back
   to a picture. */
.print-bar {
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
  padding: var(--space-16) var(--space-24) var(--space-20);
  margin-bottom: var(--space-20);
}

.print-bar h1 {
  margin: 0 0 var(--space-6);
}

.print-bar__back {
  margin: 0;
}

.print-bar__back a {
  display: inline-flex;
  align-items: center;
  min-height: var(--touch-min);
  font-weight: 700;
}

.print-bar__lead {
  max-width: var(--measure);
  margin-bottom: var(--space-16);
}

.print-bar__actions {
  margin-top: var(--space-4);
}

.print-bar__hint {
  margin: var(--space-10) 0 0;
}

/* The paper: two choices, each a whole 44px target with its words. */
.paper-choice {
  border: 0;
  padding: 0;
  margin: 0 0 var(--space-16);
  min-width: 0;
}

.paper-choice__legend {
  padding: 0;
  margin-bottom: var(--space-8);
  font-weight: 700;
}

.paper-choice__opts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-10);
}

.paper-choice__opt {
  display: inline-flex;
  align-items: center;
  gap: var(--space-10);
  min-height: var(--touch-min);
  padding: var(--space-8) var(--space-16);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  background: var(--surface-raised);
  cursor: pointer;
}

.paper-choice__opt:active {
  background: var(--tint-blue);
}

@media (hover: hover) {
  .paper-choice__opt:hover {
    background: var(--tint-blue);
  }
}
/* The chosen paper, told by more than the dot: the ghost buttons' open state. */
.paper-choice__opt:has(input:checked) {
  background: var(--tint-blue);
  border-color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent);
}

.paper-choice__opt input {
  width: var(--space-20);
  height: var(--space-20);
  margin: 0;
  accent-color: var(--accent);
}

/* "Page 1 · Letter, upright": which sheet is which, on screen only. Where
   the owner's bracket is turned (.print-doc--turn, public/js/print.js), the
   caption says so and page 2 has no paper to choose: it is Letter's. */
.print-doc__caption {
  margin: var(--space-24) 0 var(--space-8);
  font-size: var(--text-sm);
  font-weight: 700;
  color: var(--text-muted);
}

/* A dog's card by link (views/card.ejs): a plain line under page 2's caption
   saying the bracket reads best zoomed in or saved; hidden in print with it. */
.print-doc__caption--hint {
  margin-top: calc(-1 * var(--space-4));
  font-weight: 400;
}

.print-doc__turned {
  display: none;
}

.print-doc--turn .print-doc__turned {
  display: inline;
}

.print-doc--turn .paper-choice {
  display: none;
}

/* ----------------------------------------------------------- the sheets */
/* The preview: a box the shape of the sheet, as wide as the page allows,
   with the sheet in it at full size, scaled to the box's width by print.js
   (--preview). The shadow and the rounding are the box's, so neither is
   ever inside what prints. */
.sheet-box {
  position: relative;
  width: 100%;
  overflow: hidden;
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-md);
  background: var(--print-field);
}

.sheet-box:has(> .sheet--tabloid) {
  aspect-ratio: var(--paper-tabloid);
}

.sheet-box:has(> .sheet--poster) {
  aspect-ratio: var(--paper-poster);
}

.sheet-box:has(> .sheet--letter) {
  aspect-ratio: var(--paper-letter);
}

.sheet-box--keepsake {
  aspect-ratio: var(--paper-keepsake);
  width: min(100%, var(--print-preview-upright));
  margin-inline: auto;
}

.print-doc--turn .sheet-box:has(> .sheet--letter) {
  aspect-ratio: var(--paper-turned);
}

.sheet {
  position: absolute;
  top: 0;
  left: 0;
  container-type: inline-size;
  display: flex;
  flex-direction: column;
  width: var(--sheet-w);
  height: var(--sheet-h);
  overflow: hidden;
  transform: scale(var(--preview, 1));
  transform-origin: 0 0;
  background: var(--print-field);
  color: var(--print-ink);
  font-family: var(--print-font);
  line-height: 1.2;
  -webkit-print-color-adjust: exact;
  print-color-adjust: exact;
}

/* While print.js measures, the sheet is at its own size, as on paper. */
.print-doc.is-measuring .sheet {
  transform: none;
}

/* Each sheet's size, and its shape (width over height), which is all it
   keeps on paper. */
.sheet--tabloid {
  --sheet-w: var(--paper-tabloid-w);
  --sheet-h: var(--paper-tabloid-h);
  --sheet-shape: var(--paper-tabloid);
}

.sheet--poster {
  --sheet-w: var(--paper-poster-w);
  --sheet-h: var(--paper-poster-h);
  --sheet-shape: var(--paper-poster);
}

.sheet--letter {
  --sheet-w: var(--paper-letter-w);
  --sheet-h: var(--paper-letter-h);
  --sheet-shape: var(--paper-letter);
}

.print-doc--turn .sheet--letter {
  --sheet-w: var(--paper-turned-w);
  --sheet-h: var(--paper-turned-h);
  --sheet-shape: var(--paper-turned);
}

.sheet--keepsake {
  --sheet-w: var(--paper-keepsake-w);
  --sheet-h: var(--paper-keepsake-h);
  --sheet-shape: var(--paper-keepsake);
}

.sheet p {
  margin: 0;
}

/* The kiosks' diamond field, behind everything. */
.sheet__field {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  fill: var(--print-lattice);
}

.sheet > :not(.sheet__field) {
  position: relative;
}

/* The masthead: the kiosks', on paper. Its name stays on one line; a race
   day's name too long for its bar is set smaller by public/js/print.js
   (fitMast: --mast-fit, a share of the sheet's type, unset - so 1 - for a
   name that fits, Oktoberfest's among them). */
.mast {
  flex: none;
  background: var(--print-paper);
  text-align: center;
}

.mast__bar {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 1.4cqw;
  padding: 1cqw 3cqw 0;
}

.mast__rule {
  flex: 0 1 14cqw;
  height: 0.14cqw;
  background: var(--gold);
}

.mast__diamond {
  flex: none;
  width: 0.7cqw;
  height: 0.7cqw;
  background: var(--gold);
  transform: rotate(45deg);
}

.mast__title {
  flex: none;
  font-family: var(--print-title-font);
  font-weight: 800;
  font-size: calc(3.1cqw * var(--mast-fit, 1));
  line-height: 1.15;
  letter-spacing: 0.01em;
  white-space: nowrap;
  color: var(--bav-blue);
  text-shadow: var(--print-title-3d);
}

.mast__when {
  padding: 0.15cqw 2cqw 0.55cqw;
  font-size: 1cqw;
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--print-ink-2);
}

.mast__strip {
  display: block;
  width: 100%;
  height: auto;
  fill: var(--print-sky);
}

.sheet--keepsake .mast__bar {
  gap: 2cqw;
  padding-top: 2.6cqw;
}

.sheet--keepsake .mast__title {
  font-size: calc(5cqw * var(--mast-fit, 1));
}

.sheet--keepsake .mast__rule {
  flex-basis: 10cqw;
  height: 0.25cqw;
}

.sheet--keepsake .mast__diamond {
  width: 1.2cqw;
  height: 1.2cqw;
}

.sheet--keepsake .mast__when {
  font-size: 2.1cqw;
  padding-bottom: 1.4cqw;
}

/* ------------------------------------------------ the rosette, three ways */
/* display.html's drawing (views/partials/print-rosette.ejs): gold for the
   Grand Wiener, blue for a heat won, light sky for taking part. */
.rosette {
  display: block;
  height: auto;
  overflow: visible;
}

.rosette--gold {
  --petal: var(--gold);
  --petal-edge: var(--print-gold-edge);
  --ring: var(--print-win-bg);
  --tail-a: var(--bav-blue);
  --tail-b: var(--print-sky);
}

.rosette--blue {
  --petal: var(--print-sky);
  --petal-edge: var(--print-sky-edge);
  --ring: var(--print-paper);
  --tail-a: var(--bav-blue);
  --tail-b: var(--gold);
}

.rosette--ribbon {
  --petal: var(--print-sky-light);
  --petal-edge: var(--print-sky);
  --ring: var(--print-paper);
  --centre: var(--print-paper);
  --dash: var(--print-sky-light);
  --dog: var(--print-sky);
  --tail-a: var(--print-sky-light);
  --tail-b: var(--print-paper);
  --tail-edge: var(--print-sky-light);
  --stripe: var(--print-sky-light);
}

.rosette__tail-a {
  fill: var(--tail-a);
  stroke: var(--tail-edge, none);
  stroke-width: 3;
  stroke-linejoin: round;
}

.rosette__tail-b {
  fill: var(--tail-b);
  stroke: var(--tail-edge, none);
  stroke-width: 3;
  stroke-linejoin: round;
}

.rosette__stripe {
  fill: none;
  stroke: var(--stripe, var(--print-paper));
  stroke-width: 4;
  stroke-linecap: round;
}

.rosette__petals {
  fill: var(--petal);
  stroke: var(--petal-edge);
  stroke-width: 2;
}

.rosette__disc {
  fill: var(--petal);
}

.rosette__ring {
  fill: var(--ring);
}

.rosette__centre {
  fill: var(--centre, var(--bav-blue));
}

.rosette__dash {
  fill: none;
  stroke: var(--dash, var(--print-paper));
  stroke-width: 2;
  stroke-dasharray: 3 5;
}

.rosette__dog {
  color: var(--dog, var(--print-paper));
}

/* --------------------------------------------------------- the bracket */
/* One grid: the Qualifiers' columns, the left Semi-Finals, the centre, the
   right Semi-Finals, Second Chance's columns. print.js sets how many columns
   each round takes (--q-cols, --s-cols, --q-rows, --s-rows) to suit the
   paper, and --pf, the type, as large as lets every name fit whole. The two
   tracks either side of the centre are where the tree's lines turn. */
.poster {
  --pf: 0.8;
  flex: 1 1 0;
  min-height: 0;
  display: grid;
  grid-template-columns: [q-start] repeat(var(--q-cols, 3), minmax(0, 1fr)) [q-end] 0.3em [sfl-start] minmax(0, 1fr) [sfl-end] 2em [centre-start] minmax(0, 2.2fr) [centre-end] 2em [sfr-start] minmax(0, 1fr) [sfr-end] 0.3em [s-start] repeat(var(--s-cols, 3), minmax(0, 1fr)) [s-end];
  grid-template-rows: auto minmax(0, 1fr);
  column-gap: 0.55em;
  row-gap: 0.35em;
  padding: 0.7em 1.2em 0.5em;
  font-size: calc(var(--pf) * 1cqw);
}

.poster > :not(.wires) {
  position: relative;
}

.wires {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
}

.wire {
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.wire--r3 {
  stroke: var(--print-r3);
}

.wire--r4 {
  stroke: var(--print-r4);
}

.wire--waiting {
  stroke: var(--bav-blue);
}

.wire--road-edge {
  stroke: var(--print-gold-edge);
}

.wire--road {
  stroke: var(--gold);
}

.zone-title {
  grid-row: 1;
  margin: 0;
  font-size: 1.15em;
  font-weight: 900;
  line-height: 1.15;
  color: var(--round);
}

.zone-title__meta {
  font-size: 0.85em;
  font-weight: 700;
  color: var(--print-ink-2);
}

.zone-title--q {
  grid-column: q;
}

.zone-title--sfl {
  grid-column: sfl;
}

.zone-title--sfr {
  grid-column: sfr;
  text-align: right;
}

.zone-title--s {
  grid-column: s;
  text-align: right;
}

.pround--1 {
  --round: var(--print-r1);
}

.pround--2 {
  --round: var(--print-r2);
}

.pround--3 {
  --round: var(--print-r3);
}

.pround--4 {
  --round: var(--print-r4);
}

.zone {
  grid-row: 2;
  min-height: 0;
}

.zone--q, .zone--s {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  align-content: start;
  gap: 0.45em 0.55em;
}

.zone--q {
  grid-column: q;
  grid-template-rows: repeat(var(--q-rows, 5), auto);
}

.zone--s {
  grid-column: s;
  grid-template-rows: repeat(var(--s-rows, 5), auto);
}

/* The tree's outer heats spread down their side, like the Big Screen's. */
.zone--sfl, .zone--sfr {
  display: flex;
  flex-direction: column;
  justify-content: space-around;
  gap: 0.6em;
}

.zone--sfl {
  grid-column: sfl;
}

.zone--sfr {
  grid-column: sfr;
}

.zone__tbd {
  padding: 0.6em;
  border: 0.1em dashed var(--print-rule);
  border-radius: 0.45em;
  background: var(--print-paper);
  text-align: center;
  font-weight: 700;
  color: var(--print-ink-2);
}

/* The centre: the Grand Wiener, the Final under it, the Championship heats
   that feed the Final under that - and, under those, room for the lines that
   run along the foot into a Championship heat from its far side, each at a
   height of its own (--floors, from print.js), a full em clear of every
   heat. */
.centre {
  grid-column: centre;
  grid-row: 1/3;
  min-height: 0;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 1.2em;
  padding-bottom: calc(var(--floors, 0) * 0.8em + min(var(--floors, 0), 1) * 0.6em);
}

.crown {
  display: flex;
  align-items: center;
  gap: 0.9em;
  padding: 0.8em 1em;
  background: var(--print-paper);
  border: 0.3em solid var(--gold);
  border-radius: 0.9em;
}

.crown--decided {
  background: var(--print-win-bg);
}

.crown__mark {
  position: relative;
  flex: none;
  width: 32%;
}

.crown__rosette {
  width: 78%;
  margin: 0 auto;
}

.crown__photo {
  display: block;
  width: 100%;
  aspect-ratio: 1;
  border-radius: 50%;
  object-fit: cover;
  border: 0.25em solid var(--gold);
  background: var(--print-mark-bg);
}

/* Pinned inside the picture's own corner, so its petals stay clear of the
   name beside it and its tails of the crown's gold frame. */
.crown__pin {
  position: absolute;
  right: -2%;
  bottom: -3%;
  width: 34%;
}

.crown__words {
  min-width: 0;
}

.crown__title {
  font-family: var(--print-title-font);
  font-weight: 800;
  font-size: 2.3em;
  line-height: 1.08;
  color: var(--print-gold-ink);
  text-shadow: var(--print-title-3d-ink);
}

.crown__name {
  margin-top: 0.3em;
  font-size: 1.45em;
  font-weight: 900;
  line-height: 1.1;
  color: var(--print-ink);
}

.centre__final > .pheat {
  font-size: 1.12em;
}

.centre__feeders {
  display: flex;
  gap: 1.2em;
  align-items: flex-start;
}

.centre__feeders > .pheat {
  flex: 1 1 0;
  min-width: 0;
}

.pholder {
  padding: 0.8em;
  border: 0.12em dashed var(--print-r4);
  border-radius: 0.5em;
  background: var(--print-paper);
  text-align: center;
}

.pholder__name {
  font-weight: 900;
  font-size: 1.1em;
  color: var(--round);
}

.pholder__note {
  margin-top: 0.2em;
  font-weight: 700;
  color: var(--print-ink-2);
}

/* A heat: white, its top edge in its round's colour, as on the Big Screen. */
.pheat {
  --round: var(--print-r1);
  background: var(--print-paper);
  border: 0.09em solid var(--print-rule);
  border-top: 0.32em solid var(--round);
  border-radius: 0.45em;
  padding: 0.28em 0.38em 0.38em;
}

.pheat--r2 {
  --round: var(--print-r2);
}

.pheat--r3 {
  --round: var(--print-r3);
}

.pheat--r4 {
  --round: var(--print-r4);
}

/* The Final keeps the gold frame the Big Screen gives it everywhere. */
.pheat--final {
  --round: var(--print-gold-ink);
  border: 0.28em solid var(--gold);
}

.pheat__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  column-gap: 0.45em;
  font-size: 0.9em;
  font-weight: 800;
  line-height: 1.2;
  color: var(--round);
}

.pheat__part {
  display: inline-block;
  max-width: 100%;
  white-space: nowrap;
}

.pheat__tie {
  font-weight: 700;
  color: var(--print-ink-2);
}

/* A dealt heat's box on a race on two or three boxes (2.0.0, S33), after its label. */
.pheat__track {
  font-weight: 700;
  color: var(--print-ink-2);
  white-space: nowrap;
}

.pheat__tbd {
  margin-top: 0.3em;
  font-size: 0.9em;
  font-weight: 700;
  color: var(--print-ink-2);
}

.pheat__lanes {
  list-style: none;
  margin: 0.22em 0 0;
  padding: 0;
  display: grid;
  gap: 0.1em;
}

/* A lane's frame is an outline drawn inside its edge, not a border: a
   border narrower than a pixel is drawn a whole pixel wide and takes that
   room, and where WebKit prints the poster at half its paper's size (an iPad
   on Letter, _print.scss "How a sheet meets the paper") thirty lanes' worth
   ran the tallest column past its sheet. An outline takes no room, so the
   poster only scales. The padding holds the space the border did. */
.plane {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  column-gap: 0.32em;
  padding: 0.15em 0.3em 0.15em 0.18em;
  outline: 0.08em solid transparent;
  outline-offset: -0.08em;
  border-radius: 0.32em;
  font-weight: 700;
  line-height: 1.12;
}

.plane__n {
  display: grid;
  place-items: center;
  width: 1.3em;
  height: 1.3em;
  border-radius: 50%;
  background: var(--bav-blue);
  color: var(--print-paper);
  font-size: 0.8em;
  font-weight: 900;
  font-variant-numeric: tabular-nums;
}

/* Whole names: they wrap at spaces, and print.js makes the type smaller
   rather than let one run past its heat. Only a word too long to fit at the
   smallest size is broken, and only then (.fit-break). */
.plane__name {
  overflow-wrap: normal;
}

.fit-break .plane__name, .fit-break .crown__name, .fit-break .keep__name {
  overflow-wrap: anywhere;
}

.plane__tag {
  font-weight: 600;
  color: var(--print-ink-2);
  white-space: nowrap;
}

/* A raced heat: its Heat "Wiener" (or two) marked, the rest a step quieter
   - colour, never opacity. */
.pheat--done .plane {
  font-weight: 600;
  color: var(--print-ink-2);
}

.pheat--done .plane--won {
  font-weight: 900;
  color: var(--print-ink);
  background: var(--print-mark-bg);
  outline-color: var(--print-sky);
}

/* The lit road: the Grand Wiener's on the poster, the dog's own on its
   owner's print. Gold means the road and nothing else on a lane. */
.pheat .plane--road {
  font-weight: 900;
  color: var(--print-ink);
  background: var(--gold);
  outline-color: var(--print-gold-edge);
}

.rosette--lane {
  width: 1.05em;
  margin: -0.25em 0;
}

/* On a gold lane a paper halo round the petals, so a gold rosette is never
   gold on gold. */
.plane--road .rosette__petals {
  stroke: var(--print-paper);
  stroke-width: 14;
  paint-order: stroke;
}

/* The owner's bracket on a paper too small to give every lane of every heat
   a readable size (print.js, NAME_FLOOR_PT): a heat raced off the dog's road
   keeps its label and its Heat "Wiener" - both, for a tie - and no other
   lane. The dog's own heats, the heats still to race and the Championship
   keep every lane, and the key says so. */
.poster--compact .zone .pheat--done:not(.pheat--road) .plane:not(.plane--won) {
  display: none;
}

.poster--compact .zone--q, .poster--compact .zone--s {
  row-gap: 0.3em;
}

.poster--compact .zone .pheat--done:not(.pheat--road) {
  padding-bottom: 0.24em;
}

/* The key along the foot. */
.sheet__key {
  flex: none;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 0.3em 2.2em;
  padding: 0.35cqw 2cqw 0.8cqw;
  font-size: 0.9cqw;
  font-weight: 700;
  color: var(--print-ink-2);
}

.key {
  display: inline-flex;
  align-items: center;
  gap: 0.45em;
}

.key__mark {
  width: 1.2em;
}

.key__pair {
  display: inline-flex;
  gap: 0.1em;
}

.key__road {
  width: 2.4em;
  height: 1.1em;
  border-radius: 0.3em;
  background: var(--gold);
  border: 0.08em solid var(--print-gold-edge);
}

/* The compact bracket's own line in the key: shown only while the bracket really is compact. Two classes and after
   .key, so .key's display can never win over it: when both were one class and .key came later, the line showed on
   every trainer's print and card, even with every dog drawn (Devin, 2026-09-28). */
.key.key--compact {
  display: none;
}

.sheet:has(.poster--compact) .key.key--compact {
  display: inline-flex;
}

/* -------------------------------------------------------- the keepsake */
/* The Lineup's tap-to-enlarge card on Letter: the picture over the words,
   then the awards spaced like the heat introduction's Final card. --kf is
   print.js making all of it a little smaller when a long name or a long run
   of wins would not otherwise fit the page. */
.keep {
  --kf: 1;
  flex: 1 1 0;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 3.5cqw 6cqw 5.5cqw;
  font-size: calc(var(--kf) * 1cqw);
}

.keep__card {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2.6em;
  padding: 4.5em 4.5em 5em;
  text-align: center;
  background: var(--print-paper);
  border: 0.3em solid var(--print-rule);
  border-radius: 3.2em;
}

.keep--grand .keep__card, .keep--winner .keep__card {
  border: 0.6em solid var(--gold);
}

/* The pinned rosette's tails hang below the picture, into the room kept
   under it, so they never touch the title. */
.keep__pic {
  position: relative;
  flex: none;
  width: 50em;
  margin-bottom: 3.5em;
}

.keep__photo {
  display: block;
  width: 100%;
  aspect-ratio: 1;
  border-radius: 50%;
  object-fit: cover;
  border: 1em solid var(--gold);
  background: var(--print-mark-bg);
}

.keep--thanks .keep__photo {
  border-color: var(--print-sky-light);
}

.keep__photo--mark {
  display: grid;
  place-items: center;
}

.keep__dog {
  width: 62%;
  height: 62%;
  color: var(--print-mark);
}

.keep__pin {
  position: absolute;
  right: -2em;
  bottom: -5.5em;
  width: 15em;
}

.keep--thanks .keep__pin {
  width: 13.5em;
}

.keep__who {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1em;
}

.keep__title {
  font-family: var(--print-title-font);
  font-weight: 800;
  font-size: 6.4em;
  line-height: 1.08;
  color: var(--print-gold-ink);
  text-shadow: var(--print-title-3d-ink);
}

.keep__name {
  font-size: 6.2em;
  font-weight: 900;
  line-height: 1.08;
  color: var(--print-ink);
  overflow-wrap: normal;
  text-wrap: balance;
}

/* The dog's trainers, one or both, each in full (Devin via Ryan, 2026-09-25):
   "Trainer: A" / "Trainers: A & B". Whole on a line where it fits, so a long
   name breaks between words only when it must. */
.keep__handler {
  font-size: 3em;
  font-weight: 600;
  line-height: 1.25;
  color: var(--print-ink-2);
}

.keep__person {
  display: inline-block;
  max-width: 100%;
  font-weight: 700;
}

/* The record under the words: the wins, then how far the dog got. A block
   of rows, left-aligned inside it and centred under the name. Each win is a
   row of its own, like the heat introduction's Final card: its rosette - two
   for a win shared in Too Close to Call, the gold one for the Grand Wiener -
   beside where it was won. The rosettes' column is one rosette wide, or two
   when a win on the card was shared, so every row's words start at one edge;
   rows are a clear gap apart, so no rosette's tails reach the next row. */
.keep__record {
  --win-mark: 1.45em;
  --win-pair: 0.14em;
  --win-gap: 0.55em;
  --marks: var(--win-mark);
  display: grid;
  justify-items: start;
  row-gap: 0.95em;
  max-width: 100%;
  font-size: 2.8em;
  font-weight: 600;
  line-height: 1.25;
  text-align: left;
  color: var(--print-ink-2);
}

.keep__record--shared {
  --marks: calc(2 * var(--win-mark) + var(--win-pair));
}

.keep__wins {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  justify-items: start;
  row-gap: 0.95em;
}

.win {
  display: grid;
  grid-template-columns: var(--marks) minmax(0, 1fr);
  align-items: center;
  column-gap: var(--win-gap);
}

.win__marks {
  display: flex;
  gap: var(--win-pair);
}

.win__mark {
  flex: none;
  width: var(--win-mark);
}

/* Where a win was: each part whole, and a line breaks only after a part's
   dot, never before it - no line of a row starts with "·". */
.win__where {
  font-weight: 800;
  color: var(--print-ink);
}

.win__part {
  display: inline-block;
  max-width: 100%;
  white-space: nowrap;
}

.win--grand .win__where {
  color: var(--print-gold-ink);
}

/* Who a win was shared with: a line of its own under where it was won,
   beside the same two rosettes. */
.win__co {
  display: block;
}

.win__co b {
  color: var(--print-ink);
}

/* How far the dog got: plain words, no rosette, under the wins' words; for a
   dog with no win, centred under its trainers. */
.keep__last {
  padding-left: calc(var(--marks) + var(--win-gap));
}

.keep__last b {
  font-weight: 700;
  color: var(--print-ink);
  white-space: nowrap;
}

.keep__record--none {
  justify-items: center;
  text-align: center;
}

.keep__record--none .keep__last {
  padding-left: 0;
}

/* -------------------------------------------------------------- paper */
/* One named page per paper, so the owner's print can put the keepsake on
   Letter upright and the bracket sideways in one print. Each page also has
   an unnamed @page for its first paper (views/partials/print-page.ejs), for
   a browser that takes a page size but not a named page. */
@page print-tabloid {
  size: 17in 11in;
  margin: 0.3in;
}
@page print-poster {
  size: 36in 24in;
  margin: 0.3in;
}
@page print-letter {
  size: 11in 8.5in;
  margin: 0.3in;
}
@page print-keepsake {
  size: 8.5in 11in;
  margin: 0;
}
/* The owner's bracket, turned on upright Letter (.print-doc--turn). */
@page print-turned {
  size: 8.5in 11in;
  margin: 0.3in;
}
/* How a sheet meets the paper. On screen it is laid out at its paper's
   size in inches (the tokens above), so print.js fits it on the layout that
   prints. On paper it takes its size from the page instead, in its paper's
   shape (aspect-ratio), with everything inside in cqw, so the drawing only
   scales.

   Inches are not a size on paper in every browser. Chrome prints a CSS inch
   as an inch. WebKit - Safari, and every browser on an iPad or an iPhone -
   lays a print out 1.25 to 2 times as wide as the page's printable width in
   points and shrinks the result onto the page (WebCore's PrintContext), so
   there a CSS inch prints anywhere from 0.53 to 1.07 of an inch. The owner's
   turned bracket, 10.4in tall in inches, came out taller than the page and
   ran onto a third one; sized by the page, it is the same picture in both.

   Nor is the page's width enough. A sheet as wide as the page is taller
   than a page wider than the sheet's own shape, and its foot ran onto a
   second page - the key along the bottom, a sliver (Ryan, 2026-09-25): the
   poster on Legal sideways, on Tabloid or A4 sideways once Safari keeps
   room for its header and footer, the owner's print on any sideways paper.
   So a print on one paper - the poster, and the owner's print where every
   page is on the same paper (.print-doc--turn) - fits each sheet to the
   page both ways: as wide as the page, or as tall as it, whichever is less.
   The page's height can only be had as a percentage. Chrome's root is the
   page box; WebKit resolves the root's height against the page while it
   prints (RenderView's page height), but vh, and every media query, against
   the window - so none of those. The root, the body and the print page are
   the page's height, each sheet's box is a container of that size (100cqw
   by 100cqh), and the sheet is sized in it - so nothing may stand between
   the body and a sheet's box but .wrap and .print-doc (test/print-exports
   holds the page to that). The owner's print elsewhere keeps its named pages,
   each sheet the page's width: one height cannot serve an upright page and
   a sideways one, and Chrome scales each named page to the paper.
   scripts/ui-check/print-webkit.js checks both, in WebKit's print layout and
   in Chrome's PDF, at Letter, Legal, Tabloid, A4 and A3 either way round. */
@media print {
  html:has(.print-doc), body:has(.print-doc) {
    background: none;
  }
  body:has(.print-doc) {
    margin: 0;
    padding: 0;
  }
  body:has(.print-doc) .topbar,
  body:has(.print-doc) .buildstamp,
  .print-bar,
  .print-doc__caption {
    display: none !important;
  }
  body:has(.print-doc) .wrap {
    max-width: none;
    margin: 0;
    padding: 0;
  }
  .sheet-box, .sheet-box:has(> .sheet) {
    width: auto;
    aspect-ratio: auto;
    overflow: visible;
    margin: 0;
    border-radius: 0;
    box-shadow: none;
    background: none;
    break-inside: avoid;
  }
  .sheet-box ~ .sheet-box {
    break-before: page;
  }
  .sheet {
    position: relative;
    width: 100%;
    height: auto;
    aspect-ratio: var(--sheet-shape);
    transform: none;
    break-inside: avoid;
  }
  .sheet-box:has(> .sheet--tabloid) {
    page: print-tabloid;
  }
  .sheet-box:has(> .sheet--poster) {
    page: print-poster;
  }
  .sheet-box:has(> .sheet--letter) {
    page: print-letter;
  }
  .sheet-box--keepsake {
    page: print-keepsake;
  }
  /* The poster's box takes no page name. Chrome, saving the poster to PDF,
     sometimes began the document on an unnamed first page and then broke to
     the box's named one: a blank first page, the poster on page 2 (the
     hosts' copy of the poster, 30 of 80 loads; 0 of 80 with the name off).
     The poster is a one-paper print, and its size is on the unnamed @page
     that views/partials/print-page.ejs writes for the chosen paper - which
     Chrome let overrule the named page anyway - so nothing changes on paper. */
  .print-doc[data-print=poster] .sheet-box {
    page: auto;
  }
  /* A print on one paper: the page's height down to each sheet's box. */
  html:has(.print-doc[data-print=poster]), html:has(.print-doc--turn),
  body:has(.print-doc[data-print=poster]), body:has(.print-doc--turn),
  body:has(.print-doc[data-print=poster]) .wrap, body:has(.print-doc--turn) .wrap,
  .print-doc[data-print=poster], .print-doc--turn {
    height: 100%;
    min-height: 0;
  }
  .print-doc[data-print=poster] .sheet-box, .print-doc--turn .sheet-box {
    height: 100%;
    container-type: size;
  }
  .print-doc[data-print=poster] .sheet, .print-doc--turn .sheet--keepsake {
    width: min(100cqw, 100cqh * var(--sheet-shape));
    margin-inline: auto;
  }
  /* The owner's bracket where every page prints on one paper (an iPad):
     drawn sideways, then turned a quarter so it fills an upright page rather
     than a strip across its top - its top to the left edge, the way a
     printer turns a sideways page. Upright it is a frame --turned-frame
     wide, in the middle of its box, fitted the same way; the sheet is as
     wide as the frame is tall. */
  .print-doc--turn .sheet-box:has(> .sheet--letter) {
    page: print-turned;
    position: relative;
    aspect-ratio: auto;
  }
  .print-doc--turn .sheet--letter {
    --turned-frame: min(100cqw, 100cqh * var(--paper-turned-upright));
    position: absolute;
    left: calc((100cqw - var(--turned-frame)) / 2);
    width: calc(var(--turned-frame) * var(--paper-turned));
    aspect-ratio: var(--paper-turned);
    transform: translateY(calc(var(--turned-frame) * var(--paper-turned))) rotate(-90deg);
  }
}
/* --------------------------------------- a dog's card as its link's picture */
/* The picture a shared card link shows (1.12.0, views/card-image.ejs; Ryan,
   2026-10-02: "Can we get the dogs event card as the shared image served
   with the unique dog links?"): never a page anyone opens - the card printer
   lays it out in a window of exactly its size and photographs it
   (pdf-service/, src/card-image.js). The keepsake, as its print draws it,
   centred between a diamond strip along the top and the bottom - inside the
   centred square some apps crop a preview to - with Devin's two dogs at its
   sides on the Big Screen's diamonds. */
html:has(body.card-image) {
  background: var(--print-field);
}

body.card-image {
  position: relative;
  width: var(--card-image-w);
  height: var(--card-image-h);
  margin: 0;
  overflow: hidden;
  background: conic-gradient(from 45deg at 50% 50%, var(--card-image-lattice) 25%, transparent 0 50%, var(--card-image-lattice) 0 75%, transparent 0) 0 0/var(--card-image-tile) var(--card-image-tile), var(--print-field);
}

.card-image__strip {
  position: absolute;
  left: 0;
  right: 0;
  height: var(--card-image-strip);
  background: conic-gradient(from 45deg at 50% 50%, var(--print-sky) 25%, var(--print-paper) 0 50%, var(--print-sky) 0 75%, var(--print-paper) 0) 0 0/var(--card-image-strip) var(--card-image-strip);
}

.card-image__strip--top {
  top: 0;
}

.card-image__strip--bottom {
  bottom: 0;
}

.card-image__dog {
  position: absolute;
  bottom: var(--card-image-strip);
  height: var(--card-image-dog-h);
  filter: var(--card-image-dog-shadow);
}

/* Each dog a little way out from the card's edge, facing it, never behind it
   (the card is --card-image-sheet-h tall, in the keepsake's shape). */
.card-image__dog--left {
  right: calc(50% + var(--card-image-sheet-h) * var(--paper-keepsake) / 2 + var(--space-8));
}

.card-image__dog--right {
  left: calc(50% + var(--card-image-sheet-h) * var(--paper-keepsake) / 2 + var(--space-8));
}

.card-image .print-doc {
  position: absolute;
  inset: var(--card-image-strip) 0;
  display: grid;
  place-items: center;
}

.card-image .sheet-box--keepsake {
  width: auto;
  height: var(--card-image-sheet-h);
  margin: 0;
}

/* ------------------------------------------------ share with the hosts */
/* Event settings' "Share with the event hosts" card (1.10.0): the hosts'
   link under the switch, with Copy link and Make a new link. The address is
   the Screens card's .url, the link mark beside it. */
.share-link {
  margin: var(--space-14) 0 var(--space-4);
}

.share-link__address {
  display: flex;
  align-items: flex-start;
  gap: var(--space-8);
  margin: 0 0 var(--space-12);
}

.share-link__address .icon {
  flex: none;
  color: var(--text-muted);
}

.share-link__address .url {
  min-width: 0;
}

/* The hosts' page (views/host-share.ejs): the staff look with none of the
   staff chrome. No tab row, so a phone keeps no room for one under the page. */
@media (max-width: 640px), (max-width: 899px) and (max-height: 599px) {
  .share-page .wrap {
    padding-bottom: var(--space-28);
  }
}
/* The name in the bar is the race day's own on these pages (2.0.0): up to
   120 letters, which a staff bar's one line never has to hold. It wraps
   inside the bar rather than pushing the page sideways on a phone (review
   S17-RV-2); a name that fits stays on its one line as before. */
.share-page .topbar__brand {
  white-space: normal;
  min-width: 0;
}

.share-page .topbar__mark {
  flex: none;
}

.share-page .topbar__name {
  min-width: 0;
  overflow-wrap: anywhere;
}

.share-card__head {
  align-items: center;
}

.share-card__keep {
  margin: 0 0 var(--space-16);
}

.share-card__drawn {
  margin: calc(-1 * var(--space-8)) 0 var(--space-16);
}

.share-empty {
  margin: 0;
  color: var(--text-muted);
}

/* A race day's races to choose from (2.0.0, PLAN S15): All races, then each
   race with its colour's dot and its count of dogs, a page apiece, as
   Results emails' filter is drawn. Its poster has the same row, each race's
   bracket, under the way back. On the chosen one (the accent's fill) the
   dot sits in a ring of the button's own text colour, so a blue race's dot
   still shows on the blue button: a ring outside and in, as the race switch
   draws it on the blue top bar, so it reads as a dot in a disc and never as
   an empty ring. */
.share-races {
  margin: 0 0 var(--space-16);
}

.print-bar__races {
  margin: 0 0 var(--space-12);
}

.share-races .btn, .print-bar__races .btn {
  white-space: normal;
}

.share-races__count {
  font-weight: 400;
}

.share-races .btn[aria-current] .race-chip,
.print-bar__races .btn[aria-current] .race-chip {
  box-shadow: 0 0 0 var(--space-2) var(--text-on-accent), inset 0 0 0 var(--space-2) var(--text-on-accent);
}

/* Two long labels: they wrap inside the button rather than push the page
   sideways on a phone. */
.share-actions .btn {
  white-space: normal;
  text-align: left;
}

/* The list, as a table on a computer and an iPad held sideways: a row per
   dog - the dog and what it is, both trainers, the phone and the email, the
   fun fact, when the form was sent, and whether it has checked in. The CSV
   has every one of those in a column of its own. */
.share-list th, .share-list td {
  vertical-align: top;
}

.share-list td {
  font-size: var(--text-sm);
}

/* In a table laid out by its content, a column whose words may break
   anywhere can shrink to nothing while one whose words may not keeps its
   longest word: so the trainers, the phone and email and the fun fact break
   wherever they must (an email typed with no hyphen, a fun fact with no
   space), and the dog's name, which reads first, keeps its longest word whole
   in a column never narrower than a name's worth. Nothing can then push the
   table sideways, and no name breaks in the middle to make room for an email. */
.share-row__dog {
  min-width: 12ch;
}

.share-list .dogname {
  overflow-wrap: break-word;
}

.share-row__traits {
  margin-top: var(--space-2);
  color: var(--text-muted);
  font-size: var(--text-label);
  overflow-wrap: break-word;
}

.share-row__line {
  overflow-wrap: anywhere;
}

.share-row__second {
  color: var(--text-muted);
}

.share-row__fact {
  max-width: 34ch;
  color: var(--text-muted);
  overflow-wrap: anywhere;
}

.share-row__none {
  color: var(--text-muted);
  font-style: italic;
}

/* A phone number and an email are links (tel:, mailto:) - controls, so each
   has a control's height and width, as the dog's name on Check-In does. */
.share-row__reach {
  display: inline-flex;
  align-items: center;
  min-width: var(--touch-min);
  min-height: var(--touch-min);
  color: var(--accent);
  overflow-wrap: anywhere;
}

/* A phone and an upright iPad: each dog is a card of its own, as the
   import preview is on a phone - the dog at the top with its check-in pill at
   the right, then each part under a small heading of its own, since the
   table's headings are gone. Every part may break a long word (an email
   typed with no space in it), so none can push the page sideways. */
@media (max-width: 899px) {
  .share-list thead {
    display: none;
  }
  .share-list, .share-list tbody {
    display: block;
  }
  .share-list tr {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    gap: var(--space-10) var(--space-12);
    padding: var(--space-14) 0;
    border-bottom: 1px solid var(--border);
  }
  .share-list td {
    display: block;
    grid-column: 1/-1;
    min-width: 0;
    padding: 0;
    border: 0;
    overflow-wrap: anywhere;
  }
  .share-list td.share-row__dog {
    grid-column: 1;
    grid-row: 1;
  }
  .share-list td.share-row__in {
    grid-column: 2;
    grid-row: 1;
  }
  .share-list td[data-label]::before {
    content: attr(data-label);
    display: block;
    margin-bottom: var(--space-2);
    font-size: var(--text-caption);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: var(--tracking-caps);
    color: var(--text-muted);
  }
  .share-row__fact {
    max-width: var(--measure);
  }
}
/* An upright iPad has room for the trainers and the phone and email side by
   side under the dog. */
@media (min-width: 641px) and (max-width: 899px) {
  .share-list tr {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto;
  }
  .share-list td.share-row__dog {
    grid-column: 1/3;
  }
  .share-list td.share-row__in {
    grid-column: 3;
  }
  .share-list td.share-row__who {
    grid-column: 1;
  }
  .share-list td.share-row__contact {
    grid-column: 2/-1;
  }
}
/* A phone: the two buttons one above the other, each the card's width. */
@media (max-width: 640px) {
  .share-actions {
    display: grid;
    width: 100%;
  }
  .share-actions .btn {
    justify-content: flex-start;
  }
}
/* ------------------------------------------------- an Event host's pages */
/* An Event host's pages (2.0.0, S13; views/host-*.ejs): Registrations, Bracket
   and results, Results emails, each for one race day. The list itself is the
   hosts' link's (styles/_host-share.scss: a table on a computer and an iPad
   held sideways, a card per dog on a phone and an upright iPad); what is
   here is the race day's line, the other race days a host works, each race's
   count, and the read-only list of results emails. */
/* The race day the page is about, then the host's other race days, each a
   touch-sized pill that opens this page for that day. */
.host-day {
  margin-bottom: var(--space-12);
}

.host-day__name {
  color: var(--text);
}

.host-days {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-8);
  margin: 0 0 var(--space-16);
}

.host-days__day {
  display: inline-flex;
  align-items: center;
  gap: var(--space-6);
  min-height: var(--touch-min);
  padding: var(--space-6) var(--space-14);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-pill);
  background: var(--surface-raised);
  color: var(--text);
  font-size: var(--text-sm);
  font-weight: 600;
  text-decoration: none;
  overflow-wrap: anywhere;
}

.host-days__day .icon {
  flex: none;
  color: var(--text-muted);
}

.host-days__day.is-active {
  border-color: var(--accent);
  background: var(--accent);
  color: var(--text-on-accent);
}

.host-days__day.is-active .icon {
  color: var(--text-on-accent);
}

.host-days__kept {
  font-weight: 400;
}

.host-kept {
  margin-bottom: var(--space-16);
}

.host-asof {
  max-width: var(--measure);
  margin: 0 0 var(--space-16);
}

.host-none p {
  margin: 0;
}

/* Each race's dogs and check-ins, one line each, its colour's dot first. */
.host-races {
  display: grid;
  gap: var(--space-6);
  margin: 0 0 var(--space-14);
  padding: 0;
  list-style: none;
}

.host-races__race {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-4) var(--space-8);
  font-size: var(--text-sm);
}

.host-races__name {
  font-weight: 700;
  overflow-wrap: anywhere;
}

.host-races__count {
  color: var(--text-muted);
}

/* The Race column: the race's dot, then its name. */
.host-row__race {
  white-space: nowrap;
}

.host-row__race .race-chip {
  display: inline-block;
  margin-right: var(--space-6);
  vertical-align: middle;
}

/* Checked in, then Edit under it: Edit is a control, so a control's size. */
.host-row__acts {
  display: grid;
  justify-items: start;
  gap: var(--space-8);
}

.host-row__acts .btn {
  min-width: var(--touch-min);
}

/* Results emails, read only: a row per family, who and which dogs and the
   address on the left, whether it was sent and when on the right. */
.host-emails__head {
  align-items: baseline;
}

.host-emails__race {
  display: flex;
  align-items: center;
  gap: var(--space-8);
  margin: 0;
  font-size: var(--text-lg);
  overflow-wrap: anywhere;
}

.host-emails__count {
  margin: 0;
  color: var(--text-muted);
  font-size: var(--text-sm);
}

.host-emails__waiting {
  margin: 0 0 var(--space-12);
}

.host-emails__list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.host-emails__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: start;
  gap: var(--space-6) var(--space-12);
  padding: var(--space-12) 0;
  border-bottom: 1px solid var(--border);
}

.host-emails__row:last-child {
  border-bottom: 0;
}

.host-emails__who {
  min-width: 0;
}

.host-emails__people {
  font-weight: 700;
  overflow-wrap: anywhere;
}

.host-emails__dogs, .host-emails__to {
  color: var(--text-muted);
  font-size: var(--text-sm);
  overflow-wrap: anywhere;
}

/* A sent time and "before the results changed" may be longer than a pill
   usually is: it wraps, never pushing the page sideways. */
.host-emails__state {
  white-space: normal;
  text-align: center;
}

.host-emails__state--changed {
  background: var(--tint-gold);
  color: var(--on-tint-gold);
}

.host-emails__none-head {
  margin: var(--space-16) 0 0;
  color: var(--text-muted);
  font-size: var(--text-caption);
  font-weight: 700;
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
}

.host-result__later {
  margin: 0;
}

/* The poster's one link takes the card's width: it never runs out of its button. */
.host-result .race-card__links {
  grid-template-columns: minmax(0, 1fr);
}

.host-result__poster {
  white-space: normal;
}

/* A phone and an upright iPad, each dog a card: Edit under Checked in, at the card's right edge. */
@media (max-width: 899px) {
  .host-row__acts {
    justify-items: end;
  }
}
/* A phone: the state goes under the family, the whole row wide. */
@media (max-width: 640px) {
  .host-emails__row {
    grid-template-columns: minmax(0, 1fr);
  }
  .host-emails__state {
    justify-self: start;
    text-align: left;
  }
}
/* ----------------------------------------------------- results emails */
/* Results emails (1.10.2; views/results-emails.ejs): one card per e-mail
   address, the dogs' pictures, whose dogs they are and where the e-mail
   goes, whether it has been written, and under them Write email - the one
   big button the page is tapped through with - Mark not sent, and the row's
   "Asked for results" switch. A row stays the same shape sent or not: only
   the pill's words and colour and Write email's fill change, so nothing
   moves under a thumb as the list is worked through. */
.rmail-early {
  margin-bottom: var(--space-20);
}

/* The Message card: one line saying what every e-mail says, and the editor
   under it with the first e-mail's preview. */
/* The two parts as they stand: a small label over each, the words as the
   e-mail has them, line breaks and all. */
.rmail-message__said {
  display: grid;
  gap: var(--space-10);
  margin: 0;
}

.rmail-message__said dt {
  font-size: var(--text-label);
  font-weight: 700;
  color: var(--text-muted);
}

.rmail-message__said dd {
  margin: var(--space-2) 0 0;
  white-space: pre-line;
  overflow-wrap: anywhere;
}

.rmail-editor {
  margin-top: var(--space-16);
  max-width: var(--measure);
}

.rmail-editor textarea {
  width: 100%;
}

/* A refusal is said once, as an error beside the button (agent/ui-standards.md):
   drawn as nothing until it has words, and always in the page so it is read out. */
.rmail-editor__error:empty {
  display: none;
}

.rmail-editor__error:not(:empty) {
  background: var(--tint-danger);
  border: 1px solid var(--tint-danger-line);
  border-radius: var(--radius-sm);
  padding: var(--space-8) var(--space-12);
  color: var(--on-tint-danger);
  font-size: var(--text-sm);
}

.rmail-preview__title {
  font-size: var(--text-base);
  margin: var(--space-20) 0 var(--space-8);
}

.rmail-preview {
  background: var(--surface-sunken);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: var(--space-12) var(--space-14);
}

.rmail-preview__head {
  margin: 0 0 var(--space-4);
  font-size: var(--text-sm);
  overflow-wrap: anywhere;
}

.rmail-preview__label {
  display: inline-block;
  min-width: 8ch;
  color: var(--text-muted);
  font-weight: 700;
}

/* The body as Mail will show it: its own line breaks, wrapped at the card's
   edge, in the page's face rather than a typewriter's. */
.rmail-preview__body {
  margin: var(--space-12) 0 0;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  line-height: var(--leading-base);
}

/* The two lists, as a row of two buttons: the one shown solid. */
.rmail-filter {
  margin: 0 0 var(--space-12);
}

.rmail-filter .btn {
  white-space: normal;
}

/* Mark all over "Everyone who raced" while no dog that raced is marked
   asked (in the empty first list's card instead, as its main button): set
   apart from the two lists above by a wider gap and its line of words. */
.rmail-markall {
  margin: var(--space-20) 0;
}

.rmail-markall .field__hint {
  margin: 0 0 var(--space-8);
}

.rmail-markall .btn {
  white-space: normal;
}

.rmail-progress {
  margin: 0 0 var(--space-12);
  color: var(--text-muted);
  font-size: var(--text-sm);
}

.rmail-list {
  list-style: none;
  margin: 0 0 var(--space-20);
  padding: 0;
  display: grid;
  gap: var(--space-12);
}

.rmail {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  grid-template-areas: "dogs who state" "actions actions actions";
  align-items: center;
  gap: var(--space-12) var(--space-14);
  padding: var(--space-14) var(--space-16);
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
}

.rmail.is-sent {
  background: var(--surface);
}

/* A row with something to read before tapping (results-email.js rowNotes, or
   written before a change): the notes get a line of their own across the
   card. Only then, so a row without them has no empty band. */
.rmail.has-notes {
  grid-template-areas: "dogs who state" "notes notes notes" "actions actions actions";
}

/* The next row to write, once one has been: its edge in the accent. */
.rmail.is-next {
  border-color: var(--accent);
  box-shadow: var(--focus-ring);
}

/* Two pictures side by side at most, then the next line: a family of three
   dogs stays two pictures wide. */
.rmail__dogs {
  grid-area: dogs;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  max-width: calc(var(--touch-min) * 2 + var(--space-4));
}

.rmail__who {
  grid-area: who;
  min-width: 0;
}

.rmail__people {
  font-size: var(--text-sm);
  overflow-wrap: anywhere;
}

.rmail__to {
  font-size: var(--text-sm);
  color: var(--text-muted);
  overflow-wrap: anywhere;
}

/* The row's pill, and under it, on a sent row whose email went without its
   keepsake card, the "Sent without the card" tag (1.11.0, RUNTIME-6): each
   at the right edge of the column, so a row with no tag is drawn as before. */
.rmail__state {
  grid-area: state;
  align-self: start;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--space-4);
}

.rmail__state .pill {
  white-space: nowrap;
}

.pill--warn {
  background: var(--tint-gold);
  color: var(--on-tint-gold);
}

.rmail__notes {
  grid-area: notes;
  margin: 0;
  padding: var(--space-8) var(--space-12) var(--space-8) var(--space-28);
  background: var(--tint-gold);
  border: 1px solid var(--tint-gold-line);
  border-radius: var(--radius-sm);
  color: var(--on-tint-gold);
  font-size: var(--text-sm);
  overflow-wrap: anywhere;
}

.rmail__notes li + li {
  margin-top: var(--space-4);
}

.rmail__actions {
  grid-area: actions;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-10) var(--space-16);
}

/* Its words may run long while a Send waits its turn ("Waiting its turn: 2
   ahead", "Making the cards: 1 of 3…"): they wrap inside the button rather
   than push it past the card on a phone. */
.rmail__write {
  font-size: var(--text-lg);
  min-height: var(--touch-row);
  padding-inline: var(--space-24);
  white-space: normal;
}

/* Send again (1.12.0), on a sent row: a ghost button, as the row's work is
   done; its words wrap as Send's do while its cards wait their turn. */
.rmail__again {
  white-space: normal;
}

.rmail__asked {
  margin-left: auto;
}

/* A phone: the pill under the names, and Write email the row's width, so
   the thumb finds it in the same place on every row. */
@media (max-width: 640px) {
  .rmail {
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-areas: "dogs who" "state state" "actions actions";
    padding: var(--space-12);
  }
  .rmail.has-notes {
    grid-template-areas: "dogs who" "state state" "notes notes" "actions actions";
  }
  .rmail__write {
    flex: 1 1 100%;
  }
  .rmail__asked {
    margin-left: 0;
  }
  /* Across the card under the names: the tag beside the pill, or under it
     when the two do not fit one line. */
  .rmail__state {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
  }
}
.rmail-empty p {
  margin: 0;
}

.rmail-empty .rmail-empty__action {
  margin: var(--space-12) 0;
}

.rmail-empty .rmail-empty__action .btn {
  white-space: normal;
}

/* 1.11.0, the app sends (src/mail.js): the line under the heading saying
   how mail goes out on this app - test mode's one inbox, production's
   noreply@ address - in the calm blue of information, and in the danger
   tint when the app is on but cannot send. */
.rmail-mode {
  margin: 0 0 var(--space-16);
  padding: var(--space-10) var(--space-14);
  background: var(--tint-blue);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  overflow-wrap: anywhere;
}

.rmail-mode--broken {
  background: var(--tint-danger);
  border-color: var(--tint-danger-line);
  color: var(--on-tint-danger);
}

/* A Send that failed: why, across the row above its buttons, said once
   (role="alert"); Try again and the fallback follow it. */
.rmail__failed {
  flex: 1 1 100%;
  margin: 0;
  padding: var(--space-8) var(--space-12);
  background: var(--tint-danger);
  border: 1px solid var(--tint-danger-line);
  border-radius: var(--radius-sm);
  color: var(--on-tint-danger);
  font-size: var(--text-sm);
  overflow-wrap: anywhere;
}

/* Microsoft's own code and message, for Ryan, under the plain words. */
.rmail__failed-detail,
.test-email__detail {
  display: block;
  margin-top: var(--space-4);
  font-size: var(--text-sm);
}

/* A Send that may have gone (a time-out, or an answer that did not come
   back): a warning in the gold tint, not a failure - the row is marked
   sent, and the line says how to check before sending it again. */
.rmail__failed.is-unsure {
  background: var(--tint-gold);
  border-color: var(--tint-gold-line);
  color: var(--on-tint-gold);
}

/* The row's pill while its Send has failed: "Send failed". */
.pill--bad {
  background: var(--tint-danger);
  color: var(--on-tint-danger);
}

/* The fallback, Write email through this device's own Mail: a ghost button
   whose second line says whose account it sends from. */
.rmail__fallback {
  white-space: normal;
  text-align: left;
}

.rmail__fallback-note {
  font-weight: 400;
}

@media (max-width: 640px) {
  .rmail__fallback {
    flex: 1 1 100%;
  }
}
/* Send log (1.12.0): Results emails' link to it, under the line saying how
   mail goes out, for the admins. */
.rmail-log-link {
  margin: 0 0 var(--space-16);
}

/* The send log (1.12.0, views/results-emails-log.ejs): one card an email,
   newest first - its pill, when and its tags on one line, then a short list
   of label and value, the value wrapping (an address breaks anywhere), and
   why it did not go in the danger tint (the gold one for "may have gone").
   Drawn once, with no script: nothing on it moves after it opens. */
.sendlog-back {
  margin: var(--space-8) 0 var(--space-16);
}

.sendlog-count {
  margin: 0 0 var(--space-12);
  color: var(--text-muted);
  font-size: var(--text-sm);
}

.sendlog-empty p {
  margin: 0;
}

.sendlog {
  list-style: none;
  margin: 0 0 var(--space-20);
  padding: 0;
  display: grid;
  gap: var(--space-12);
}

.sendlog__entry {
  padding: var(--space-12) var(--space-16);
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
}

.sendlog__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-6) var(--space-10);
}

.sendlog__head .pill {
  white-space: nowrap;
}

.sendlog__when {
  font-weight: 700;
}

.sendlog__tag {
  background: var(--tint-blue);
  color: var(--accent);
}

.sendlog__facts {
  display: grid;
  gap: var(--space-4);
  margin: var(--space-10) 0 0;
  font-size: var(--text-sm);
}

.sendlog__facts > div {
  display: grid;
  grid-template-columns: var(--sendlog-label) minmax(0, 1fr);
  gap: var(--space-10);
}

.sendlog__facts dt {
  color: var(--text-muted);
}

.sendlog__facts dd {
  margin: 0;
  overflow-wrap: anywhere;
}

.sendlog__reason {
  margin: var(--space-10) 0 0;
  padding: var(--space-8) var(--space-12);
  background: var(--tint-danger);
  border: 1px solid var(--tint-danger-line);
  border-radius: var(--radius-sm);
  color: var(--on-tint-danger);
  font-size: var(--text-sm);
  overflow-wrap: anywhere;
}

.sendlog__reason--warn {
  background: var(--tint-gold);
  border-color: var(--tint-gold-line);
  color: var(--on-tint-gold);
}

.sendlog__note {
  margin: var(--space-8) 0 0;
}

.sendlog-pages {
  margin: 0 0 var(--space-20);
}

/* Event settings' Test e-mail card (1.11.0): the address and the button on
   one line where they fit, and the answer under them - sent in the hops
   tint, not sent in the danger tint - drawn as nothing until there is one. */
.test-email__form {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--space-10) var(--space-12);
  max-width: var(--measure);
}

.test-email__form .field {
  flex: 1 1 16rem;
  margin: 0;
}

.test-email__answer:empty {
  display: none;
}

.test-email__answer {
  margin: var(--space-12) 0 0;
  padding: var(--space-8) var(--space-12);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  overflow-wrap: anywhere;
  max-width: var(--measure);
}

.test-email__answer.is-sent {
  background: var(--tint-hops);
  border: 1px solid var(--tint-hops-line);
  color: var(--on-tint-hops);
}

.test-email__answer.is-not-sent {
  background: var(--tint-danger);
  border: 1px solid var(--tint-danger-line);
  color: var(--on-tint-danger);
}

/* Event settings' Card PDFs check (1.11.0): the verdict, then each link it
   tried, in the Test e-mail answer's box. */
.card-pdf__verdict {
  margin: 0;
  font-weight: 600;
}

.card-pdf__steps {
  margin: var(--space-6) 0 0;
  padding-left: var(--space-20);
}

/* Check every card (1.11.0): what the check found, in the calm blue of
   information, and each card that could not be made in a list in the
   danger tint under it, with the card maker's own words for Ryan. */
.rmail-check .field__hint {
  margin: 0 0 var(--space-12);
  max-width: var(--measure);
}

.rmail-check__said {
  margin: 0 0 var(--space-12);
  padding: var(--space-8) var(--space-12);
  background: var(--tint-blue);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  overflow-wrap: anywhere;
  max-width: var(--measure);
}

.rmail-check__failed {
  margin: 0 0 var(--space-12);
  padding: var(--space-8) var(--space-12) var(--space-8) var(--space-28);
  background: var(--tint-danger);
  border: 1px solid var(--tint-danger-line);
  border-radius: var(--radius-sm);
  color: var(--on-tint-danger);
  font-size: var(--text-sm);
  overflow-wrap: anywhere;
  max-width: var(--measure);
}

.rmail-check__failed li + li {
  margin-top: var(--space-4);
}

.rmail-check__detail {
  display: block;
  margin-top: var(--space-2);
}

/* The dogs that raced with no address a mail app can use. */
.rmail-none__list {
  margin: var(--space-12) 0 0;
  padding-left: var(--space-20);
}

.rmail-none__list li {
  margin: 0 0 var(--space-4);
  overflow-wrap: anywhere;
}

.rmail-none__dog {
  display: inline-flex;
  align-items: center;
  min-height: var(--touch-min);
  min-width: var(--touch-min);
  font-weight: 700;
}

.rmail-none__people {
  color: var(--text-muted);
  font-size: var(--text-sm);
}

/* Results by email on a dog's page: its switch, and a line saying whether
   the e-mail has been written, with the way to the page. */
.dog-results {
  margin-top: var(--space-16);
}

.dog-results .toggle {
  padding-block: 0;
}

.dog-results__state {
  margin: var(--space-4) 0 0;
}

/* ------------------------------------------ the start needs help (draft)
   A DRAFT for next year (src/lineup-help.js; Ryan, race night 2026-09-26:
   "I spent a lot of time bouncing between start and finish"). When the start
   taps Need help on the Lineup, every staff screen that polls shows this bar
   across the top, OVER the top bar - fixed, so it never pushes anything
   down, and a pick never moves a button (test/race-control-no-grow.test.js's
   rule) - until someone taps Sorted here or at the start. Solid --danger, no
   pulse and no sound: the clock ticking is the "still waiting" signal.
   Sorted stays at the right edge in every look, so the bar never moves the
   button a thumb is going for; On my way sits beside it until someone taps
   it. On a phone the words take a row of their own over the buttons. The
   bar is as tall in every look (the buttons' row keeps its height when they
   go), so nothing in it moves when it changes.
   6px above and below the buttons keep it 56px, inside a 58px top bar, so
   on a desktop or a sideways iPad the bar arrives over the top bar alone
   and nothing pinned under that moves. Where it is the taller of the two
   (a phone: 44px top bar, the bar's words on a row of their own), staff.js
   measures it as the top of the page while it is up (measureChrome), so
   Race Control's pinned heat and the round headings stick under it rather
   than behind it. */
.help-bar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: var(--z-help);
  display: flex;
  align-items: center;
  gap: var(--space-8) var(--space-12);
  padding: max(var(--space-6), env(safe-area-inset-top)) max(var(--space-16), env(safe-area-inset-right)) var(--space-6) max(var(--space-16), env(safe-area-inset-left));
  background: var(--danger);
  color: var(--text-on-danger);
  box-shadow: var(--shadow-lg);
}

/* "Sorted at the start": the start said so itself, for a few seconds. */
.help-bar.is-sorted {
  background: var(--success);
  color: var(--text-on-success);
}

.help-bar__mark {
  flex: none;
  width: var(--space-24);
  height: var(--space-24);
}

.help-bar__words {
  flex: 1 1 auto;
  margin: 0;
  font-size: var(--text-base);
  font-weight: 600;
  line-height: var(--leading-tight);
}

.help-bar__title {
  font-weight: 800;
}

.help-bar__time {
  font-variant-numeric: tabular-nums;
}

.help-bar__buttons {
  flex: none;
  display: flex;
  justify-content: flex-end;
  gap: var(--space-8);
  min-height: var(--touch-min);
}

/* The buttons read against the bar in light and dark alike: the bar's own
   text colour as their fill, the bar's colour as their words. */
.help-bar .help-bar__btn {
  min-height: var(--touch-min);
  min-width: var(--touch-min);
  background: var(--text-on-danger);
  border-color: var(--text-on-danger);
  color: var(--danger);
}

.help-bar .help-bar__btn:active {
  opacity: 0.85;
}

.help-bar .help-bar__btn[hidden] {
  display: none;
}

@media (max-width: 640px) {
  .help-bar {
    flex-wrap: wrap;
  }
  .help-bar__words {
    flex: 1 1 0;
    font-size: var(--text-sm);
  }
  .help-bar__buttons {
    flex: 1 1 100%;
  }
}
/* ------------------------------------------ in the box (draft)
   A DRAFT for next year (src/lineup-marks.js; Ryan, 2026-09-27: "I would
   rather lineup tap the lane box when they have the dog in the box"). Once
   the start has marked the first dog of the heat at the line in its box,
   Race Control's line under the buttons - "Tap the dog that won, then Record
   Wiener." - says "3 of 5 in the boxes · lanes 2 and 5 still empty" instead,
   and each lane number is red or green (below). Words that change
   in a line that is already there, and a colour: a mark never moves a button
   (test/race-control-no-grow.test.js's rule). The line holds, unseen, every
   wording it can take for this heat in the same grid cell
   (.current-marks__room), so it is always as tall as the longest and never
   grows or shrinks when a mark lands. */
.current-marks {
  display: grid;
}

.current-marks > span {
  grid-area: 1/1;
}

.current-marks__room {
  visibility: hidden;
}

.current-marks--some .current-marks__words {
  color: var(--text);
}

/* Red and green lane numbers (1.12.0; Ryan, 2026-09-30, FB-20260930-RY-05:
   "Can we make a dog lane that isn't checked in red not blue then it goes
   green when the dog is in? Also, carry those markers down into the heat
   cards on both /race and /bracket?"). In the heat at the line, while the
   Lineup's tap marks dogs ready (Staff.boxState), a lane number is --danger
   until its dog is in its box (.lane__n--out) and --success with a check
   once it is (.lane__n--in): in Race Control's box and slim bar, on that
   heat's card on Race Control and on Bracket Builder, and in the megaphone's
   Missing at the line. Every other heat, Up next included, and every heat
   while the tap does something else, keeps the blue it always had.
   The two fills are nearly one colour to a red-blind eye (1.34:1 light,
   1.09:1 dark), so the check is what says "in": --text-on-success on
   --success, a disc on the badge's lower right corner, ringed in the colour
   of what the badge sits on (--lane-check-ring) so it reads apart from the
   badge. Its size and its place are shares of the badge, so it keeps off
   the digit on every badge - 30px on a card, 26px in a phone's box, 24px in
   the slim bar's chips: it stands further past the bottom edge than the
   side, where the digit's corner is, and its ring starts below the digit.
   The badge changes only its colours and the check is
   absolutely placed, in the badge from the first draw and unseen while the
   dog is out: nothing grows or moves when a mark lands or comes off (the
   no-grow rule). Numerals: white on red 5.3:1, dark on the dark theme's red
   6.3:1, white on green 7.1:1, dark on the dark theme's green. */
.lane__n--out {
  position: relative;
  background: var(--danger);
  color: var(--text-on-danger);
}

.lane__n--in {
  position: relative;
  background: var(--success);
  color: var(--text-on-success);
}

.lane__n .lane__check {
  position: absolute;
  right: -15%;
  bottom: -22%;
  width: 46%;
  height: 46%;
  border-radius: var(--radius-pill);
  background: var(--success);
  color: var(--text-on-success);
  stroke-width: 3.2;
  box-shadow: 0 0 0 var(--space-2) var(--lane-check-ring, var(--surface-raised));
  visibility: hidden;
}

.lane__n--in .lane__check {
  visibility: visible;
}

/* The ring is the colour of what the badge sits on: a raised card unless
   one of these says otherwise. */
.current-bar__dog {
  --lane-check-ring: var(--surface);
}

.heat--final, .current-lane.is-picked, .current-bar__dog.is-picked {
  --lane-check-ring: var(--tint-gold);
}

/* The same count where the heat is pinned (Ryan, 2026-09-29,
   FB-20260929-RY-27: "Add that to the sticky part."), never making it
   taller. Above 640px, in the pinned box's heading row once it is past the
   controls - where the line above steps aside - as "3 of 5 in the boxes":
   it takes only the room left on the heading's line (a flex basis of 0, so
   it can never push the row onto a second line), set against the right
   beside "At the line", and cut with an ellipsis where that room is short;
   the cards under it still show which lanes are in, in green. Not seen
   anywhere else: in the box unpinned the line under the buttons says it
   all, and on a phone the box is not pinned. */
.current-count {
  display: none;
}

.current-panel.is-past-controls .current-count:not([hidden]) {
  display: block;
  flex: 1 1 0;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  text-align: right;
  font-size: var(--text-sm);
  font-weight: 700;
  color: var(--text);
}

/* On a phone, in the slim bar: "3 of 5 in" as a third line under the
   heat's short name, holding the room of its widest words unseen from the
   start, so the name above it never moves when the first dog is marked -
   three lines of --text-micro are still shorter than the dogs' 44px chips
   beside them, so the bar keeps its height - and each chip's lane number is
   red or green, as in the full box (above). */
.current-bar__count {
  display: grid;
  color: var(--text);
}

.current-bar__count > span {
  grid-area: 1/1;
}

.current-bar__count-room {
  visibility: hidden;
}

/* The megaphone's "Missing at the line" (public/js/announce.js laneButton):
   the same red and green lane numbers - unless the lane is called, which
   keeps the call's gold (_announcer.scss) and shows no check - and "Not in
   the box yet" under the trainers of each lane whose dog is not in, in
   --danger ink as a warning word on the button's own surface (5.3:1 light,
   5.6:1 dark). A dog marked in while the sheet is open hides the words and
   keeps their line (visibility, never display), so no lane changes size
   under a finger. */
.announce-call.is-called .lane__check {
  visibility: hidden;
}

.placement-choice .announce-call__box {
  font-weight: 800;
  color: var(--danger);
}

.announce-call__box.is-hidden {
  visibility: hidden;
}

/* Event settings: "Tap a dog to see its picture bigger" while the marks have
   the lanes - greyed and not-allowed, as a disabled button is, with the line
   under it saying why (settings.js laneMarksSwitch). */
.toggle--taken {
  cursor: not-allowed;
  color: var(--text-muted);
}

.toggle--taken .toggle__box {
  background: var(--surface-sunken);
  box-shadow: inset 0 0 0 1px var(--border);
}

/* The heat at the line's "At the line" pill once every dog of it is in its
   box (Ryan, 2026-10-02: "turn green when all dogs are lined up in their
   lanes. Similar to how the lane numbers switch from red to green"):
   --success instead of --danger, the same words, and the lane numbers'
   check on its top right corner, so it reads to a red-blind eye too.
   The check is in the pill from the first draw and unseen while red, and
   it is absolutely placed, so the pill keeps its size and nothing in the
   card's heading moves (Staff.dressLinePill). On Race Control's box and the
   heat cards on Race Control and Bracket Builder. */
.pill--live {
  position: relative;
}

.pill--live.pill--in {
  background: var(--success);
  color: var(--text-on-success);
}

.pill .pill__check {
  position: absolute;
  top: -0.5em;
  right: -0.45em;
  width: 1.2em;
  height: 1.2em;
  border-radius: var(--radius-pill);
  background: var(--success);
  color: var(--text-on-success);
  stroke-width: 3.2;
  box-shadow: 0 0 0 var(--space-2) var(--lane-check-ring, var(--surface-raised));
  visibility: hidden;
}

.pill--in .pill__check {
  visibility: visible;
}

/* The question before a result when the Lineup never marked some lanes
   (1.12.0, FB-20260929-RY-29; race.js askNeverMarked): each such dog as a
   row - its red lane number, picture, name over trainers - and, at its end,
   "Raced" until it is tapped, then "Didn't race" on the gold a picked card
   wears. Both words sit in one cell, the one not in force unseen
   (visibility, never display), so a tap changes no row's width or height
   and moves no button. */
.unready-lane .lane__n, .unready-lane .dogphoto {
  flex: none;
}

.unready-lane .placement-choice__who {
  flex: 1 1 auto;
}

.unready-lane__said {
  display: grid;
  flex: none;
  justify-items: end;
}

.unready-lane__said > span {
  grid-area: 1/1;
}

.placement-choice .unready-lane__raced {
  color: var(--text-muted);
}

.placement-choice .unready-lane__didnt {
  font-weight: 800;
  color: var(--on-tint-gold);
  visibility: hidden;
}

.unready-lane[aria-pressed=true] .unready-lane__raced {
  visibility: hidden;
}

.unready-lane[aria-pressed=true] .unready-lane__didnt {
  visibility: visible;
}

/* A move waiting for the Lineup (1.12.0, FB-20260929-RY-30; Staff.waitingNotes):
   under Race Control's heat at the top and on Bracket Builder's card of the
   heat at the line, one note per move - the move icon and "Pretzel moves to
   Heat 7 once the Lineup takes it out of its box." - with Cancel the move
   and Move now under it; a dropped move's line is a --danger note with its
   OK. Drawn only while there is one: a move waiting is something new to
   read, not a mark landing, so it may take its own room. */
.pending-moves {
  display: grid;
  gap: var(--space-8);
  margin-top: var(--space-10);
}

.pending-move {
  display: grid;
  gap: var(--space-8);
  margin: 0;
}

.pending-move__words {
  display: flex;
  align-items: flex-start;
  gap: var(--space-8);
  margin: 0;
  overflow-wrap: break-word;
}

.pending-move__icon {
  flex: none;
  margin-top: var(--space-2);
}

.pending-move__actions {
  margin: 0;
}

.heat .pending-moves {
  margin-top: var(--space-8);
}

/* ---------------------------------------------------- what's new checks */
/* What's new's checks (1.11.0, views/whats-new.ejs, public/js/whats-new.js):
   each logged-in account's own "Checked, works" or "Needs work" under every
   change, a note, and the count of what is left. Built for Devin's iPad by
   touch: every control is a 44px .btn--sm, the pill over each change keeps
   one height whatever it says, and Add a note comes after the two buttons,
   so no tap ever moves the button under the finger. */
/* Your checks: the counts and the two switches share a row while there is
   room, and wrap under each other on a phone. */
.review-bar__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-12);
}

.review-bar__counts {
  min-width: 0;
}

.review-bar__counts p {
  margin: 0;
}

.review-bar__mine {
  font-weight: 700;
}

.review-bar__other {
  color: var(--text-muted);
  font-size: var(--text-sm);
  margin-top: var(--space-4);
}

.review-bar__hint {
  margin: var(--space-8) 0 0;
  max-width: var(--measure);
}

/* Only what I haven't checked and Only what needs work (1.12.0), side by
   side while there is room, one under the other when not, each the toggle's
   own 48px row. The count after Only what needs work keeps its figures one
   width, so its words do not shift as it goes from 9 to 10. */
.review-bar__filters {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0 var(--space-20);
}

.review-bar__needs-count {
  font-variant-numeric: tabular-nums;
}

/* This list's catch-up, under a hairline, at the button's own width. */
.review-bar__catch-up {
  border-top: 1px solid var(--border);
  margin-top: var(--space-14);
  padding-top: var(--space-14);
}

.review-bar__catch-up h3 {
  margin: 0 0 var(--space-4);
  font-size: var(--text-base);
}

.review-bar__catch-up .field__hint {
  margin: 0 0 var(--space-10);
  max-width: var(--measure);
}

/* The state over each change: gold until it is marked, green for Checked,
   works, red-tinted for Needs work, and gold again, with the news icon, for
   Changed since you checked (still to check) - never only the colour, as the
   words and the icon say it too. The same tints count what is left on each
   fold's row and in the footer. After .pill and its dark-mode colour. */
.review__state {
  margin: 0 0 var(--space-6);
}

/* Its icon beside its words, never on a line box of its own: the pill is the
   same height whether it says Not checked yet or carries a check. */
.review__pill {
  display: inline-flex;
  align-items: center;
  gap: var(--space-4);
}

.review__pill .icon {
  vertical-align: 0;
}

.review__pill--none,
.review__pill--changed,
.review__count--left {
  background: var(--tint-gold);
  color: var(--on-tint-gold);
}

.review__pill--verified {
  background: var(--tint-hops);
  color: var(--on-tint-hops);
}

.review__pill--needs_work,
.review__count--needs {
  background: var(--tint-danger);
  color: var(--on-tint-danger);
}

.release__group-name .review__count {
  margin-left: var(--space-6);
}

.buildstamp__left {
  margin-left: var(--space-6);
  letter-spacing: 0;
}

/* With the count, the footer's stamp is wider than a phone: "started" and its
   time wrap as one, never "UTC" alone on a line (views/partials/footer.ejs). */
.buildstamp__started {
  white-space: nowrap;
}

/* The release's fold row takes its two counts on a line of their own when
   the phone is too narrow for them beside "87 changes to look at". */
.release-list .release__more > summary .release__toggle {
  flex-wrap: wrap;
}

/* The slot the script fills: it holds the buttons' height before they are
   there, so the page does not jump as they arrive. */
.review {
  margin-top: var(--space-10);
}

.review:empty {
  min-height: var(--touch-min);
}

.review__buttons {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-10);
}

/* The lit button says which mark this change has, beside its aria-pressed:
   filled in the mark's tint and edged in its colour, the way an open ghost
   button is (DS-70), never the solid green of Add to roster. */
.review__btn--works[aria-pressed=true] {
  background: var(--tint-hops);
  color: var(--on-tint-hops);
  border-color: var(--success);
  box-shadow: inset 0 0 0 1px var(--success);
}

.review__btn--needs[aria-pressed=true] {
  background: var(--tint-danger);
  color: var(--on-tint-danger);
  border-color: var(--danger);
  box-shadow: inset 0 0 0 1px var(--danger);
}

/* Each change's two ways out (1.11.0, views/whats-new.ejs): Walk me through
   it, only where walkthroughs are on, and Copy feedback for Claude, in a row
   of their own under the checks, never beside the mark buttons, so the
   button a mark is made with never moves (a note box opening above them
   does move them down). Under a hairline, in the accent's words with no box
   around them, so they read as ways off the change and never as a third and
   fourth mark button; each is still a 44px .btn--sm, its words from the
   change's own left edge. Each keeps its words whole, wrapping inside itself
   rather than run past a phone; where several walkthroughs show a change,
   "Walk me through it:" heads their titles on a line of its own. */
.release__feedback {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0 var(--space-16);
  margin-top: var(--space-10);
  padding-top: var(--space-4);
  border-top: 1px solid var(--border);
}

.release__feedback .btn {
  max-width: 100%;
  white-space: normal;
  text-align: left;
}

.release__feedback .btn--ghost {
  background: transparent;
  border-color: transparent;
  padding-left: 0;
  padding-right: 0;
  font-weight: 600;
}

.release__feedback .btn--ghost:active {
  background: transparent;
  color: var(--accent-hover);
}

.release__walks {
  flex-basis: 100%;
  margin: var(--space-6) 0 0;
  font-size: var(--text-sm);
  font-weight: 700;
}

/* The change Back came to after Walk me through it (#change-<key>,
   public/js/whats-new-feedback.js): washed blue beside its blue edge, until
   the page is left, as a part of Event settings reached by a link is edged. */
.release__change.is-arrived {
  background: var(--tint-blue);
  border-left-width: var(--target-edge);
  border-left-color: var(--accent);
}

/* Copy all my feedback for Claude, in Your checks, under a hairline as the
   catch-up is. */
.review-bar__copy-all {
  border-top: 1px solid var(--border);
  margin-top: var(--space-14);
  padding-top: var(--space-14);
}

.review-bar__copy-all .field__hint {
  margin: var(--space-8) 0 0;
  max-width: var(--measure);
}

/* Where the browser would not copy: the block, selected, to copy by hand. */
.release__by-hand {
  margin-top: var(--space-10);
  max-width: var(--measure);
}

.release__by-hand textarea {
  width: 100%;
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  line-height: var(--leading-base);
}

/* A note a later change answered (1.12.0, public/js/whats-new.js
   paintAnswered): under the change's words, in the gold of Changed since you
   checked and edged on the left in its line colour, the answer - its
   change's words a link to it on this page, then what to look at - and the
   note it moved as You wrote, muted. Gone once the change is marked again. */
.review__answered {
  margin: 0 0 var(--space-8);
  padding: var(--space-8) var(--space-12);
  max-width: var(--measure);
  background: var(--tint-gold);
  color: var(--on-tint-gold);
  border-left: var(--target-edge) solid var(--tint-gold-line);
  border-radius: var(--radius-sm);
}

.review__answered p {
  margin: 0;
  overflow-wrap: anywhere;
}

.review__answered p + p {
  margin-top: var(--space-6);
}

.review__answered-line .icon {
  vertical-align: -0.125em;
}

.review__answered-by {
  font-weight: 700;
}

.review__answered-wrote {
  color: var(--text-muted);
  font-size: var(--text-sm);
}

.review__answered-note {
  white-space: pre-line;
}

/* Your note, and Edit note under it. */
.review__note {
  margin-top: var(--space-8);
}

.review__note-words {
  margin: 0 0 var(--space-6);
  overflow-wrap: anywhere;
}

.review__note-text {
  white-space: pre-line;
}

/* The note's box: its label, the box, how many characters, Save and Cancel. */
.review__editor {
  margin-top: var(--space-10);
  max-width: var(--measure);
}

.review__editor textarea {
  width: 100%;
}

.review__editor .field__hint {
  margin: var(--space-4) 0 var(--space-8);
}

.review__editor-buttons {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-10);
}

/* Ryan's page only: the other staff member's mark and note, under a dashed
   rule so it never reads as his own. */
.review__others {
  list-style: none;
  margin: var(--space-8) 0 0;
  padding: var(--space-8) 0 0;
  border-top: 1px dashed var(--border);
  font-size: var(--text-sm);
  display: grid;
  gap: var(--space-6);
}

.review__other-head {
  display: block;
}

.review__other-status--verified {
  color: var(--success);
  font-weight: 700;
}

.review__other-status--needs_work {
  color: var(--danger);
  font-weight: 700;
}

/* A mark made before the change's words changed: its words say so ("before
   the words changed"), and its status is muted, never read as current. */
.review__other-status--changed {
  color: var(--text-muted);
  font-weight: 700;
}

.review__other-note {
  display: block;
  white-space: pre-line;
  overflow-wrap: anywhere;
  color: var(--text-muted);
}

/* Only what I haven't checked, Only what needs work, or both (1.12.0): by
   data attributes alone (no :has), so it draws the same in every Safari.
   Only what I haven't checked keeps what is left to check - unmarked, or
   Changed since you checked - and every screen and release with some
   (data-left). Only what needs work keeps what this list marked Needs work,
   its words changed since or not (data-mine-needs), and every screen and
   release with some (data-needing). Both keep either, so only Checked, works
   goes. A change marked since a switch changed keeps data-kept, counted in
   both, and stays in view until a switch changes again. */
.release-list[data-only-unchecked]:not([data-only-needs]) .release__change[data-mine]:not([data-mine=none]):not([data-mine=changed]):not([data-kept]),
.release-list[data-only-unchecked]:not([data-only-needs]) .release__group[data-left="0"],
.release-list[data-only-unchecked]:not([data-only-needs]) .release[data-left="0"],
.release-list[data-only-needs]:not([data-only-unchecked]) .release__change[data-mine]:not([data-mine-needs]):not([data-kept]),
.release-list[data-only-needs]:not([data-only-unchecked]) .release__group[data-needing="0"],
.release-list[data-only-needs]:not([data-only-unchecked]) .release[data-needing="0"],
.release-list[data-only-unchecked][data-only-needs] .release__change[data-mine]:not([data-mine=none]):not([data-mine=changed]):not([data-mine-needs]):not([data-kept]),
.release-list[data-only-unchecked][data-only-needs] .release__group[data-left="0"][data-needing="0"],
.release-list[data-only-unchecked][data-only-needs] .release[data-left="0"][data-needing="0"] {
  display: none;
}

/* The app's phone rule (styles/_phone.scss), a phone turned sideways with it,
   so a landscape phone gets the phone's buttons and not the iPad's row. */
@media (max-width: 640px), (max-width: 899px) and (max-height: 599px) {
  /* Two equal halves on a phone: "Checked, works" may wrap inside its half
     rather than squeeze, and Add a note takes the next row. */
  .review__buttons {
    display: grid;
    grid-template-columns: 1fr 1fr;
  }
  /* Less room around the icon and the words than a .btn--sm, so "Checked,
     works" keeps one line in its half on a 393px phone; on the smallest
     phones it still wraps inside its half rather than squeeze. */
  .review__btn {
    white-space: normal;
    justify-content: flex-start;
    text-align: left;
    gap: var(--space-4);
    padding-left: var(--space-8);
    padding-right: var(--space-8);
  }
  .review__add-note {
    grid-column: 1/-1;
    justify-self: start;
  }
  .review-bar__filters,
  .review-bar__filter {
    width: 100%;
  }
  /* Full width, its words from the start beside its icon: centred, two lines
     of words left the icon alone at the far edge. */
  .review-bar__catch-up .btn {
    width: 100%;
    white-space: normal;
    justify-content: flex-start;
    text-align: left;
  }
  .review-bar__copy-all .btn {
    width: 100%;
    white-space: normal;
    justify-content: flex-start;
    text-align: left;
  }
}
/* ---------------------------------------------------------- walkthroughs */
/* Walkthroughs (1.11.0, dev and stage only; views/walkthroughs.ejs,
   public/js/walkthroughs.js): one card per walkthrough, grouped by screen,
   under a card that says who loaded what last. Tokens only, a hairline
   aside; every control is a .btn, 44px at least. */
/* Who loaded what last, and whether the event from before walkthroughs is
   kept. Put back stays in its place, greyed while nothing is kept, so the
   card is as tall with a copy as without one (the no-grow rule). */
.walk-status__line {
  margin: 0 0 var(--space-6);
  max-width: var(--measure);
}

.walk-status__saved {
  font-size: var(--text-sm);
  color: var(--text-muted);
}

.walk-status__actions {
  margin-top: var(--space-12);
}

/* A Load or a Put back refused: its words under its button, until the next
   Load or Put back - a toast goes before a reason this long is read. Drawn
   as nothing until it has words (a live region has to be there first). Put
   back done with the event's countdown stopped says so the same way, in a
   plain note (.walk-status__stopped). */
.walk-status__refused,
.walk-status__stopped {
  margin: var(--space-12) 0 0;
  max-width: var(--measure);
}

.walk__refused {
  margin: 0 0 var(--space-12);
  max-width: var(--measure);
}

.walk-status__refused:empty,
.walk-status__stopped:empty,
.walk__refused:empty {
  display: none;
}

/* Every button on the page may take two lines rather than run past the
   window: "Put back the event from before walkthroughs" and a skip-ahead
   such as "Set the countdown to 10 seconds" are wider than a phone. */
.walk-status .btn,
.walk .btn {
  max-width: 100%;
  white-space: normal;
  text-align: left;
}

/* The screens, each a jump to its group: the list is long (thirty cards). */
.walk-jump {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-8);
  margin: var(--space-16) 0 0;
}

.walk-group {
  margin-top: var(--space-28);
}

.walk-group__title {
  margin: 0 0 var(--space-12);
}

/* A walkthrough's card: its title and, on the one loaded last, a green
   Loaded tag, which takes no room while it is not shown. */
.walk__head {
  align-items: center;
  margin-bottom: var(--space-8);
}

.walk__title {
  margin: 0;
  flex: 1 1 auto;
  min-width: 0;
  font-size: var(--text-lg);
  line-height: var(--leading-tight);
}

.walk__loaded {
  display: none;
}

/* The card What's new's Walk me through it opened (#walk-<key>): edged in
   blue, as the one loaded last is edged in green, which wins where both - and
   a blue edge down its left side, the width of the edge a part of Event
   settings reached by a link wears, so it reads from arm's length. */
.walk:target {
  border-color: var(--accent);
  box-shadow: inset var(--target-edge) 0 0 var(--accent), var(--shadow-md);
}

.walk.is-loaded {
  border-color: var(--success);
}

.walk.is-loaded .walk__loaded {
  display: inline-block;
}

.walk__changed {
  margin: 0 0 var(--space-12);
  max-width: var(--measure);
}

.walk__situation,
.walk__sets {
  margin: 0 0 var(--space-8);
  max-width: var(--measure);
  font-size: var(--text-sm);
}

.walk__actions {
  margin: var(--space-12) 0;
}

/* Where Load would be, on a walkthrough with nothing to load. */
.walk__nothing {
  margin: var(--space-12) 0;
  font-size: var(--text-sm);
  color: var(--text-muted);
  max-width: var(--measure);
}

/* Drawn as nothing until it has words (a live region has to be there first). */
.walk__missed:empty {
  display: none;
}

/* The two folds - the What's new changes it shows, and its steps with the
   feedback - each behind one full-height row, as What's new folds a release:
   the chevron points right while closed and down while open. */
.walk__shows > summary,
.walk__more > summary {
  list-style: none;
  cursor: pointer;
  color: var(--accent);
  font-size: var(--text-sm);
  font-weight: 700;
  border-radius: var(--radius-sm);
}

.walk__shows > summary::-webkit-details-marker,
.walk__more > summary::-webkit-details-marker {
  display: none;
}

.walk__toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-6);
  min-height: var(--touch-min);
}

.walk__shows > summary:active,
.walk__more > summary:active {
  color: var(--accent-hover);
}

@media (hover: hover) {
  .walk__shows > summary:hover,
  .walk__more > summary:hover {
    text-decoration: underline;
  }
}
.walk__chevron {
  flex: none;
  transform: rotate(-90deg);
  transition: transform var(--dur-fast) var(--ease);
}

.walk__shows[open] > summary .walk__chevron,
.walk__more[open] > summary .walk__chevron {
  transform: none;
}

/* Out of layout while closed, as What's new's folds are. */
.walk__shows:not([open]) > .walk__covers,
.walk__more:not([open]) > .walk-steps,
.walk__more:not([open]) > .walk-feedback {
  display: none;
}

.walk__more {
  border-top: 1px solid var(--border);
  margin-top: var(--space-8);
}

.walk__covers {
  margin: var(--space-4) 0 var(--space-12);
  padding-left: var(--space-20);
  display: grid;
  gap: var(--space-8);
  max-width: var(--measure);
  font-size: var(--text-sm);
}

/* The steps, numbered, each its words and then its button: Open a screen
   in a new tab, or a skip-ahead. */
.walk-steps {
  margin: var(--space-8) 0 0;
  padding-left: var(--space-24);
  display: grid;
  gap: var(--space-14);
}

.walk-step__say {
  margin: 0;
  max-width: var(--measure);
}

.walk-step__go {
  margin-top: var(--space-8);
}

/* Under a skip-ahead greyed on a walkthrough not loaded last. */
.walk-step__wait {
  margin: var(--space-4) 0 0;
}

/* A step's dog page with no such dog yet (views/walkthrough-dog.ejs): what to
   do first, then Race Control and Try again. */
.walk-dog__words {
  margin: 0 0 var(--space-12);
  max-width: var(--measure);
}

.walk-dog .btn {
  max-width: 100%;
  white-space: normal;
  text-align: left;
}

/* How did it go: Works or Needs work (the one chosen washed blue, as the
   Break card's chosen timer is), the note, Copy feedback for Claude, and
   the block it copies, always there to read. */
.walk-feedback {
  margin-top: var(--space-20);
  padding-top: var(--space-14);
  border-top: 1px solid var(--border);
  max-width: var(--measure);
}

.walk-feedback__title {
  margin: 0 0 var(--space-8);
  font-size: var(--text-base);
}

.walk-feedback__result {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-8);
  margin: 0 0 var(--space-14);
}

.walk-feedback__result .btn[aria-pressed=true] {
  background: var(--tint-blue);
  color: var(--accent);
  border-color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent);
}

.walk-feedback__copied {
  margin: var(--space-8) 0 0;
  font-size: var(--text-sm);
  font-weight: 600;
}

.walk-feedback__copied:empty {
  display: none;
}

.walk-feedback__block {
  margin-top: var(--space-12);
}

.walk-feedback__block textarea {
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  line-height: var(--leading-base);
}

/* ------------------------------------------- export and import (1.11.1) */
/* Export and import (views/database.ejs, public/js/database.js): two cards
   of the page's own, built from the shared parts - .card, .note, .btnrow,
   .field, .tablewrap - and these few rules for what only this page has. */
/* A file name or a switch's name inside a sentence, set apart as text to
   type or look for. */
.database-name {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  overflow-wrap: anywhere;
}

.database-hint {
  margin: var(--space-10) 0 0;
}

.database-steps {
  margin: 0 0 var(--space-16);
  padding-left: var(--space-24);
}

.database-steps li {
  margin-bottom: var(--space-6);
}

.database-form {
  margin-bottom: var(--space-4);
}

.database-preview,
.database-progress,
.database-done {
  margin-top: var(--space-20);
  padding-top: var(--space-16);
  border-top: 1px solid var(--border);
}

.database-preview h3,
.database-progress h3,
.database-done h3 {
  font-size: var(--text-base);
  margin: 0 0 var(--space-12);
}

/* Where the file came from, its version, its time and its rows: each a
   label over its value, as many to a row as fit, never cut. */
.database-facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(14ch, 1fr));
  gap: var(--space-12) var(--space-20);
  margin: 0 0 var(--space-16);
}

.database-facts dt {
  font-size: var(--text-label);
  font-weight: 700;
  color: var(--text-muted);
}

.database-facts dd {
  margin: var(--space-2) 0 0;
  font-weight: 700;
  overflow-wrap: anywhere;
}

/* Each table, its rows in the file and here now: the names may wrap, the
   numbers line up on the right. */
.database-tables {
  margin-bottom: var(--space-16);
  font-size: var(--text-sm);
}

/* The two count headings may take two lines on a phone, so the names have
   the room; a name breaks after an underscore (<wbr>), and inside a word
   only when one part alone is wider than its column. */
.database-tables th {
  white-space: normal;
}

.database-tables td {
  padding-block: var(--space-6);
}

.database-tables__name {
  overflow-wrap: anywhere;
}

.database-tables__n {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.database-tables td.database-tables__n {
  white-space: nowrap;
}

.database-tables tr.is-changed td {
  background: var(--tint-gold);
}

.database-progress__words {
  font-weight: 700;
  margin: 0 0 var(--space-8);
}

/* What the page is doing keeps its room while it is empty, so words
   arriving move nothing under it (the no-grow rule); a refusal is said once,
   in the danger note, drawn as nothing until it has words. */
.database-status {
  min-height: calc(1em * var(--leading-base));
  margin: var(--space-12) 0 0;
  font-size: var(--text-caption);
  font-weight: 700;
  color: var(--text-muted);
}

.note.database-error {
  margin: var(--space-14) 0 0;
  white-space: pre-line;
}

.note.database-error:empty {
  margin: 0;
  padding: 0;
  border: 0;
}

.database-put-back {
  margin-top: var(--space-12);
}

/* Staff logins (1.12.0, views/logins.ejs, public/js/logins.js) and the
   Welcome page (views/welcome.ejs). Tokens only. */
.logins-add {
  margin: var(--space-12) 0 var(--space-20);
}

.logins {
  display: grid;
  gap: var(--space-20);
}

.logins-group {
  margin: 0;
}

.logins-group__title {
  margin: 0;
}

.logins-group__line {
  margin: var(--space-4) 0 var(--space-12);
  color: var(--text-muted);
  font-size: var(--text-sm);
}

.logins-none {
  margin: 0;
  color: var(--text-muted);
}

.logins-list {
  list-style: none;
  margin: 0;
  padding: 0;
  border-bottom: 1px solid var(--border);
}

.logins-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-8) var(--space-14);
  min-height: var(--touch-row);
  padding: var(--space-10) 0;
  border-top: 1px solid var(--border);
}

.logins-row__text {
  flex: 1 1 16rem;
  min-width: 0;
}

.logins-row__who {
  display: flex;
  align-items: center;
  gap: var(--space-8);
  min-width: 0;
  font-weight: 700;
}

.logins-row__who .person-badge {
  flex: none;
}

/* A long name is cut, as on every private screen: one line, an ellipsis. */
.logins-row__name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.logins-row__you {
  flex: none;
  font-weight: 400;
  color: var(--text-muted);
}

.logins-row__line {
  margin: var(--space-2) 0 0;
  font-size: var(--text-sm);
  color: var(--text-muted);
  overflow-wrap: anywhere;
}

.logins-row__line--waiting {
  color: var(--text);
}

.logins-row__acts {
  flex: 0 1 auto;
  min-width: 0;
  margin: 0;
}

/* Switched off: the app's disclosure, a 44px summary, closed by default. */
.logins-off__summary {
  display: flex;
  align-items: center;
  min-height: var(--touch-min);
  font-size: var(--text-lg);
  font-weight: 700;
  cursor: pointer;
}

/* The Add and Change dialog's level list keeps its one line under each choice. */
.choice-list__words {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.logins-consequence:empty,
.logins-refusal:empty {
  display: none;
}

/* The one-time password dialog: large, monospace, selectable. */
.logins-password__pair {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  gap: var(--space-8) var(--space-14);
  align-items: baseline;
  margin: var(--space-14) 0;
}

.logins-password__pair dt {
  font-weight: 700;
}

.logins-password__pair dd {
  margin: 0;
  min-width: 0;
}

.logins-password__value {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: var(--text-xl);
  font-weight: 700;
  overflow-wrap: anywhere;
  user-select: all;
  -webkit-user-select: all;
}

/* The Welcome page: the login card, its lead, and Log out under it. */
.welcome__lead {
  margin-bottom: var(--space-14);
}

.welcome__error:empty {
  display: none;
}

.welcome__out {
  display: flex;
  justify-content: center;
  margin: var(--space-14) 0 0;
}

/* The race days a person works (2.0.0, S13): its line on their row, and the
   ticks in Add a login and Race days. */
.logins-row__days {
  color: var(--text);
}

.logins-days {
  margin-top: var(--space-8);
}

/* -------------------------------------------------------------- race days */
/* Race days, a race day and Set up a race (2.0.0, slice S10;
   views/race-days.ejs, race-day.ejs and race-setup.ejs). Phone first: one
   column under 640px, the cards side by side only where they fit at their
   own width, every control 44px, and a long name wraps rather than
   pushing the page sideways. A race's colour is only ever its --race-*
   token (styles/_tokens.scss). */
.race-days__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-10) var(--space-16);
}

.race-days__head h1 {
  margin: 0;
  min-width: 0;
  overflow-wrap: anywhere;
}

.race-days__head + .sub {
  margin-top: var(--space-8);
}

.race-days__group {
  margin-bottom: var(--space-28);
}

.race-days__heading {
  display: flex;
  align-items: center;
  gap: var(--space-8);
  margin: 0 0 var(--space-12);
  font-size: var(--text-lg);
}

.race-days__count {
  padding: var(--space-2) var(--space-8);
  border-radius: var(--radius-pill);
  background: var(--surface-sunken);
  color: var(--text-muted);
  font-size: var(--text-caption);
  font-weight: 700;
}

.race-days__empty {
  margin: 0 0 var(--space-12);
  color: var(--text-muted);
}

/* The way back up: the race day from Set up a race, the list from a race day. */
.back-link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-4);
  min-height: var(--touch-min);
  padding-right: var(--space-8);
  color: var(--accent);
  font-weight: 700;
  text-decoration: none;
  overflow-wrap: anywhere;
}

.back-link__icon {
  flex: none;
  transform: rotate(90deg);
}

/* A status, beside a race day's name. */
.day-pill {
  display: inline-flex;
  align-items: center;
  padding: var(--space-2) var(--space-10);
  border: 1px solid transparent;
  border-radius: var(--radius-pill);
  font-size: var(--text-caption);
  font-weight: 800;
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  white-space: nowrap;
}

.day-pill--upcoming {
  background: var(--tint-blue);
  border-color: var(--border-strong);
  color: var(--accent);
}

.day-pill--open {
  background: var(--success);
  color: var(--text-on-success);
}

.day-pill--finished {
  background: var(--surface-sunken);
  border-color: var(--border);
  color: var(--text-muted);
}

.day-pill--main {
  background: var(--tint-gold);
  border-color: var(--tint-gold-line);
  color: var(--on-tint-gold);
}

/* The list: each race day a card that opens its page. */
.day-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 340px), 1fr));
  gap: var(--space-12);
  margin: 0;
  padding: 0;
  list-style: none;
}

.day-card {
  position: relative;
  display: grid;
  gap: var(--space-6);
  min-height: var(--touch-min);
  padding: var(--space-16) var(--touch-min) var(--space-16) var(--space-16);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface-raised);
  box-shadow: var(--shadow-sm);
  color: var(--text);
  text-decoration: none;
}

.day-card:active {
  background: var(--tint-blue);
}

@media (hover: hover) {
  .day-card:hover {
    border-color: var(--accent);
  }
}
.day-card__top {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-6) var(--space-10);
}

.day-card__name {
  min-width: 0;
  font-size: var(--text-base);
  font-weight: 800;
  overflow-wrap: anywhere;
}

.day-card__where,
.day-card__count {
  color: var(--text-muted);
  font-size: var(--text-sm);
}

.day-card__races {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-6) var(--space-14);
}

.day-card__go {
  position: absolute;
  top: 50%;
  right: var(--space-14);
  color: var(--text-muted);
  transform: translateY(-50%) rotate(-90deg);
}

.race-tag {
  display: inline-flex;
  align-items: center;
  gap: var(--space-6);
  min-width: 0;
  font-size: var(--text-sm);
  font-weight: 600;
  overflow-wrap: anywhere;
}

/* A form's refusal, said once (agent/ui-standards.md): drawn as nothing
   until it has words. */
.note.form-error {
  margin: var(--space-12) 0 0;
}

.note.form-error:empty {
  margin: 0;
  padding: 0;
  border: 0;
}

.field__optional {
  color: var(--text-muted);
  font-weight: 400;
}

/* A race day's details, new or changed. */
.day-form h2 {
  margin-top: 0;
}

.day-form__fields {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr));
  gap: 0 var(--space-16);
  max-width: var(--measure);
}

.day-form__name {
  grid-column: 1/-1;
}

.day-form__date {
  width: 100%;
  min-height: var(--touch-base);
  padding: var(--space-10) var(--space-14);
  border: 1px solid var(--border-field);
  border-radius: var(--radius-md);
  background: var(--surface-raised);
  color: var(--text);
  font: inherit;
}

/* Open race day and Finish race day, and taking a race day away. */
.day-run h2,
.day-remove h2 {
  margin-top: 0;
}

.day-run__words {
  max-width: var(--measure);
  margin: 0 0 var(--space-12);
}

.day-kept {
  margin-bottom: var(--space-20);
}

/* A race day's races: each a card, its colour on its edge. */
.race-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 320px), 1fr));
  gap: var(--space-14);
}

.race-card {
  min-width: 0;
  padding: var(--space-14) var(--space-16);
  border: 1px solid var(--border);
  border-left: var(--space-6) solid var(--race-blue);
  border-radius: var(--radius-md);
  background: var(--surface-raised);
}

.race-card--blue {
  border-left-color: var(--race-blue);
}

.race-card--orange {
  border-left-color: var(--race-orange);
}

.race-card--teal {
  border-left-color: var(--race-teal);
}

.race-card--plum {
  border-left-color: var(--race-plum);
}

.race-card--pink {
  border-left-color: var(--race-pink);
}

.race-card--green {
  border-left-color: var(--race-green);
}

.race-card__head {
  display: flex;
  align-items: center;
  gap: var(--space-8);
}

.race-card__name {
  min-width: 0;
  margin: 0;
  font-size: var(--text-lg);
  overflow-wrap: anywhere;
}

.race-card__who {
  margin: var(--space-2) 0 0;
  color: var(--text-muted);
  font-size: var(--text-sm);
  overflow-wrap: anywhere;
}

.race-card__facts {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  gap: var(--space-6) var(--space-12);
  margin: var(--space-12) 0 var(--space-14);
}

.race-card__facts > div {
  display: contents;
}

.race-card__facts dt {
  padding-top: var(--space-2);
  color: var(--text-muted);
  font-size: var(--text-caption);
  font-weight: 700;
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
}

.race-card__facts dd {
  margin: 0;
  font-size: var(--text-sm);
  overflow-wrap: anywhere;
}

.race-card__bracket--done {
  color: var(--success);
  font-weight: 700;
}

/* Two to a row on a phone (each at least two and a half touch rows wide), more where they fit. */
.race-card__links {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, var(--touch-base) * 2.5), 1fr));
  gap: var(--space-8);
}

.race-card__links .btn {
  justify-content: flex-start;
  text-align: left;
}

/* "Bracket Builder" spells itself out only where there is room, as its tab does. */
@media (max-width: 640px) {
  .race-card__rest {
    display: none;
  }
}
/* A race day's tracks. */
.track-list {
  display: grid;
  margin: 0;
  padding: 0;
  list-style: none;
}

.track-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-8) var(--space-16);
  padding: var(--space-12) 0;
  border-top: 1px solid var(--border);
}

.track-row:first-child {
  padding-top: 0;
  border-top: 0;
}

.track-row__words {
  display: grid;
  gap: var(--space-4);
  min-width: 0;
}

.track-row__name {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-4) var(--space-8);
  font-weight: 800;
  overflow-wrap: anywhere;
}

.track-row__sub {
  color: var(--text-muted);
  font-size: var(--text-sm);
  overflow-wrap: anywhere;
}

.track-form {
  margin-top: var(--space-16);
  padding-top: var(--space-16);
  border-top: 1px solid var(--border);
}

.track-form__head {
  margin: 0 0 var(--space-10);
  font-size: var(--text-base);
}

.track-form__fields {
  display: grid;
  grid-template-columns: minmax(0, 1fr) max-content;
  gap: 0 var(--space-12);
  max-width: var(--measure);
}

.track-form__fields .field {
  margin-bottom: 0;
}

.track-form__fields + .field__hint {
  margin: var(--space-6) 0 var(--space-4);
}

.track-form .toggle + .field__hint {
  margin: 0 0 var(--space-12);
}

/* Set up a race. */
.setup-form {
  max-width: var(--measure);
}

.setup-form .choice-list {
  margin-bottom: var(--space-16);
}

.setup-form .toggle + .field__hint {
  margin: 0 0 var(--space-12);
}

.setup-presets .field__hint {
  margin: var(--space-8) 0 0;
}

.setup-colours__opts {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 130px), 1fr));
}

.setup-colour {
  padding-left: var(--space-12);
}

.setup-colour__chip {
  width: var(--space-20);
  height: var(--space-20);
}

.setup-colour__words {
  display: grid;
  min-width: 0;
}

.setup-colour__taken {
  color: var(--text-muted);
  font-size: var(--text-caption);
  font-weight: 600;
}

.setup-tracks:disabled .choice-list__opt {
  cursor: default;
  color: var(--text-muted);
}

.setup-tracks__opts {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 220px), 1fr));
}

.setup-track__words {
  display: grid;
  gap: var(--space-2);
  min-width: 0;
  overflow-wrap: anywhere;
}

.setup-track__sub {
  color: var(--text-muted);
  font-size: var(--text-caption);
  font-weight: 600;
}

.setup-tracks__new {
  margin: var(--space-12) 0 0;
}

.setup-tracks .field__hint {
  margin: var(--space-8) 0 0;
}

.setup-form__buttons {
  margin-top: var(--space-20);
}

/* Add to this race day's second choice, greyed while it cannot be used (More lanes off, or no race to give lanes to). */
.add-choice__when {
  color: var(--text-muted);
  font-weight: 700;
}

/* More lanes (2.0.0, S36): a race card's Final on and its Change; on Set up a
   race, Final on's rounds on every track, and the More lanes card - the race's
   tracks with Take off, then Add a track, its choices one to a row, since a
   track another race uses says so at length. */
.race-card__final {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-4) var(--space-10);
}

.race-card__final-name {
  font-weight: 700;
}

.setup-tracks__spread {
  margin: var(--space-12) 0 0;
}

.more-lanes {
  max-width: var(--measure);
}

.more-lanes h2 {
  margin-top: 0;
}

.more-lanes__tracks {
  margin-bottom: var(--space-16);
}

.more-lanes__opts {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
}

.more-lanes__new {
  margin: var(--space-12) 0 0;
}

.more-lanes__form .choice-list {
  margin-bottom: var(--space-16);
}

/* Who works this race day (2.0.0, S13; views/partials/race-day-staff.ejs): Staff
   logins' rows, then Add someone and Add a helper for this race day. */
.day-staff__list {
  margin-bottom: var(--space-14);
}

.day-staff__add {
  margin-top: var(--space-4);
}

/* Add someone's choices: each person's badge, then their name and level. */
.day-staff__person .person-badge {
  flex: none;
}

/* ----------------------------------------------------------- track line */
/* A track this race shares with another race (2.0.0, S16), on Race Control
   and Bracket Builder (public/js/staff.js renderTrackLine): the start-line
   icon, whose heat is at its line and whose turn is next, and - while
   another race's heat stands there, for an Event manager - Take the track.
   One row where it fits, as tall with Take the track as without it, so the
   page under it does not move when the box changes hands; on a phone the
   icon and the words keep their row and the button goes under them, its
   44px kept. Hidden on a track of the race's own. */
.track-line {
  display: flex;
  min-height: calc(var(--touch-min) + var(--space-8) * 2 + var(--space-2));
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-8) var(--space-12);
  margin-bottom: var(--space-14);
  padding: var(--space-8) var(--space-14);
  background: var(--tint-blue);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text);
  font-size: var(--text-sm);
}

.track-line[hidden] {
  display: none;
}

.track-line > .icon {
  color: var(--accent);
}

/* Room for a phone's line beside the icon (an 18em start), growing into the rest of a wider screen's row. */
.track-line__words {
  flex: 1 1 18em;
  min-width: 0;
  font-weight: 600;
  overflow-wrap: anywhere;
}

/* On the line's blue, the ghost button keeps a raised face so it reads as a button. */
.track-line__take {
  flex: none;
  background: var(--surface-raised);
}

/* ----------------------------------------------------------- track strip */
/* A race on two or three boxes (2.0.0, S33; public/js/race.js
   renderTrackStrip): one chip per box under Race Control's heading, the box
   the page looks through pressed, each naming what is at its line. A tap
   looks through that box at once. The chips share the row equally at every
   width, two or three across; on a phone a chip's words take two lines,
   kept whether they need them or not, so a change of words never moves the
   page under it. */
.track-strip {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  gap: var(--space-8);
  margin-bottom: var(--space-14);
}

.track-strip[hidden] {
  display: none;
}

.track-chip {
  display: grid;
  align-content: center;
  gap: var(--space-2);
  min-width: 0;
  min-height: var(--touch-base);
  padding: var(--space-6) var(--space-12);
  background: var(--surface-raised);
  color: var(--text);
  border: 2px solid var(--border);
  border-radius: var(--radius-md);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

@media (hover: hover) {
  .track-chip:hover {
    border-color: var(--accent);
  }
}
.track-chip:focus-visible {
  border-color: var(--accent);
}

/* The box looked through: the accent edge, doubled inside, and the blue wash. */
.track-chip[aria-pressed=true] {
  border-color: var(--accent);
  background: var(--tint-blue);
  box-shadow: inset 0 0 0 1px var(--accent);
}

.track-chip__name {
  display: flex;
  align-items: center;
  gap: var(--space-6);
  min-width: 0;
  font-size: var(--text-sm);
}

.track-chip__name strong {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.track-chip__status {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text-muted);
  font-size: var(--text-sm);
  line-height: 1.35;
}

@media (max-width: 640px) {
  .track-chip {
    padding: var(--space-6) var(--space-10);
  }
  .track-chip__status {
    display: -webkit-box;
    min-height: calc(var(--text-sm) * 1.35 * 2);
    white-space: normal;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
  }
}
/* A heat's box (S33; Staff.trackTag), on both boards' heat cards: before
   its status pill, the two kept together at the card's right, and under the
   heat's name together where they do not fit beside it (Staff.heatMarks). */
.heat__marks {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-6);
  margin-left: auto;
}

.track-tag {
  padding: var(--space-2) var(--space-8);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  color: var(--text-muted);
  font-size: var(--text-caption);
  font-weight: 700;
  white-space: nowrap;
}

/* ----------------------------------------------------------- race strip */
/* Race Control's two strips (2.0.0; MORE-LANES.md "Race Control on a
   phone": "under the header and beside S21's race strip"): the race strip
   (S21, views/race.ejs, public/js/race-strip.js) - each of this login's
   races on the race day, what is at its line, a tap opening that race's
   Race Control - and the track strip (S33, _track-line.scss). One row
   where both fit (an iPad upright and wider, two boxes), each a row of its
   own on a phone or for three boxes, so neither is squeezed into chips too
   narrow to read. The row
   holds the margin the track strip had alone; one whose only strip is
   hidden (a finished race day hides the track strip) takes no room.
   Side by side, the two strips stand further apart than their own chips,
   so the races read as one group and the boxes as another. */
.rc-strips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-8) var(--space-24);
  margin-bottom: var(--space-14);
}

.rc-strips > .race-strip,
.rc-strips > .track-strip {
  flex: 1 1 22em;
  min-width: 0;
  margin-bottom: 0;
}

.rc-strips:has(> .track-strip[hidden]:only-child) {
  display: none;
}

/* With the race strip beside it, a box chip has about half the width it
   had alone, so its words get the two lines the race chips' have, as on a
   phone (_track-line.scss): "All dogs race’s Heat 3 is at the line, yours
   next" and "done for the Second Chance" are read whole, not cut at
   "All dogs race’s Heat 3 i…", and both strips' chips stand the same
   height. Three boxes take a row of their own, as wide as they had before
   the race strip came, rather than three chips in half a row (review
   S21-RV-1). */
.rc-strips > .race-strip ~ .track-strip .track-chip__status {
  display: -webkit-box;
  min-height: calc(var(--text-sm) * 1.35 * 2);
  white-space: normal;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

.rc-strips > .race-strip ~ .track-strip:has(> .track-chip:nth-child(3)) {
  flex-basis: 100%;
}

/* The races: two across on a phone, as many as fit wider, each chip a
   link (44px and up) with the race's colour, its name and its status on
   two lines kept whether they need them or not, so a change of words
   never moves the page under it. The page's own race is pressed, as the
   track strip's box is. */
.race-strip {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 9.5em), 1fr));
  gap: var(--space-8);
}

.race-strip__chip {
  display: grid;
  align-content: center;
  gap: var(--space-2);
  min-width: 0;
  min-height: var(--touch-base);
  padding: var(--space-6) var(--space-12);
  background: var(--surface-raised);
  color: var(--text);
  border: 2px solid var(--border);
  border-radius: var(--radius-md);
  text-decoration: none;
}

@media (hover: hover) {
  .race-strip__chip:hover {
    border-color: var(--accent);
  }
}
.race-strip__chip:focus-visible {
  border-color: var(--accent);
}

.race-strip__chip[aria-current=page] {
  border-color: var(--accent);
  background: var(--tint-blue);
  box-shadow: inset 0 0 0 1px var(--accent);
}

.race-strip__name {
  display: flex;
  align-items: center;
  gap: var(--space-6);
  min-width: 0;
  font-size: var(--text-sm);
}

.race-strip__name strong {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.race-strip__status {
  display: -webkit-box;
  min-width: 0;
  min-height: calc(var(--text-sm) * 1.35 * 2);
  overflow: hidden;
  color: var(--text-muted);
  font-size: var(--text-sm);
  line-height: 1.35;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

/* ------------------------------------------------- Check-In: All my races */
/* Check-In's race chips (2.0.0, S21; views/roster.ejs #raceFilter,
   public/js/roster.js): All my races and one button a race, the one the
   list is on solid, the others ghost, as the filter row's are. A race's
   name may be long ("Mixed breed wiener race"), so a chip wraps its words
   rather than pushing the row past a phone's edge; on a phone All my races
   takes the row's width and the races share the rows under it, two
   across. On a solid chip the race's dot sits in a white ring, as on the
   approval queue's Approve into, so a blue race's dot still shows. */
.race-filter {
  margin-bottom: var(--space-14);
}

.race-filter .btn {
  min-width: 0;
  max-width: 100%;
  white-space: normal;
  text-align: left;
}

.race-filter .btn:not(.btn--ghost) .race-chip {
  box-shadow: 0 0 0 var(--space-2) var(--text-on-accent), inset 0 0 0 var(--space-2) var(--text-on-accent);
}

@media (max-width: 640px) {
  .race-filter {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .race-filter .btn:first-child {
    grid-column: 1/-1;
  }
}
/* A dog's race on its row while the list shows every race: its dot and
   its name, beside the dog's heat pill where it has one. */
.roster-item__race {
  display: inline-flex;
  align-items: center;
  gap: var(--space-6);
  margin-right: var(--space-6);
}

/* ---------------------------------------------- the race-day page */
/* The race-day page for phones (2.0.0, PLAN S17; views/day.ejs, drawn by
   public/js/day.js): every race of the race day as it runs, on the hosts'
   chrome, for anyone with the address. A normal page that scrolls, made for
   a phone 390 wide first: the race day's name and date, Follow your dog,
   then a card per race in its own colour - the race chips' --race-* tokens
   on its edge, as Race days draws a race's card. Two cards side by side
   where there is room (an iPad, a computer), one above the other on a phone.

   A dog's name is never cut here: the public reads this page, so a long
   name wraps (agent/ui-standards.md, "Long names"). The followed dog is
   picked out in the gold tint wherever it is named; the line that says
   where it is keeps two lines of room, so the cards never move as it
   changes (the no-grow rule). Nothing on a card is a control: the one
   control is Follow your dog's box and its Clear, at full size. */
.live-day {
  min-width: 0;
}

.live-head,
.live-follow {
  max-width: var(--measure);
}

.live-head {
  margin: 0 0 var(--space-14);
}

.live-title {
  margin: 0 0 var(--space-4);
  font-size: var(--text-xl);
  line-height: var(--leading-tight);
  overflow-wrap: anywhere;
}

.live-when {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-4) var(--space-12);
  margin: 0;
  color: var(--text-muted);
  font-size: var(--text-sm);
}

.live-status {
  display: inline-flex;
  align-items: center;
  gap: var(--space-6);
  color: var(--success);
  font-weight: 600;
}

.live-status__dot {
  width: var(--space-8);
  height: var(--space-8);
  border-radius: var(--radius-pill);
  background: currentColor;
}

.live-status.is-finished {
  color: var(--text-muted);
}

/* Follow your dog: the box and its Clear on one row, the line under them
   with two lines of room. 16px in the box, so a phone does not zoom in. */
.live-follow {
  margin: 0 0 var(--space-16);
  padding: var(--space-12) var(--space-14);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface-raised);
}

.live-follow__row {
  display: flex;
  align-items: stretch;
  gap: var(--space-8);
}

.live-follow__input {
  flex: 1 1 auto;
  min-width: 0;
  font-size: var(--text-base);
}

/* The browser's own small clear cross in the box is hidden: Clear beside it is the one, at full size. */
.live-follow__input::-webkit-search-cancel-button {
  display: none;
  -webkit-appearance: none;
  appearance: none;
}

.live-follow__clear {
  flex: none;
  min-width: var(--touch-min);
  padding: 0 var(--space-10);
}

.live-follow__clear[hidden] {
  display: none;
}

.live-follow__said {
  min-height: calc(2em * var(--leading-base));
  margin: var(--space-6) 0 0;
  line-height: var(--leading-base);
}

/* The races: one column on a phone, as many as fit at forty characters a
   card above that. */
.live-races {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 40ch), 1fr));
  gap: var(--space-14);
  align-items: start;
}

.live-race {
  min-width: 0;
  padding: var(--space-14) var(--space-16);
  border: 1px solid var(--border);
  border-left: var(--space-6) solid var(--race-blue);
  border-radius: var(--radius-md);
  background: var(--surface-raised);
  box-shadow: var(--shadow-sm);
}

.live-race--blue {
  border-left-color: var(--race-blue);
}

.live-race--orange {
  border-left-color: var(--race-orange);
}

.live-race--teal {
  border-left-color: var(--race-teal);
}

.live-race--plum {
  border-left-color: var(--race-plum);
}

.live-race--pink {
  border-left-color: var(--race-pink);
}

.live-race--green {
  border-left-color: var(--race-green);
}

.live-race__head {
  display: flex;
  align-items: flex-start;
  gap: var(--space-10);
}

.live-race__head .race-chip {
  flex: none;
  margin-top: var(--space-6);
}

.live-race__words {
  min-width: 0;
}

.live-race__name {
  margin: 0;
  font-size: var(--text-lg);
  line-height: var(--leading-tight);
  overflow-wrap: anywhere;
}

.live-race__where {
  margin: var(--space-2) 0 0;
  color: var(--text-muted);
  font-size: var(--text-sm);
  overflow-wrap: anywhere;
}

.live-race__note {
  margin: var(--space-10) 0 0;
  padding: var(--space-8) var(--space-10);
  border-radius: var(--radius-sm);
  background: var(--tint-blue);
  color: var(--text);
  font-size: var(--text-sm);
}

/* A card's parts: at the line, up next, the winners, the last winner. */
.live-block {
  margin-top: var(--space-14);
  padding-top: var(--space-12);
  border-top: 1px solid var(--border);
}

.live-block__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-4) var(--space-8);
  margin: 0 0 var(--space-8);
  color: var(--text-muted);
  font-size: var(--text-label);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
}

.live-block__head .icon {
  flex: none;
  color: var(--accent);
}

.live-block__what {
  color: var(--text);
  font-weight: 400;
  letter-spacing: normal;
  text-transform: none;
  overflow-wrap: anywhere;
}

/* The box a heat is at, on a race on two or three boxes (2.0.0, S35): a tag
   between the block's name and its heat, its name as staff set it up. */
.live-block__box {
  padding: var(--space-2) var(--space-8);
  border-radius: var(--radius-pill);
  background: var(--surface-sunken);
  color: var(--text);
  font-weight: 700;
  letter-spacing: normal;
  text-transform: none;
  overflow-wrap: anywhere;
}

.live-block__hint {
  margin: 0;
  color: var(--text-muted);
  font-size: var(--text-sm);
}

.live-block--places .live-block__head {
  color: var(--text);
  font-size: var(--text-base);
  letter-spacing: normal;
  text-transform: none;
}

.live-lanes,
.live-places {
  display: grid;
  gap: var(--space-6);
  margin: 0;
  padding: 0;
  list-style: none;
}

.live-lane,
.live-place {
  display: flex;
  align-items: center;
  gap: var(--space-10);
  min-width: 0;
  border-radius: var(--radius-sm);
}

.live-lane__no {
  display: inline-grid;
  flex: none;
  place-items: center;
  width: var(--lane-badge);
  height: var(--lane-badge);
  border-radius: var(--radius-pill);
  background: var(--surface-sunken);
  color: var(--text);
  font-size: var(--text-sm);
  font-weight: 700;
}

.live-lanes--line .live-lane__no {
  background: var(--accent);
  color: var(--text-on-accent);
}

.live-lanes--line .live-name {
  font-weight: 600;
}

.live-name {
  min-width: 0;
  overflow-wrap: anywhere;
}

.live-place__no {
  flex: none;
  padding: var(--space-2) var(--space-10);
  border-radius: var(--radius-pill);
  background: var(--silver);
  color: var(--text-on-silver);
  font-size: var(--text-sm);
  font-weight: 700;
}

.live-place--1 .live-place__no {
  background: var(--gold);
  color: var(--text-on-gold);
}

.live-place--3 .live-place__no {
  background: var(--bronze);
  color: var(--text-on-bronze);
}

.live-place--1 .live-name {
  font-size: var(--text-lg);
  font-weight: 700;
}

.live-winner {
  margin: 0;
  overflow-wrap: anywhere;
}

/* The followed dog: its lane or place washed gold, its name bold, wherever it is named. */
.live-lane.is-followed,
.live-place.is-followed {
  background: var(--tint-gold);
  box-shadow: 0 0 0 var(--space-4) var(--tint-gold);
}

.live-name.is-followed {
  color: var(--on-tint-gold);
  font-weight: 700;
}

.live-winner .live-name.is-followed {
  padding: 0 var(--space-4);
  border-radius: var(--radius-sm);
  background: var(--tint-gold);
}

.live-quiet {
  margin: var(--space-16) 0;
  color: var(--text-muted);
}

/* ------------------------------------- a trainer's dog page and link */
/* A trainer's own dog page (2.0.0, PLAN S18; views/dog-follow.ejs,
   public/js/dog-follow.js): one dog's race day on the trainer's phone, on
   the hosts' chrome. Phone first - one column, read top to bottom: the
   dog, where it is now, its day, its fun fact, the other races. Nothing on
   it is a control: where the keepsake card is, it says so in words. */
.follow {
  max-width: var(--measure);
  margin: 0 auto;
}

.follow .card {
  margin-bottom: var(--space-16);
  padding: var(--space-20);
}

.follow h2 {
  font-size: var(--text-lg);
  margin: 0 0 var(--space-12);
}

/* The dog: its picture (or the dog mark in a disc), its name, its race's
   colour and name, its trainers by first name. A long name wraps, never
   cut: this is the trainer's own page. */
.follow-head__dog {
  display: flex;
  align-items: center;
  gap: var(--space-16);
}

.follow-head__photo,
.follow-head__mark {
  flex: none;
  width: var(--avatar-lg);
  height: var(--avatar-lg);
  border-radius: 50%;
}

.follow-head__photo {
  object-fit: cover;
  background: var(--surface-sunken);
}

.follow-head__mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--tint-blue);
  color: var(--accent);
}

.follow-head__mark .icon {
  width: var(--avatar-sm);
  height: var(--avatar-sm);
}

.follow-head__words {
  min-width: 0;
}

.follow-head__name {
  margin: 0 0 var(--space-6);
  overflow-wrap: anywhere;
}

.follow-head__race {
  display: flex;
  align-items: center;
  gap: var(--space-8);
  margin: 0 0 var(--space-4);
  font-weight: 700;
}

.follow-head__trainer {
  margin: 0;
  color: var(--text-muted);
  overflow-wrap: anywhere;
}

/* "Live · updates by itself", with a green dot; a kept race day's line is
   grey, its dot too. */
.follow-live {
  display: flex;
  align-items: center;
  gap: var(--space-8);
  margin: var(--space-16) 0 0;
  font-size: var(--text-sm);
  color: var(--text-muted);
}

.follow-live__dot {
  flex: none;
  width: var(--space-10);
  height: var(--space-10);
  border-radius: 50%;
  background: var(--success);
}

.follow-live--kept .follow-live__dot {
  background: var(--border-strong);
}

/* Where the dog is now. Its first lines keep their room whichever moment
   it is, so the cards under it move as little as can be when it changes. */
.follow-moment {
  border-left: var(--space-6) solid var(--accent);
}

.follow-moment--line {
  border-left-color: var(--danger);
}

.follow-moment--waiting {
  border-left-color: var(--gold);
}

.follow-moment--over {
  border-left-color: var(--gold);
}

.follow-moment--out,
.follow-moment--before,
.follow-moment--away {
  border-left-color: var(--border-strong);
}

.follow-moment__eyebrow {
  margin: 0 0 var(--space-4);
  font-size: var(--text-caption);
  font-weight: 700;
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--text-muted);
}

.follow-moment--line .follow-moment__eyebrow {
  color: var(--danger);
}

.follow-moment__title {
  margin: 0;
  font-size: var(--text-xl);
  line-height: var(--leading-tight);
  overflow-wrap: anywhere;
}

.follow-moment__round {
  margin: var(--space-4) 0 0;
  font-weight: 700;
}

.follow-moment__line {
  margin: var(--space-8) 0 0;
}

.follow-moment__go,
.follow-keepsake {
  display: flex;
  align-items: flex-start;
  gap: var(--space-8);
  margin: var(--space-14) 0 0;
  font-weight: 700;
}

.follow-moment__go .icon,
.follow-keepsake .icon {
  flex: none;
  margin-top: var(--space-2);
}

/* "Head to ..." names the box as staff named it on a race on two or three
   boxes (S35): free text of up to 40 characters, which may be one long
   word, so it wraps inside the note rather than run past it (S35-RV-2).
   The lane under each heat of the dog's day names the box too. */
.follow-moment__go > span {
  min-width: 0;
  overflow-wrap: anywhere;
}

/* The heat's line-up: a row a lane, the lane's number in the accent disc
   the staff screens give it, the trainer's own dog washed gold and named
   "Your dog". */
.follow-lineup,
.follow-podium,
.follow-history,
.follow-others__list {
  list-style: none;
  margin: var(--space-16) 0 0;
  padding: 0;
}

.follow-lineup__lane {
  display: flex;
  align-items: center;
  gap: var(--space-12);
  min-height: var(--touch-min);
  padding: var(--space-4) var(--space-8);
  border-radius: var(--radius-sm);
}

.follow-lineup__lane + .follow-lineup__lane {
  margin-top: var(--space-4);
}

.follow-lineup__lane.is-own {
  background: var(--tint-gold);
  color: var(--on-tint-gold);
  font-weight: 700;
}

.follow-lineup__num {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--lane-badge);
  height: var(--lane-badge);
  border-radius: 50%;
  background: var(--accent);
  color: var(--text-on-accent);
  font-size: var(--text-sm);
  font-weight: 700;
}

.follow-lineup__name {
  flex: 1 1 auto;
  min-width: 0;
  overflow-wrap: anywhere;
}

.follow-lineup__you {
  flex: none;
  font-size: var(--text-caption);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
}

/* The race's top dogs once it is over. */
.follow-podium li {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--space-4) var(--space-12);
  padding: var(--space-8) 0;
  border-top: 1px solid var(--border);
}

.follow-podium__label {
  color: var(--text-muted);
}

.follow-podium__name {
  font-weight: 700;
  overflow-wrap: anywhere;
}

/* Where the keepsake card is, in words, under the podium (review S18-RV-1). */
.follow-keepsake {
  margin-top: var(--space-16);
  font-weight: 400;
}

/* The dog's day: a row a heat - where, the lane, how it went. */
.follow-history__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--space-2) var(--space-12);
  align-items: center;
  padding: var(--space-10) 0;
  border-top: 1px solid var(--border);
}

.follow-history__row:first-child {
  border-top: 0;
  padding-top: 0;
}

.follow-history__where {
  font-weight: 700;
  overflow-wrap: anywhere;
}

.follow-history__lane {
  grid-column: 1;
  color: var(--text-muted);
  font-size: var(--text-sm);
  overflow-wrap: anywhere;
}

.follow-history__result {
  grid-column: 2;
  grid-row: 1/span 2;
  white-space: normal;
  text-align: center;
  max-width: 16ch;
}

.follow-fact p {
  margin: 0;
  overflow-wrap: anywhere;
}

/* One line on each other race of the day. */
.follow-others__list {
  margin-top: 0;
}

.follow-others__list li {
  display: flex;
  align-items: baseline;
  gap: var(--space-10);
  padding: var(--space-6) 0;
}

.follow-others__list .race-chip {
  position: relative;
  top: var(--space-2);
}

.follow-others__words {
  display: flex;
  flex-direction: column;
  min-width: 0;
  overflow-wrap: anywhere;
}

/* A finished race day on screen (2.0.0, slice S19). A race whose race day is
   finished is kept exactly as it ended, and the server refuses every change
   to it but its results emails and Copy card link (src/access.js). Its
   pages say so in a banner under their heading (views/partials/
   finished-banner.ejs) and offer nothing that would change it: the
   scripts draw no such control (public/js/staff.js Staff.kept()), and
   what the page's own markup carries steps aside here, on <body
   data-finished="true"> (views/partials/header.ejs), before any script
   runs - so nothing is ever drawn and then taken away. Each rule that
   sets something aside starts at body[data-finished], so it wins over the
   screen's own wherever the partial sits in the cascade. */
/* The banner: the flag Finish race day wears, then its words. */
.kept-banner {
  display: flex;
  align-items: flex-start;
  gap: var(--space-10);
  margin: 0 0 var(--space-20);
}

.kept-banner__icon {
  flex: none;
  margin-top: var(--space-2);
}

.kept-banner__words {
  margin: 0;
  min-width: 0;
}

/* Race Control: the pace line (the banner takes its place), the old-format
   warning that points at Reset, the Big Screen controls card (its modes,
   Break, the heat introduction, Message the Announcer and Put on the
   Announcer), Late arrivals, the corrections hint, and the phone's slim bar,
   which carries the box's buttons. The box itself, the Championship card
   and the bracket stay, drawn with nothing to tap (public/js/race.js). */
body[data-finished=true] #paceLine,
body[data-finished=true] #formatWarning,
body[data-finished=true] #screenControls,
body[data-finished=true] .card[data-anchor=late],
body[data-finished=true] #correctionsHint,
body[data-finished=true] .current-bar {
  display: none;
}

/* Screens: each screen's Settings, which leads to an Event settings section
   a finished race day's page no longer draws. Open and Copy link stay. */
body[data-finished=true] a[href$="/settings#bigScreenCard"],
body[data-finished=true] a[href$="/settings#announcerCard"],
body[data-finished=true] a[href$="/settings#lineupCard"] {
  display: none;
}

/* Check-In: the line saying the bracket can be built. */
body[data-finished=true] #buildReady {
  display: none;
}

/* Registration: Add a dog, Import spreadsheet and their panels, and Check
   for new registrations. Its counts and the queue's list stay. */
body[data-finished=true] #toggleAdd,
body[data-finished=true] #toggleImport,
body[data-finished=true] #addForm,
body[data-finished=true] #importPanel,
body[data-finished=true] #checkRegistrations {
  display: none;
}

/* Event settings: a finished race's settings are kept as they ended, so
   its page is the banner and Sharing and emails alone - the hosts' link to
   copy, whether card links are on, Test email and Card PDFs' check - with
   no Make a new link or Make new links, and no switch but Dog cards by
   link's while its links are off (data-links), since a finished race day's
   links may still be switched on, never off. The overview and its icon row, which
   lead to the five sections set aside, go with them, and the page is one
   column at every width. */
body[data-finished=true] .settings {
  display: block;
}

body[data-finished=true] #settingsNav,
body[data-finished=true] #settingsQuick,
body[data-finished=true] #everyScreen,
body[data-finished=true] #bigScreenCard,
body[data-finished=true] #announcerCard,
body[data-finished=true] #lineupCard,
body[data-finished=true] #scheduleCard,
body[data-finished=true] #hostShareCard .toggle,
body[data-finished=true] #hostShareNew,
body[data-finished=true] #cardLinksCard[data-links=on] .toggle,
body[data-finished=true] #cardLinksCard[data-links=on] #cardLinksDay,
body[data-finished=true] #cardLinksCard .btnrow:has(> #cardLinksNew) {
  display: none;
}

/* A dog's page: its picture as it was kept (views/dog.ejs), in the photo
   button's place and size but not a control - no pointer, and nothing that
   moves or darkens under a press or a hover. */
.dog-head__photo--kept {
  cursor: default;
}

.dog-head__photo--kept:active .dogphoto {
  transform: none;
}

@media (hover: hover) {
  .dog-head__photo--kept:hover .dogphoto {
    border-color: var(--border);
  }
}
/* A finished race day's race cards (views/race-day.ejs): who won leads the
   card, its Champion in the colour a done bracket wears. */
.race-card__won dd {
  font-weight: 700;
}

.race-card__won:first-child dd {
  color: var(--success);
}

/* ------------------------------------------- messages to the Announcer */
/* Race Control's side of Messages to the Announcer (public/js/announce.js),
   and the words of a note as the Announcer draws them, which Event
   settings' preview shares. Event settings' own rules - the notes part, the
   quick messages - are in _settings.scss. Tokens only.

   The message is gold everywhere. The Announcer draws its note in the
   kiosks' winner gold, so Race Control shows the same note, small, in the
   staff gold tint with a --gold edge, and the Announcer button is tinted
   gold with a gold dot while a message is on its screen. The Big Screen's
   active mode is a solid gold fill; a pale tint with a dot is a different
   thing to the eye. */
/* The Big Screen controls' heading carries the Announcer button at its
   right, in the room beside the title, centred on the title's line, with
   14px between the 44px row and the mode buttons under it - the spacing in
   the approved rc2 pictures (the lead's ruling: "14px under it", not the
   10px of a later prototype). The button once reached up into the card's
   padding so the card was no taller for it; the design gives the row its
   own height instead, so the buttons under it are not crowded. */
.card__head--tools {
  align-items: center;
  flex-wrap: nowrap;
  margin: 0 0 var(--space-14);
}

.card__head--tools > h2 {
  flex: 1 1 auto;
  min-width: 0;
  line-height: var(--leading-base);
}

.card__head--tools > .btn {
  flex: none;
}

/* "Message the Announcer" where it fits; "Message" on a phone, the same
   short word as the phone bar's, so the heading stays one row from 360px
   up (at 320, fitCard below). The dialog's Clear loses its " from the
   Announcer" there too. The button's name says the whole thing either way. */
@media (max-width: 640px) {
  .announce-btn__lead, .announce-btn__rest {
    display: none;
  }
}
/* Where even "Message" would break the heading over two lines (a 320px
   phone), the megaphone alone, measured (race.js fitCard): the slim bar's
   Message is the megaphone alone at that width too. Still 44x44. */
.announce-btn--bare > span {
  display: none;
}

.announce-btn--bare {
  min-width: var(--touch-min);
  justify-content: center;
}

/* A message on the Announcer's screen: the note's gold as a tint, a dark
   gold line inside the edge (the 3:1 a selected state needs, as
   .placement-choice does) and a gold dot in the corner. On its way, waiting
   for the next heat, or hidden: the dot alone (Ryan, ~16:10). Its words stay
   the same; its accessible name says which. */
.announce-btn {
  position: relative;
}

.announce-btn.is-showing {
  background: var(--tint-gold);
  border-color: var(--gold);
  box-shadow: inset 0 0 0 1px var(--on-tint-gold);
  color: var(--on-tint-gold);
}

.announce-btn.is-showing:active {
  background: var(--tint-gold);
}

@media (hover: hover) {
  .announce-btn.is-showing:hover {
    background: var(--tint-gold);
  }
}
.announce-btn.is-showing::after,
.announce-btn.is-pending::after {
  content: "";
  position: absolute;
  top: var(--space-4);
  right: var(--space-4);
  width: var(--space-8);
  height: var(--space-8);
  border-radius: var(--radius-pill);
  background: var(--gold);
  box-shadow: 0 0 0 var(--space-2) var(--surface-raised);
}

/* Seen: the announcer tapped a waiting message up out of its "New message"
   tag (Ryan, 2026-09-26). Gold as on its screen, and a small check where the
   dot is - the third state. Drawn with two borders, not a character. */
.announce-btn.is-showing.is-seen::after {
  top: var(--space-4);
  right: var(--space-4);
  width: var(--space-8);
  height: var(--space-4);
  border-radius: 0;
  background: none;
  border-left: var(--space-2) solid var(--on-tint-gold);
  border-bottom: var(--space-2) solid var(--on-tint-gold);
  box-shadow: none;
  transform: rotate(-45deg);
}

/* ------------------------------- Race Control's copy of the Announcer's note */
/* Under the mode buttons, only while a message is up: the note as the
   announcer sees it, small - the same gold, the megaphone disc, the heading
   on its own line and "- " lines as a list after gold diamonds - with when
   it was sent, whether the Announcer has it, and its actions. */
.mini-note {
  margin-top: var(--space-14);
  padding: var(--space-12) var(--space-14) var(--space-14);
  border: 1px solid var(--tint-gold-line);
  border-left: var(--status-edge) solid var(--gold);
  border-radius: var(--radius-md);
  background: var(--tint-gold);
  color: var(--text);
}

.mini-note__top {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  column-gap: var(--space-10);
}

.mini-note__mark {
  display: grid;
  place-items: center;
  width: var(--avatar-sm);
  height: var(--avatar-sm);
  border-radius: var(--radius-pill);
  background: var(--surface-raised);
  box-shadow: inset 0 0 0 var(--space-2) var(--gold);
  color: var(--on-tint-gold);
}

.mini-note__who {
  min-width: 0;
}

.mini-note__label {
  margin: 0;
  font-size: var(--text-caption);
  font-weight: 800;
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--on-tint-gold);
}

.mini-note__when {
  margin: 0;
  font-size: var(--text-sm);
  color: var(--text-muted);
  overflow-wrap: break-word;
}

.mini-note__actions {
  margin-top: 0;
  gap: var(--space-8);
  justify-content: flex-end;
}

.mini-note__text {
  margin-top: var(--space-10);
  max-width: var(--measure);
  font-size: var(--text-base);
  line-height: var(--leading-base);
  font-weight: 700;
  overflow-wrap: break-word;
}

/* Not on the Announcer yet: its tablet has not picked the message up. */
.mini-note--stale {
  border-left-color: var(--danger);
}

.mini-note--stale .mini-note__when {
  color: var(--on-tint-danger);
}

/* Hidden while "To read out" is switched off: a quieter, grey note. */
.mini-note--hidden {
  background: var(--surface-sunken);
  border-color: var(--border);
  border-left-color: var(--border-strong);
}

/* A phone: the words first, then the actions under them. */
@media (max-width: 640px) {
  .mini-note {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    column-gap: var(--space-10);
    align-items: center;
  }
  .mini-note__top {
    display: contents;
  }
  .mini-note__text {
    grid-column: 1/-1;
    margin-top: var(--space-8);
  }
  .mini-note__actions {
    grid-column: 1/-1;
    order: 1;
    margin-top: var(--space-10);
    justify-content: flex-start;
  }
}
/* In the "on the Announcer now" view, the words alone in the note's gold. */
.announce-view__note {
  margin-top: var(--space-14);
}

.announce-view__note > .mini-note__text {
  margin-top: 0;
}

/* Typed words as the Announcer draws them (Staff.noteWords, the .rich-*
   blocks in _rich-text.scss): Race Control's copy and the Event settings
   preview, in the note's gold. */
:where(.mini-note__text, .read-preview__words) > * {
  margin: 0;
}

:where(.mini-note__text, .read-preview__words) .rich-head {
  color: var(--on-tint-gold);
}

/* ------------------------------------------------------------- the sheet */
/* "Missing at the line" (1.11.0, "Call a missing dog in one tap"): the
   sheet's first section, and the "on the Announcer now" view's under the
   message. One full-width row per filled lane, as the dialogs list dogs to
   pick (DS-04): the lane number, the picture with Show dog photos on, the
   name and the trainers, whole - a name wraps at its spaces, never cut -
   and at least --touch-base tall. A called lane is the house's picked gold
   with "Called · tap when found". The rows are drawn once as the sheet
   opens and a tap closes it, so nothing on them ever changes size under a
   finger. The heat lining up behind gets a smaller heading and tighter
   rows under the heat at the line's. */
.announce-calls__title {
  margin-top: var(--space-12);
  margin-bottom: var(--space-2);
}

.announce-calls__hint {
  margin: 0 0 var(--space-8);
}

.announce-calls__list {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-6);
  margin: 0;
  padding: 0;
  list-style: none;
}

.placement-choice.announce-call {
  width: 100%;
  min-height: var(--touch-base);
  flex-direction: row;
  align-items: center;
  gap: var(--space-10);
  padding: var(--space-8) var(--space-12);
}

.announce-call .lane__n, .announce-call .dogphoto {
  flex: none;
}

.announce-call__who {
  display: grid;
  gap: var(--space-2);
  min-width: 0;
  overflow-wrap: break-word;
}

.announce-call__name {
  font-size: var(--text-base);
  line-height: var(--leading-tight);
}

.placement-choice .announce-call__people {
  color: var(--text-muted);
}

.placement-choice .announce-call__status {
  font-weight: 800;
  color: var(--on-tint-gold);
}

.announce-call.is-called .lane__n {
  background: var(--gold);
  color: var(--text-on-gold);
}

.announce-calls__next {
  margin: var(--space-14) 0 var(--space-6);
  font-size: var(--text-sm);
  font-weight: 700;
  color: var(--text-muted);
}

.announce-calls__list--next .placement-choice.announce-call {
  min-height: var(--touch-min);
  padding-block: var(--space-6);
}

.announce-calls__list--next .announce-call__name {
  font-size: var(--text-sm);
}

/* After the lanes, the typed message's part of the sheet, set apart by a
   rule and its own heading, so its intro never reads as about the lanes. */
.announce-dialog__or {
  margin: var(--space-16) 0 var(--space-2);
  padding-top: var(--space-12);
  border-top: 1px solid var(--border);
}

/* The view: the lanes under the message, above "Send a new one". */
.announce-view .announce-calls {
  margin-top: var(--space-4);
}

/* The house dialog (.placement-dialog), its width and pinned button row
   unchanged: pick, type, Send. */
.announce-picks {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-6);
  margin: 0;
  padding: 0;
  list-style: none;
}

/* One line each, cut at the row's end: a tap puts the whole of it in the
   box below, where it is read before anything is sent. */
.placement-choice.announce-pick {
  width: 100%;
  flex-direction: row;
  align-items: center;
  padding: var(--space-8) var(--space-12);
}

.placement-choice .announce-pick__rest {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-size: var(--text-sm);
  color: var(--text-muted);
}

.placement-choice .announce-pick__rest--one {
  font-size: var(--text-base);
  color: var(--text);
}

.announce-pick__first {
  font-weight: 700;
  color: var(--text);
}

.announce-dialog__manage {
  margin: var(--space-6) 0 0;
}

/* The box grows with the message, so the whole of it is in view: at least
   three lines. field-sizing is current Chrome's and Safari's where it has
   shipped; elsewhere Staff.fitRows grows `rows` as it is typed. */
.announce-dialog textarea {
  field-sizing: content;
  min-height: calc(3lh + 2 * var(--space-10));
  line-height: var(--leading-base);
  resize: none;
}

.announce-count {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-12);
}

.announce-count .field__hint {
  margin: 0;
}

.announce-count__n {
  flex: none;
  font-variant-numeric: tabular-nums;
  font-weight: 700;
}

.announce-count__n--over {
  color: var(--danger);
}

/* Send to (Ryan, 2026-09-26): one row of three choices - Announcer, Lineup,
   Both - a radio group of the house choice cards, each at least the touch
   minimum, the Announcer picked every time the sheet opens on a new
   message. One row at every width, so the sheet is no taller than a row. */
.announce-to {
  margin-top: var(--space-12);
}

.announce-to__label {
  margin: 0 0 var(--space-6);
  font-size: var(--text-sm);
  font-weight: 700;
  color: var(--text);
}

.announce-to__choices {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-8);
}

.placement-choice.announce-to__choice {
  align-items: center;
  justify-content: center;
  padding: var(--space-8) var(--space-6);
  text-align: center;
}

.placement-choice .announce-to__word {
  font-size: var(--text-base);
  font-weight: 700;
}

.announce-dialog__force {
  margin-top: var(--space-12);
}

.announce-dialog__force .field__hint {
  margin: 0;
}

/* Folded away for the Lineup alone, it keeps its room (seen, not there), so
   a tap on Lineup never moves the row under the finger on a sheet scrolled
   to its foot: a pick never moves a button (review F9). Typing still folds
   it away whole, below. */
.announce-dialog__force.is-folded {
  visibility: hidden;
}

/* Editing: "Clear from the Announcer" in the pinned row, at its far end,
   apart from Update, so it is in view however short the screen ("Clear" on
   a phone, by .announce-btn__lead). */
.announce-dialog .btnrow > .announce-dialog__clear {
  margin-left: auto;
}

/* On a phone the three share one row - Update, Cancel, Clear - a little
   tighter at their sides, so the row stays one row and the switch above it
   stays in view on an iPhone SE. Still 48px tall. */
@media (max-width: 640px) {
  .announce-dialog > .btnrow:last-child > .btn {
    padding-inline: var(--space-12);
  }
}
/* A short screen - an iPhone SE, a sideways iPad with its bars - keeps the
   quick messages, the box, the switch and the pinned row in view: the
   intro, which the title and the copy under the mode buttons already say,
   gives way first. */
@media (max-height: 760px) {
  .announce-dialog .announce-dialog__intro {
    display: none;
  }
}
/* Typing on a phone (or wherever a keyboard is up): the keyboard takes the
   lower half of the screen, so while the box has focus the sheet sits in
   what is left (announce.js writes --visible-height and --visible-top from
   visualViewport), at the top, with the intro, the quick messages and the
   switch folded away and Send pinned at its foot, just above the keys - the
   missing-dog lanes too, which a tap sends without the box. They come back
   when the box loses focus. */
.announce-dialog--typing {
  margin-top: calc(var(--visible-top, 0px) + var(--space-8));
  margin-bottom: auto;
  max-height: calc(var(--visible-height, 100dvh) - 2 * var(--space-8));
}

.announce-dialog--typing .announce-calls,
.announce-dialog--typing .announce-dialog__or,
.announce-dialog--typing .announce-dialog__intro,
.announce-dialog--typing .announce-dialog__picks,
.announce-dialog--typing .announce-to,
.announce-dialog--typing .announce-dialog__force {
  display: none;
}

.announce-dialog--typing .dialog-fields {
  margin-top: var(--space-10);
}

/* ------------------------------------------------- the not-shown notice */
/* Ryan, 2026-09-24: "a small translucent non-touch impacting toast notice
   flashing until resolved". Near the bottom, over the tab row's clearance
   like the toast, never over the pinned heat or the phone's slim bar at the
   top. It takes no taps at any moment: whatever is under it still works.
   It pulses slowly - opacity only, never a strobe - and holds still where
   motion is reduced. Under the toast (--z-conn), which is only up a moment.
   It never covers a button either: announce.js lifts it (--notice-lift)
   clear of any control under it, the toast's Undo included, and steps it
   aside (.is-tucked) where that would take it too high, or while the copy
   under the mode buttons - the same words, with Edit and Clear - is in
   view. */
.announce-notice {
  position: fixed;
  left: 50%;
  bottom: calc(var(--staff-tabbar-height, 0px) + var(--space-20) + var(--notice-lift, 0px));
  transition: bottom var(--dur-fast) var(--ease);
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: var(--space-8);
  width: max-content;
  max-width: min(92vw, var(--measure));
  padding: var(--space-8) var(--space-14);
  border-radius: var(--radius-md);
  background: color-mix(in srgb, var(--text) 80%, transparent);
  color: var(--surface);
  font-size: var(--text-sm);
  font-weight: 600;
  line-height: var(--leading-tight);
  box-shadow: var(--shadow-lg);
  pointer-events: none;
  opacity: 0;
  visibility: hidden;
  z-index: var(--z-conn);
}

.announce-notice .icon {
  flex: none;
  color: var(--gold);
}

.announce-notice.is-up {
  visibility: visible;
  opacity: 1;
  animation: announce-pulse 2.4s ease-in-out infinite alternate;
}

.announce-notice.is-up.is-tucked {
  visibility: hidden;
  opacity: 0;
  animation: none;
}

@keyframes announce-pulse {
  from {
    opacity: 1;
  }
  to {
    opacity: 0.55;
  }
}
@media (prefers-reduced-motion: reduce) {
  .announce-notice.is-up {
    animation: none;
    opacity: 0.92;
  }
  .announce-notice {
    transition: none;
  }
}
/* Put on the Announcer (2.0.0, S41; views/partials/announcer-pick.ejs), on
   a race day of two races or more: under the hint's line, which race the
   race day's Announcer shows and the 44px button that puts this race on it,
   side by side where they fit and the button under the words where they do
   not (a phone). The words take what is left, so the button never moves
   when they change. Tokens only. */
.announcer-pick {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-8) var(--space-12);
  margin-top: var(--space-8);
}

.announcer-pick__words {
  flex: 1 1 0;
  min-width: min(100%, 16em);
  margin: 0;
}

.announcer-pick__btn {
  flex: none;
}

/* ------------------------------------------------------- Event settings */
/* Event settings (views/settings.ejs, public/js/settings.js), arranged in six
   sections in 1.11.0 (the settings revamp, Ryan 2026-09-28): an overview of
   the sections at the top, then each section a card of parts. Tokens only.

   Hints are 14px on this page only: here the words under a switch are read
   before it is flipped, not glanced at beside a race. Everywhere else a
   .field__hint stays 12px. */
.settings .field__hint {
  font-size: var(--text-sm);
  line-height: var(--leading-base);
}

/* ------------------------------------------------------------ overview */
/* #settingsNav: one link per section - its icon, its name, and what it is set
   to - in one markup at every width, moved by CSS alone: rows in a white card
   on a phone, three tiles across on an upright iPad, a side list that stays in
   view from 900px. The summary under each name keeps two lines of room, and
   holds unseen in the same grid cell the widest words it can take
   (.settings-nav__room, src/event-settings.js SUMMARY_ROOM), so it is as
   tall as its widest at every width and a change of words - typing in the
   notes, a poll - moves no row or tile (no-grow). No link holds a switch. */
.settings-nav {
  margin: 0 0 var(--space-20);
}

.settings-nav__list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.settings-nav__item {
  display: grid;
  margin: 0;
  padding: 0;
}

.settings-nav__link {
  display: flex;
  align-items: center;
  gap: var(--space-12);
  min-height: var(--touch-row);
  color: var(--text);
  text-decoration: none;
}

.settings-nav__icon {
  color: var(--accent);
}

.settings-nav__words {
  display: grid;
  flex: 1 1 auto;
  min-width: 0;
}

.settings-nav__name {
  font-size: var(--text-base);
  font-weight: 700;
  line-height: var(--leading-base);
}

.settings-nav__summary {
  display: grid;
  min-height: calc(2 * var(--text-sm) * var(--leading-base));
  font-size: var(--text-sm);
  line-height: var(--leading-base);
  color: var(--text-muted);
  overflow-wrap: break-word;
}

.settings-nav__summary > span {
  grid-area: 1/1;
  min-width: 0;
}

.settings-nav__room {
  visibility: hidden;
}

/* The sprite's chevron points down; a phone's rows turn it to point on. */
.settings-nav__chevron {
  display: none;
  color: var(--text-muted);
  transform: rotate(-90deg);
}

.settings-nav__link:active {
  background: var(--tint-blue);
}

@media (hover: hover) {
  .settings-nav__link:hover {
    background: var(--tint-blue);
  }
}
/* A phone: the six rows in one white card, each at least a full row with its
   two summary lines, a hairline between them and a chevron at the end. Not
   sticky itself: once it has scrolled away the compact row of the six icons
   takes over under the top bar (below), and the button back to the top
   returns to the list. */
@media (max-width: 640px) {
  .settings-nav {
    padding: var(--space-4) var(--space-16);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    background: var(--surface-raised);
    box-shadow: var(--shadow-md);
  }
  .settings-nav__link {
    padding: var(--space-6) 0;
  }
  .settings-nav__item + .settings-nav__item > .settings-nav__link {
    border-top: 1px solid var(--border);
  }
  .settings-nav__chevron {
    display: block;
  }
}
/* An upright iPad: the same links as tiles, three across - Every screen, The
   Big Screen and The Announcer, then The Lineup, Schedule, and Sharing and
   emails - the icon and the name on the first line and the summary under
   them. Not sticky either: the compact row below takes over once they have
   scrolled away. */
@media (min-width: 641px) and (max-width: 899px) {
  .settings-nav__list {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--space-12);
  }
  .settings-nav__link {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    align-content: start;
    align-items: center;
    column-gap: var(--space-8);
    row-gap: var(--space-6);
    padding: var(--space-16);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface-raised);
  }
  .settings-nav__words {
    display: contents;
  }
  .settings-nav__summary {
    grid-column: 1/-1;
  }
}
/* From 900px - an iPad on its side, a computer - the page has two columns:
   the overview is a side list --settings-side wide beside the sections, and
   stays in view under the measured top bar (it never reads the window's
   height). The section crossing the line under the chrome is marked
   aria-current="location" (settings.js, an IntersectionObserver) with the
   choice list's checked look. Seven rows would still fit a sideways iPad
   without a max-height. */
@media (min-width: 900px) {
  .settings {
    display: grid;
    grid-template-columns: var(--settings-side) minmax(0, 1fr);
    grid-template-rows: auto 1fr;
    grid-template-areas: "nav head" "nav body";
    column-gap: var(--space-24);
    align-items: start;
  }
  .settings__head {
    grid-area: head;
    min-width: 0;
  }
  .settings__body {
    grid-area: body;
    min-width: 0;
  }
  .settings-nav {
    grid-area: nav;
    position: sticky;
    top: calc(var(--staff-nav-height, 58px) + var(--space-16));
    margin: 0;
  }
  .settings-nav__link {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    align-content: center;
    align-items: center;
    column-gap: var(--space-8);
    padding: var(--space-6) var(--space-10);
    border: 1px solid transparent;
    border-radius: var(--radius-md);
  }
  .settings-nav__words {
    display: contents;
  }
  .settings-nav__summary {
    grid-column: 1/-1;
  }
  .settings-nav__link[aria-current=location] {
    background: var(--tint-blue);
    border-color: var(--accent);
    box-shadow: inset 0 0 0 1px var(--accent);
  }
}
/* Decision 13 (Ryan, 2026-09-28): on an upright iPad, a compact row of the
   six sections' icons, one 44px link each named for its section, fixed under
   the measured top bar once the tiles have scrolled away (settings.js adds
   .is-shown), the section in view marked as the side list marks it. Always
   laid out at this size, only unseen until then, so it never moves the page
   as it comes and goes, and settings.js can measure it for the room a link
   lands under (--settings-quick-height). Since 1.12.0 on a phone too, either
   way up (FB-20260929-RY-21, Ryan: "Would be nice to have a mobile sticky
   navigation though with icons for quick jumping around the page."), once
   the overview's rows have scrolled away. Not from 900px, where the side
   list stays. */
.settings-quick {
  display: none;
}

@media (max-width: 899px) {
  .settings-quick {
    display: grid;
    grid-template-columns: repeat(6, minmax(0, 1fr));
    gap: var(--space-6);
    position: fixed;
    top: var(--staff-nav-height, 58px);
    left: 0;
    right: 0;
    z-index: var(--z-sticky);
    padding: var(--space-6) var(--layout-gutter);
    border-bottom: 1px solid var(--border);
    background: var(--surface-raised);
    box-shadow: var(--shadow-lg);
    visibility: hidden;
  }
  .settings-quick.is-shown {
    visibility: visible;
  }
}
/* A phone, and a phone on its side: the gaps and the sides a little tighter,
   so six 44px links fit across the narrowest phone (320px) beside its safe
   areas, and the row a little shorter on a phone held sideways, where the
   page has under 400px of height between the top bar and the tab row. */
@media (max-width: 640px), (max-width: 899px) and (max-height: 599px) {
  .settings-quick {
    gap: var(--space-4);
    padding: var(--space-4) max(var(--space-8), env(safe-area-inset-right)) var(--space-4) max(var(--space-8), env(safe-area-inset-left));
  }
}
.settings-quick__link {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: var(--touch-min);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface-raised);
  color: var(--accent);
}

.settings-quick__link:active {
  background: var(--tint-blue);
}

@media (hover: hover) {
  .settings-quick__link:hover {
    background: var(--tint-blue);
  }
}
.settings-quick__link[aria-current=location] {
  background: var(--tint-blue);
  border-color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent);
}

/* While the compact row can show, whatever the browser scrolls into view - a
   control taking focus - lands under it as well as under the top bar. */
:root:has(.settings-quick) {
  scroll-padding-top: calc(var(--staff-nav-height, 58px) + var(--staff-current-height, 0px) + var(--settings-quick-height, 0px) + var(--space-8));
}

/* ------------------------------------------------------------- sections */
/* Each section card: its h2, its purpose line under it, then its parts. A
   section or a part reached by a link lands just under the chrome (and the
   compact row on an upright iPad or a phone) and wears a static accent edge
   on its left (:target) until another link is followed: no flash, no fade,
   no timer. The page's scroll-padding (above, and :root's) already keeps the
   chrome and the row clear; a link's own margin is the little more that
   puts the section on the line settings.js marks the section in view by
   (the chrome, the row and --space-16), so the section a link goes to is
   the one marked. Until 1.12.0 the margin held the chrome's height again,
   on top of the padding: a link landed a whole top bar further down, and the
   section before it was the one marked. */
.settings-section > .card__head {
  margin-bottom: 0;
}

.settings-line {
  margin: var(--space-4) 0 var(--space-14);
  font-size: var(--text-sm);
  line-height: var(--leading-base);
  color: var(--text-muted);
}

.settings-section, .settings-part, .settings-block {
  scroll-margin-top: var(--space-8);
}

.settings-part, .settings-block {
  position: relative;
}

.settings-section:target {
  box-shadow: inset var(--target-edge) 0 0 var(--accent), var(--shadow-md);
}

.settings-part:target::before, .settings-block:target::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: calc(-1 * var(--space-12));
  width: var(--target-edge);
  border-radius: var(--radius-pill);
  background: var(--accent);
}

/* Parts, each after a hairline. Sub-headings are h3 at 16px, a step under
   the section's 18px h2. A part that opens its card sits under the card's
   purpose line, not a sub-heading's gap below it; a switch or a choice that
   opens a part after the rule gets the room above it a sub-heading would
   have had. */
.settings-part + .settings-part {
  margin-top: var(--space-24);
  padding-top: var(--space-4);
  border-top: 1px solid var(--border);
}

.settings-sub {
  margin: var(--space-20) 0 var(--space-8);
  font-size: var(--text-base);
  font-weight: 700;
  line-height: var(--leading-base);
}

.settings-sub__hint {
  margin: 0 0 var(--space-10);
}

.settings-line + .settings-part > .settings-sub:first-child {
  margin-top: 0;
}

.settings-part + .settings-part > .toggle:first-child {
  margin-top: var(--space-20);
}

.settings-part + .settings-part > .choice-list:first-child {
  margin-top: var(--space-20);
}

/* "Introduce each heat" for each round (1.11.0): the four rounds one under
   another, each its own row of the three choices under the round's name,
   and the hint on what each choice does under the last. Devin's dogs in each
   round (1.12.0) are laid out the same way. */
.intro-rounds, .mascot-rounds {
  display: grid;
  gap: var(--space-16);
  margin-bottom: var(--space-12);
}

/* "recommended" (decision 8): a small muted tag after the words of each
   ruled default option. It marks a fixed option, so it never moves. */
.choice-list__tag {
  font-size: var(--text-caption);
  font-weight: 600;
  color: var(--text-muted);
}

/* Use recommended, under its group, 44px. */
.settings-reset {
  margin-top: var(--space-12);
}

/* Break between rounds: its four settings nested under the break's own
   switch behind a rule, For the last one level further under the heats.
   Nothing here greys while the break is off (decision 7: set them ahead). */
.settings-nested {
  margin-top: var(--space-12);
  padding-left: var(--space-16);
  border-left: var(--space-2) solid var(--border-strong);
}

.settings-nested > .settings-block + .settings-block {
  margin-top: var(--space-16);
}

.settings-nested > .settings-reset {
  margin-top: var(--space-16);
}

/* For the last while the next round's heats are off: the .toggle--taken
   look on a choice list - greyed, nothing to press, its value kept. */
.choice-list--taken {
  color: var(--text-muted);
}

.choice-list--taken .choice-list__opt,
.choice-list--taken .choice-list__opt:has(input:checked),
.choice-list--taken .choice-list__opt:active {
  cursor: default;
  border-color: var(--border);
  background: var(--surface-sunken);
  box-shadow: none;
  color: var(--text-muted);
}

@media (hover: hover) {
  .choice-list--taken .choice-list__opt:hover {
    background: var(--surface-sunken);
  }
}
/* A status line (the notes', the quick messages', the hosts' link's) stays
   in the page while empty, and holds unseen in the same grid cell the
   longest thing it says (.settings-status__room; the quick messages' is the
   longest move line for the list as it stands, settings.js), so words
   arriving or changing - a poll saying another device saved the notes -
   move nothing under them (no-grow). The margins are the hint's own: 4px
   above, and the 1em a paragraph had below. */
.settings-status {
  display: grid;
  min-height: calc(var(--text-sm) * var(--leading-base));
  margin: var(--space-4) 0 var(--space-14);
}

.settings-status > .field__hint {
  grid-area: 1/1;
  min-width: 0;
  margin: 0;
}

.settings-status__room {
  visibility: hidden;
}

/* The section's last row: a link to where the rest of the results email is
   written, set apart like a part. */
.settings-foot {
  margin-top: var(--space-24);
  padding-top: var(--space-4);
  border-top: 1px solid var(--border);
}

.settings-row-link {
  display: flex;
  align-items: center;
  gap: var(--space-12);
  min-height: var(--touch-base);
  margin-top: var(--space-16);
  padding: var(--space-10) var(--space-12);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface-raised);
  color: var(--text);
  font-weight: 600;
  text-decoration: none;
}

.settings-row-link > .icon:first-child {
  color: var(--accent);
}

.settings-row-link__words {
  flex: 1 1 auto;
  min-width: 0;
}

.settings-row-link__chevron {
  color: var(--text-muted);
  transform: rotate(-90deg);
}

.settings-row-link:active {
  background: var(--tint-blue);
}

@media (hover: hover) {
  .settings-row-link:hover {
    background: var(--tint-blue);
  }
}
/* -------------------------------------------------- the Announcer's parts */
/* The Champion's "To read out", with a preview drawn by the Announcer's own
   rule beside the box - on an upright iPad and from 1024px - and under it on
   a phone and at 900-1023px, where the side list leaves the content column
   too narrow for two. */
.read-edit {
  display: grid;
  gap: var(--space-16);
}

@media (min-width: 641px) and (max-width: 899px), (min-width: 1024px) {
  .read-edit {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    align-items: start;
  }
}
.read-edit__field {
  margin: 0;
}

.read-preview {
  margin: 0;
  min-width: 0;
}

.read-preview__caption {
  margin: 0 0 var(--space-6);
  font-size: var(--text-sm);
  font-weight: 700;
  color: var(--text-muted);
}

.read-preview__card {
  padding: var(--space-12) var(--space-14);
  border: 1px solid var(--tint-gold-line);
  border-left: var(--status-edge) solid var(--gold);
  border-radius: var(--radius-md);
  background: var(--surface-raised);
}

.read-preview__label {
  margin: 0 0 var(--space-6);
  font-size: var(--text-caption);
  font-weight: 800;
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--on-tint-gold);
}

.read-preview__words {
  display: grid;
  gap: var(--space-2);
  font-size: var(--text-sm);
  line-height: var(--leading-base);
  font-weight: 600;
  overflow-wrap: break-word;
}

.read-preview__none {
  margin: 0;
  font-size: var(--text-sm);
  color: var(--text-muted);
}

/* Quick messages, one row each: the whole text, then Edit and a remove
   button kept apart from Edit - or, while the order is being changed, Move
   up and Move down, 44x44 each. A removed one keeps its place with Undo. On
   a phone the words take the row's width and the buttons the line under
   them. */
.pick-list {
  display: grid;
  gap: var(--space-8);
  margin: 0 0 var(--space-12);
  padding: 0;
  list-style: none;
}

.pick-row {
  display: flex;
  align-items: center;
  gap: var(--space-10);
  padding: var(--space-8) var(--space-8) var(--space-8) var(--space-14);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface-raised);
}

/* A row's words drawn as the Announcer draws them (Staff.noteWords): the
   .rich-* blocks stacked, never the marks. */
.pick-row__text {
  flex: 1 1 auto;
  min-width: 0;
  margin: 0;
  display: grid;
  gap: var(--space-2);
  font-size: var(--text-base);
  overflow-wrap: break-word;
}

.pick-row__tools {
  display: flex;
  flex: none;
  align-items: center;
  gap: var(--space-10);
}

.pick-row__remove {
  margin-left: var(--space-10);
  min-width: var(--touch-min);
  padding: var(--space-6);
}

.pick-row__move {
  min-width: var(--touch-min);
  padding: var(--space-6);
}

.pick-row--removed {
  border-style: dashed;
  color: var(--text-muted);
}

.pick-row--removed .pick-row__text {
  font-size: var(--text-sm);
}

@media (max-width: 640px) {
  .pick-row {
    display: grid;
    gap: var(--space-8);
    padding-top: var(--space-10);
  }
  .pick-row > .btn {
    justify-self: start;
  }
}
