/* Raybike UI. The rules are written down in docs/DESIGN.md; this file implements them as tokens (1), components (2)
   and the screen layouts that arrange those components (3). Screens add no looks of their own, and nothing here uses
   a size that is not a token. tools/ui-audit.mjs measures the result on the tablet.
   Performance rules for the tablet: no backdrop-filter, no blurred shadows, opaque surfaces over the 3D canvas. */

/* ================================================= 1. tokens ================================================= */
:root {
  color-scheme: dark;

  /* surfaces: neutral graphite, darkest to lightest */
  --surface-0: #0b0c0f;                    /* page; ink on coloured fills; empty tracks */
  --surface-1: #15171c;                    /* panels, lists, dialogs, the ride HUD */
  --surface-2: #20232a;                    /* controls; the selected row */
  --surface-3: #2d313a;                    /* pressed control; the selected tab */
  --line: rgb(255 255 255 / 0.08);         /* hairlines inside a surface, panel edges */
  --line-strong: rgb(255 255 255 / 0.18);  /* control edges */
  --scrim: rgb(11 12 15 / 0.8);            /* over the 3D world, behind dialogs and full-screen menus */
  --scrim-0: rgb(11 12 15 / 0);
  --scrim-floor: rgb(11 12 15 / 0.92);     /* the dark the ride cover's scrim ends in */

  /* text */
  --text-1: #f4f5f7;                       /* values, titles: 16:1 on surface-1 */
  --text-2: #b9bec8;                       /* labels, body: 9.5:1 */
  --text-3: #8b919d;                       /* captions and hints in menus only, never in the ride HUD: 5.5:1 */

  /* roles */
  --accent: #ffb40a;                       /* the one primary action per screen, the selection marker: the logo's lightning */
  --accent-press: #d99500;
  --on-accent: #2a1c00;
  --positive: #3ddc97;
  --warning: #ffcf3f;                      /* attention, and what was earned: stars, medals */
  --danger: #ff6b6b;
  --danger-press: #d64f4f;
  --on-danger: #2a0606;
  --bolt: #6fe3ff;                         /* the garage currency */
  --effort-low: #7289dc;                   /* the effort meter: below, on and above the segment's target */
  --effort-on: #3ddc97;
  --effort-high: #ffb40a;
  --zone: var(--positive);                 /* colour of the current segment's intensity, set from the ride */
  /* the run (the same colours as in the world, game/Run.js) */
  --heart: #ffc533;
  --coin: #ffcf3f;
  --spark: #a6ff5e;                        /* round 7: sparks, the ride's own experience (lime: apart from coins, bolts, danger, the boss) */
  --up-rapid: #40bfff;
  --up-shield: #ffc733;
  --up-orb: #b866ff;
  --boss: #c45cff;
  /* round 7: an item's rarity (its celebration's bar and caption, the offer's cards; gfx/Ceremony.js RARITY in the world) */
  --rarity-common: #ffd77a;
  --rarity-rare: #5cbcff;
  --rarity-epic: #e070ff;

  /* type: one family. Menus use h1, h2, num, title, body/strong and label; the ride HUD uses hero, hud-*. */
  --font: system-ui, "Segoe UI", Roboto, sans-serif;
  --t-h1: 700 40px/1.1 var(--font);        /* the selected ride, dialog titles */
  --t-h2: 700 28px/1.15 var(--font);       /* screen titles; HUD banner */
  --t-num: 700 28px/1 var(--font);         /* stat values */
  --t-title: 600 20px/1.25 var(--font);    /* buttons, row titles, table values */
  --t-body: 500 16px/1.4 var(--font);      /* running text: the smallest sentence in a menu */
  --t-strong: 600 16px/1.25 var(--font);   /* tabs */
  --t-label: 700 14px/1.2 var(--font);     /* captions in caps and digits only */
  --t-hero: 800 56px/1 var(--font);        /* ride HUD: the power reading */
  --t-hud-value: 800 44px/1 var(--font);   /* ride HUD: cadence, energy, speed */
  --t-hud-mid: 800 32px/1 var(--font);     /* ride HUD: place, segment, score */
  --t-hud-text: 700 22px/1.1 var(--font);  /* ride HUD: the smallest running text */
  --t-hud-label: 700 16px/1 var(--font);   /* ride HUD: captions in caps and digits only */
  --logo-l: 80px;                          /* height of the logo artwork on home and start-up */

  /* control heights: every control is one of these, and controls in one row share one */
  --h-s: 48px;                             /* inside the ride HUD; inside a tab bar */
  --h-m: 56px;                             /* the default: buttons, steppers, tab bars, everything in a header */
  --h-l: 72px;                             /* main navigation; list rows */

  /* spacing */
  --s0: 2px; --s1: 4px; --s2: 8px; --s3: 12px; --s4: 16px; --s5: 24px; --s6: 32px; --s7: 40px;
  /* radii */
  --r-xs: 3px; --r-s: 8px; --r-m: 12px; --r-l: 16px; --r-pill: 999px;

  /* grid: 12 fluid columns with 24 px gutters between 40 px side margins; panes are whole columns. At the reference
     width of 1280 px a column is 78 px (spans 384, 486, 588, 690, 792). Dialogs keep the reference widths of 6 and 8 columns. */
  --margin: 40px;
  --margin-y: 24px;
  --gutter: 24px;
  --col: calc((100vw - 2 * var(--margin) - 11 * var(--gutter)) / 12);
  --span-3: calc(3 * var(--col) + 2 * var(--gutter));
  --span-4: calc(4 * var(--col) + 3 * var(--gutter));
  --span-5: calc(5 * var(--col) + 4 * var(--gutter));
  --span-6: calc(6 * var(--col) + 5 * var(--gutter));
  --span-7: calc(7 * var(--col) + 6 * var(--gutter));
  --span-8: calc(8 * var(--col) + 7 * var(--gutter));
  --dialog-s: 588px;
  --dialog-l: 792px;
  /* ride HUD (round 10): the width of the effort gauge's meter at the right edge
     (the watts' plate riding on it included), and of
     the top centre's feed (the boss's health, the banner, the celebrations, a toast), which stays clear of the hearts and
     the score beside it */
  --gauge: 120px;
  --feed: 640px;
  --chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23f4f5f7' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 9l7 7 7-7'/%3E%3C/svg%3E");

  font: var(--t-body);
}

* { box-sizing: border-box; margin: 0; }
html, body { height: 100%; overflow: hidden; background: var(--surface-0); color: var(--text-1); touch-action: none; user-select: none; -webkit-tap-highlight-color: transparent; }
[hidden] { display: none !important; }
b, output, .num { font-variant-numeric: tabular-nums; }
h1 { font: var(--t-h1); letter-spacing: -0.01em; }
h2 { font: var(--t-h2); letter-spacing: -0.01em; }
.label { display: flex; align-items: center; gap: var(--s2); font: var(--t-label); letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-3); }
.label .icon { width: 20px; height: 20px; }
.hint { color: var(--text-3); }
.spacer { flex: 1; }
.compact-only { display: none; }           /* shown only in the compact layout; .wide-only is its opposite (section 4) */
#stage { position: fixed; inset: 0; width: 100%; height: 100%; display: block; }

/* ================================================ 2. components ================================================ */

/* ---- icon: one sprite, drawn on a 24 grid with 2 px round strokes; 24 px in controls, 1.2 em beside text ---- */
.icon-sprite { position: absolute; width: 0; height: 0; overflow: hidden; }
.icon { flex: none; width: 1.2em; height: 1.2em; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.icon.fill { fill: currentColor; }

/* ---- logo: the supplied artwork (lightning R and white letters, black keyed out; tools/make-icons.mjs), sized by height ---- */
.logo { flex: none; display: block; width: auto; }
.logo-l { height: var(--logo-l); }

/* ---- button: secondary (default) / primary / quiet / danger; heights from the control scale ---- */
.btn { flex: none; display: inline-flex; align-items: center; justify-content: center; gap: var(--s2); height: var(--h-m); padding: 0 var(--s5); font: var(--t-title); color: var(--text-1);
  background: var(--surface-2); border: 1px solid var(--line-strong); border-radius: var(--r-m); white-space: nowrap; transition: transform 0.08s ease-out, background-color 0.08s; }
.btn:active { background: var(--surface-3); transform: scale(0.96); }
.btn:disabled { opacity: 0.4; transform: none; }
.btn .icon { width: 24px; height: 24px; }
.btn-primary { background: var(--accent); border-color: transparent; color: var(--on-accent); }
.btn-primary:active { background: var(--accent-press); }
.btn-primary:disabled { background: var(--surface-2); border-color: var(--line-strong); color: var(--text-1); }
.btn-quiet { background: transparent; border-color: transparent; color: var(--text-2); }
.btn-danger { color: var(--danger); }
.btn-danger.armed { background: var(--danger); border-color: transparent; color: var(--on-danger); }
.btn-danger.armed:active { background: var(--danger-press); }
.btn-s { height: var(--h-s); padding: 0 var(--s4); }
.btn-l { height: var(--h-l); }
.btn-icon { width: var(--h-m); padding: 0; }
.btn-icon.btn-s { width: var(--h-s); }
/* navigation button: icon, a title over one line that says what waits behind it, chevron */
.btn-nav { justify-content: flex-start; gap: var(--s4); text-align: left; }
.btn-nav .icon:last-child { margin-left: auto; opacity: 0.6; }
.btn-nav .icon { width: 20px; height: 20px; }   /* round 10: the navigation's icons smaller (the user) */
/* a count beside a title (the bolts on home's Garage tile): a small pill, its topic's icon and the number in the topic's
   colour (the icon map); read-only, so not dressed as a control */
.count { display: inline-flex; align-items: center; gap: var(--s1); margin-left: var(--s3); padding: var(--s0) var(--s3); border-radius: var(--r-pill);
  background: var(--surface-3); font: var(--t-strong); }
.count .icon { width: 16px; height: 16px; }
.count.bolts { color: var(--bolt); }
.btn-text { display: flex; flex-direction: column; min-width: 0; }
.btn-text strong { font: inherit; }
.btn-text span { font: var(--t-body); color: var(--text-2); }
.btn-primary .btn-text span { color: inherit; opacity: 0.8; }

/* ---- device status: the states of the bike, the heart-rate monitor and Home Assistant in one control (home, ride selection,
   the ride HUD); it opens the Devices screen. Per device its icon and a mark (check, dots, cross, dash) in the state's
   colour, so colour is never the only signal; the bpm while a monitor reports (ui/DeviceStatus.js) ---- */
.devstat { gap: var(--s4); padding: 0 var(--s4); }
.devstat[data-alert="true"] { border-color: var(--danger); }
.devstat-item { display: inline-flex; align-items: center; gap: var(--s1); }
.devstat-item .icon.mark { width: 20px; height: 20px; }
.devstat-item b { font: inherit; font-variant-numeric: tabular-nums; }
.devstat-text { font: inherit; }
.ok > .mark, .device-state.ok { color: var(--positive); }
.wait > .mark, .device-state.wait { color: var(--warning); }
.bad > .mark, .device-state.bad { color: var(--danger); }
.off > .mark, .device-state.off { color: var(--text-2); }
.wait > .mark, .device-state.wait .icon { animation: blink 0.8s steps(2) infinite; }
@keyframes blink { to { opacity: 0.25; } }

/* ---- panel: the one container ---- */
.panel { display: flex; flex-direction: column; gap: var(--s4); padding: var(--s5); background: var(--surface-1); border: 1px solid var(--line); border-radius: var(--r-l); }

/* ---- stat: a value over its caption, as type (no box). `.stats` lays them out in --cols equal columns ---- */
.stats { --cols: 3; display: flex; flex-wrap: wrap; gap: var(--s4) var(--s5); }
.stats > .stat { flex: 0 0 calc((100% - (var(--cols) - 1) * var(--s5)) / var(--cols) - 0.1px); }
.stats.inline { gap: var(--s4) var(--s7); }
.stats.inline > .stat { flex: none; }
.stat { display: flex; flex-direction: column; gap: var(--s1); min-width: 0; }
.stat b { display: flex; align-items: baseline; gap: var(--s1); min-height: 28px; font: var(--t-num); white-space: nowrap; }
.stat b small { font: var(--t-title); line-height: 1; color: var(--text-2); }
.stat b .stars { align-self: center; font-size: 28px; }
.stat b > .icon { align-self: center; width: 24px; height: 24px; margin-right: var(--s1); }   /* a value led by its topic's icon */
.stat > span { font: var(--t-label); letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-3); white-space: nowrap; }
.stat.end { margin-left: auto; align-items: flex-end; }
.stat.reward b { color: var(--warning); }
.stat.bolts b { color: var(--bolt); }

/* ---- header stat: one read-only fact at the right of a screen header (balance, streak) ---- */
.head-stat { display: inline-flex; align-items: baseline; gap: var(--s2); font: var(--t-title); color: var(--text-2); white-space: nowrap; }
.head-stat b { font: var(--t-num); color: var(--text-1); }
.head-stat .icon { align-self: center; width: 24px; height: 24px; }
.head-stat.bolts .icon, .head-stat.bolts b { color: var(--bolt); }
.head-stat.reward .icon, .head-stat.reward b { color: var(--warning); }

/* ---- tabs: one bar on the control scale; the selected tab is the lighter surface ---- */
.tabs { flex: none; display: flex; gap: var(--s1); height: var(--h-m); padding: var(--s1); background: var(--surface-1); border-radius: var(--r-m); }
.tab { flex: 1 1 0; display: inline-flex; align-items: center; justify-content: center; min-width: 0; padding: 0 var(--s2); font: var(--t-strong); color: var(--text-2); background: transparent; border: 0; border-radius: var(--r-s); white-space: nowrap; transition: background-color 0.08s; }
.tab:active { background: var(--surface-2); }
.tab[aria-selected="true"] { background: var(--surface-3); color: var(--text-1); }

/* ---- stepper: minus, value, plus ---- */
.stepper { flex: none; display: flex; align-items: center; gap: var(--s2); }
.stepper output { min-width: 88px; text-align: center; font: var(--t-num); }
.stepper output small { font: var(--t-title); color: var(--text-2); margin-left: var(--s1); }

/* ---- difficulty: the one control for how hard rides are (caption, stepper, help) ---- */
.difficulty { display: flex; flex-direction: column; gap: var(--s4); min-width: 0; }
.difficulty-row { display: flex; align-items: center; gap: var(--s4); }
.difficulty-row .caption { flex: 1; display: flex; flex-direction: column; gap: var(--s1); min-width: 0; }
.difficulty-row .caption em { font: var(--t-body); font-style: normal; color: var(--text-2); white-space: nowrap; }
.difficulty .help { padding: var(--s4); font: var(--t-body); color: var(--text-1); background: var(--surface-2); border-radius: var(--r-m); }
.difficulty .help b { font-weight: 700; }
/* ---- sound: Music and Effects, a row each (home's Sound dialog and the pause menu; SoundSettings in kit.js) ---- */
.sound { display: flex; flex-direction: column; gap: var(--s4); min-width: 0; }
/* a switch in a settings row: On or Off in words, one width (Settings, Graphics) */
.difficulty-row > .btn.toggle { flex: none; min-width: 120px; }
.difficulty-row > .btn.toggle[aria-pressed="true"] .icon { color: var(--positive); }
/* Settings: the settings on the left (Sound, Graphics, the way to Devices), the credits on the right */
#settings .difficulty-row .caption em { white-space: normal; }


/* ---- list: one surface, rows divided by hairlines. A row has a lead, one or two lines of text and a trailing slot;
        a button row can be chosen, and the chosen one carries the accent marker ---- */
.list { display: flex; flex-direction: column; min-height: 0; padding: 0; list-style: none; background: var(--surface-1); border: 1px solid var(--line); border-radius: var(--r-l); }
.row { flex: none; display: flex; align-items: center; gap: var(--s4); width: 100%; min-height: var(--h-l); padding: var(--s2) var(--s5) var(--s2) var(--s4); text-align: left; font: var(--t-body); color: var(--text-1);
  background: transparent; border: 0; border-top: 1px solid var(--line); border-radius: 0; }
.row:first-child { border-top-color: transparent; }
button.row { height: var(--h-l); transition: background-color 0.08s; }
button.row:active { background: var(--surface-3); }
.row.selected { background: var(--surface-2); box-shadow: inset 4px 0 0 var(--accent); }
.row-lead { flex: none; display: flex; align-items: center; justify-content: center; width: 40px; height: 40px; color: var(--text-2); }
.row-lead .icon { width: 28px; height: 28px; }
.row-lead.paint { border-radius: 50%; box-shadow: inset 0 0 0 2px rgb(255 255 255 / 0.5); }
.row-text { flex: 1; display: flex; flex-direction: column; gap: var(--s1); min-width: 0; }
.row-text strong { display: flex; align-items: center; gap: var(--s3); min-width: 0; font: var(--t-title); white-space: nowrap; }
.row-text strong > span:not([class]) { min-width: 0; overflow: hidden; text-overflow: ellipsis; }   /* the title's text gives way ... */
.row-text strong > [class] { flex: none; }                                                           /* ... what follows it (stars, pips) does not */
.row-text > span { color: var(--text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.row-text > span:empty { display: none; }
.row-text > span > span + span::before { content: "\b7"; margin: 0 0.4em; }
.row-text .season .icon { width: 16px; height: 16px; margin-right: 0.3em; vertical-align: -3px; }   /* a ride's season in its detail line */
.row-trail { flex: none; display: flex; align-items: center; gap: var(--s5); }
.row-head, .list-head { min-height: 0; font: var(--t-label); letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-3); }
.row-head { position: sticky; top: 0; z-index: 1; padding-top: var(--s3); padding-bottom: var(--s3); background: var(--surface-1); border-bottom: 1px solid var(--line); }
.row-head .row-text strong { font: inherit; letter-spacing: inherit; }
.row-head .row-lead { height: 0; }
.row-head + .row { border-top-color: transparent; }
.list-head { padding: var(--s4) var(--s5) var(--s2) var(--s4); border-top: 1px solid var(--line); }
.row-head + .list-head { border-top-color: transparent; }
.list-head + .row { border-top-color: transparent; }
.cell { flex: none; text-align: right; font: var(--t-title); font-variant-numeric: tabular-nums; white-space: nowrap; }
.cell small { margin-left: var(--s1); font: var(--t-body); color: var(--text-2); }
.row-head .cell { font: inherit; }

/* ---- swatches: a choice of colours as a list row (the garage's skin tones): 56 px rounds, edged like a paint lead; the chosen
        one has the accent ring (a surface-1 gap, then the accent) and a check in ink that reads on it. The row's 12 px above
        and below keep the ring clear of the list's edge ---- */
.swatches { flex-wrap: wrap; gap: var(--s4); padding-top: var(--s3); padding-bottom: var(--s3); }
.swatch { flex: none; display: inline-flex; align-items: center; justify-content: center; width: var(--h-m); height: var(--h-m); padding: 0; color: var(--text-1);
  border: 0; border-radius: 50%; box-shadow: inset 0 0 0 2px rgb(255 255 255 / 0.5); transition: transform 0.08s; }
.swatch.light { color: var(--surface-0); }
.swatch:active { transform: scale(0.96); }
.swatch[aria-checked="true"] { box-shadow: inset 0 0 0 2px rgb(255 255 255 / 0.5), 0 0 0 3px var(--surface-1), 0 0 0 6px var(--accent); }
.swatch .icon { width: 24px; height: 24px; }
/* shop swatches (the garage's kits and designs): the same rounds, each saying whether it is owned - a badge at its foot,
   the bolt where it can be bought, the lock where bolts are short; a design is a rounded square holding its jersey */
.swatch.shop { position: relative; }
.swatch-badge { position: absolute; right: -4px; bottom: -4px; display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px;
  border-radius: 50%; background: var(--surface-1); box-shadow: 0 0 0 1px var(--line-strong); color: var(--bolt); }
.swatch-badge.short { color: var(--text-3); }
.swatch .swatch-badge .icon { width: 16px; height: 16px; }
.swatch.tile { border-radius: var(--r-m); background: var(--surface-2); box-shadow: inset 0 0 0 1px var(--line-strong); }
.swatch.tile[aria-checked="true"] { box-shadow: inset 0 0 0 1px var(--line-strong), 0 0 0 3px var(--surface-1), 0 0 0 6px var(--accent); }
.swatch.tile canvas { width: var(--h-m); height: var(--h-m); border-radius: var(--r-m); }
.swatch.tile > .icon { position: absolute; color: var(--text-1); filter: drop-shadow(0 0 2px rgb(0 0 0 / 0.8)); }
.designs { gap: var(--s3); }
/* a group of kits: its name in a column of its own, the swatches beside it (stacked on a narrow list) */
.kit-group { display: grid; grid-template-columns: 96px 1fr; align-items: center; }
.kit-group > span { font: var(--t-body); color: var(--text-2); }
.kit-group > .swatches { display: flex; padding: 0; border: 0; min-height: 0; }
@media (max-width: 1279px) { .kit-group { grid-template-columns: 1fr; gap: var(--s2); } }

/* ---- sparkline: a ride's intensity profile in the space of an icon ---- */
.spark { flex: none; width: 96px; height: 36px; fill: var(--text-3); }
.selected .spark { fill: var(--text-1); }

/* ---- place badge, stars, level pips ---- */
.badge { flex: none; display: flex; align-items: center; justify-content: center; width: 40px; height: 40px; border-radius: 50%; font: var(--t-title); background: var(--surface-3); color: var(--text-1); }
.badge.gold { background: var(--warning); color: #3a2600; }
.badge.silver { background: #c6ccd8; color: #20242e; }
.badge.bronze { background: #d98f52; color: #2e1503; }
.badge.none { background: transparent; box-shadow: inset 0 0 0 1px var(--line-strong); color: var(--text-3); }
.badge .icon { width: 20px; height: 20px; }
.stars { display: inline-flex; gap: var(--s0); color: var(--warning); }
.stars .icon { width: 1em; height: 1em; }
.stars .off { color: var(--text-3); opacity: 0.6; }
.pips { display: inline-flex; gap: var(--s1); }
.pips i { width: 16px; height: 6px; border-radius: var(--r-xs); background: var(--surface-3); }
.pips i.on { background: var(--text-1); }

/* ---- meter: a share of something ---- */
.meter { height: 8px; border-radius: var(--r-pill); background: var(--surface-0); overflow: hidden; }
.meter i { display: block; height: 100%; width: 0; border-radius: var(--r-pill); background: var(--text-1); }

/* ---- price and state at the end of a row ---- */
.cost { display: inline-flex; align-items: center; gap: var(--s2); font: var(--t-title); color: var(--bolt); }
.cost.short { color: var(--text-3); }
.state { display: inline-flex; align-items: center; gap: var(--s2); font: var(--t-title); color: var(--text-2); }
.state.on { color: var(--positive); }

/* ---- notice (stays), toast (leaves), empty state ---- */
.notice { display: flex; align-items: center; gap: var(--s3); padding: var(--s4); border-radius: var(--r-m); background: var(--surface-2); color: var(--text-1); font: var(--t-body); }
.notice .icon { width: 24px; height: 24px; color: var(--warning); }
/* toast: top centre, in the first free line from the top (kit.js PlacedToast sets left and top; DESIGN.md, "Toast") */
.toast { position: fixed; left: 0; top: 0; z-index: 20; max-width: min(var(--dialog-s), 100vw - 2 * var(--margin)); padding: var(--s3) var(--s5); font: var(--t-title); color: var(--text-1); text-align: center;
  background: var(--surface-3); border: 1px solid var(--line-strong); border-radius: var(--r-m); visibility: hidden; pointer-events: none; }
.toast.show { visibility: visible; }
.toast.in-hud { max-width: var(--feed); }   /* in the ride HUD: in the top centre, between the hearts and the score */
.empty { flex: 1; align-items: center; justify-content: center; gap: var(--s4); text-align: center; color: var(--text-2); }
.empty > .icon { width: 48px; height: 48px; color: var(--text-3); }
.empty strong { font: var(--t-h2); color: var(--text-1); }
.empty p { max-width: 420px; }

/* ---- screen scaffold: header row at the top margin (back, title, right slot), then the body on the grid.
        A scrim keeps text readable over the live world and leaves the rest of it visible ---- */
.screen { position: fixed; inset: 0; display: flex; flex-direction: column; }
.menu-screen { gap: var(--gutter); padding: var(--margin-y) var(--margin); }
.menu-screen:not([hidden]), .overlay:not([hidden]) > .dialog { animation: screen-in 0.18s ease-out; }
@keyframes screen-in { from { opacity: 0; transform: translateY(8px); } }
.scrim-top { background: linear-gradient(180deg, var(--scrim) 0, var(--scrim-0) 168px); }
.scrim-left { background: linear-gradient(180deg, var(--scrim) 0, var(--scrim-0) 168px), linear-gradient(90deg, var(--scrim) 0, rgb(11 12 15 / 0.55) 30%, var(--scrim-0) 56%); }
.scrim-full { background: var(--scrim); }
.screen-head { flex: none; display: flex; align-items: center; gap: var(--s4); height: var(--h-m); }
.screen-head h2 { margin-right: auto; }
.screen-body { flex: 1; display: flex; gap: var(--gutter); min-height: 0; }
.pane { display: flex; flex-direction: column; gap: var(--s4); min-width: 0; min-height: 0; }
.pane-4 { flex: 0 0 var(--span-4); }
.pane-5 { flex: 0 0 var(--span-5); }
.pane-6 { flex: 0 0 var(--span-6); }
.pane-fill { flex: 1; }
.pane-3 { flex: 0 0 var(--span-3); }
.pane.scroll > * { flex: none; }

/* ---- scrolling: lists, a scrolling pane and a dialog's body scroll vertically when their content is taller than they
        are. While there is more below, the bottom edge fades out under a chevron (kit.js scrollHint sets .more). The
        browser's own scrollbar is off: on the tablet it stays on screen as a grey bar over the edge of the panels. ---- */
.list, .pane.scroll, .dialog-body { --fade: var(--surface-1); overflow: hidden auto; touch-action: pan-y; scrollbar-width: none; }
.pane.scroll { --fade: var(--surface-0); }
.scrolls::after { content: ""; display: none; position: sticky; bottom: 0; z-index: 2; flex: none; height: 48px; margin-top: -48px;
  background: var(--chevron) center bottom var(--s1) / 20px no-repeat, linear-gradient(180deg, transparent, var(--fade) 80%); pointer-events: none; }
.scrolls.more::after { display: block; }

/* ---- dialog scaffold: pause and summary are the same box, 6 or 8 columns wide ---- */
.overlay { align-items: center; justify-content: center; padding: var(--margin-y) var(--margin); background: var(--scrim); z-index: 10; }
.dialog { display: flex; flex-direction: column; gap: var(--s5); width: var(--dialog-l); max-width: 100%; max-height: 100%; padding: var(--s6); background: var(--surface-1); border: 1px solid var(--line); border-radius: var(--r-l); }
.dialog-narrow { width: var(--dialog-s); }
.dialog-head { display: flex; flex-direction: column; gap: var(--s2); }
.dialog-head h1 { display: flex; align-items: center; gap: var(--s4); }
.dialog-head h1 .stars { font-size: 32px; }
.dialog-head p { color: var(--text-2); font: var(--t-title); font-weight: 500; }
.dialog-body { display: flex; flex-direction: column; gap: var(--s5); min-height: 0; }
.dialog-body > * { flex: none; }
.dialog-actions { display: flex; gap: var(--s4); }
.dialog-actions .btn { flex: 1 1 0; min-width: 0; }
.group { display: flex; flex-direction: column; gap: var(--s3); padding-top: var(--s4); border-top: 1px solid var(--line); }
/* points: what a dialog explains before it asks (icon, a title, one or two lines) */
.points { display: flex; flex-direction: column; gap: var(--s4); }
.point { display: flex; align-items: flex-start; gap: var(--s4); }
.point .icon { width: 24px; height: 24px; }
:where(.point .icon) { color: var(--text-2); }   /* weightless, so a topic's colour (the icon map, below) wins over it */
.point div { flex: 1; display: flex; flex-direction: column; gap: var(--s1); min-width: 0; }
.point strong { font: var(--t-title); }
.point span { color: var(--text-2); }
/* a point with a way out (the credits): the button opens the source in the browser */
.point > a.btn { flex: none; align-self: center; }
/* setting: a caption with the control that changes it (on/off says its state in words); field: a text box on the control
   scale, the app's only one (the Home Assistant dialog). The page turns off selection and touch gestures; the box needs both. */
.setting { display: flex; align-items: center; gap: var(--s4); }
.setting .caption { flex: 1; display: flex; flex-direction: column; gap: var(--s1); min-width: 0; }
.setting .caption strong { font: var(--t-title); }
.setting .caption span { color: var(--text-2); }
.setting .btn[aria-pressed="true"] .icon { color: var(--positive); }
.group.setting { flex-direction: row; }       /* a setting as a group of a dialog (the pause menu's devices) */
.field { display: flex; flex-direction: column; gap: var(--s2); }
.field input { width: 100%; min-width: 0; height: var(--h-m); padding: 0 var(--s4); font: var(--t-body); color: var(--text-1); background: var(--surface-0);
  border: 1px solid var(--line-strong); border-radius: var(--r-m); outline: none; user-select: text; touch-action: manipulation; }
.field input:focus { border-color: var(--text-2); }
.field input::placeholder { color: var(--text-3); }

/* ---- device card (Devices screen, ui/Devices.js): icon, name and the device's name; the state in words with its mark;
   live readings as stats; when it last reported; help when something went wrong; the actions that fit; a setting ---- */
.device-head { display: flex; align-items: center; gap: var(--s4); }
.device-state { display: flex; align-items: center; gap: var(--s2); font: var(--t-title); }
.device-state .icon { width: 24px; height: 24px; }
.device .notice { align-items: flex-start; }
.device-actions { display: flex; flex-wrap: wrap; gap: var(--s3); }
.device-actions .btn { flex: 1 1 auto; padding: 0 var(--s4); }
.head-stat.bad, .head-stat.bad .icon { color: var(--danger); }

/* ---- start-up and rotate: opaque full-screen states above everything. Loading shows the logo, a meter and one line of
        status, and fades into the screen behind it; a failure is a dialog that says what to do ---- */
.boot { z-index: 30; background: var(--surface-0); transition: opacity 0.25s ease-out; }
.boot.done { opacity: 0; pointer-events: none; }
.boot-box { display: flex; flex-direction: column; align-items: center; gap: var(--s5); width: var(--dialog-s); max-width: 100%; text-align: center; color: var(--text-2); }
.boot-box .meter { align-self: stretch; margin: 0 var(--s7); }
.boot-box .meter i { transition: width 0.3s ease-out; }
.boot-box > .icon { width: 48px; height: 48px; color: var(--text-3); }
.boot-box strong { font: var(--t-h2); color: var(--text-1); }
.dialog-head .detail { font: var(--t-body); color: var(--text-3); overflow-wrap: anywhere; }
.rotate { display: none; z-index: 40; background: var(--surface-0); }
@media (orientation: portrait) { .rotate { display: flex; } }

/* ---- chart: bars on a time axis; width is duration, height and colour are intensity ---- */
.bars { display: flex; align-items: flex-end; gap: var(--s0); width: 100%; height: 100%; }
.bars i { position: relative; flex-basis: 0; min-width: 1px; border-radius: var(--r-xs) var(--r-xs) 0 0; }
.bars i span { position: absolute; left: 0; right: 0; bottom: var(--s0); text-align: center; font: var(--t-label); line-height: 1; color: var(--surface-0); }
.chart { position: relative; height: 148px; padding-bottom: 28px; }   /* round 10: 148 px (176 before), so the world above Free ride's panel gets the room */
.chart-ftp { position: absolute; left: 0; right: 0; margin-bottom: 28px; border-top: 1px dashed var(--line-strong); pointer-events: none; }
.chart-ftp span { position: absolute; right: 0; bottom: var(--s1); font: var(--t-label); letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-2); }
.chart-axis { position: absolute; left: 0; right: 0; bottom: 0; height: 17px; }
.chart-axis span { position: absolute; font: var(--t-label); letter-spacing: 0.04em; text-transform: uppercase; color: var(--text-3); white-space: nowrap; }
.chart-axis span.end { transform: translateX(-100%); }

/* ---- item tile (game/items.js, ui/itemTiles.js): an item outside the world (the pause menu, the summary): a 52 px tile
        edged in the item's colour (--up, set per item), its icon in that colour and its level as pips; weapons square,
        relics round. Round 10: the ride HUD no longer has an item bar, the items are worn on the rider ---- */
.item-tiles { display: flex; flex-wrap: wrap; gap: var(--s2); }
.slot { --up: var(--line-strong); flex: none; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--s1); width: 52px; height: 52px;
  border: 2px solid var(--up); border-radius: var(--r-m); background: var(--surface-2); }
.slot.relic { border-radius: 50%; }
.slot > .icon { flex: none; width: 26px; height: 26px; color: var(--up); }
.slot .pips { gap: var(--s1); }
.slot .pips i { width: 6px; height: 6px; }
.slot .pips i.on { background: var(--up); }

/* ---- icon map (round 10, docs/DESIGN.md "Icon map"): one icon per topic everywhere, and a topic with a colour of its own
        carries it wherever its icon appears: icons.js gives every icon its topic's class (i-<name>). These rules weigh
        nothing (:where), so a state (a star not earned, a lost heart) or a filled surface (a primary button, a done stop)
        still decides the colour. Bolts are always cyan, coins gold, sparks lime, the boss purple. The colour is also the
        icon's --topic, so a place that colours its icons by context (a banner's kind, the summary's news) colours only
        the icons without a topic: `color: var(--topic, <its own>)` ---- */
:where(.icon.i-bolt) { --topic: var(--bolt); }
:where(.icon.i-coin) { --topic: var(--coin); }
:where(.icon.i-spark) { --topic: var(--spark); }
:where(.icon.i-star, .icon.i-score, .icon.i-xp) { --topic: var(--warning); }
:where(.icon.i-boss) { --topic: var(--boss); }
:where(.icon.i-heart) { --topic: var(--heart); }
:where(.icon.i-bolt, .icon.i-coin, .icon.i-spark, .icon.i-star, .icon.i-score, .icon.i-xp, .icon.i-boss, .icon.i-heart) { color: var(--topic); }
/* on a filled surface the icon is the surface's ink, whatever its topic */
.btn-primary .icon, .path-node .icon, .strip-stop .icon, .badge .icon { color: inherit; }

/* ================================================ 3. screen layouts ================================================ */

/* ---- home: brand top left, bike and utilities top right, profile and navigation bottom left, the rider on the right ---- */
#home .screen-head { align-items: flex-start; }
.home-col { justify-content: flex-end; gap: var(--s3); }
/* the version and the legal pages under home's tiles: small and quiet, each link still a 48 px touch target */
.legal-links { display: flex; align-items: center; gap: var(--s4); margin: 0; padding: 0 var(--s2); color: var(--text-3); }
.legal-links span { margin-right: auto; }
.legal-links a { display: inline-flex; align-items: center; min-height: 48px; color: var(--text-2); text-decoration: none; }
.legal-links a:hover, .legal-links a:focus-visible { color: var(--text-1); text-decoration: underline; }
.profile-top { display: flex; align-items: baseline; justify-content: space-between; gap: var(--s4); }
.profile-top strong { display: inline-flex; align-items: center; gap: var(--s2); font: var(--t-title); }
.profile-top strong .icon { width: 24px; height: 24px; }
.profile-top span { color: var(--text-2); }

/* ---- side screens (round 10, the user; ui/SideScreen.js, one component for Free ride and the campaign, so a change to one
        is a change to both): the rider in the hero shot on the left (main.js heroShot side -1), and the screen's UI in one
        panel at the bottom right, as tall as its content and never taller than the screen (the user: "don't waste the
        space, push the upper part down"): its header, its own part (the rides, or the path), the brief of the ride Start
        rides. Five columns, 420 to 600 px, so its left edge stays right of 0.35 of the width at every size (the hero shot
        keeps its rider there at 4:3 too). In the screen's top left corner the choice, go back or start (the user): Back and
        Start; the panel's top stays below their foot, so the top band is the world's ---- */
.side-screen { align-items: flex-end; justify-content: flex-end; }
.side { flex: 0 1 auto; width: clamp(420px, var(--span-5), 600px); min-height: 0; max-height: calc(100% - var(--h-l)); gap: var(--s2); padding: var(--s4); }
.side > * { flex: none; }
/* the header is compact (round 10, the user): 48 px controls, the title over a caption where one is needed */
.side > .screen-head { height: var(--h-s); gap: var(--s2); }
.head-title { margin-right: auto; display: flex; flex-direction: column; min-width: 0; }
.head-title h2 { line-height: 1; white-space: nowrap; }     /* the title and its caption within the 48 px row, on one line each */
.head-title .label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* the top left corner, over the sky (round 10, the user: "either start or go back"): Back, a 72 px round button on the
   panels' surface, and beside it Start, the screen's one primary button, raised ("a nice, round or more elevated start
   button"): 72 px, a pill, its word at --t-h2, lifted on a solid edge of the accent's pressed shade and ringed by a solid,
   translucent band of the accent (no blur: the tablet's rule); pressed, it sinks onto its edge. The hero shot keeps the
   rider below them */
.side-launch { position: absolute; top: var(--margin-y); left: var(--margin); display: flex; align-items: center; gap: var(--s4); }
.side-back { width: var(--h-l); height: var(--h-l); border-radius: 50%; background: var(--surface-1); border: 1px solid var(--line-strong); color: var(--text-1); }
.side-back .icon { width: 28px; height: 28px; }
.side-start { height: var(--h-l); min-width: 208px; padding: 0 var(--s6); gap: var(--s3); font: var(--t-h2);
  border-radius: var(--r-pill); box-shadow: 0 6px 0 var(--accent-press), 0 0 0 6px rgb(255 180 10 / 0.25); }
.side-start .icon { width: 28px; height: 28px; }
.side-start:active { transform: translateY(4px); box-shadow: 0 2px 0 var(--accent-press), 0 0 0 6px rgb(255 180 10 / 0.25); }
.side-start:disabled { box-shadow: none; }
/* a list as the own part (Free ride's rides): not a box of its own, its rows run to the panel's edges (one element, the
   user); eight and a half rows at most, then it scrolls (the user: "don't let it expand if the list is bigger"); where the
   screen is too low for the panel it gives way first, down to three rows */
.side > .list { flex: 0 100 auto; min-height: 150px; max-height: calc(8.5 * var(--h-s));
  margin: 0 calc(-1 * var(--s4)); background: none; border: 0; border-radius: 0; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
/* a ride as one line (round 10, the user: eight or nine rows in view): 48 px, the touch minimum (the user asked for half
   of the list row's 72 px): its profile in the zone colours, its name, how long and how much energy, its stars or a lock */
.row.line { height: var(--h-s); min-height: var(--h-s); gap: var(--s3); padding: 0 var(--s4); }
.row.line .spark { width: 64px; height: 28px; }
.row.line .row-trail { gap: var(--s3); }
.row-facts { font: var(--t-body); color: var(--text-2); white-space: nowrap; }
.row.line .stars { font-size: 16px; }
.row.line .row-trail > .icon { width: 20px; height: 20px; color: var(--text-3); }
/* the brief (ui/SideScreen.js brief()): the ride's name with the difficulty's stepper at the row's end, in its own small
   grey box (ui/kit.js DifficultyStepper: the quiet minus and plus, 48 px, the watts between them); its facts; a note where
   one is needed (what opens a level still shut; the campaign complete); its profile with the route's stops and their
   stems, 144 px. On a screen too low for the panel the profile gives way after the list (down to 108 px), then the brief
   scrolls */
.brief { flex: 0 1 auto; min-height: 0; display: flex; flex-direction: column; gap: var(--s2); }
.brief > * { flex: none; }
.title-row { display: flex; align-items: center; gap: var(--s3); }
.title-row > .level-title { flex: 1; min-width: 0; font: var(--t-h2); }
.title-row > .stepper { flex: none; gap: 0; height: var(--h-s); background: var(--surface-2); border-radius: var(--r-m); }
.title-row > .stepper .btn { color: var(--text-1); }
.title-row > .stepper output { min-width: 56px; font: var(--t-title); }
.title-row > .stepper output small { font: var(--t-body); }
.side .stats.inline { gap: var(--s4) var(--s5); }
.brief > .chart.profile { flex: 0 1 144px; height: auto; min-height: 108px; }
.side .chart-ftp { display: none; }                         /* the difficulty is on the stepper; the bars carry their watts */
.side.done :is(.stepper, .brief > .stats, .brief > .chart) { display: none; }   /* nothing is next (the campaign complete): the note says so, Start is disabled */
/* Start with a second line (the summary's way on: "Next level" over where it goes) */
.side-start.btn-two { font: var(--t-title); text-align: left; }
.side-start.btn-two .btn-text span { font: var(--t-body); }

/* ---- the summary (round 10, the user: "much smaller ... what is earned is needed ... where we are in the campaign, short";
        main.js #summary): a side screen. Its header is the outcome with its stars over one quiet line (the ride, its time,
        energy, average power, heart rate); then what was earned in one row of values led by their icons, the XP's with
        the level's bar; where the campaign stands (Campaign.js pathLine) and the one thing the ride changed there; one
        note (the difficulty, nothing saved) ---- */
#summary .head-title h2 { display: flex; align-items: center; gap: var(--s3); }
#summary .head-title h2 .stars { font-size: 24px; gap: 2px; }
#summary .side > .screen-head { height: auto; min-height: var(--h-s); }
.sum-body { display: flex; flex-direction: column; gap: var(--s4); padding-top: var(--s2); border-top: 1px solid var(--line); }
.sum-earned { gap: var(--s4) var(--s5); }
.sum-earned .stat { position: relative; }
.sum-earned .stat.loss :is(b, span) { color: var(--danger); }   /* the items lost: bad news, in words and colour */
.level-bar { display: block; height: 4px; margin-top: var(--s1); }
.level-bar i { background: var(--warning); }   /* the XP topic colour */
.sum-campaign { display: flex; flex-direction: column; gap: var(--s2); }
.sum-event { display: flex; align-items: center; gap: var(--s3); font: var(--t-title); color: var(--text-1); }
.sum-event span { min-width: 0; }
.sum-event .icon { flex: none; width: 24px; height: 24px; color: var(--topic, var(--accent)); }
.sum-event.loss .icon { color: var(--danger); }
.sum-note { display: flex; align-items: center; gap: var(--s3); font: var(--t-body); color: var(--text-2); }
.sum-note .icon { flex: none; width: 20px; height: 20px; color: var(--text-3); }
/* the campaign in one line: the world's name, its levels as pips, its bosses beaten */
.path-line { display: flex; align-items: center; gap: var(--s3); }
.path-line .path-cap { color: var(--text-1); }
.path-pips { display: flex; align-items: center; gap: var(--s1); }
.path-pips i { width: 14px; height: 14px; border-radius: 50%; border: 2px solid var(--line-strong); }
.path-pips i[data-state="open"] { border-color: var(--text-2); }
.path-pips i[data-state="finished"] { border-color: var(--accent); }
.path-pips i[data-state="complete"] { border-color: var(--accent); background: var(--accent); }
.path-pips i.here { box-shadow: 0 0 0 2px var(--surface-1), 0 0 0 4px var(--text-1); }
.path-line .path-tally { margin-left: auto; }

/* ---- history: totals, week and records in the side pane; finished rides as a table ---- */
#historyTotals { --cols: 2; }
.week { display: flex; align-items: flex-end; gap: var(--s2); height: 80px; }
.day { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: flex-end; gap: var(--s1); height: 100%; }
.day b, .day span { font: var(--t-label); text-transform: uppercase; }
.day i { width: 100%; min-height: 2px; border-radius: var(--r-xs) var(--r-xs) 0 0; background: var(--surface-3); }
.day.today i { background: var(--text-1); }
.day span { letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-3); }
.day.today span { color: var(--text-1); }
.records { display: flex; flex-direction: column; gap: var(--s3); }
.record { display: flex; align-items: center; gap: var(--s3); }
.record span { flex: 1; display: flex; flex-direction: column; min-width: 0; color: var(--text-1); }
.record span em { font-style: normal; color: var(--text-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.record b { font: var(--t-title); }
.col-time { width: 72px; } .col-power { width: 104px; } .col-energy { width: 104px; } .col-score { width: 88px; }

/* ---- garage: the rider and bike on the left (the 3D world shows through), the shop on the right ---- */
.garage-stage { justify-content: flex-end; }
.look { flex-direction: row; align-items: center; gap: var(--s4); min-height: 104px; }
.look .row-text strong, .look .row-text span { white-space: normal; }
.perk.maxed .row-lead { color: var(--text-3); }

/* ---- devices: three cards side by side, each a scrolling pane (the card, then its settings, then its help) ---- */
#devices .difficulty-row { flex-wrap: wrap; }
#devices .difficulty-row .caption { flex: 1 1 100%; }
#devHa .stats { --cols: 2; }

/* ---- ride HUD (round 10): as little as reads at a glance, so the world shows. Top left: pause and the hearts (the device
        status only while something is wrong). Top centre, one plate: the ride strip, power against target with heart
        rate and energy, the effort meter, the segment and the next one; under it the boss's health, the banner and the
        celebrations. Top right: the score with its multiplier, coins and sparks. The bottom is the world's (the items are
        worn on the rider, the route is on the strip). Plates are opaque (surface-1); captions are --t-hud-label,
        everything else --t-hud-text or larger. The whole HUD stays touchable: swipes on it change lanes. ---- */
.hud { --margin: 24px; justify-content: space-between; padding: var(--margin); }
/* Each HUD plate is its own compositor layer, sized to the plate, so the tablet composites only the plates over the
   canvas. Measured once each in a frozen busy frame: 58.2 fps before, 58.9 after, which is within the run-to-run spread
   (HUD hidden: 58.3 to 59.1); the cause of the HUD's cost was not measured. */
.hud .plate, .hud .banner { will-change: transform; }
/* Every live number in the ride HUD holds still (round 10, the user: "always the same, fixed size ... it should not move all
   the time and wiggle around"): tabular figures, each number's width reserved for its largest realistic value and the
   number set to its right edge (in the purse, which holds two, each number by its icon), so no plate resizes or moves and
   no digit jitters as the values change (as the ride bar).
   The reserves: the score 999,999 and its multiplier x10; coins 9,999 and the spark level 99; the watts 9999 on the
   needle and the target 9999 W; heart rate 999 and energy 9,999 kcal. Only a value past its reserve widens its plate */
#hud * { font-variant-numeric: tabular-nums; }   /* (an id: the plates' font shorthands would reset it otherwise) */
#hScore { flex: 1; min-width: 7ch; text-align: right; }
#hMult { min-width: calc(3.2ch + var(--s2)); text-align: right; }
#hCoins { min-width: 4.6ch; }
#hSparkLevel { min-width: 2ch; }
#hPower { min-width: 4ch; text-align: right; }
#hTarget { min-width: calc(6.2ch + 2 * var(--s3)); text-align: center; }
#hBpm { min-width: 3ch; text-align: right; }
#hKcal { min-width: 4.6ch; text-align: right; }
.plate { display: flex; align-items: center; gap: var(--s3); padding: 0 var(--s4); background: var(--surface-1); border-radius: var(--r-m); }
.hud-top { display: flex; align-items: flex-start; gap: var(--s4); }
/* the top left is one column (the hearts, the score and the purse, the device status while something is wrong); the top
   right holds pause alone */
.hud-side { flex: 1 1 0; display: flex; flex-direction: column; align-items: flex-start; gap: var(--s3); min-width: 0; }
.hud-side > * { flex: none; }
.hud-side.end { flex-direction: row; justify-content: flex-end; align-items: flex-start; }   /* the frame rate (when shown), then pause */
/* the frame-rate readout (Settings, Show frame rate): a fixed width, so it holds still */
.fps { height: var(--h-s); gap: var(--s1); padding: 0 var(--s3); align-items: baseline; padding-top: 13px; font: var(--t-hud-label); letter-spacing: 0.08em; color: var(--text-2); }
.fps b { min-width: 3ch; text-align: right; font: var(--t-hud-text); letter-spacing: 0; color: var(--text-1); }
.hud-stack { display: flex; flex-direction: column; align-items: stretch; gap: var(--s3); }   /* the score over the purse, one width */
.hud-center { flex: none; display: flex; flex-direction: column; align-items: center; gap: var(--s3); width: var(--feed); }   /* only what comes and goes: the top centre is the world's otherwise */
/* the power test is a measurement, not a run: no hearts, score or purse, no route's stops (game/RideSession.js) */
.hud.calm :is(.hearts, .score, .purse, .strip-stop) { display: none; }
.hud .devstat { font: var(--t-hud-text); }
/* a device lost during a ride: one line under the workout, like the banner, with Reconnect (ui/DeviceStatus.js LostPrompt) */
.lost { flex: none; width: max-content; max-width: var(--feed); padding: var(--s2) var(--s2) var(--s2) var(--s4); font: var(--t-hud-text); color: var(--text-1); box-shadow: inset 0 -4px 0 var(--danger); }
.lost > span { min-width: 0; }   /* its words take two lines where the feed is narrow; the icon and Reconnect keep their size */
.lost > :not(span) { flex: none; }
.lost > .icon { width: 28px; height: 28px; color: var(--danger); }
.lost .btn { font: var(--t-hud-text); }

/* hearts: full ones filled, lost ones as outlines; they shake on a hit and pop when one comes back */
.hearts { height: var(--h-s); gap: var(--s1); padding: 0 var(--s3); }
.hearts .icon { width: 28px; height: 28px; color: var(--heart); }
.hearts .icon.fill { fill: currentColor; }
.hearts .icon.off { color: var(--text-2); fill: none; }
.hearts.hurt { animation: shake 0.35s linear; }
@keyframes shake { 25% { transform: translateX(-6px); } 75% { transform: translateX(6px); } }

/* the effort gauge (round 10, the user's sketches of 2026-11-13): at the right edge under the pause button, with no plate behind it,
   so the world shows round it. The target in the segment's zone colour (a zone pill) on top; the effort meter standing
   upright on its own, 0 to 1.5 x target from the bottom, edged in surface-1 so it reads on sky, snow and road: the target
   band (85 to 112 %) in the zone colour (one colour says "ride here": the pill's, the band's, the verges' in the world), the
   surge filling in amber above it, the white needle; the watts ridden in a small plate that rides with the needle, on the
   meter's left; heart rate (in its zone's colour, the heart filled during a heart streak, a dash without a reading) and
   energy as small plates under it. Effort sets fire rate, damage, the multiplier and the surge; it never moves the rider. */
.hud-right { position: absolute; right: var(--margin); top: calc(var(--margin) + var(--h-s) + var(--s3)); bottom: calc(var(--margin) + var(--h-m) + var(--s7) + var(--s3));
  display: flex; flex-direction: column; align-items: flex-end; justify-content: center; gap: var(--s3); pointer-events: none; }
.hud-right > * { flex: none; }
/* the meter: an unpainted box as tall as the space allows (up to 240 px), the track at its right, the needle across it */
.hud-right > .gauge-meter { position: relative; flex: 0 1 240px; width: var(--gauge); min-height: 48px; margin: var(--s4) 0; }
.gauge-track { position: absolute; right: var(--s1); top: 0; bottom: 0; width: 14px; border-radius: var(--r-pill); box-shadow: 0 0 0 2px var(--surface-1);
  background: linear-gradient(0deg, var(--surface-3) 0 74.7%, rgb(255 180 10 / 0.22) 74.7%); overflow: hidden; }
.effort-band { position: absolute; left: 0; right: 0; bottom: 56.7%; height: 18%; background: var(--zone); box-shadow: 0 -2px 0 var(--surface-1); }
.effort-surge { position: absolute; left: 0; right: 0; top: 0; bottom: 74.7%; background: var(--effort-high);
  transform-origin: 50% 100%; transform: scaleY(0); transition: transform 0.25s linear; }   /* scaled, not resized */
/* the needle is a pointer that carries the watts: the white bar is drawn by ::before; the watts' plate rides on its left */
.gauge-needle { position: absolute; right: -2px; bottom: 0; width: 26px; height: 6px; margin-bottom: -3px; transition: bottom 0.15s linear; z-index: 1; }
.gauge-needle::before { content: ""; position: absolute; inset: 0; border-radius: var(--r-xs); background: var(--text-1); box-shadow: 0 0 0 2px var(--surface-1); }
.gauge-watts { position: absolute; right: 32px; top: 50%; gap: var(--s1); align-items: baseline; padding: var(--s1) var(--s3); transform: translateY(-50%); white-space: nowrap; }
.gauge-watts b { font: var(--t-hud-mid); color: var(--text-1); }
.gauge-watts small { font: var(--t-hud-label); color: var(--text-2); }
.hud.surging .gauge-watts { box-shadow: inset 0 0 0 3px var(--accent); }   /* a surge rings the watts */
/* heart rate and energy: each a small plate, its icon, its value; kcal keeps its unit (the heart rate's icon says bpm) */
.gauge-stats { display: flex; flex-direction: column; align-items: stretch; gap: var(--s3); }   /* one width, the wider's */
.gauge-stat { height: var(--h-s); gap: var(--s2); padding: 0 var(--s3); font: var(--t-hud-text); color: var(--text-2); white-space: nowrap; }
.gauge-stat b { margin-left: auto; font: var(--t-hud-mid); color: var(--text-1); }   /* the value at the plate's right */
.gauge-stat .icon { flex: none; width: 24px; height: 24px; }
/* heart rate: the bpm in its zone's colour (Garmin's: blue 2, green 3, orange 4, red 5) */
[data-zone="2"] { --hr-zone: var(--effort-low); }
[data-zone="3"] { --hr-zone: var(--positive); }
[data-zone="4"] { --hr-zone: var(--accent); }
[data-zone="5"] { --hr-zone: var(--danger); }
.gauge-stat.hr[data-zone] b, .gauge-stat.hr[data-zone] .icon { color: var(--hr-zone, var(--text-2)); }
.gauge-stat.hr:not([data-zone]) b { color: var(--text-2); }
.gauge-stat.hr.streak .icon { fill: currentColor; }

/* the ride bar along the bottom (round 10, the user's sketches): the segment as a zone pill with its time left in it; the ride
   as a thin bar (ui/RideStrip.js): its segments as a low profile, width how long, height and colour how hard, the ridden part
   under the scrim, a white tick for now; the route's stops standing on it as pins (their icon in a round head on a stem; in
   the accent once passed, the boss in --boss); under the bar, out of the pins' way, the target now under the tick and the next
   segment's target under its start in its zone colour (blinking in its last ten seconds); the time ridden of the total. Every
   text that changes has a fixed width and tabular figures: nothing moves when a time, a target or a segment's name changes */
.hud-bottom { display: flex; flex-direction: column; align-items: stretch; gap: var(--s3); }
.hud-bottom > .test { align-self: center; }
.ridebar { height: var(--h-m); gap: var(--s4); font: var(--t-hud-text); color: var(--text-2); white-space: nowrap; font-variant-numeric: tabular-nums; }
.zone-pill { padding: var(--s0) var(--s3); border-radius: var(--r-pill); background: var(--zone); color: var(--surface-0); font: var(--t-hud-text); white-space: nowrap; }
/* the phase pill: as wide as the longest segment's name ("Summit push") with a time of minutes */
.seg-pill { flex: none; display: inline-flex; align-items: baseline; justify-content: space-between; gap: var(--s2); width: 232px; }
.seg-pill > b:first-child { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.seg-pill > b:last-child { flex: none; }
.strip-track { position: relative; flex: 1; min-width: 0; height: 40px; }   /* the bar (16 px) over the labels' lane */
.strip-bars { position: absolute; left: 0; top: 0; width: 100%; height: 16px; display: block; }
.strip-past { position: absolute; left: 0; top: 0; height: 16px; width: 0; background: var(--scrim); }
.strip-now { position: absolute; top: -4px; height: 24px; left: 0; width: 4px; margin-left: -2px; border-radius: var(--r-xs); background: var(--text-1); box-shadow: 0 0 0 2px var(--surface-1); z-index: 2; }
.strip-label { position: absolute; top: 22px; transform: translateX(-50%); font: var(--t-hud-label); color: var(--text-1); white-space: nowrap; }
.strip-label.soon { animation: blink 0.8s steps(2) infinite; }
.strip-stop { position: absolute; top: -50px; display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; margin-left: -14px; border-radius: 50%;
  background: var(--surface-1); box-shadow: inset 0 0 0 2px currentColor; color: var(--text-1); z-index: 1; }
.strip-stop::after { content: ""; position: absolute; top: 100%; left: 50%; width: 2px; height: 18px; margin-left: -1px; background: currentColor; }
.strip-stop .icon { width: 16px; height: 16px; }
.strip-stop.done { color: var(--accent); }
.strip-stop.boss { color: var(--boss); }
.strip-time { flex: none; display: inline-flex; align-items: baseline; justify-content: flex-end; gap: var(--s1); min-width: 168px; }   /* "59:59 / 60:00" */
.strip-time b { color: var(--text-1); }

/* the boss's health, during the fight (its name is the world's boss) */
.boss-bar { flex-direction: column; align-items: stretch; gap: var(--s2); width: min(100%, 480px); padding: var(--s2) var(--s4) var(--s3); }
.boss-bar .cap { display: flex; align-items: center; gap: var(--s2); font: var(--t-hud-label); letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-1); }
.boss-bar .cap .icon { width: 24px; height: 24px; color: var(--boss); }
.boss-bar .meter { height: 12px; }
.boss-bar .meter i { width: 100%; background: var(--boss); transform-origin: 0 50%; transition: transform 0.2s linear; }   /* scaled, not resized: no layout on every hit */

/* a secret place while time is stopped: the gauge pedalling fills (the boss bar's component, in the accent), and at the right
   of its caption how fast it fills against riding at target ("x1.6"), so harder riding shows at once */
.secret-bar .cap .icon { color: var(--accent); }
.secret-bar .meter i { background: var(--accent); transform: scaleX(0); }
.secret-bar .rate { margin-left: auto; font: var(--t-hud-text); letter-spacing: 0; text-transform: none; color: var(--accent); }

/* a celebration while time is stopped, under the workout like the banner (the camera is turned round to look back at the
   rider while it shows, or raised for the boss's fall, so the item hovering in the world stands below it, main.js,
   gfx/Ceremony.js). Round 10: the item is in the world, so the plate only names it:
   its icon in its colour, a caption (rarity in its colour, New or its level), the name, and what it does when it is new;
   a bar in the rarity's colour. The boss's fall: the boss's name and "Boss defeated!" */
.cheer { --cheer: var(--rarity-common); flex: none; gap: var(--s4); width: max-content; max-width: var(--feed); padding: var(--s3) var(--s5) var(--s4) var(--s4);
  color: var(--text-1); box-shadow: inset 0 -4px 0 var(--cheer); }
.cheer.pop { animation: banner-in 0.25s ease-out; }
.cheer-icon { flex: none; width: 40px; height: 40px; color: var(--cheer); }
.cheer-words { display: flex; flex-direction: column; gap: var(--s1); min-width: 0; }
.cheer .cap { font: var(--t-hud-label); letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-2); }
.cheer .cap em, .offer-card .cap em { font-style: normal; color: var(--cheer); }
.cheer b { font: var(--t-hud-mid); }
.cheer-words > span:last-child { font: var(--t-hud-text); color: var(--text-2); }
.cheer.rare, .offer-card.rare { --cheer: var(--rarity-rare); }
.cheer.epic, .offer-card.epic { --cheer: var(--rarity-epic); }
.cheer.victory { --cheer: var(--warning); }
.cheer.victory .cheer-icon { color: var(--boss); }
/* a spark level's choice: one card per lane, as the lanes lie on screen (watched from the front, the camera turned round:
   right to left, .mirrored): the item's icon in its colour, its name, the rarity and New or level; the card of the lane
   the rider is in is marked */
.offer { display: flex; gap: var(--s4); max-width: var(--feed); }
.offer.mirrored { flex-direction: row-reverse; }
.offer-card { --cheer: var(--rarity-common); flex: 1 1 0; flex-direction: column; justify-content: center; gap: var(--s1); width: 200px; min-width: 0; padding: var(--s3) var(--s3) var(--s4);
  text-align: center; color: var(--text-1); box-shadow: inset 0 -4px 0 var(--cheer); }
.offer-card > .icon { width: 32px; height: 32px; }
.offer-card b { font: var(--t-hud-text); max-width: 100%; }   /* a long name takes two lines in a narrow card */
.offer-card .cap { font: var(--t-hud-label); letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-2); white-space: nowrap; }
.offer-card.on { background: var(--surface-2); box-shadow: inset 0 0 0 3px var(--accent), inset 0 -4px 0 var(--cheer); }

/* the power test (js/ui/TestHud.js): a plate at the bottom centre: the step of 16 and its time left, the step's target and
   the line to stay above (in the warm-up, how the test works in one sentence), the best minute and the FTP it gives.
   Under the line, the plate's edge turns red and a red-barred line under the workout counts the seconds down. */
.test { flex-direction: column; align-items: stretch; gap: var(--s2); width: 600px; max-width: var(--feed); padding: var(--s3) var(--s5); }
.test-head { display: flex; align-items: baseline; gap: var(--s4); font: var(--t-hud-mid); white-space: nowrap; }
.test-clock { margin-left: auto; }
.test-line, .test-best { font: var(--t-hud-text); color: var(--text-2); }
.test-line b, .test-best b { color: var(--text-1); }
.test.under { box-shadow: inset 0 0 0 3px var(--danger); }
.test.under .test-line b { color: var(--danger); }
.test-alert { flex: none; width: max-content; max-width: var(--feed); padding: var(--s3) var(--s5); font: var(--t-h2); color: var(--text-1); box-shadow: inset 0 -4px 0 var(--danger); }
.test-alert .icon { flex: none; width: 28px; height: 28px; color: var(--danger); }
.test-alert .secs { display: inline-block; min-width: 2ch; font: inherit; text-align: right; }   /* "ends in 10 s" to "9 s": one width */

/* the score with its multiplier and its charge; the purse: coins (bolts after the ride) and sparks (the spark level, and
   a thin bar filling to the next) */
.score { flex-direction: column; align-items: stretch; gap: var(--s1); padding: var(--s2) var(--s4); }
.score .val { display: flex; align-items: center; gap: var(--s2); font: var(--t-hud-mid); white-space: nowrap; }
.score .val .icon { flex: none; width: 24px; height: 24px; }
#hMult { margin-left: auto; padding-left: var(--s2); font: var(--t-hud-text); font-weight: 800; font-variant-numeric: tabular-nums; color: var(--accent); }
#hMult.popped { animation: pop 0.5s ease-out; }
.score .meter { height: 6px; }
.score .meter i { width: 100%; background: var(--accent); transform-origin: 0 50%; transform: scaleX(0); transition: transform 0.4s linear; }
.purse { justify-content: space-between; height: var(--h-s); gap: var(--s5); font: var(--t-hud-mid); }
.tally { position: relative; display: inline-flex; align-items: center; gap: var(--s2); }
.tally .icon { flex: none; width: 24px; height: 24px; }
.tally.popped, .hearts.popped { animation: pop 0.5s ease-out; }
.sparks .meter { position: absolute; left: 0; right: 0; bottom: -6px; height: 4px; }
.sparks .meter i { width: 100%; background: var(--spark); transform-origin: 0 50%; transform: scaleX(0); transition: transform 0.3s linear; }
@keyframes pop { 40% { transform: scale(1.18); } }
/* the summary's lines under Earned, what points, sparks and coins did (one line each; the icons carry their topic's colour) */


@keyframes nudge { to { transform: scale(1.06); } }

/* banner: the engine's Banner adds .show, a kind and .pop. One line at the top centre (under the boss's health), led by its
   kind's icon and marked by a bar in the kind's colour; at most as wide as the top centre's feed. Round 10: few and short (docs/DESIGN.md "Banner"); what the world shows is not said here. */
.banner { --kind: var(--text-2); display: none; flex: none; align-items: center; gap: var(--s3); width: max-content; max-width: var(--feed); padding: var(--s2) var(--s4) var(--s3);
  font: var(--t-hud-text); color: var(--text-1); background: var(--surface-1); border-radius: var(--r-m); box-shadow: inset 0 -4px 0 var(--kind); }
.banner.show { display: flex; }
.banner > .icon { flex: none; width: 24px; height: 24px; color: var(--topic, var(--kind)); }   /* a topic keeps its colour (the bolts stay cyan) */
.banner.pop { animation: banner-in 0.2s ease-out; }
@keyframes banner-in { from { transform: scale(0.94); opacity: 0; } }
.banner.good { --kind: var(--positive); }
.banner.bad { --kind: var(--danger); }
.banner.gate, .banner.reward, .banner.time, .banner.secret { --kind: var(--accent); }
.banner.boss { --kind: var(--boss); }
.banner.sparks { --kind: var(--spark); }

/* ================================================ 4. other screen sizes ================================================
   The grid is fluid, so most of the layout follows the viewport by itself (1024 x 600 to 1920 x 1200, landscape). Two
   steps do the rest: "compact" below the reference width of 1280 px re-divides the columns and drops what does not fit,
   and "short" below 720 px high (a browser window with its bars showing) tightens the vertical rhythm. No type size changes. */

/* ---- compact: narrower than 1280 px ---- */
@media (max-width: 1279px) {
  .wide-only { display: none; }
  .compact-only { display: revert; }
  .week { gap: var(--s1); }
  #home .pane-4 { flex-basis: var(--span-5); }
  /* ride selection: the row's sparkline narrows, the facts close up */
  .spark { width: 64px; }
  .stats.inline { gap: var(--s4) var(--s5); }
  .side .stats.inline { column-gap: var(--s4); }       /* the three facts stay on one line in the narrower panel */
  /* garage: 5 + 7, so that an upgrade's description still fits its row; the bar under the rider stacks */
  #garage .pane-6 { flex-basis: var(--span-7); }
  .look { flex-direction: column; align-items: stretch; }
  /* history: totals as a list (caption left, value right); the table drops its energy column, which the side pane covers */
  #historyTotals { --cols: 1; gap: var(--s3); }
  #historyTotals .stat { flex-direction: row-reverse; align-items: baseline; justify-content: space-between; }
  .col-energy { display: none; }
  .row-trail { gap: var(--s4); }
  /* devices: a card's readings become a list (caption left, value right), as the history totals do, and a setting's
     switch goes under its caption: a card is too narrow here for captions side by side */
  #devices .stats { --cols: 1; gap: var(--s3); }
  #devices .stats .stat { flex-direction: row-reverse; align-items: baseline; justify-content: space-between; }
  #devices .setting { flex-wrap: wrap; }
  #devices .setting .caption { flex: 1 1 100%; }
  /* ride HUD: the top centre's feed narrows (a long banner takes two lines) */
  :root { --feed: 460px; }
}

/* ---- short: lower than 720 px ---- */
@media (max-height: 720px) {
  :root { --margin-y: 16px; }
  .side { max-height: 100%; }                          /* a side panel takes the whole height where it needs it */
  .menu-screen { gap: var(--s4); }
  .chart { height: 108px; }
  .dialog { gap: var(--s4); padding: var(--s5); }
  .dialog-body { gap: var(--s3); }
  .group { gap: var(--s2); padding-top: var(--s3); }
  .look { min-height: 0; }
  /* the chart's difficulty label goes under its line, clear of the facts above the chart */
  .chart-ftp span { bottom: auto; top: var(--s1); }
}
/* ================================================ 5. campaign (round 5, linear since round 6) ================================
   The campaign's briefing (docs/DESIGN.md "The campaign briefing", "Path"; js/ui/Campaign.js): no level to choose. Round 10:
   a side screen (above, ui/SideScreen.js): its own part is the path; the level's brief and Start are the side screen's. */

/* ---- path: the way through the campaign, in two rows. The worlds: four cells, each its state's icon (a check once its bosses
   are beaten, a lock while it is shut), its name and its bosses beaten; the one being ridden is surface-2. Then every level
   of that world as a stop, and every level ends in its boss, so a level's node is its boss: edged in --boss while the boss
   stands, filled in the accent once it is beaten (a done stop, as on the route), a lock while the level is shut. The line
   between two stops is lit in the accent as far as the way is open, dashed beyond. The next level has a white ring. Under
   each node its number, then its stars. What opens the next world is said under the row. Not a control. ---- */
.path-band { flex: none; display: flex; flex-direction: column; gap: var(--s3); }
.path-worlds { display: flex; padding: 0; list-style: none; }
/* the levels: two rows of five (the user's mockup), each row's line running through its stops */
.path { position: relative; display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); row-gap: var(--s3); padding: 0; list-style: none; }
/* the link from the end of the first row to the start of the second (Campaign.js #linkRows): one way, lit or dashed */
.path-link { position: absolute; left: 0; top: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; fill: none; stroke: var(--line-strong); stroke-width: 4;
  stroke-dasharray: 8 6; stroke-linejoin: round; }
.path-link.lit { stroke: var(--accent); stroke-dasharray: none; }
.path-stop { position: relative; flex: 1 1 0; display: flex; flex-direction: column; align-items: center; gap: var(--s1); min-width: 0; }
.path .path-stop::before { content: ""; position: absolute; top: 26px; right: 50%; width: 100%; height: 4px;
  background: repeating-linear-gradient(90deg, var(--line-strong) 0 8px, transparent 8px 14px); }
.path .path-stop:nth-child(5n + 1)::before { display: none; }   /* a row starts without a line in */
.path .path-stop.lit::before { background: var(--accent); }
.path-node { position: relative; z-index: 1; display: flex; align-items: center; justify-content: center; width: 56px; height: 56px; border-radius: 50%;
  background: var(--surface-1); border: 2px solid var(--boss); color: var(--boss); }
.path-node .icon { width: 32px; height: 32px; }
.path-stop[data-state="locked"] > .path-node { border: 2px dashed var(--line-strong); color: var(--text-3); }
.path-stop[data-state="complete"] > .path-node { background: var(--accent); border-color: transparent; color: var(--on-accent); }
.path .path-stop.here > .path-node { box-shadow: 0 0 0 3px var(--surface-1), 0 0 0 5px var(--text-1); }
.path-cap { font: var(--t-label); letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-1); white-space: nowrap; }
.path-stop[data-state="locked"] .path-cap { color: var(--text-2); }
.path-stop .stars { gap: 1px; font-size: 14px; }
.path-stop[data-state="locked"] .stars { visibility: hidden; }
.path-tally { display: flex; align-items: center; gap: var(--s1); min-height: 16px; font: var(--t-label); color: var(--text-1); }
.path-tally .icon { width: 16px; height: 16px; }
/* the worlds' row: cells, not nodes */
.path-worlds { gap: var(--s1); }
.path-worlds .path-stop { align-items: flex-start; gap: var(--s0); padding: var(--s2); border-radius: var(--r-m); }
.path-worlds .path-stop.here { background: var(--surface-2); }
.path-worlds .path-node { display: none; }                 /* the state is on the tally's line: the name has the cell's width */
.path-worlds .path-stop[data-state="complete"] .path-tally .icon { color: var(--accent); }
.path-worlds .path-tally { letter-spacing: 0.08em; text-transform: uppercase; }   /* a caption: "3 MORE" */
.path-worlds .path-stop[data-state="locked"] .path-tally { color: var(--text-2); }
.path-worlds .path-stop[data-state="locked"] .i-lock { color: var(--text-3); }

/* profile chart: the route's stops (gates, chest, ambush, the boss) as small markers in a band above the bars */
.chart.profile { height: 208px; padding-top: var(--s6); }
.chart-stops { position: absolute; left: 0; right: 0; top: 0; height: var(--s5); }
.stop { position: absolute; top: 0; display: flex; align-items: center; justify-content: center; width: var(--s5); height: var(--s5); margin-left: calc(var(--s5) / -2);
  border-radius: 50%; background: var(--surface-2); border: 2px solid var(--line-strong); color: var(--text-1); }
.stop .icon { width: 14px; height: 14px; }
.stop.boss { border-color: var(--boss); color: var(--boss); }
/* a stop's stem: from under its pin down through the bars to the axis, at the moment it comes (over the bars, thin) */
.stop-line { position: absolute; top: var(--s5); bottom: 28px; width: 2px; margin-left: -1px; background: var(--text-2); pointer-events: none; }
.stop-line.boss { background: var(--boss); }
.goals { display: flex; flex-direction: column; gap: var(--s2); }
.goal { display: flex; align-items: center; gap: var(--s3); color: var(--text-2); }
.goal .icon { width: 20px; height: 20px; color: var(--text-3); }
/* the side screens' note line (ui/SideScreen.js brief) */
.goals.inline { flex-direction: row; flex-wrap: wrap; gap: var(--s2) var(--s4); }
/* home: the campaign's lines give way (ellipsis) before the chevron */
[data-go="campaign"] .btn-text { flex: 1 1 0; }
[data-go="campaign"] .btn-text > * { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* a way on in two lines (the summary's Next level over where it goes; home's Continue) */
.btn-two .btn-text { flex: 0 1 auto; }
.btn-two .btn-text > * { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: 1.2; }
@media (max-height: 720px) {
  .path-band { gap: var(--s2); }
  .chart.profile { height: 140px; }
  .path-node { width: 40px; height: 40px; }
  .path-node .icon { width: 24px; height: 24px; }
  .path .path-stop::before { top: 18px; }
  .path-stop .stars { font-size: 12px; }
}

/* ---- the Home Assistant guide (ui/HaGuide.js, ui/markdown.js): docs/home-assistant/README.md as running text, its
   sections as a contents list, every code block with Copy ---- */
.guide-toc { gap: var(--s1); padding: var(--s3); }
#haguide .head-label { color: var(--text-3); }
.head-actions { display: flex; gap: var(--s2); }
.head-actions .btn { gap: var(--s2); text-decoration: none; }
.guide-link { justify-content: flex-start; text-align: left; white-space: normal; height: auto; min-height: var(--h-s, 48px); }
.prose { gap: var(--s4); color: var(--text-2); max-width: 880px; }
.prose h2, .prose h3, .prose h4 { margin: 0; color: var(--text-1); scroll-margin-top: var(--s4); }
.prose h3 { font: var(--t-h2); margin-top: var(--s5); }
.prose h3:first-child { margin-top: 0; }
.prose h4 { font: var(--t-title); margin-top: var(--s3); }
.prose p { margin: 0; }
.prose strong { color: var(--text-1); font-weight: 600; }
.prose a:not(.btn) { color: var(--accent); overflow-wrap: anywhere; }
.code-bar a.btn { text-decoration: none; }
.prose code { font-family: ui-monospace, "Cascadia Mono", Consolas, monospace; font-size: 0.92em; color: var(--text-1); background: var(--surface-2); padding: 0 var(--s1); border-radius: var(--r-xs); overflow-wrap: anywhere; }
.prose ul, .prose ol { margin: 0; padding-left: var(--s5); display: flex; flex-direction: column; gap: var(--s3); }
.prose li { padding-left: var(--s1); }
.prose li > :is(p, .code-block, ul, ol) + * { margin-top: var(--s3); }
.prose li > :is(p, ul, ol, .code-block) { margin-top: var(--s2); }
.prose li > :first-child { margin-top: 0; }
.code-block { display: flex; flex-direction: column; background: var(--surface-0); border: 1px solid var(--line); border-radius: var(--r-m); overflow: hidden; }
.code-bar { display: flex; align-items: center; justify-content: flex-end; gap: var(--s2); padding: var(--s1) var(--s2) 0; }
.code-bar .label { margin-right: auto; padding-left: var(--s2); color: var(--text-3); }
.code-bar .btn { gap: var(--s2); }
.code-block pre { margin: 0; padding: var(--s2) var(--s4) var(--s4); overflow: auto; max-height: 520px; }
.code-block pre code { background: none; padding: 0; font-size: 14px; line-height: 1.5; white-space: pre; overflow-wrap: normal; color: var(--text-1); }
.table-wrap { overflow-x: auto; }
.prose table { border-collapse: collapse; width: 100%; }
.prose th, .prose td { text-align: left; vertical-align: top; padding: var(--s2) var(--s3); border-bottom: 1px solid var(--line); }
.prose th { color: var(--text-1); font-weight: 600; }
.prose td:first-child { white-space: nowrap; }
@media (max-width: 1279px) { #haguide .pane-3 { display: none; } }

/* ---- Impressum and Datenschutz in a sheet over the game (ui/LegalSheet.js): the legal kit's page in an iframe ---- */
.legal-overlay { z-index: 30; }
.legal-sheet { width: min(1040px, 100%); height: 100%; gap: var(--s4); padding: var(--s4) var(--s5) var(--s5); }
.legal-sheet .screen-head { height: auto; }
.legal-frame { flex: 1; width: 100%; min-height: 0; border: 1px solid var(--line); border-radius: var(--r-m); background: #fff; }
