  :root{
    /* Avstand fra skjermkanten til innholdet øverst. Bakgrunnen går helt opp, innholdet holder seg
       klar av statuslinjen (iOS toner toppen ut bak den). Rotert telefon har ingen statuslinje og ingen
       øy i den kanten, så der nullstilles begge: se regelen rett under denne blokken. */
    --top-clear:calc(max(env(safe-area-inset-top), 20px) + 32px);
    --top-clear-tight:calc(max(env(safe-area-inset-top), 20px) + 14px);
    --green-1:#1f7a41;
    --green-2:#1b6b39;
    --line: rgba(255,255,255,0.55);
    --navy:#0f1f36;
    --navy-2:#16294a;
    --accent:#ff8a3d;
    --accent-ink:#5a2d0c;
    --danger:#e63946;
    --orange:#d4900a; /* amber, not pure orange - kept a deliberate hue gap from --accent (both used to sit ~11 degrees apart on the color wheel and read as "the same orange") */
    --period-blue:#0a84ff;
    --bench-bg:#eef1f0;
    --card:#ffffff;
    --ink:#16202b;
    --ink-soft:#6b7686;
    --radius-lg:22px;
    --radius-md:14px;
    --radius-sm:10px;
    --shadow-sm:0 2px 6px rgba(15,31,54,.16);
    --shadow-md:0 8px 22px rgba(15,31,54,.22);
    --glass-blur:24px;
    --scrollbar-thumb: rgba(15,31,54,.25);
    --scrollbar-thumb-hover: rgba(15,31,54,.4);
  }
  /* Bare ekte rotert telefon: iPad beholder statuslinjen i landskap, og forhåndsvisningen på Mac er alltid stående. */
  @media (orientation: landscape) and (max-height: 500px){
    #viewport-frame:not(.desktop-preview){ --top-clear:calc(env(safe-area-inset-top) + 4px); --top-clear-tight:calc(env(safe-area-inset-top) + 4px); }
  }
  /* Dark screens/cards swap to a light-on-dark thumb instead of the
     default dark-on-light one - covers every dark surface that scrolls
     (Historikk/Innstillinger/Register share .history-screen; the dark
     modal cards - login, "Mitt lag", #settingsModal's own pre-match card -
     share .modal-card-dark), so any scrollable content nested inside
     picks up the right contrast automatically via inheritance, without
     listing each inner list/textarea individually. */
  .history-screen, .modal-card-dark, html, body{
    --scrollbar-thumb: rgba(255,255,255,.28);
    --scrollbar-thumb-hover: rgba(255,255,255,.45);
  }
  /* Global scrollbar restyle - thin, rounded, matches the app's own
     palette instead of the browser's stock scrollbar (only visible on
     desktop browsers; touch/iOS scrollbars are the system's own floating
     overlay style regardless and ignore this). Firefox via scrollbar-*,
     Chrome/Safari/Edge via the ::-webkit- pseudo-elements. */
  *{
    scrollbar-width: thin;
    scrollbar-color: var(--scrollbar-thumb) transparent;
  }
  *::-webkit-scrollbar{ width:8px; height:8px; }
  /* Chrome/Edge draw classic up/down (and left/right) arrow buttons at
     each end of the track by default - straight-edged rectangles that
     poke out past any rounded corner on the scrolling container (e.g.
     Innstillinger's card), since the browser draws them flush with the
     box's raw edge with no awareness of its border-radius. Hiding them
     removes the only part of the scrollbar that actually overflowed the
     curve - the thin track/thumb itself sitting a few px further in
     already clears it. */
  *::-webkit-scrollbar-button{ display:none; width:0; height:0; }
  *::-webkit-scrollbar-track{ background:transparent; margin:4px 0; }
  *::-webkit-scrollbar-thumb{
    background:var(--scrollbar-thumb); border-radius:999px;
  }
  *::-webkit-scrollbar-thumb:hover{ background:var(--scrollbar-thumb-hover); }
  *::-webkit-scrollbar-corner{ background:transparent; }
  /* Chrome doesn't reliably apply ::-webkit-scrollbar rules matched only
     via the universal selector to the ROOT/viewport scrollbar itself
     (as opposed to an inner scrolling div) - html/body are what actually
     scroll here on tall content (see the "Normal document flow" comment
     below on #viewport-frame), so restate the same rules explicitly
     targeting them to make sure that one definitely picks it up too. */
  html::-webkit-scrollbar, body::-webkit-scrollbar{ width:8px; height:8px; }
  html::-webkit-scrollbar-button, body::-webkit-scrollbar-button{ display:none; width:0; height:0; }
  html::-webkit-scrollbar-track, body::-webkit-scrollbar-track{ background:transparent; margin:4px 0; }
  html::-webkit-scrollbar-thumb, body::-webkit-scrollbar-thumb{
    background:var(--scrollbar-thumb); border-radius:999px;
  }
  html::-webkit-scrollbar-thumb:hover, body::-webkit-scrollbar-thumb:hover{ background:var(--scrollbar-thumb-hover); }

  *{ box-sizing:border-box; -webkit-tap-highlight-color:transparent; }
  html,body{
    min-height:100%; margin:0; padding:0;
    overscroll-behavior:none;
    overflow-x:hidden; /* horizontal scroll has no legitimate use here (unlike the small vertical safety-valve above/below) - html needed this too, not just body, since either can end up the actual horizontally-scrolling element depending on content. */
    /* Stops iOS's rubber-band bounce: without this, dragging past the top
       edge (even with nothing to actually scroll) still elastically slides
       the whole page - including header - down/up, which is what let the
       header slide directly under the status bar's own translucent overlay
       and read as "diffuse" (see header's padding-top above). touch-action
       only blocks the browser's own default panning/zooming gesture, not
       tap/click dispatch, so buttons everywhere are unaffected - but it
       also blocks the browser's default SCROLLING gesture wherever it
       isn't explicitly re-enabled, so every genuinely scrollable region
       (#field, #bench, .modal-card, .suggest-list, etc.) re-enables
       touch-action:pan-y itself below. */
    touch-action:none;
    background:var(--navy);
    font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","Helvetica Neue",Arial,sans-serif;
    color:var(--ink);
  }
  body{ -webkit-user-select:none; user-select:none; }

  /* ---------- Layout ---------- */
  /* Normal document flow, sized with min-height - NOT position:fixed+inset:0
     with a hard height. That locked-canvas approach is exactly the thing
     iOS standalone-PWA mode is unreliable about: WebKit's viewport-height
     recalculation in that mode doesn't always land on the real usable
     height, and with overflow:hidden any shortfall was invisible - it
     just clipped the bottom of the app (the "black bar" bug). Two other
     production PWAs of ours (flykalk.no, anestesiappen.no) use this exact
     min-height + normal-flow approach and don't hit this - min-height
     gives the same full-bleed look when content fits (the near-universal
     case), and on the rare device/content combination where it doesn't
     quite fit, the page scrolls instead of silently cutting content off.
     The letterboxed "phone in a dark room" mockup below is a separate,
     opt-in ADDITION (.desktop-preview) purely for previewing the app in
     a wide desktop browser window - a real phone never carries that
     class and never needs to. */
  /* height (not min-height): #field-wrap below is flex:0 0 68% of #main,
     which needs #app's height to be DEFINITE to resolve that percentage
     correctly - min-height alone left it indefinite and collapsed the
     pitch/bench split. Still no position:fixed and no overflow:hidden
     though, so if 100dvh is ever a hair off from the real safe area in
     standalone PWA mode, the excess/shortfall is a small natural scroll
     or gap - never an invisibly clipped, locked-in black bar. */
  #viewport-frame{
    position:relative; /* containing block for the modals that are its direct children (siblings of #app, not nested inside it) - without this they fall back to the initial containing block instead of this box */
    height:100svh; height:100dvh;
    background:var(--navy);
  }
  #app{
    position:relative; /* containing block for its position:absolute children (.launcher-screen, modals, etc.) - not for #app's own placement */
    height:100svh; height:100dvh;
    display:flex; flex-direction:column;
    background:var(--navy);
  }

  /* ---------- Desktop preview (letterboxed to a phone aspect ratio) ---------- */
  /* Added by updateFrameFit() only when it detects a non-touch (mouse/
     trackpad) context whose window isn't already phone-shaped - e.g. a
     resized Mac browser. Ratio matches iPhone 17 / 17 Pro / 17 Pro Max
     (~440x956 pt). Desktop browsers don't have the iOS cold-launch
     viewport-timing issue above, so measuring against 100vw/100dvh here
     is fine - this path is never used on a real phone. */
  #viewport-frame.desktop-preview{
    position:static; inset:auto;
    width:100vw;
    height:100vh; height:100dvh;
    display:flex; align-items:center; justify-content:center;
  }
  #viewport-frame.desktop-preview #app{
    position:relative;
    width:min(100vw, calc((100vh) * 440 / 956));
    width:min(100vw, calc((100dvh) * 440 / 956));
    height:min(100vh, calc(100vw * 956 / 440));
    height:min(100dvh, calc(100vw * 956 / 440));
    max-width:100vw; max-height:100vh; max-height:100dvh;
    border-radius:30px;
    border:1px solid rgba(255,255,255,.14);
    box-shadow:0 0 60px rgba(0,0,0,.5);
  }
  /* No landscape override here (removed - see git history for the old
     "rotated preview window" rule): `orientation: landscape` reflects the
     BROWSER WINDOW's raw aspect ratio, which is true for virtually every
     normal desktop window (wider than tall), not just an intentionally
     rotated phone preview. That made the desktop-preview frame collapse
     to a short, full-width strip in any ordinary browser window instead
     of showing the normal upright phone mockup - real device rotation
     testing goes through devtools' touch-emulated device toolbar instead,
     which makes updateFrameFit() detect it as a touch device and drop
     .desktop-preview entirely, using the real full-bleed CSS (which DOES
     correctly respond to actual orientation) - so this rule's intended
     case never actually reached it, only the broken one did. */

  /* ---------- Launcher (the very first screen) ---------- */
  .launcher-screen{
    position:absolute; inset:0; z-index:300;
    display:flex; flex-direction:column; align-items:center; justify-content:center;
    background:linear-gradient(165deg, var(--navy-2), var(--navy) 55%, #081120);
    padding:28px 26px;
    transition:opacity .3s ease;
  }
  .launcher-screen.hidden{ opacity:0; pointer-events:none; }
  .launcher-tile{
    position:relative;
    width:100%; max-width:320px;
    border-radius:26px; overflow:hidden;
    background:rgba(255,255,255,.09);
    border:1px solid rgba(255,255,255,.20);
    -webkit-backdrop-filter:blur(20px) saturate(160%);
    backdrop-filter:blur(20px) saturate(160%);
    box-shadow:0 10px 26px rgba(0,0,0,.4);
    cursor:pointer;
    transition:transform .5s cubic-bezier(.22,1,.36,1), box-shadow .5s cubic-bezier(.22,1,.36,1);
  }
  .launcher-tile.lifted{
    transform:translateY(-12px) scale(1.045);
    box-shadow:0 34px 60px rgba(0,0,0,.55), 0 0 0 1px rgba(255,255,255,.35) inset;
  }
  .launcher-tile-art{
    width:100%; aspect-ratio:10/7;
    background:repeating-linear-gradient(180deg, var(--green-1) 0 22px, var(--green-2) 22px 44px);
  }
  .launcher-tile-art svg{ width:100%; height:100%; display:block; }
  .launcher-tile-body{ padding:18px 20px 20px; }
  .launcher-tile-title{ margin:0 0 5px; font-size:16px; font-weight:700; color:#fff; }
  .launcher-tile-sub{ margin:0; font-size:13px; line-height:1.45; color:rgba(255,255,255,.72); }
  .launcher-credit{
    margin-top:26px; font-size:12px; font-weight:500; font-style:italic;
    letter-spacing:.03em; color:rgba(255,255,255,.36);
  }
  /* Short "vX.Y" tap target under the credit line - opens
     #versionHistoryModal with the full version + a changelog (see
     VERSION_HISTORY in app.js). Kept visually quiet, same family as
     .launcher-credit above it, so it reads as a footnote, not a button. */
  .launcher-version{
    display:block; margin:6px auto 0; padding:4px 8px; border:none; background:none;
    font:inherit; font-size:11px; font-weight:600; letter-spacing:.03em;
    color:rgba(255,255,255,.3); cursor:pointer;
  }
  .launcher-version:active{ color:rgba(255,255,255,.55); }
  @media (prefers-reduced-motion: reduce){
    .launcher-tile{ transition:opacity .3s ease; }
    .launcher-tile.lifted{ transform:none; opacity:.85; }
  }

  /* Glass popup shown on tapping the launcher tile: choose new vs. join an
     existing session. Nested INSIDE .launcher-tile (which is already
     position:relative + border-radius + overflow:hidden) so inset:0 makes
     it exactly cover the tile's full pitch-graphic + body area for free,
     with matching rounded corners via the parent's clip - no separate
     sizing/radius to keep in sync. A single element (not an outer
     transparent overlay + an inner glass card) with one opacity+transform
     transition, so the opaque background and the text/buttons always
     appear together - a separate translucent+backdrop-filter card here
     previously caused a visible lag where the blur "caught up" a beat
     after the content had already faded in. */
  .launcher-choice-overlay{
    position:absolute; inset:0; z-index:5;
    display:flex; align-items:center; justify-content:center;
    padding:26px 22px;
    background:linear-gradient(165deg, var(--navy-2), var(--navy) 65%, #081120);
    opacity:0; pointer-events:none; transform:scale(1.04);
    transition:opacity .28s ease, transform .28s ease;
  }
  .launcher-choice-overlay.open{ opacity:1; pointer-events:auto; transform:scale(1); }
  .launcher-choice-overlay.shake{ animation:shake .4s; }
  .launcher-choice-panel{ width:100%; }
  .launcher-choice-title{ margin:0 0 4px; color:#fff; font-size:19px; font-weight:700; text-align:center; }
  .launcher-choice-sub{ margin:0 0 20px; color:rgba(255,255,255,.65); font-size:13px; text-align:center; }
  .glass-choice-btn{
    display:block; width:100%; padding:14px; margin-bottom:10px;
    border:1px solid rgba(255,255,255,.22); border-radius:var(--radius-sm);
    background:rgba(255,255,255,.08); color:#fff;
    font-size:15px; font-weight:700; cursor:pointer;
    transition:transform .12s ease, background .15s ease;
  }
  /* Author display:block above beats the browser's default [hidden]{display:
     none} regardless of specificity - without this, #continueSessionChoiceBtn
     stayed visible on a brand-new app with no session to continue from. */
  .glass-choice-btn[hidden]{ display:none; }
  .glass-choice-btn:active{ transform:scale(.97); background:rgba(255,255,255,.16); }
  .glass-choice-btn.primary{ background:var(--accent); color:var(--accent-ink); border-color:transparent; }
  .glass-choice-btn.primary:active{ opacity:.85; }
  /* "Ny økt" when a session already exists - starting over would discard
     it, so it needs the same press-again-to-confirm pattern as "Avslutt
     og nullstill" rather than firing on a single tap. */
  .glass-choice-btn.armed{ background:var(--danger); color:#fff; border-color:transparent; }
  .launcher-choice-cancel{
    display:block; width:100%; margin:4px 0 0; padding:10px;
    background:none; border:none; color:rgba(255,255,255,.5);
    font-size:14px; font-weight:600; cursor:pointer;
  }
  .launcher-choice-back{
    position:absolute; top:18px; left:16px;
    width:30px; height:30px; border-radius:50%; border:none;
    background:rgba(255,255,255,.10); color:#fff; font-size:19px;
    display:flex; align-items:center; justify-content:center; cursor:pointer;
  }
  .launcher-join-input{
    width:100%; margin:6px 0 12px; text-align:center;
    font-size:28px; font-weight:700; letter-spacing:8px;
    padding:12px; border:1px solid rgba(255,255,255,.25); border-radius:var(--radius-sm);
    background:rgba(255,255,255,.10); color:#fff;
  }
  .launcher-join-input::placeholder{ color:rgba(255,255,255,.35); }
  .launcher-join-error{ color:#ff8a8a; font-size:13px; text-align:center; margin:0 0 8px; }
  .launcher-join-enter{
    display:flex; align-items:center; justify-content:center;
    width:52px; height:52px; margin:6px auto 0; border-radius:50%; border:none;
    background:var(--accent); color:var(--accent-ink); font-size:22px; font-weight:800;
    cursor:pointer;
  }
  .launcher-join-enter:active{ transform:scale(.94); }
  @media (prefers-reduced-motion: reduce){
    .launcher-choice-overlay{ transition:opacity .3s ease; transform:none; }
  }

  /* ---------- Myntkast / Innstillinger launcher tiles + screen ----------
     .side-col deliberately sets no height of its own: align-items on
     .launcher-tiles-row is left at flexbox's default (stretch, not
     flex-start), which hands .side-col the exact same height as
     .launcher-tile. justify-content:center then centers the fixed-size
     Myntkast+gap+Innstillinger pair within that stretched height, so
     Myntkast sits a bit below Trenerappen's own top and Innstillinger a bit
     above its bottom, symmetrically, by whatever's left over - not a fixed
     pixel margin that only happens to work for one line count. (An earlier
     version used space-between to land exactly on both edges - moved off
     that on purpose, to leave Innstillinger some breathing room from the
     Historikk tile below it.) */
  .launcher-tiles-row{ display:flex; gap:12px; width:100%; max-width:380px; }
  .launcher-tiles-row .launcher-tile{ max-width:none; flex:1 1 auto; min-width:0; }
  .side-col{ flex:0 0 104px; width:104px; display:flex; flex-direction:column; justify-content:center; gap:14px; }
  .coin-tile, .settings-tile{
    position:relative; flex:0 0 auto;
    width:104px; aspect-ratio:1/1; border-radius:22px; overflow:hidden;
    border:1px solid rgba(255,255,255,.20);
    box-shadow:0 10px 26px rgba(0,0,0,.4);
    cursor:pointer;
    transition:transform .15s ease, box-shadow .15s ease;
    display:flex; align-items:center; justify-content:center;
    /* Excludes the label zone's height from the centering box below, so the
       icon centers between the tile's top edge and the top of the label -
       not the tile's true vertical middle, which would sit low once the
       label scrim visually claims the bottom third. */
    padding-bottom:32px;
  }
  .coin-tile:active, .settings-tile:active{ transform:scale(.96); }
  .coin-tile{ background:radial-gradient(circle at 35% 28%, #ffe6a8, #d9a02f 55%, #a9760f 100%); }
  .settings-tile{ background:linear-gradient(160deg,#2c3e52,#1a2738); }
  .coin-tile-art{ width:40%; height:40%; display:flex; }
  .coin-tile-art svg{ width:100%; height:100%; display:block; }
  .settings-tile-art{ width:40%; height:40%; display:flex; }
  .settings-tile-art svg{ width:100%; height:100%; display:block; }
  /* Label sits INSIDE the square as an overlay on a bottom scrim, rather
     than as a strip below it - that extra strip was exactly what made the
     tile taller than it is wide despite the art itself being square. */
  .tile-label-scrim{
    position:absolute; left:0; right:0; bottom:0;
    padding:16px 6px 8px;
    background:linear-gradient(to top, rgba(0,0,0,.55), rgba(0,0,0,0));
  }
  .tile-label-scrim p{ margin:0; font-size:11.5px; font-weight:700; color:#fff; text-align:center; }

  /* ---------- "Mitt lag" account button ---------- */
  /* Same status-bar clearance formula as `header` above (see its own long
     comment for why: a plain 16px/20px sat right under the status bar's
     translucent overlay, reading as faded/half-invisible - both on
     notched devices, where env(safe-area-inset-top) alone still wasn't
     quite enough, AND on rectangular-corner ones, where env() reports 0
     despite the OS bar still visually sitting there). Reusing the same
     already-tuned --top-clear instead of re-deriving
     a new magic number for yet another top-of-screen control. */
  .account-btn{
    position:absolute; top:var(--top-clear); right:16px; z-index:1;
    display:flex; flex-direction:column; align-items:center; gap:4px;
    background:none; border:none; cursor:pointer; padding:0;
  }
  .account-btn-circle{
    width:34px; height:34px; border-radius:50%;
    background:rgba(255,255,255,.09); border:1px solid rgba(255,255,255,.22);
    -webkit-backdrop-filter:blur(20px) saturate(160%); backdrop-filter:blur(20px) saturate(160%);
    display:flex; align-items:center; justify-content:center;
  }
  .account-btn:active .account-btn-circle{ transform:scale(.92); }
  .account-label{ font-size:9.5px; font-weight:700; color:rgba(255,255,255,.55); letter-spacing:.02em; }

  /* ---------- Historikk launcher tile ---------- */
  .history-tile{
    position:relative;
    width:100%; max-width:380px; margin-top:12px;
    display:flex; align-items:center; gap:12px;
    border-radius:18px; padding:12px 16px;
    background:rgba(255,255,255,.09); border:1px solid rgba(255,255,255,.20);
    -webkit-backdrop-filter:blur(20px) saturate(160%);
    backdrop-filter:blur(20px) saturate(160%);
    box-shadow:0 10px 26px rgba(0,0,0,.4);
    cursor:pointer;
    transition:transform .15s ease;
  }
  .history-tile:active{ transform:scale(.97); }
  /* Same author-display-beats-[hidden] gotcha as elsewhere in this file -
     .history-tile's own unconditional display:flex above has the same
     specificity as the browser's default [hidden]{display:none} and comes
     later in the cascade, so it silently wins unless this is spelled out
     explicitly too. See updateHistoryTileVisibility() in app.js - not
     logged in means this attribute is set, and it has to actually work. */
  .history-tile[hidden]{ display:none; }
  /* .history-tile-badge (the lock/unlock status badge) is gone - the whole
     tile is either in the DOM or isn't now, see updateHistoryTileVisibility()
     in app.js. #lockClosedSymbol/#lockOpenSymbol themselves are untouched,
     .lock-icon below is still used by the login popup's own icon. */
  .lock-icon{
    width:20px; height:20px; display:block;
    filter:drop-shadow(0 2px 3px rgba(0,0,0,.55)) drop-shadow(0 1px 2px rgba(0,0,0,.4));
  }
  .lock-icon[hidden]{ display:none; }
  .history-tile-icon{ width:30px; height:30px; flex-shrink:0; }
  .history-tile-body{ flex:1 1 auto; min-width:0; }
  .history-tile-title{ margin:0 0 2px; font-size:14px; font-weight:700; color:#fff; }
  .history-tile-sub{
    margin:0; font-size:12px; color:rgba(255,255,255,.65); line-height:1.35;
    white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
  }
  .history-tile-chevron{ color:rgba(255,255,255,.5); font-size:18px; flex-shrink:0; }

  .coin-flip-screen{
    position:absolute; inset:0; z-index:310;
    display:flex; align-items:center; justify-content:center;
    background:linear-gradient(165deg, var(--navy-2), var(--navy) 55%, #081120);
    padding:28px 26px;
    opacity:0; pointer-events:none; transform:scale(1.03);
    transition:opacity .3s ease, transform .3s ease;
  }
  .coin-flip-screen.open{ opacity:1; pointer-events:auto; transform:scale(1); }
  .coin-flip-close{
    /* max(env(...), 20px): older rectangular-corner iPads/iPhones (no
       notch, no Dynamic Island) report 0 for every safe-area-inset even in
       standalone PWA mode, despite the OS status bar still visually
       occupying ~20pt up there - env(safe-area-inset-top) alone left this
       sitting right under it, faded/cut by the status bar overlay. --top-clear
       matches `header`'s own
       tuned value - even a real notch/Dynamic Island's reported inset
       wasn't quite enough on its own before that bump, same symptom, see
       header's comment for the fuller story. */
    position:absolute; top:var(--top-clear); left:16px;
    width:36px; height:36px; border-radius:50%; border:none;
    background:rgba(255,255,255,.10); color:#fff; font-size:20px;
    display:flex; align-items:center; justify-content:center; cursor:pointer;
  }
  .coin-flip-panel{ width:100%; max-width:320px; }
  .coin-flip-title{ margin:0 0 4px; color:#fff; font-size:19px; font-weight:700; text-align:center; }
  .coin-flip-sub{ margin:0 0 18px; color:rgba(255,255,255,.65); font-size:13px; text-align:center; }
  .coin-color-section{ margin-bottom:18px; }
  .coin-color-section:last-of-type{ margin-bottom:6px; }
  .coin-color-label{
    margin:0 0 8px; color:rgba(255,255,255,.85); font-size:12px; font-weight:700;
    text-transform:uppercase; letter-spacing:.04em;
  }
  .coin-color-grid{ display:flex; flex-wrap:wrap; gap:10px; }
  .coin-swatch{
    width:34px; height:34px; border-radius:50%; border:2px solid rgba(255,255,255,.25);
    cursor:pointer; padding:0; position:relative; flex-shrink:0;
    transition:transform .15s ease, border-color .15s ease;
  }
  .coin-swatch[data-color="white"]{ border-color:rgba(255,255,255,.55); }
  .coin-swatch.selected{ border-color:#fff; transform:scale(1.12); box-shadow:0 0 0 2px rgba(255,255,255,.25); }
  .coin-swatch.selected::after{
    content:'✓'; position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
    color:#fff; font-size:15px; font-weight:800; text-shadow:0 1px 2px rgba(0,0,0,.5);
  }
  .coin-swatch[data-color="white"].selected::after{ color:#16202b; text-shadow:none; }
  .coin-swatch:active{ transform:scale(.92); }
  .coin-color-error{ color:#ff8a8a; font-size:13px; text-align:center; margin:10px 0 4px; }

  .coin-stage{ display:flex; align-items:center; justify-content:center; height:240px; perspective:900px; }
  .coin-arc.flying{ animation:coinArc 3s cubic-bezier(.33,0,.2,1); }
  @keyframes coinArc{
    0%{ transform:translateY(0) scale(.88); }
    45%{ transform:translateY(-120px) scale(1.18); }
    100%{ transform:translateY(0) scale(1); }
  }
  .coin{
    width:110px; height:110px; position:relative;
    transform-style:preserve-3d;
  }
  .coin-face{
    position:absolute; inset:0; border-radius:50%;
    display:flex; align-items:center; justify-content:center;
    background:radial-gradient(circle at 35% 30%, #ffe6a8, #d9a02f 55%, #a9760f 100%);
    backface-visibility:hidden; -webkit-backface-visibility:hidden;
    box-shadow:0 6px 18px rgba(0,0,0,.4), inset 0 0 0 4px rgba(255,255,255,.25);
  }
  .coin-face svg{ width:56%; height:56%; }
  .coin-face-back{ transform:rotateY(180deg); }
  .coin-result-badge{
    width:110px; height:110px; margin:0 auto 18px; border-radius:50%;
    display:flex; align-items:center; justify-content:center;
    background:radial-gradient(circle at 35% 30%, #ffe6a8, #d9a02f 55%, #a9760f 100%);
    box-shadow:0 6px 18px rgba(0,0,0,.4), inset 0 0 0 4px rgba(255,255,255,.25);
  }
  .coin-result-badge svg{ width:56%; height:56%; }
  .coin-result-text{ margin:0 0 20px; color:#fff; font-size:17px; font-weight:700; text-align:center; line-height:1.4; }
  @media (prefers-reduced-motion: reduce){
    .coin-flip-screen{ transition:opacity .3s ease; transform:none; }
    .coin-arc.flying{ animation:none; }
  }

  /* ---------- Historikk (full-screen, reachable from the launcher tile) ----------
     Column layout (not coin-flip-screen's centered one) since this holds a
     potentially long, independently-scrolling list rather than one small
     centered panel. */
  .history-screen{
    position:absolute; inset:0; z-index:310;
    display:flex; flex-direction:column;
    background:linear-gradient(165deg, var(--navy-2), var(--navy) 55%, #081120);
    padding:calc(var(--top-clear) + 41px) 20px calc(env(safe-area-inset-bottom) + 20px);
    opacity:0; pointer-events:none; transform:scale(1.03);
    transition:opacity .3s ease, transform .3s ease;
  }
  .history-screen.open{ opacity:1; pointer-events:auto; transform:scale(1); }
  /* Settings/Register reuse .history-screen but don't need the big top
     reservation Historikk's own floating close button wants - Settings
     puts its back button inline in the header row instead (no floating
     button to clear), and Register's centered card doesn't sit under one
     either, so both can start much closer to the top. */
  .history-screen.compact-top{ padding-top:var(--top-clear); }
  /* margin-bottom: the back button/title row otherwise sat right on top
     of whatever comes next (e.g. #settingsStorageNote) - noticeably
     cramped on the compact-top screens, which have no floating button
     reserving extra room above the first in-flow content like Historikk's
     own (non-compact) header effectively gets. Fixed by adding room BELOW
     the row, not by moving the row itself further up - moving it up is
     exactly what re-creates the faded/diffuse-near-the-status-bar problem
     the +26px top offset above exists to avoid. */
  .history-header{ display:flex; align-items:center; justify-content:space-between; gap:12px; margin-bottom:18px; }
  .history-header-actions{ display:flex; align-items:center; gap:8px; }
  .screen-back-btn{
    width:30px; height:30px; border-radius:50%; flex:0 0 auto;
    background:rgba(255,255,255,.1); border:none; color:#fff;
    font-size:18px; display:flex; align-items:center; justify-content:center; cursor:pointer;
  }
  .history-header.compact-header{ justify-content:flex-start; }
  .history-title{ margin:0; color:#fff; font-size:19px; font-weight:700; }
  .history-select-mode-btn{
    border:1px solid rgba(255,255,255,.25); border-radius:999px;
    background:rgba(255,255,255,.08); color:#fff;
    font-size:12.5px; font-weight:700; padding:6px 12px; cursor:pointer;
    flex-shrink:0;
  }
  .history-select-mode-btn.active{ background:var(--accent); color:var(--accent-ink); border-color:transparent; }
  .history-info-note{ margin:8px 0 14px; color:rgba(255,255,255,.55); font-size:12px; line-height:1.5; }
  .history-list{ flex:1 1 auto; min-height:0; overflow-y:auto; touch-action:pan-y; display:flex; flex-direction:column; gap:10px; padding-bottom:4px; }
  .history-empty{ color:rgba(255,255,255,.55); font-size:13.5px; text-align:center; margin-top:40px; line-height:1.6; }

  .history-row{
    background:rgba(255,255,255,.08); border:1px solid rgba(255,255,255,.14);
    border-radius:var(--radius-md); overflow:hidden; flex-shrink:0;
  }
  .history-row-main{ display:flex; align-items:center; gap:10px; padding:12px 14px; cursor:pointer; }
  .history-row-check{ width:20px; height:20px; flex-shrink:0; accent-color:var(--accent); }
  .history-row-text{ flex:1 1 auto; min-width:0; }
  .history-row-top{ display:flex; justify-content:space-between; align-items:baseline; gap:10px; }
  .history-row-opponent{ color:#fff; font-weight:700; font-size:14px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
  .history-row-score{ color:#fff; font-weight:800; font-variant-numeric:tabular-nums; white-space:nowrap; }
  .history-row-date{ margin-top:2px; color:rgba(255,255,255,.55); font-size:11.5px; }
  /* Same double-tap-to-confirm pattern used everywhere else destructive in
     this app (Avslutt og nullstill, Overfør økt-eier) - a single stray tap
     on a small icon in a scrolling list is exactly the kind of accident
     this guards against, without needing a full confirm modal per row. */
  .history-row-delete{
    flex-shrink:0; width:30px; height:30px; border-radius:50%; border:none;
    background:rgba(255,255,255,.10); color:rgba(255,255,255,.75); font-size:13px;
    display:flex; align-items:center; justify-content:center; cursor:pointer;
    transition:background .15s ease, color .15s ease;
  }
  .history-row-delete.armed{ background:var(--danger); color:#fff; }
  .history-row-visibility{
    flex-shrink:0; width:30px; height:30px; border-radius:50%; border:none;
    background:rgba(255,255,255,.10);
    display:flex; align-items:center; justify-content:center; cursor:pointer;
  }
  .history-row-visibility.visible{ background:rgba(50,215,75,.16); }
  .history-row-chevron{
    flex-shrink:0; color:rgba(255,255,255,.5); font-size:12px; transition:transform .2s ease;
  }
  .history-row.expanded .history-row-chevron{ transform:rotate(180deg); }
  .history-row-detail{ padding:0 14px 12px; }
  .history-row-player{
    display:flex; justify-content:space-between; gap:8px;
    font-size:12.5px; color:rgba(255,255,255,.8); padding:3px 0;
  }
  .history-row-player-name{ white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
  .history-row-player-stats{ font-variant-numeric:tabular-nums; white-space:nowrap; color:rgba(255,255,255,.6); }
  .history-row-goals-title{
    margin-top:8px; padding-top:8px; border-top:1px solid rgba(255,255,255,.14);
    font-size:10.5px; font-weight:800; letter-spacing:.05em; text-transform:uppercase;
    color:rgba(255,255,255,.5);
  }
  .history-row-goal{
    display:flex; align-items:center; gap:8px;
    font-size:12.5px; color:rgba(255,255,255,.8); padding:3px 0;
  }
  /* Small colored dot instead of repeating "hjemme/borte" in text - green
     for our own goals (matches the app's other "positive/active" green,
     e.g. multiBytte's ring), muted grey for the opponent's. */
  .history-row-goal::before{
    content:''; width:7px; height:7px; border-radius:50%; flex:0 0 auto;
    background:#32b45a;
  }
  .history-row-goal-away::before{ background:rgba(255,255,255,.35); }
  .history-row-goal-time{ font-variant-numeric:tabular-nums; color:rgba(255,255,255,.6); }
  .history-row-goal-scorer{ white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }

  .history-select-bar{
    display:flex; align-items:center; gap:10px; margin-top:12px; flex-shrink:0;
    padding-top:12px; border-top:1px solid rgba(255,255,255,.14);
  }
  /* Author display:flex above beats the browser's default [hidden]{display:
     none} regardless of specificity - same gotcha documented elsewhere in
     this file (.settings-row[hidden] etc.) - toggled via the `hidden`
     property in renderHistoryList(). */
  .history-select-bar[hidden]{ display:none; }
  .history-select-bar-btn{
    border:1px solid rgba(255,255,255,.25); border-radius:999px;
    background:rgba(255,255,255,.08); color:#fff; font-size:12.5px; font-weight:700;
    padding:8px 14px; cursor:pointer;
  }
  .history-select-bar-btn.danger{ background:var(--danger); border-color:transparent; }
  .history-select-bar-btn.danger:disabled{ opacity:.4; cursor:not-allowed; }
  .history-select-count{ flex:1 1 auto; text-align:center; color:rgba(255,255,255,.7); font-size:12.5px; }
  @media (prefers-reduced-motion: reduce){
    .history-screen{ transition:opacity .3s ease; transform:none; }
  }

  /* ---------- Header ---------- */
  header{
    position:relative;
    flex:0 0 auto;
    display:flex; align-items:center; justify-content:space-between;
    padding:10px 14px;
    /* max(env(...), 20px): a rectangular-corner iPad/iPhone (no notch, no
       Dynamic Island) reports 0 for env(safe-area-inset-top) even running
       as a standalone home-screen app, but the OS status bar still sits
       there visually - with just env() alone (0 on that device) the header
       buttons' top edge landed right under the status bar's own
       translucent overlay, reading as "cut off / faded at the top". 20px
       is a safe floor close to that status bar's real height, applied
       only as a minimum - notched devices' real (larger) inset still wins.
       The buffer past that in --top-clear (bumped several times - even on a real notch/Dynamic Island device, reported
       env(safe-area-inset-top) wasn't quite enough clearance and every
       header button's top edge still read as faded/diffuse, same symptom
       as the no-notch case above just on a device the 20px floor doesn't
       even apply to) pushes every header button fully clear of that
       translucent status-bar overlay instead of just grazing its lower
       edge. Note this only covers the RESTING position - iOS's
       rubber-band bounce still slides the header (not position:fixed, see
       #app above) briefly up under that overlay while actively dragging
       past the top edge; fixing that needs a touchmove guard, deliberately
       not added (yet). */
    padding-top:var(--top-clear);
    background:linear-gradient(180deg, rgba(22,41,74,.82), rgba(15,31,54,.82));
    -webkit-backdrop-filter:blur(var(--glass-blur)) saturate(160%);
    backdrop-filter:blur(var(--glass-blur)) saturate(160%);
    border-bottom:1px solid rgba(255,255,255,.08);
    color:#fff;
  }
  /* A plain 3rd flex child between undoBtn and .header-actions (see header
     markup) - header's own justify-content:space-between then places it
     in the natural gap between them. True mathematical center of the
     whole header was tried first and rejected: the play/avslutt/
     innstillinger cluster is wide enough that dead-center overlapped
     "Avslutt", not the gap the request actually meant. */
  /* Rotated phone: the back button and the start/avslutt/settings group
     sit right at the screen's rounded outer corners otherwise - nudge both
     inward from the safe area. Portrait is untouched.
     :not(.desktop-preview) - `orientation: landscape` reflects the real
     BROWSER WINDOW's raw aspect ratio, true for virtually every ordinary
     desktop window, not just a genuinely rotated phone. #app's own box
     inside .desktop-preview is already always letterboxed upright (see
     that rule's own comment) regardless of the window's shape, so this
     "rotated phone" treatment must stay scoped OUT of it - otherwise the
     content inside a correctly upright-shaped preview box still tried to
     lay itself out as if landscape, which is exactly the "banen klemt inn
     i en smal stripe" bug this comment is here to prevent regressing. */
  @media (orientation: landscape){
    #viewport-frame:not(.desktop-preview) header{
      padding-left: calc(14px + env(safe-area-inset-left) + 14px);
      padding-right: calc(14px + env(safe-area-inset-right) + 14px);
    }
  }
  .header-actions{ display:flex; align-items:center; gap:16px; }
  .icon-btn{
    position:relative;
    width:38px; height:38px; border-radius:50%;
    border:none; background:rgba(255,255,255,.12); color:#fff;
    font-size:18px; display:flex; align-items:center; justify-content:center;
    cursor:pointer;
  }
  .icon-btn:active{ background:rgba(255,255,255,.22); }
  .icon-btn:disabled{ opacity:.28; cursor:default; }
  .icon-btn:disabled:active{ background:rgba(255,255,255,.12); }
  .goal-icon{ width:21px; height:21px; display:block; }
  .icon-btn-badge{
    position:absolute; top:-2px; right:-2px;
    min-width:16px; height:16px; padding:0 3px; border-radius:50%;
    background:var(--accent); color:var(--accent-ink);
    font-size:10px; font-weight:800; line-height:16px; text-align:center;
    box-shadow:0 1px 3px rgba(0,0,0,.35);
  }
  .end-btn{
    border:1px solid rgba(255,69,58,.45);
    background:rgba(255,69,58,.18); color:#ff6259;
    padding:8px 13px; border-radius:100px;
    font-size:13px; font-weight:700; cursor:pointer;
    -webkit-backdrop-filter:blur(10px);
    backdrop-filter:blur(10px);
  }
  .end-btn:active{ transform:scale(.96); }

  /* ---------- Main split ---------- */
  #main{
    position:relative;
    flex:1 1 auto; min-height:0;
    display:flex; flex-direction:column;
  }

  #field-wrap{
    flex:0 0 68%;
    position:relative;
    overflow:hidden;
    background:
      repeating-linear-gradient(180deg, var(--green-1) 0 40px, var(--green-2) 40px 80px);
  }
  .pitch-lines{ position:absolute; inset:0; width:100%; height:100%; display:block; }
  .match-clock-wrap{
    position:absolute; top:10px; left:50%; transform:translateX(-50%);
    display:flex; flex-direction:row; align-items:center; gap:10px;
    z-index:5;
  }
  .match-clock-center{
    display:flex; flex-direction:column; align-items:center; gap:4px;
  }
  /* Home/away goal counters flanking the clock - tap to register a goal
     (home opens the player picker, away registers anonymously - see
     onHomePlayerPicked/onAwayScoreTap), long-press to correct one. Styled
     as a small companion scoreboard to the main digital clock, not part
     of it, so it doesn't compete with the click-to-open-duration-picker
     affordance on #matchClock itself. */
  .score-btn{
    -webkit-appearance:none; appearance:none; border:none; cursor:pointer;
    background:rgba(0,0,0,.45); border:1px solid rgba(255,255,255,.18);
    border-radius:8px; color:#f4f4f2;
    font-family:'SF Mono','Courier New',ui-monospace,monospace;
    font-size:22px; font-weight:800; font-variant-numeric:tabular-nums;
    min-width:30px; padding:6px 10px;
    text-shadow:0 1px 3px rgba(0,0,0,.6);
    transition:transform .12s ease, background .15s ease;
    -webkit-touch-callout:none; touch-action:manipulation;
  }
  .score-btn:active{ transform:scale(.92); background:rgba(0,0,0,.6); }
  /* Team code identifying which side is "us" at a glance, upright to the
     left of the score - like a real broadcast scoreboard's 3-letter
     abbreviation. Positioned OUTSIDE .match-clock-wrap's own box (right:100%
     of itself, i.e. flush against the wrap's left edge) rather than as a
     normal-flow flex child, specifically so it does NOT count towards the
     wrap's width - home score/clock/away score stay the thing that's
     centered on the pitch (left:50%/translateX(-50%) on the wrap), with
     NØT hanging off to the left of that centered group, uncounted. */
  .home-team-label{
    position:absolute; right:100%; top:50%; transform:translateY(-50%);
    margin-right:6px;
    font-size:11px; font-weight:800; letter-spacing:.05em;
    text-transform:uppercase;
    color:rgba(255,255,255,.8);
    text-shadow:0 1px 2px rgba(0,0,0,.5);
    white-space:nowrap;
  }
  /* Mirror of .home-team-label, hanging off the wrap's right edge instead -
     shows the opponent's 3-letter code once set (see #opponentModal), or
     "?" as a standing invitation to tap and set one. A real <button> (home's
     is static text, hardcoded to this team) so it doubles as the "lurt
     sted" to open/edit that anytime, not just via the automatic prompts. */
  .away-team-label{
    position:absolute; left:100%; top:50%; transform:translateY(-50%);
    margin-left:6px;
    border:none; background:none; padding:0; cursor:pointer;
    font-family:inherit; font-size:11px; font-weight:800; letter-spacing:.05em;
    text-transform:uppercase;
    color:rgba(255,255,255,.8);
    text-shadow:0 1px 2px rgba(0,0,0,.5);
    white-space:nowrap;
  }
  .away-team-label:active{ opacity:.7; }
  /* Extra visual invitation while no opponent has been set - a bare "?" in
     11px translucent text (matching NØT's understated broadcast-graphic
     look) turned out too easy to miss as something tappable. Once a real
     code is set it drops back to that same quiet look (.away-team-label
     alone, above) since at that point it's just information, not a
     prompt. Toggled in renderScore(). */
  .away-team-label.unset{
    background:rgba(255,255,255,.14);
    border:1px dashed rgba(255,255,255,.55);
    border-radius:999px;
    padding:2px 7px;
    font-size:12px;
  }
  #matchClock{
    background:linear-gradient(180deg,#222,#000);
    border:2px solid rgba(255,255,255,.22);
    border-radius:8px;
    padding:5px 22px;
    font-family:'SF Mono','Courier New',ui-monospace,monospace;
    font-size:30px; font-weight:700; color:#f4f4f2;
    font-variant-numeric:tabular-nums; letter-spacing:3px;
    text-shadow:0 0 6px rgba(255,255,255,.6), 0 0 16px rgba(255,255,255,.3);
    box-shadow:0 8px 20px rgba(0,0,0,.65), inset 0 0 10px rgba(0,0,0,.6);
    cursor:pointer;
  }
  #matchClock:active{ transform:scale(.97); }
  /* Small always-visible countdown beside the main (frozen) clock - counts
     down to kampvarighet, then flips to orange "+MM:SS" overtime (see
     matchCountdownMs). Deliberately much smaller than the big clock. */
  #matchCountdown{
    background:rgba(0,0,0,.45);
    border:1px solid rgba(255,255,255,.15);
    border-radius:6px;
    padding:3px 9px;
    font-family:'SF Mono','Courier New',ui-monospace,monospace;
    font-size:14px; font-weight:700; color:rgba(244,244,242,.82);
    font-variant-numeric:tabular-nums; letter-spacing:1px;
    text-shadow:0 1px 3px rgba(0,0,0,.6);
    pointer-events:none;
    transition:color .2s ease, border-color .2s ease;
  }
  #matchCountdown.overtime{ color:var(--orange); border-color:rgba(212,144,10,.4); }
  /* Formation layout (see fieldFormationRows() in app.js): #field is a
     column of .field-row bands, one per tactical line, DOM-ordered top
     (closest to the match clock/goal the home team attacks) to bottom
     (closest to #bench-wrap below) - so the row array app.js builds
     bottom-to-top (keeper first) gets appended in reverse. */
  /* padding-top: 98px, not a token-ish 22px - .match-clock-wrap (score
     buttons + the big digital clock) is absolutely positioned over #field
     at top:10px and measures ~76px tall on its own, so it occupies
     #field-wrap's own top ~86px regardless of what #field does. The old
     free-wrapping layout rarely put a token that close to the top edge
     (tokens centered as a loose blob via align-content:center), so this
     never mattered - real bug once the formation layout started pinning
     the forward row right at #field's own padding-top: its avatar rendered
     hidden behind the clock. 98px clears that 86px with a little room to
     spare; applyFieldDensity()'s own row-height budget in app.js uses the
     same number, so keep them in sync if this ever changes. */
  #field{
    position:relative;
    height:100%;
    display:flex; flex-direction:column;
    padding:98px calc(16px + env(safe-area-inset-right)) 16px calc(16px + env(safe-area-inset-left));
    overflow-y:auto; touch-action:pan-y;
    gap:6px;
  }
  .field-row{
    display:flex; flex-direction:row; flex-wrap:wrap;
    align-items:center; justify-content:center;
    gap:24px 18px;
    flex:1 1 0; min-height:0;
  }
  /* The goalkeeper doesn't get its own evenly-shared line like the other
     rows (flex:1 above) - it sits at a fixed size closest to the bench,
     pulled up toward the defense row above it a little instead of a full
     row gap, so it reads as "just behind the back line" rather than a 4th
     evenly-spaced row. --field-keeper-nudge is computed in
     applyFieldDensity() from the actual leftover space in this row's
     budget (not a flat ratio of avatar size, which once overlapped the row
     above - see that function's own comment) so it can never overshoot
     into content the row above still needs. */
  .field-row-keeper{
    flex:0 0 auto;
    margin-top:calc(-1 * var(--field-keeper-nudge, 0px));
  }
  /* The row directly above the keeper (always defense) - the keeper's own
     upward pull above could leave its timer text sitting right on top of
     the defense row's timer text, worst right under the center defender
     (the keeper is horizontally centered, so it lines up under whichever
     defender happens to be in the middle). Moving the keeper down instead
     isn't safe - #field's bottom padding is tight, and the pitch's own
     goal-box line (drawn by #pitchLines, not part of this flex layout at
     all) sits close enough there that a lower keeper risks its white
     timer text landing right on that white line. Nudging the row ABOVE it
     up a little instead keeps the keeper exactly where it already was.
     applyFieldDensity() reserves room for this fixed 6px in its own
     height budget - keep both in sync if this changes. */
  .field-row-pre-keeper{
    margin-bottom:6px;
  }

  #bench-wrap{
    flex:0 0 32%;
    position:relative;
    background:var(--bench-bg);
    border-top:1px solid rgba(15,31,54,.08);
    display:flex; flex-direction:column;
    min-height:0;
  }

  /* Rotated phone: bench moves to a left-hand column, pitch takes the rest
     of the width on the right - order swapped via flex `order`, source
     order (and therefore all the JS that reads/writes these elements)
     stays untouched. :not(.desktop-preview) - see the identical guard on
     the header padding-rule above for why. */
  @media (orientation: landscape){
    #viewport-frame:not(.desktop-preview) #main{ flex-direction:row; }
    #viewport-frame:not(.desktop-preview) #field-wrap{ flex:1 1 auto; order:2; }
    #viewport-frame:not(.desktop-preview) #bench-wrap{
      flex:0 0 36%;
      order:1;
      border-top:none;
      border-right:1px solid rgba(15,31,54,.08);
    }
  }
  .bench-label-row{
    display:flex; align-items:center; justify-content:space-between; gap:10px;
    padding:10px calc(16px + env(safe-area-inset-right)) 2px calc(16px + env(safe-area-inset-left));
  }
  .bench-label{
    font-size:17px; font-weight:800; letter-spacing:-.01em;
    color:var(--ink);
    /* Without this, .bench-label-row's justify-content:space-between only
       guarantees a gap when the two sides' natural widths leave room - in
       landscape #bench-wrap is just ~36% of the screen (see below), so a
       long label + the action buttons could butt right up against each
       other. min-width:0 lets the label actually shrink/truncate instead,
       and the row's own `gap` above becomes a real minimum. */
    flex:1 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
  }
  .bench-actions{ display:flex; gap:8px; flex-shrink:0; }
  .bench-action-btn{
    display:flex; align-items:center; gap:5px;
    border:none; border-radius:999px;
    background:rgba(15,31,54,.08); color:var(--ink-soft);
    font-size:12px; font-weight:700; padding:6px 10px;
    cursor:pointer;
    transition:transform .12s ease, background .15s ease, color .15s ease;
  }
  /* Fixed-size box (not just font-size on the raw glyph) so .btn-badge
     below has a predictable corner to anchor against - the arrow unicode
     characters carry a lot of invisible side-bearing inside their own
     character cell, which throws off any offset measured from the glyph's
     text metrics instead of a real box. */
  .bench-action-btn .btn-icon{
    position:relative; display:inline-flex; align-items:center; justify-content:center;
    width:17px; height:15px; font-size:14px; line-height:1;
  }
  /* "?" (Forslag) / "×2" (multiBytte) riding the icon's corner like a
     notification badge - explains what the arrows mean without needing
     the word label, which is exactly what's missing once landscape mode
     hides it below. Sits mostly ABOVE the button's own pill/circle
     background (not just above the glyph inside it) so it still reads
     clearly at a glance rather than blending into the button. */
  .btn-badge{
    position:absolute; top:-15px; right:-8px;
    display:flex; align-items:center; justify-content:center;
    min-width:14px; height:14px; padding:0 3.5px;
    font-size:9px; font-weight:800; line-height:1; color:#fff;
    background:var(--navy); border:1.5px solid var(--bench-bg); border-radius:999px;
    box-shadow:0 1px 2px rgba(15,31,54,.35);
  }
  .bench-action-btn:active{ transform:scale(.94); }
  .bench-action-btn.shake{ animation:shake .4s; }
  /* Sits beside multiBytte, hidden (zero-width, not just transparent -
     see .multi-cancel-btn's resting state below) until multi-select mode
     is actually active, then springs into view - a plain, always-
     tappable "×" to bail out entirely (clear the marks and exit the
     mode), rather than a drag gesture the button offers no hint of. */
  .multi-select-group{ display:flex; flex-direction:row; align-items:center; }
  .multi-cancel-btn{
    -webkit-appearance:none; appearance:none;
    width:0; height:26px; flex-shrink:0; padding:0; margin-left:0;
    border-radius:50%; border:none;
    background:var(--danger); color:#fff;
    font-size:15px; font-weight:800; line-height:1; cursor:pointer;
    display:flex; align-items:center; justify-content:center;
    opacity:0; overflow:hidden; pointer-events:none;
    transform:scale(.4);
    transition:width .28s cubic-bezier(.25,.8,.25,1), margin-left .28s cubic-bezier(.25,.8,.25,1),
      opacity .2s ease, transform .28s cubic-bezier(.25,.8,.25,1);
  }
  /* Toggled by updateMultiSelectUI() instead of the hidden attribute -
     .multi-cancel-btn already sets its own `display`, which silently
     overrides the browser's default [hidden]{display:none} rule (author
     styles beat that default regardless of specificity), so `hidden`
     alone never actually hid it. This class-driven width/opacity
     transition doubles as the "row flies left to make room" animation:
     multiBytte has nothing pinning it in place, so the row (right-
     aligned via .bench-label-row's space-between) naturally slides left
     as this widens beside it. */
  .multi-cancel-btn.visible{
    width:26px; margin-left:8px; opacity:1; transform:scale(1); pointer-events:auto;
  }
  .multi-cancel-btn:active{ transform:scale(.9); }
  /* Landscape: #bench-wrap is only ~36% of the width (see the layout
     media query above), too narrow for icon+text pills without wrapping
     - icon-only circular buttons instead, same icons the legend already
     explains, so nothing is lost. max-width caps this to phone-sized
     screens - the largest iPhone landscape width is ~956px (17 Pro Max),
     the smallest iPad landscape width is 1024px (mini), so 980px sits in
     the gap between them and keeps the text label on iPads. */
  @media (orientation: landscape) and (max-width: 980px){
    #viewport-frame:not(.desktop-preview) .bench-action-btn{
      width:38px; height:38px; padding:0; border-radius:50%;
      justify-content:center;
    }
    #viewport-frame:not(.desktop-preview) .bench-action-btn .btn-label{ display:none; }
    #viewport-frame:not(.desktop-preview) .bench-action-btn .btn-icon{ width:19px; height:17px; font-size:16px; }
    /* Kept nearly as large as the portrait badge (14px), not shrunk down -
       this is the one place the badge has to carry the icon's meaning
       entirely on its own, since .btn-label is hidden right above. */
    #viewport-frame:not(.desktop-preview) .btn-badge{ top:-13px; right:-7px; min-width:13px; height:13px; font-size:8.5px; }
  }
  .multi-select-btn.active{ background:rgba(230,57,70,.12); color:var(--danger); }
  .multi-select-btn.ready{ background:rgba(31,157,76,.14); color:#1f9d4c; }
  #bench{
    flex:1; min-height:0; overflow-y:auto; touch-action:pan-y;
    display:flex; flex-wrap:wrap; align-content:flex-start;
    gap:14px;
    padding:8px calc(16px + env(safe-area-inset-right)) calc(env(safe-area-inset-bottom) + 14px) calc(16px + env(safe-area-inset-left));
  }

  /* ---------- Play/Pause pill ---------- */
  #playPauseBtn{
    display:flex; align-items:center; gap:6px;
    border:1px solid rgba(50,215,75,.4);
    background:rgba(50,215,75,.16); color:#32d74b;
    padding:8px 14px; border-radius:100px;
    font-size:14px; font-weight:700;
    cursor:pointer;
    transition:background .2s ease, color .2s ease, border-color .2s ease;
  }
  #playPauseBtn.running{
    border-color:rgba(212,144,10,.45);
    background:rgba(212,144,10,.18); color:#e4a41c;
  }
  #playPauseBtn:active{ transform:scale(.96); }
  #playPauseBtn .pp-icon{ font-size:13px; line-height:1; }

  /* ---------- Tokens ---------- */
  .token{
    touch-action:none; -webkit-touch-callout:none;
    display:flex; flex-direction:column; align-items:center;
    cursor:grab;
  }
  /* Field-token/avatar size is set per render from state.fieldSize (see
     applyFieldDensity) via these custom properties, so more utespillere
     packed onto the same pitch get proportionally smaller icons instead of
     overflowing. Bench tokens stay a fixed size - only the field gets denser. */
  .field-token{
    width:var(--field-token-width, 96px);
  }
  .bench-token{ width:64px; }

  .avatar{
    width:var(--field-avatar-size, 68px); height:var(--field-avatar-size, 68px);
    border-radius:50%;
    display:flex; align-items:center; justify-content:center;
    font-weight:700; font-size:var(--field-avatar-font, 19px); color:#fff;
    background:linear-gradient(160deg,var(--navy-2),var(--navy));
    box-shadow:var(--shadow-sm);
    border:2px solid rgba(255,255,255,.55);
    position:relative;
    transition:box-shadow .15s ease, width .2s ease, height .2s ease;
  }
  .bench-avatar{
    width:48px; height:48px; font-size:14px;
    background:linear-gradient(160deg,#8c95a3,#6b7686);
    border-color:rgba(255,255,255,.7);
  }
  .badge-warning{
    position:absolute; top:-7px; right:-7px;
    width:var(--field-badge-size, 23px); height:var(--field-badge-size, 23px);
    border-radius:50%;
    color:#fff;
    font-size:var(--field-badge-font, 14px); font-weight:800;
    display:flex; align-items:center; justify-content:center;
    box-shadow:0 1px 3px rgba(0,0,0,.35);
    transition:background .25s ease;
  }
  /* Orange, still - byttetiden (standard spilletid) er nådd. */
  .badge-warning.due{ background:var(--orange); }
  /* Same orange, gently pulsing - spilt 50% mer enn byttetiden; only this
     tier pulses, so the pulse itself signals "this one's more overdue" -
     deliberately not red, so the swap-arrows badge doesn't need a second
     color to explain (see #1070/updateTimersOnly in app.js for the glyph). */
  .badge-warning.overtime{ background:var(--orange); animation:badgePulse 1.4s ease-in-out infinite; }
  @keyframes badgePulse{
    0%,100%{ opacity:1; }
    50%{ opacity:.55; }
  }
  /* Personal goal tally, centered under the initials inside the same
     circle (not a corner badge like the others above) - blank at 0 (see
     goalBadgeHtml), white otherwise, yellow for the/a tied top scorer.
     Blue avatar background + yellow number is a deliberate complementary
     pairing (see computeGoalBadges/renderField) for max legibility. */
  .badge-goals{
    position:absolute; left:50%; bottom:7px; transform:translateX(-50%);
    font-size:12px; font-weight:800; line-height:1; color:#fff;
    text-shadow:0 1px 2px rgba(0,0,0,.5);
    pointer-events:none;
  }
  .badge-goals.top-scorer{ color:#ffd60a; }
  .bench-avatar .badge-goals{ bottom:4px; font-size:10px; }

  /* Used to float centered over the pitch itself (left:50%/top:68%) with
     pointer-events:none on the card so it never blocked a drag on the
     token underneath it - except that meant tapping anywhere on the card
     that WASN'T the "i"/swap button (both individually re-enabled) fell
     straight through onto whatever field token happened to be directly
     behind it, which could silently trigger a real position swap with a
     neighbor instead of doing nothing. Real bug, only found by actually
     tapping around inside the popup, not by reading the CSS. Docks over
     the match-clock-wrap/score area at the top of #field-wrap instead -
     never geometrically over a token no matter the formation, so it can
     safely be a normal, fully-interactive card (no pointer-events:none)
     with its own explicit close button, plus the document-level "tap
     outside to close" listener in app.js. */
  #selectionInfo{
    position:absolute; left:50%; top:10px; transform:translateX(-50%) translateY(-6px);
    max-width:calc(100% - 32px);
    background:rgba(15,31,54,.94); color:#fff;
    -webkit-backdrop-filter:blur(var(--glass-blur)) saturate(160%);
    backdrop-filter:blur(var(--glass-blur)) saturate(160%);
    border:1px solid rgba(255,255,255,.16);
    padding:9px 62px 9px 16px; border-radius:16px;
    box-shadow:var(--shadow-md);
    text-align:left;
    opacity:0; pointer-events:none;
    transition:opacity .2s ease, transform .2s ease;
    z-index:50;
  }
  #multiSwapWarning{
    position:absolute; left:50%; top:50%; transform:translate(-50%,-50%);
    background:rgba(230,57,70,.92); color:#fff;
    -webkit-backdrop-filter:blur(var(--glass-blur)) saturate(160%);
    backdrop-filter:blur(var(--glass-blur)) saturate(160%);
    border:1px solid rgba(255,255,255,.2);
    padding:12px 20px; border-radius:16px;
    font-size:13px; font-weight:700; text-align:center; line-height:1.4;
    box-shadow:var(--shadow-md);
    opacity:0; pointer-events:none;
    transition:opacity .2s ease;
    z-index:20;
    max-width:78%;
  }
  #multiSwapWarning.visible{ opacity:1; }
  /* "Slipp for å bytte" - vises mens en dratt spiller svever over en gyldig bytte-
     partner (se showDropHint() i app.js). Samme glasskort og samme plassering som
     #selectionInfo over, men aldri klikkbart. */
  #dropHint{
    position:absolute; left:50%; top:10px; transform:translateX(-50%) translateY(-6px);
    max-width:calc(100% - 32px);
    background:rgba(15,31,54,.94); color:#fff;
    -webkit-backdrop-filter:blur(var(--glass-blur)) saturate(160%);
    backdrop-filter:blur(var(--glass-blur)) saturate(160%);
    border:1px solid rgba(255,255,255,.16);
    padding:9px 16px; border-radius:16px;
    box-shadow:var(--shadow-md);
    text-align:center;
    opacity:0; pointer-events:none;
    transition:opacity .15s ease, transform .15s ease;
    z-index:51;
  }
  #dropHint.visible{ opacity:1; transform:translateX(-50%) translateY(0); }
  #dropHint .dh-names{ display:flex; align-items:center; justify-content:center; gap:8px; min-width:0; }
  #dropHint .dh-name{ font-size:13px; font-weight:700; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:7.5em; }
  #dropHint .dh-arrows{ color:rgba(255,255,255,.55); font-size:13px; }
  #dropHint .dh-action{
    margin-top:7px; padding-top:7px; border-top:1px solid rgba(255,255,255,.12);
    display:flex; align-items:center; justify-content:center; gap:6px;
  }
  #dropHint .dh-action svg{ display:block; }
  #dropHint .dh-label{ font-size:10.5px; font-weight:700; color:#32b45a; letter-spacing:.02em; }
  /* Symbol og grønn tekst pulserer forsiktig (opacity + en liten skalering) så det er tydelig at slippet er det som skjer nå. */
  @keyframes dropHintPulse{ 0%,100%{ opacity:.6; transform:scale(.97); } 50%{ opacity:1; transform:scale(1.04); } }
  #dropHint.visible .dh-action{ animation:dropHintPulse 1.3s ease-in-out infinite; }
  @media (prefers-reduced-motion: reduce){ #dropHint{ transition:opacity .15s ease; transform:translateX(-50%); } #dropHint.visible{ transform:translateX(-50%); } #dropHint.visible .dh-action{ animation:none; } }

  /* Trekant som dukker opp underveis (se updateRankBadgesOnly()): tones inn i stedet for å poppe. */
  @keyframes rankLiveIn{ from{ opacity:0; transform:scale(.5); } to{ opacity:1; transform:none; } }
  .badge-rank.rank-live{ animation:rankLiveIn .25s cubic-bezier(.2,.9,.3,1); }
  @media (prefers-reduced-motion: reduce){ .badge-rank.rank-live{ animation:none; } }
  #selectionInfo.visible{ opacity:1; pointer-events:auto; transform:translateX(-50%) translateY(0); }
  #selectionInfo .si-name-row{
    display:flex; align-items:center; gap:7px; min-width:0;
  }
  #selectionInfo .si-name{
    font-size:13px; font-weight:700;
    white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
  }
  /* Same short position tag that used to sit on the field token itself
     (see fieldPositionTagFor()/buildFieldTokenEl() in app.js) - moved in
     here, to the right of the name, once the popup stopped floating over
     the pitch. hidden (not just empty) when the selected player has no
     position (bench selection, or fieldSize < 5 - see fieldHasPositions()). */
  #selectionInfo .si-pos-tag{
    flex:0 0 auto;
    font-size:10.5px; font-weight:800; letter-spacing:.03em; text-transform:uppercase;
    color:rgba(255,255,255,.75);
    background:rgba(255,255,255,.14);
    border:1px solid rgba(255,255,255,.22);
    border-radius:999px;
    padding:2px 8px;
  }
  #selectionInfo .si-pos-tag[hidden]{ display:none; }
  /* "i" button - opens infoPopupModal with the T/K explanation (see
     INFO_TEXTS.playerTime). Sits left of .si-close-btn (see below), both
     in the card's top-right corner - the card itself is fully interactive
     now (no more pointer-events:none, see #selectionInfo's own comment),
     so these two no longer need their own pointer-events:auto override. */
  #selectionInfo .si-info-btn{
    position:absolute; top:7px; right:34px;
    width:16px; height:16px; border:none; background:none; padding:0;
    color:rgba(255,255,255,.55); font-size:10px; font-weight:700; font-style:italic;
    display:flex; align-items:center; justify-content:center; line-height:1;
    cursor:pointer;
  }
  /* Same invisible hit-area widening as .stack-btn-split .info-btn::before -
     the glyph itself stays 16px so it doesn't visually crowd the corner,
     but the actual tappable area is roughly doubled (~32px) since 16px was
     too fiddly to hit reliably on a phone. */
  #selectionInfo .si-info-btn::before{
    content:''; position:absolute; top:-8px; bottom:-8px; left:-8px; right:-8px;
  }
  #selectionInfo .si-info-btn:active{ transform:scale(.8); }
  /* Just a red × glyph - closes the popup outright (same as tapping
     outside it). No circle/background of its own; same invisible hit-area
     widening trick as .si-info-btn::before keeps it easy to tap without
     needing a visible shape to carry that larger hitbox. */
  #selectionInfo .si-close-btn{
    position:absolute; top:6px; right:6px;
    width:16px; height:16px; border:none; background:none; padding:0;
    color:var(--danger); font-size:15px; font-weight:700; line-height:1;
    display:flex; align-items:center; justify-content:center;
    cursor:pointer;
  }
  #selectionInfo .si-close-btn::before{
    content:''; position:absolute; top:-8px; bottom:-8px; left:-8px; right:-8px;
  }
  #selectionInfo .si-close-btn:active{ transform:scale(.8); }
  /* Spillertid/Innbyttertid, each on its own row, with T ("totaltid" -
     cumulativeFieldMs/benchMs) and K ("kamptid" - currentPeriodFieldMs/
     BenchMs, i.e. since the last Kampslutt) as separate grid columns, so
     the two letters always land directly above one another regardless of
     how many digits either time happens to have. */
  /* Half a line's worth of extra breathing room below the name/position
     row before the times start. */
  #selectionInfo .si-stats-grid{
    margin-top:9px; display:grid;
    grid-template-columns:64px 1px auto;
    column-gap:6px; row-gap:1px;
    font-size:11px; font-weight:600; color:rgba(255,255,255,.85);
    font-variant-numeric:tabular-nums; white-space:nowrap;
  }
  #selectionInfo .si-stats-grid .si-label{ text-align:left; }
  #selectionInfo .si-stats-grid .si-divider{
    width:1px; align-self:stretch; background:rgba(255,255,255,.18); justify-self:center;
  }
  /* "T: 0:00 · K: 0:00" as one grid cell (not four separate ones with a
     column-gap between each) - a real space character between "T:" and
     the value reads as one gap, a 6px grid gap on top of that read as two
     different-sized gaps and confused what belonged together. */
  #selectionInfo .si-stats-grid .si-times{ text-align:left; }
  #selectionInfo .si-stats-grid .si-sep{ margin:0 5px; opacity:.55; }
  /* Reveals that a selected token can be swapped by tapping another one -
     tapping this button itself doesn't swap anything, it just explains the
     gesture (see INFO_TEXTS.playerSwap, text depends on whether the
     selected player is on the field or the bench). Own row so it's
     discoverable without competing with the T/K grid above; centered as
     one inline-flex group (not flex-centered as separate items) so the
     icon and label move together regardless of the pill's padding. */
  #selectionInfo .si-swap-row{
    margin-top:8px; padding-top:7px; border-top:1px solid rgba(255,255,255,.12);
    text-align:center; pointer-events:auto;
  }
  #selectionInfo .si-swap-btn{
    display:inline-flex; align-items:center; gap:6px;
    border:none; background:none; padding:4px 8px; margin:0;
    cursor:pointer;
  }
  #selectionInfo .si-swap-btn svg{ display:block; }
  #selectionInfo .si-swap-btn:active{ transform:scale(.94); }
  #selectionInfo .si-swap-label{ font-size:10.5px; font-weight:700; color:#32b45a; letter-spacing:.02em; }

  .label{
    margin-top:6px; font-size:12px; font-weight:600;
    color:#fff; text-shadow:0 1px 3px rgba(0,0,0,.55);
    max-width:78px; text-align:center; line-height:1.15;
    white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
  }
  .field-token .label{ font-size:var(--field-label-font, 15px); font-weight:700; max-width:var(--field-label-max-w, 96px); margin-top:8px; }
  .bench-token .label{ color:var(--ink); text-shadow:none; max-width:64px; }

  .timer{
    margin-top:2px; font-size:13px; font-weight:700;
    font-variant-numeric:tabular-nums;
    color:#fff; text-shadow:0 1px 3px rgba(0,0,0,.55);
  }
  .field-token .timer{ font-size:var(--field-timer-font, 16px); margin-top:3px; }
  .timer.overtime{ color:#ff3b30; }
  .timer.due{ color:var(--orange); }
  .stopwatch{
    margin-top:2px; font-size:12px; font-weight:600;
    font-variant-numeric:tabular-nums; color:var(--ink-soft);
  }
  /* Ventetiden er nådd (når byttetiden måler ventetid, se benchLevel() i app.js). */
  .stopwatch.due{ color:var(--orange); font-weight:800; }
  .stopwatch.over{ color:#ff3b30; font-weight:800; }
  .bench-avatar .badge-warning{ width:19px; height:19px; font-size:12px; top:-6px; right:-6px; }

  .token.selected .avatar{
    box-shadow:0 0 0 4px rgba(255,138,61,.65), var(--shadow-sm);
  }
  .token.multi-checked .avatar{
    box-shadow:0 0 0 4px rgba(50,180,90,.7), var(--shadow-sm);
  }
  /* Pulsing (vs. multi-checked's solid) green ring - marks the bench
     player a "Forslag" tap recommends completing the swap with, so it
     reads as "please tap me" rather than "already chosen". */
  .token.suggested-partner .avatar{
    animation:suggestPulse 1.3s ease-in-out infinite;
  }
  @keyframes suggestPulse{
    0%,100%{ box-shadow:0 0 0 4px rgba(50,180,90,.75), var(--shadow-sm); }
    50%{ box-shadow:0 0 0 8px rgba(50,180,90,.3), var(--shadow-sm); }
  }
  /* Live drag-over cue - shown on whichever token is currently under the
     cursor/finger while dragging another token over it, so it's clear a
     drop here will swap the two (see onMove's hover detection). Covers
     the initials with a green tint + swap icon rather than a corner badge,
     since a corner would collide with the warning/rank badges that can
     already be showing on the same avatar. */
  .token.drop-target .avatar{
    box-shadow:0 0 0 4px rgba(50,180,90,.85), var(--shadow-sm);
  }
  .token.drop-target .avatar::after{
    content:'⇄';
    position:absolute; inset:0; border-radius:50%;
    display:flex; align-items:center; justify-content:center;
    background:rgba(31,157,76,.6);
    color:#fff; font-size:22px; font-weight:800;
  }
  /* Quick confirmation flash on a token that just landed in a swap - same
     green as the drag-over cue above, just pulsed once instead of held,
     rather than a movement animation. */
  .token.swap-flash .avatar{
    animation:swapFlash .3s ease-out;
  }
  @keyframes swapFlash{
    0%{ box-shadow:0 0 0 0 rgba(50,180,90,.85), var(--shadow-sm); }
    45%{ box-shadow:0 0 0 9px rgba(50,180,90,.5), var(--shadow-sm); }
    100%{ box-shadow:0 0 0 0 rgba(50,180,90,0), var(--shadow-sm); }
  }
  /* Small solid triangle with a true, uniform white outline (an SVG
     polygon's stroke, not a CSS border-triangle) - up-triangles (most/
     most2) point up, down-triangles (least/least2) point down. */
  .badge-rank{
    position:absolute; top:-4px; left:-4px;
    display:block;
    filter:drop-shadow(0 1px 2px rgba(0,0,0,.35));
  }
  .badge-rank.rank-most polygon{ fill:#dc2626; }
  .badge-rank.rank-most2 polygon{ fill:var(--orange); }
  .badge-rank.rank-least2 polygon{ fill:#7dd3fc; }
  .badge-rank.rank-least polygon{ fill:#2563eb; }
  .badge-check{
    position:absolute; top:-6px; right:-6px;
    width:19px; height:19px; border-radius:50%;
    background:#32b45a; color:#fff;
    font-size:11px; font-weight:800;
    display:flex; align-items:center; justify-content:center;
    box-shadow:0 1px 3px rgba(0,0,0,.35);
  }
  /* Field tokens can carry both the check (multi-select) and the warning
     (time-up) badge at once - bottom-right keeps them from overlapping. */
  .field-token .badge-check{ top:auto; bottom:-6px; right:-6px; }

  .drag-source-hidden{ opacity:.22; pointer-events:none; }
  .ghost-token{
    opacity:.95; transform:scale(1.06);
    transition:transform .12s ease;
  }
  .ghost-token .avatar{
    box-shadow:0 16px 30px rgba(0,0,0,.4), 0 4px 10px rgba(0,0,0,.22);
  }

  /* Quick, quiet settle for every token whenever the field/bench list is
     rebuilt (after a swap, undo, multi-send, etc.) - keeps re-sorted/moved
     tokens from just snapping into their new spot. Deliberately short and
     understated; the more theatrical reorgIn below is reserved for the
     rare full-squad reorganize after "Kampslutt". */
  @keyframes tokenSettle{
    from{ opacity:0; transform:scale(.94) translateY(3px); }
    to{ opacity:1; transform:scale(1) translateY(0); }
  }
  .token-enter{ animation:tokenSettle .2s cubic-bezier(.25,.8,.25,1) both; }
  .token.drop-rejected{ animation:shake .3s; }

  @keyframes reorgOut{
    from{ opacity:1; transform:scale(1); }
    to{ opacity:0; transform:scale(.55); }
  }
  @keyframes reorgIn{
    from{ opacity:0; transform:scale(.55) translateY(10px); }
    to{ opacity:1; transform:scale(1) translateY(0); }
  }
  .token.reorg-out{ animation:reorgOut .6s ease forwards; }
  .token.reorg-in{ animation:reorgIn .65s cubic-bezier(.25,.8,.25,1) backwards; }

  /* ---------- Settings modal ---------- */
  .modal{
    position:absolute; inset:0; z-index:100;
    overflow:hidden; /* .modal-card below sits at translateY(100%) when closed - its own height off-screen, below this box. Without this, that off-screen box leaks into the page's scrollable area (see #app/#viewport-frame above, which deliberately no longer clip so a genuine iOS viewport-height shortfall scrolls into view instead of being hidden - this local clip keeps that from also exposing every closed modal's parked position as bogus page height). */
    background:rgba(8,14,26,.42);
    -webkit-backdrop-filter:blur(var(--glass-blur)) saturate(140%);
    backdrop-filter:blur(var(--glass-blur)) saturate(140%);
    display:flex; align-items:flex-end; justify-content:center;
    padding:0 16px calc(env(safe-area-inset-bottom) + 16px);
    opacity:0; pointer-events:none;
    transition:opacity .35s cubic-bezier(.25,.8,.25,1);
  }
  .modal.open{ opacity:1; pointer-events:auto; }
  .modal-card{
    width:100%; max-width:520px;
    background:var(--card);
    border-radius:var(--radius-lg);
    padding:20px;
    max-height:88%; overflow-y:auto; touch-action:pan-y;
    box-shadow:var(--shadow-md);
    transform:translateY(100%);
    transition:transform .35s cubic-bezier(.25,.8,.25,1);
  }
  .modal.open .modal-card{ transform:translateY(0); }
  .modal-card.shake{ animation:shake .4s; }

  /* Centered, frosted-glass variant of the shared bottom-sheet .modal -
     used for short informational popups (the info-btn "?" tooltips) where
     a full bottom sheet would feel heavy for a sentence or two of text. */
  .modal.modal-centered{ align-items:center; padding:0 28px; }
  .modal-centered .modal-card{
    width:100%; max-width:380px;
    max-height:none;
    background:rgba(255,255,255,.78);
    -webkit-backdrop-filter:blur(var(--glass-blur)) saturate(180%);
    backdrop-filter:blur(var(--glass-blur)) saturate(180%);
    border:1px solid rgba(255,255,255,.5);
    border-radius:var(--radius-lg);
    box-shadow:var(--shadow-md);
    padding:20px 20px 22px;
    transform:scale(.92);
    transition:transform .3s cubic-bezier(.25,.8,.25,1);
  }
  /* Real bug this shipped as: #historyLoginModal/#accountLoginModal both
     carry .modal-card-dark specifically to get a dark navy card instead of
     this rule's light one (see .modal-card-dark's own comment below), but
     .modal-centered .modal-card above is two classes (specificity 0,2,0) -
     plain .modal-card-dark (0,1,0) can never outrank that regardless of
     source order, so every "dark" login card actually rendered with this
     light background instead. Matching the specificity here (also 0,2,0,
     and after the rule above so it wins the tie) is what lets the darker
     background actually win for these two. */
  .modal-centered .modal-card-dark{ background:rgba(12,26,46,.92); }
  .modal-centered.open .modal-card{ transform:scale(1); }
  @media (prefers-reduced-motion: reduce){
    .modal-centered .modal-card{ transition:none; }
  }
  @keyframes shake{
    0%,100%{ transform:translateX(0); }
    20%{ transform:translateX(-8px); }
    40%{ transform:translateX(7px); }
    60%{ transform:translateX(-5px); }
    80%{ transform:translateX(3px); }
  }
  .modal-card h2{ margin:0 0 4px; font-size:19px; }
  .modal-card .sub{ margin:0 0 16px; font-size:13px; color:var(--ink-soft); }
  /* Dark/glass variant of the frosted .modal-centered card - originally
     built only for #historyLoginModal (a plain white card would've clashed
     with the gold/silver padlock artwork, and a login is explicitly a
     "security" moment the rest of the app's white modals don't need to
     signal), now a shared class so #accountLoginModal (Mitt lag) can reuse
     the same look instead of redefining it. Also reused bare (no
     .modal-card) on Register's own .account-form-card, and inside
     #settingsModal (not .modal-centered) alongside plain .modal-card -
     both of those already get this rule's background fine, so it's left
     here as the general case. */
  .modal-card-dark{
    background:rgba(12,26,46,.92);
    border:1px solid rgba(255,255,255,.16);
    text-align:center;
    /* Trims the "empty air above the icon" feel - the shared
       .modal-centered .modal-card's 20px top padding was tuned for plain
       text-only info popups, more than this icon+heading layout needs. */
    padding-top:14px;
  }
  .modal-card-dark h2{ color:#fff; }
  .modal-card-dark .sub{ color:rgba(255,255,255,.6); }
  .modal-card-dark .info-popup-close{ background:rgba(255,255,255,.12); color:#fff; }
  .modal-card-dark .info-popup-close:active{ background:rgba(255,255,255,.2); }
  .modal-card-dark .login-link-row{ margin:16px 0 0; font-size:12.5px; color:rgba(255,255,255,.55); }
  .login-link-btn{
    border:none; background:none; padding:0; font:inherit;
    color:var(--accent); font-weight:700; cursor:pointer;
  }
  .history-login-card-icon{ width:48px; height:48px; margin:0 auto 8px; display:block; filter:drop-shadow(0 3px 8px rgba(0,0,0,.35)); }
  #historyLoginFormFields{ display:flex; flex-direction:column; gap:12px; }
  #historyLoginFormFields[hidden]{ display:none; }
  .history-login-locked-note{
    background:rgba(230,57,70,.14); border:1px solid rgba(230,57,70,.32);
    border-radius:12px; padding:14px 16px;
  }
  .history-login-locked-note[hidden]{ display:none; }
  .history-login-locked-note p{ margin:0; color:#ffc3c9; font-size:13.5px; line-height:1.5; }
  /* Shown only when this settings screen was reached by joining a code
     that turned out to have no players - a wrong/stale code (there's no
     server-side cleanup of old sessions, see leaveSession()) looks
     identical to a genuine brand-new empty session otherwise, so this
     gives the person something to double-check before they start typing
     a whole new roster into what might be the wrong session. */
  .joined-empty-note{
    margin:0 0 16px; padding:10px 12px;
    background:rgba(212,144,10,.12); border:1px solid rgba(212,144,10,.35);
    border-radius:var(--radius-sm);
    font-size:12.5px; line-height:1.42; color:#8a5300;
  }
  /* Neutral (not amber-warning) - explains why several fields below are
     greyed out/hidden, not that something's wrong. */
  .master-only-note{
    margin:0 0 16px; padding:10px 12px;
    background:#eef1f0; border:1px solid #dde1e6;
    border-radius:var(--radius-sm);
    font-size:12.5px; line-height:1.42; color:var(--ink-soft);
  }
  .modal-card label.section-label,
  .modal-card div.section-label{
    display:block; font-size:12px; font-weight:700; color:var(--ink-soft);
    margin:16px 0 8px;
  }
  .modal-card label.section-label[hidden],
  .modal-card div.section-label[hidden]{ display:none; }
  /* "Kampresultat" summary popup (see showMatchSummaryThen in app.js) -
     light-themed siblings of the dark .history-row-goal/.history-row-player
     rows the "Historikk" screen uses for the same data, since this modal
     sits on the normal white .modal-card instead of that screen's dark
     background. */
  .match-summary-goals{
    max-height:150px; overflow-y:auto; touch-action:pan-y;
    margin-bottom:4px;
  }
  .match-summary-goal{
    display:flex; align-items:center; gap:8px;
    font-size:13px; color:var(--ink); padding:4px 0;
  }
  .match-summary-goal::before{
    content:''; width:7px; height:7px; border-radius:50%; flex:0 0 auto;
    background:#32b45a;
  }
  .match-summary-goal-away::before{ background:#c7ccd3; }
  .match-summary-goal-time{
    font-variant-numeric:tabular-nums; color:var(--ink-soft); flex:0 0 auto;
  }
  .match-summary-goal-scorer{
    flex:1 1 auto; min-width:0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
  }
  .match-summary-empty{ font-size:13px; color:var(--ink-soft); padding:4px 0; }

  /* Sun+S (win) / raincloud+T (loss) badge, pinned in the card's own corner -
     absent entirely for a draw (see showMatchSummaryThen), on purpose: no
     symbol has been agreed on for that case yet, and a plain score reads
     fine on its own. */
  .match-summary-weather{
    position:absolute; top:14px; right:18px;
    display:flex; flex-direction:column; align-items:center; gap:2px;
  }
  /* Same author-display-beats-[hidden] gotcha as elsewhere in this file -
     without this, BOTH the sun/S and raincloud/T badges stayed visible at
     once regardless of showMatchSummaryThen()'s .hidden assignments (most
     visible on a draw, where JS correctly hides both but neither actually
     disappeared) - only caught by actually rendering this popup in a
     browser, not by reading the JS. */
  .match-summary-weather[hidden]{ display:none; }
  .match-summary-weather-letter{ font-size:15px; font-weight:800; line-height:1; }
  .match-summary-weather-letter-win{ color:#ff8a00; }
  .match-summary-weather-letter-loss{ color:#5b9bd5; }
  .match-summary-weather-letter-draw{ color:#8b93a1; }
  @keyframes matchSummarySunPulse{ 0%,100%{ transform:scale(1); } 50%{ transform:scale(1.06); } }
  @keyframes matchSummaryRaindrop{ 0%{ transform:translateY(-2px); opacity:0; } 30%{ opacity:1; } 100%{ transform:translateY(9px); opacity:0; } }
  .match-summary-sun-rays{ transform-origin:center; animation:matchSummarySunPulse 3.2s ease-in-out infinite; }
  .match-summary-rain-drop{ animation:matchSummaryRaindrop 1.3s linear infinite; }
  .match-summary-rain-drop:nth-child(2){ animation-delay:.35s; }
  .match-summary-rain-drop:nth-child(3){ animation-delay:.7s; }
  @media (prefers-reduced-motion: reduce){
    .match-summary-sun-rays, .match-summary-rain-drop{ animation:none; }
  }
  /* Room for the corner badge above, since the headline can otherwise run
     under it on a long opponent name. */
  #matchSummaryModal .modal-card h2{ padding-right:46px; }
  .match-summary-duration, .match-summary-swaps{
    margin:2px 0 0; font-size:12.5px; color:var(--ink-soft);
  }
  .match-summary-duration{ font-variant-numeric:tabular-nums; }
  .match-summary-swaps{ margin-bottom:14px; }
  /* Extra breathing room above "Spilletid" specifically (not "Mål") to read
     as a clear second section, not just another line under the goal list. */
  .match-summary-playtime-label{ margin-top:26px; }

  .match-summary-playtime-wrap{ position:relative; }
  /* Spans the header + however many player rows render below it, without
     any height math - top:0/bottom:0 fill exactly as much as the sibling
     content ends up being, whether there are 3 players or 20. */
  .match-summary-playtime-spine{
    position:absolute; top:22px; bottom:2px; right:64px; width:1px;
    background:#e7e9ec;
  }
  .match-summary-players-head, .match-summary-player{
    display:flex; align-items:center; gap:10px;
  }
  .match-summary-players-head{
    padding-bottom:6px; font-size:10.5px; font-weight:700; color:var(--ink-soft); letter-spacing:.06em;
  }
  .match-summary-players{
    max-height:150px; overflow-y:auto; touch-action:pan-y; margin-bottom:4px;
  }
  .match-summary-player{
    font-size:13px; color:var(--ink); padding:6px 4px; border-radius:6px;
  }
  .match-summary-player:nth-child(even){ background:rgba(22,32,43,.035); }
  .match-summary-player-name{
    flex:1 1 auto; min-width:0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
  }
  .match-summary-player-goals{ color:var(--ink-soft); font-size:11.5px; }
  .match-summary-player-k, .match-summary-player-t{
    width:56px; flex:0 0 auto; text-align:center; font-variant-numeric:tabular-nums;
  }
  .match-summary-player-t{ width:64px; }
  .match-summary-player .match-summary-player-k,
  .match-summary-player .match-summary-player-t{ text-align:right; padding-right:2px; color:var(--ink-soft); }

  /* Only shown when the coach said "lagre" but the shared-Historikk insert
     actually failed (see archiveInProgressMatch/endMatchPeriod) - without
     this, that failure only ever reached console.warn, so a match believed
     saved could silently never make it to the database. */
  .match-summary-history-note{
    margin:10px 0 0; padding:9px 11px; border-radius:10px;
    background:rgba(230,57,70,.09); border:1px solid rgba(230,57,70,.3);
    color:#b53341; font-size:12.5px; line-height:1.4;
  }

  /* #opponentModal's two fields - bigger and more considered than a bare
     .name-row input (that class needed its .name-row parent for styling,
     which these never had - the plain, unstyled browser input that left
     them looking small/dated). */
  .opponent-field-wrap{ margin-bottom:4px; }
  .opponent-name-input, .opponent-abbr-input{
    width:100%; border:1.5px solid #dde1e6; border-radius:12px;
    padding:14px 15px; font-size:16px; color:var(--ink);
    background:#fafbfc;
    transition:border-color .15s ease, background .15s ease, box-shadow .15s ease;
  }
  .opponent-name-input:focus, .opponent-abbr-input:focus{
    outline:none; border-color:var(--accent); background:#fff;
    box-shadow:0 0 0 3px rgba(255,138,61,.15);
  }
  /* Same glass recipe as .history-tile-badge/.launcher-tile, not the pale
     #fafbfc opponent-name-input above - used on dark backgrounds (the
     Historikk login popup, the dark .history-screen), not a white card. */
  /* Was a dark/glass field (white text on translucent white-on-dark) to
     match the dark card around it - but every field here also carries a
     real autocomplete="username"/"current-password"/"new-password" (that's
     the whole point - Safari's own password manager needs it), and real
     iOS Safari paints its AutoFill highlight as a native, non-CSS-paintable
     light overlay on top of fields like that (unlike desktop browsers,
     where the usual inset-box-shadow trick can repaint it), so an
     autofilled value rendered white-on-white regardless of any autofill
     CSS override attempted here. .name-row input[type=text] (the player
     name fields) never had this problem for an unrelated reason - it's
     autocomplete="off", so iOS never shows its AutoFill layer there at
     all - not something this field can copy without breaking the actual
     password-manager integration. Going light instead removes the
     collision itself: no fight left over what color iOS's overlay paints,
     since this field already looks that way. */
  .text-input{
    width:100%; border:1.5px solid #dde1e6; border-radius:12px;
    padding:14px 15px; font-size:16px; color:#1a2738;
    background:#fafbfc;
    transition:border-color .15s ease, box-shadow .15s ease;
  }
  .text-input::placeholder{ color:#8a94a3; }
  .text-input:focus{
    outline:none; border-color:var(--accent);
    box-shadow:0 0 0 3px rgba(255,138,61,.25);
  }
  .text-input:-webkit-autofill,
  .text-input:-webkit-autofill:hover,
  .text-input:-webkit-autofill:focus{
    -webkit-text-fill-color:#1a2738;
    caret-color:#1a2738;
    -webkit-box-shadow:0 0 0 1000px #fafbfc inset;
    box-shadow:0 0 0 1000px #fafbfc inset;
    transition:background-color 9999s ease-in-out 0s;
  }
  /* Styled like a little scoreboard code (echoes #matchClock's digital
     look) rather than a plain text field, since that's exactly what it
     becomes on the pitch - see .away-team-label. */
  .opponent-abbr-input{
    max-width:130px;
    font-family:'SF Mono','Courier New',ui-monospace,monospace;
    font-weight:800; font-size:20px; letter-spacing:.14em;
    text-align:center; text-transform:uppercase;
  }
  #nameRows{ display:flex; flex-direction:column; gap:8px; }
  .name-row{ display:flex; gap:8px; align-items:center; }
  /* A fixed floor of rows (see syncNameRows) - "+ Legg til spiller" below
     adds one more, explicitly, on its own tap. */
  .name-row-enter{ animation:nameRowEnter .3s cubic-bezier(.25,.8,.25,1) both; }
  @keyframes nameRowEnter{
    from{ opacity:0; transform:translateY(-4px); }
    to{ opacity:1; transform:translateY(0); }
  }
  .add-player-row-btn{
    width:100%; margin-top:8px; padding:11px;
    border:1px dashed #c7ccd3; border-radius:var(--radius-sm);
    background:transparent; color:var(--ink-soft);
    font-size:14px; font-weight:700; cursor:pointer;
    transition:transform .1s ease;
  }
  .add-player-row-btn:active{ transform:scale(.98); }
  .name-input-wrap{ position:relative; flex:1; }
  /* Small red "!" attached to any field that's blocking progress (e.g. a
     required name field left empty, an invalid join code) - pairs with the
     .shake on the surrounding modal-card, see markFieldInvalid/shakeElement. */
  .field-invalid-wrap{ position:relative; }
  .field-invalid-badge{
    position:absolute; top:-7px; right:-7px;
    width:18px; height:18px; border-radius:50%;
    background:var(--danger); color:#fff;
    font-size:12px; font-weight:800; line-height:1;
    display:flex; align-items:center; justify-content:center;
    box-shadow:0 1px 3px rgba(0,0,0,.35);
    pointer-events:none;
  }
  .name-row input[type=text]{
    width:100%; border:1px solid #dde1e6; border-radius:var(--radius-sm);
    padding:10px 44px 10px 12px; font-size:15px; background:#fafbfc;
  }
  .name-row input[type=text]:focus{ outline:2px solid var(--accent); border-color:transparent; }
  /* Same divider+"x" language as the confirm-arm buttons elsewhere (Kampslutt,
     Avslutt og nullstill, etc.) - just without their circle, since this one
     lives inside a text field rather than on a pill button. */
  .name-row-clear-divider{
    position:absolute; top:8px; bottom:8px; right:38px; width:1px;
    background:#dde1e6; pointer-events:none;
  }
  .name-row-clear-btn{
    position:absolute; top:0; bottom:0; right:0; width:38px;
    display:flex; align-items:center; justify-content:center;
    background:none; border:none; padding:0; margin:0;
    font-size:20px; font-weight:800; line-height:1; color:var(--ink-soft);
    cursor:pointer; transition:transform .1s ease; outline:none;
  }
  .name-row-clear-btn:active{ transform:scale(.8); }
  .suggest-list{
    position:absolute; left:0; right:0; top:calc(100% + 4px);
    background:#fff; border:1px solid #e1e4e9; border-radius:var(--radius-sm);
    box-shadow:var(--shadow-md);
    max-height:190px; overflow-y:auto; touch-action:pan-y;
    z-index:50;
  }
  .suggest-list.hidden{ display:none; }
  .suggest-item{
    padding:11px 12px; font-size:14px; color:var(--ink);
    border-bottom:1px solid #f0f1f3;
  }
  .suggest-item:last-child{ border-bottom:none; }
  .suggest-item:active{ background:#f5f6f8; }
  /* A player currently on the field can't be renamed/removed from settings
     - the input is disabled (grayed) and there's a lock note instead of the
     remove button. Only innbyttere stay editable. */
  .name-row.locked .name-input-wrap input{
    background:#eceef0; color:var(--ink-soft); cursor:not-allowed;
  }
  .locked-row-note{
    width:34px; height:34px; flex:0 0 auto;
    display:flex; align-items:center; justify-content:center;
    font-size:15px; opacity:.55;
  }
  .duration-input{ display:flex; align-items:center; gap:8px; }
  .duration-input input{
    width:64px; border:1px solid #dde1e6; border-radius:var(--radius-sm);
    padding:10px; font-size:15px; text-align:center; background:#fafbfc;
  }
  .duration-input input:disabled{ background:#eceef0; color:var(--ink-soft); cursor:not-allowed; opacity:.45; }
  /* All four of these (fieldSizeInput, matchDurationInput, durMin, durSec)
     are type=number for the numeric keyboard on mobile, but the up/down
     spinner arrows that comes with that on desktop/some Android browsers
     were never wanted UI - typing the number directly is the only input
     method intended here. -moz-appearance:textfield removes Firefox's;
     the pseudo-elements below remove Chrome/Safari's (Firefox has no such
     pseudo-elements, so that rule harmlessly matches nothing there). */
  .duration-input input[type=number]{ -moz-appearance:textfield; }
  .duration-input input[type=number]::-webkit-outer-spin-button,
  .duration-input input[type=number]::-webkit-inner-spin-button{
    -webkit-appearance:none; margin:0;
  }
  .duration-input span{ font-size:13px; color:var(--ink-soft); }
  .locked-note{ font-size:11px; font-weight:600; color:var(--ink-soft); }

  .settings-row{
    display:flex; align-items:center; justify-content:space-between;
    margin-top:18px;
  }
  /* Same author-display-beats-[hidden] gotcha as elsewhere in this file -
     needed now that #shareSessionRow/#rankCumulativeRow toggle .hidden
     for non-master users (see openSettings/isMaster in app.js). */
  .settings-row[hidden]{ display:none; }
  .settings-row-label{ font-size:14px; font-weight:600; color:var(--ink); }
  /* ---------- Pre-match Innstillinger (#settingsModal), dark redesign ----------
     Deliberate redesign (not the settings-card collision fix above, which
     only restored the old white look) - matches the dark "Innstillinger"
     screen's own language, on top of the proven .modal-card-dark card
     (see index.html: same class the login popup uses, background solid
     enough at 92% opacity to stay readable, not the near-transparent
     glass this modal briefly had by accident). Everything below just
     re-colors this one modal's own form controls for that dark card -
     none of it touches the light modal-card other screens still use. */
  #settingsModal .modal-card{ text-align:left; }
  #settingsModal .joined-empty-note{
    background:rgba(212,144,10,.16); border-color:rgba(212,144,10,.4); color:#ffcf7a;
  }
  #settingsModal .master-only-note{
    background:rgba(255,255,255,.07); border-color:rgba(255,255,255,.16); color:rgba(255,255,255,.65);
  }
  #settingsModal .section-label{ color:rgba(255,255,255,.5); }
  #settingsModal .locked-note{ color:rgba(255,255,255,.45); }
  #settingsModal .settings-row-label{ color:#fff; }
  #settingsModal .info-btn{ color:rgba(255,255,255,.5); }
  #settingsModal .name-row input[type=text],
  #settingsModal .duration-input input{
    background:rgba(255,255,255,.08); border-color:rgba(255,255,255,.22); color:#fff;
  }
  #settingsModal .name-row input[type=text]::placeholder,
  #settingsModal .duration-input input::placeholder{ color:rgba(255,255,255,.35); }
  #settingsModal .duration-input input:disabled,
  #settingsModal .name-row.locked .name-input-wrap input{
    background:rgba(255,255,255,.04); color:rgba(255,255,255,.3);
  }
  #settingsModal .duration-input span{ color:rgba(255,255,255,.55); }
  #settingsModal .name-row-clear-divider{ background:rgba(255,255,255,.2); }
  #settingsModal .name-row-clear-btn{ color:rgba(255,255,255,.5); }
  #settingsModal .add-player-row-btn{ border-color:rgba(255,255,255,.3); color:rgba(255,255,255,.55); }
  #settingsModal .export-link-btn{ border-color:rgba(255,255,255,.3); color:rgba(255,255,255,.65); }
  #settingsModal .suggest-list{ background:#1a2a44; border-color:rgba(255,255,255,.18); }
  #settingsModal .suggest-item{ color:#fff; border-bottom-color:rgba(255,255,255,.08); }
  #settingsModal .suggest-item:active{ background:rgba(255,255,255,.08); }
  /* Same glass toggle as the new Innstillinger screen's .toggle.dark - this
     modal's toggles (shareSessionToggle,
     rankByCumulativeToggle) get it by scope instead of an HTML class on
     each, since there'd be no light-surface use of them left in here to
     keep separate. */
  #settingsModal .toggle-slider{
    background:rgba(255,255,255,.10); border:1px solid rgba(255,255,255,.22);
    -webkit-backdrop-filter:blur(10px) saturate(160%); backdrop-filter:blur(10px) saturate(160%);
    box-shadow:inset 0 1px 2px rgba(0,0,0,.25);
  }
  #settingsModal .toggle input:checked + .toggle-slider{ background:rgba(50,215,75,.35); border-color:rgba(50,215,75,.55); }
  .settings-row-label-wrap{ display:flex; align-items:center; gap:6px; }
  .share-mode-row{ margin-top:12px; }
  .share-mode-row[hidden]{ display:none; }
  .segmented{
    display:flex; background:#eef0f2; border-radius:100px; padding:3px; gap:2px;
  }
  .segmented-btn{
    border:none; background:none; padding:6px 12px; border-radius:100px;
    font-size:12px; font-weight:700; color:var(--ink-soft); cursor:pointer;
    transition:background .15s ease, color .15s ease;
  }
  .segmented-btn.active{ background:#fff; color:var(--ink); box-shadow:0 1px 3px rgba(0,0,0,.15); }
  .segmented.dark{ background:rgba(255,255,255,.07); border-radius:12px; padding:4px; gap:4px; }
  .segmented.dark .segmented-btn{ color:rgba(255,255,255,.55); flex:1; text-align:center; border-radius:9px; min-height:36px; }
  .segmented.dark .segmented-btn.active{ background:#2f4a86; color:#fff; box-shadow:inset 0 0 0 1px #6a90e0; }

  .link-row{
    display:flex; align-items:center; justify-content:space-between; gap:12px; width:100%;
    background:none; border:0; padding:13px 14px; color:#fff; font-family:inherit;
    font-size:13.5px; font-weight:600; text-align:left; cursor:pointer;
  }
  .link-row span:last-child{ color:rgba(255,255,255,.4); font-size:18px; }
  .link-row:active{ background:rgba(255,255,255,.06); }

  /* ---------- Glemt kampslutt (forgottenModal / manualClockModal / clockHint) ---------- */
  #forgottenModal, #manualClockModal{ z-index:120; }
  .forgotten-card .sub{ text-align:left; }
  .forgotten-prev{ border:1px solid rgba(255,255,255,.14); border-radius:12px; overflow:hidden; margin:0 0 14px; font-size:12.5px; }
  .forgotten-prev div{ display:flex; justify-content:space-between; gap:10px; padding:8px 12px; color:rgba(255,255,255,.85); }
  .forgotten-prev div + div{ border-top:1px solid rgba(255,255,255,.08); }
  .forgotten-prev .fp-to{ color:#32b45a; font-weight:700; font-variant-numeric:tabular-nums; white-space:nowrap; }
  .forgotten-actions{ display:flex; flex-direction:column; gap:8px; }
  .forgotten-row{ display:flex; gap:8px; }
  .forgotten-row button{ flex:1; }
  .manual-steps{ display:flex; justify-content:space-around; gap:10px; margin:14px 0 6px; }
  .manual-step{ display:flex; flex-direction:column; align-items:center; gap:6px; }
  .manual-step-label{ font-size:10.5px; letter-spacing:.08em; text-transform:uppercase; color:rgba(255,255,255,.55); font-weight:700; }
  .manual-preview{ text-align:center; margin:10px 0 0; }
  .manual-preview b{ color:#fff; }
  /* Overtidsklokka er trykkbar når kamptiden har gått ut. */
  #matchCountdown.overtime{ pointer-events:auto; cursor:pointer; }
  /* Hintet etter «Fortsett»: klokka lyser og en gul boble peker opp mot den i 10 sekunder. */
  #clockHint{
    position:absolute; left:50%; top:96px; transform:translate(-50%, 6px); z-index:55; width:min(72%, 260px);
    background:rgba(40,32,6,.96); color:#ffcf5a; border:1px solid rgba(255,207,90,.7); border-radius:12px;
    padding:8px 12px; text-align:center; font-size:12px; font-weight:700; line-height:1.3;
    box-shadow:var(--shadow-md); opacity:0; pointer-events:none; transition:opacity .35s ease, transform .35s ease;
  }
  #clockHint::before{
    content:""; position:absolute; left:50%; top:-6px; width:10px; height:10px; background:rgba(40,32,6,.96);
    border-left:1px solid rgba(255,207,90,.7); border-top:1px solid rgba(255,207,90,.7); transform:translateX(-50%) rotate(45deg);
  }
  #clockHint.visible{ opacity:1; transform:translate(-50%, 0); }
  .clock-hint-arrow{ display:inline-block; animation:clockHintBob 1s ease-in-out infinite; }
  @keyframes clockHintBob{ 0%,100%{ transform:translateY(0); } 50%{ transform:translateY(-3px); } }
  #matchClock.clock-halo{ animation:clockHalo 1.4s ease-in-out infinite; border-color:rgba(255,207,90,.8); }
  @keyframes clockHalo{
    0%,100%{ box-shadow:0 0 0 0 rgba(255,207,90,0), 0 0 8px 2px rgba(255,207,90,.35); }
    50%{ box-shadow:0 0 0 7px rgba(255,207,90,.18), 0 0 20px 6px rgba(255,207,90,.6); }
  }
  @media (prefers-reduced-motion: reduce){
    .clock-hint-arrow{ animation:none; }
    #matchClock.clock-halo{ animation:none; box-shadow:0 0 0 4px rgba(255,207,90,.4); }
  }

  /* ---------- Felles innstillings-byggeklosser (hjemskjermen + pre-kamp-vinduet) ---------- */
  .settings-group[hidden]{ display:none; }
  .settings-pad{ padding:12px 14px; }
  /* Lag-kortet holder navnefeltene, og forslagslisten som dukker opp under et
     felt (.suggest-list, absolutt plassert) må kunne stikke utenfor kortet - det
     vanlige .settings-group-card har overflow:hidden og klippet den av. Kortet
     løftes også over kortene under (hvert har backdrop-filter, altså egen
     stablingskontekst som ellers tegnes oppå listen). */
  .settings-group-card.settings-pad{ overflow:visible; position:relative; z-index:5; }
  .settings-intro{
    margin:0 0 16px; padding:10px 12px; border-radius:12px; font-size:12px; line-height:1.4;
    color:rgba(255,255,255,.85); background:rgba(106,144,224,.12); border:1px solid rgba(106,144,224,.35);
  }
  .settings-intro b{ font-weight:700; color:#fff; }
  #settingsModal .settings-group{ margin:0 0 18px; }
  #settingsModal .settings-group-card .settings-row{ margin-top:0; padding:13px 14px; }
  #settingsModal .settings-group-card .settings-row + .settings-row,
  #settingsModal .settings-group-card .settings-row + #transferOwnerRow{ border-top:1px solid rgba(255,255,255,.08); }
  #settingsModal .settings-group-card #transferOwnerRow{ padding:10px 14px; }
  #settingsModal .name-row:first-child{ margin-top:0; }
  .step-row{ display:flex; align-items:center; justify-content:space-between; gap:12px; padding:13px 14px; }
  .step-row[hidden], .step-col[hidden]{ display:none; }
  .step-row + .step-row, .step-row + .step-col{ border-top:1px solid rgba(255,255,255,.08); }
  .step-text{ min-width:0; }
  .step-col{ padding:13px 14px; }
  .step-col + .step-row, .step-col .segmented{ margin-top:0; }
  .step-col .segmented{ margin-top:10px; }
  /* Bryteren under Byttetid hører til raden over: ingen strek mellom. */
  .step-row + .step-col.swap-basis-col{ border-top:0; padding-top:0; }
  .swap-basis-col .segmented{ margin-top:0; }
  .swap-basis-col .toggle-row-sub, #settingsSwapBasisNote{ margin-top:8px; }

  /* Tallknapper: stort tall mellom to firkantede knapper, hele tall, ingen tastatur. */
  .numstep{ display:inline-flex; align-items:center; gap:12px; flex:none; user-select:none; -webkit-user-select:none; touch-action:manipulation; }
  .numstep button{
    width:46px; height:46px; border-radius:14px; background:rgba(106,144,224,.22); border:1px solid rgba(106,144,224,.5);
    color:#fff; font-family:inherit; font-size:24px; line-height:1; cursor:pointer; -webkit-tap-highlight-color:transparent;
    transition:transform .12s cubic-bezier(.2,.9,.3,1), background .12s;
  }
  .numstep button:active{ transform:scale(.88); background:rgba(106,144,224,.4); }
  .numstep button:disabled{ opacity:.35; cursor:default; }
  .numstep button:disabled:active{ transform:none; background:rgba(106,144,224,.22); }
  .numstep-val{ display:inline-flex; flex-direction:column; align-items:center; min-width:54px; line-height:1.05; font-variant-numeric:tabular-nums; color:#fff; overflow:hidden; }
  .numstep-val b{ font-size:27px; font-weight:800; }
  .numstep-val small{ font-size:11px; color:rgba(255,255,255,.55); }

  /* Symboler ved titlene (samme som på banen/i symbolforklaringen), dempet til 75 % styrke og 80 % størrelse. */
  .choice-title{ display:flex; align-items:center; gap:8px; }
  .sym, .sym-ico, .sym-warn, .sym-lcd, .sym-endp{ opacity:.75; --k:.8; flex:none; }
  .sym{ display:inline-flex; align-items:center; gap:2px; }
  .sym svg{ display:block; width:calc(14px * var(--k)); height:calc(14px * var(--k)); }
  .sym-ico{ position:relative; width:calc(24px * var(--k)); height:calc(24px * var(--k)); border-radius:calc(8px * var(--k)); background:rgba(255,255,255,.14); display:inline-grid; place-items:center; font-size:calc(13px * var(--k)); color:#fff; font-weight:400; }
  .sym-ico i{ position:absolute; top:calc(-5px * var(--k)); right:calc(-6px * var(--k)); font-family:inherit; font-size:calc(9px * var(--k)); font-weight:800; line-height:1; font-style:normal; background:var(--accent); color:var(--accent-ink); border-radius:99px; padding:calc(2px * var(--k)) calc(4px * var(--k)); }
  .sym-warn{ width:calc(22px * var(--k)); height:calc(22px * var(--k)); border-radius:50%; background:#ff9500; color:#fff; display:inline-grid; place-items:center; font-size:calc(12px * var(--k)); font-weight:800; }
  .sym-lcd{ font-family:ui-monospace, Menlo, monospace; font-size:calc(11px * var(--k)); font-weight:700; line-height:1; color:#fff; background:#000; border:1px solid #444; border-radius:calc(6px * var(--k)); padding:calc(4px * var(--k)) calc(6px * var(--k)); letter-spacing:.08em; }
  .sym-endp{ font-size:calc(10px * var(--k)); font-weight:800; line-height:1; color:#ff7a7a; background:rgba(220,38,38,.2); border:1px solid rgba(220,38,38,.5); border-radius:99px; padding:calc(3px * var(--k)) calc(8px * var(--k)); }

  /* Sammenleggbar Kampoppførsel (pre-kamp-vinduet). */
  .fold-h{ display:flex; align-items:center; justify-content:space-between; gap:10px; width:100%; background:transparent; border:0; color:#fff; padding:14px; text-align:left; font-family:inherit; cursor:pointer; -webkit-tap-highlight-color:transparent; }
  .fold-text{ display:flex; flex-direction:column; gap:1px; min-width:0; }
  .fold-t{ font-weight:700; font-size:13.5px; }
  .fold-s{ color:rgba(255,255,255,.5); font-size:11.5px; }
  .chev{ width:10px; height:10px; border-right:2px solid rgba(255,255,255,.55); border-bottom:2px solid rgba(255,255,255,.55); transform:rotate(45deg); transition:transform .22s cubic-bezier(.2,.9,.3,1); margin-right:4px; flex:none; }
  .fold[data-open="true"] .chev{ transform:rotate(-135deg) translate(-2px,-2px); }
  .fold-b{ display:grid; grid-template-rows:0fr; transition:grid-template-rows .24s cubic-bezier(.2,.9,.3,1); }
  .fold[data-open="true"] .fold-b{ grid-template-rows:1fr; }
  .fold-in{ overflow:hidden; min-height:0; }
  .fold-in .choice-row:first-child{ border-top:1px solid rgba(255,255,255,.08); }
  #settingsModal .fold-in .choice-row{ margin-top:0; }

  /* ---------- Kampoppførsel: to-knapps-valg (initChoices() i app.js) ----------
     Et valg mellom to likeverdige alternativer, ikke av/på: en markering
     (.choice-thumb) glir mellom de to knappene, og hjelpeteksten under
     forklarer hva som gjelder nå. Brukes både i Innstillinger-skjermen og
     i pre-kamp-innstillingene (#settingsModal). */
  .choice-row{ display:block; }
  .choice-row[hidden]{ display:none; } /* samme author-display-slår-[hidden]-felle som .settings-row */
  .settings-group-card .choice-row{ padding:13px 14px; }
  .settings-group-card .choice-row + .choice-row{ border-top:1px solid rgba(255,255,255,.08); }
  #settingsModal .choice-row{ margin-top:18px; }
  .choice-rule{ font-size:11.5px; color:rgba(255,255,255,.5); margin:0 4px 10px; }
  .choice-rule b{ color:#fff; font-weight:600; }
  .choice-title{ font-size:13.5px; font-weight:600; color:#fff; margin:0 0 2px; }
  .choice-q{ font-size:11.5px; color:rgba(255,255,255,.5); line-height:1.4; margin:0; }
  .choice-seg{
    position:relative; display:grid; grid-template-columns:1fr 1fr; margin-top:10px; padding:4px;
    background:rgba(255,255,255,.07); border-radius:12px;
    touch-action:pan-y; user-select:none; -webkit-user-select:none;
  }
  .choice-thumb{ position:absolute; top:4px; bottom:4px; left:4px; width:calc(50% - 4px); z-index:1; pointer-events:none; will-change:transform; }
  .choice-pill{
    position:absolute; inset:0; background:#2f4a86; border:1px solid #6a90e0; border-radius:9px;
    transition:transform .14s cubic-bezier(.2,.9,.3,1);
  }
  .choice-seg.press .choice-pill{ transform:scale(.97); }
  .choice-seg button{
    position:relative; z-index:2; background:transparent; border:0; color:rgba(255,255,255,.55);
    padding:8px 6px; min-height:44px; font-family:inherit; font-size:11.5px; font-weight:600; line-height:1.2; cursor:pointer;
    transition:color .14s ease-out; -webkit-tap-highlight-color:transparent;
  }
  .choice-seg button.act{ color:#fff; }
  .choice-seg:focus-within{ outline:2px solid #6a90e0; outline-offset:2px; }
  .choice-seg button:focus{ outline:none; }
  /* Hjelpeteksten: den usynlige .choice-measure holder av plass til den
     lengste av de to tekstene, så raden under ikke hopper når teksten
     bytter mellom én og to linjer. */
  .choice-help{ display:grid; margin-top:8px; font-size:11.5px; line-height:1.35; color:rgba(255,255,255,.55); }
  .choice-measure, .choice-live{ grid-area:1 / 1; }
  .choice-measure{ display:grid; visibility:hidden; }
  .choice-measure span{ grid-area:1 / 1; }

  /* ---------- "Mitt lag": register screen + Innstillinger screen ----------
     See #registerScreen/#settingsScreen in index.html and the accompanying
     app.js handlers - both are placeholders backed by localStorage only
     (loadCoachDefaults/saveCoachDefaults) until a real Supabase table for
     team accounts exists. */
  .account-form-wrap{ flex:1 1 auto; display:flex; align-items:center; justify-content:center; }
  .account-form-card{ width:100%; max-width:320px; border-radius:var(--radius-lg); padding:26px 22px 24px; box-shadow:var(--shadow-md); }
  .account-form-card .text-input{ margin:0; }

  .settings-screen-body{ flex:1 1 auto; overflow-y:auto; touch-action:pan-y; width:100%; max-width:380px; margin:0 auto; padding-bottom:12px; }
  .settings-group{ margin-bottom:22px; }
  .settings-group-title{
    font-size:11px; font-weight:700; letter-spacing:.08em; text-transform:uppercase;
    color:rgba(255,255,255,.45); margin:0 0 8px;
  }
  /* Named settings-group-card, not settings-card - .settings-card already
     existed (see far below, on #settingsModal's own card: "position:
     relative; padding-top:50px", clearance for its × button) for a totally
     unrelated purpose. Reusing that name here silently merged the two:
     this rule's background/overflow overrode .modal-card's own white
     background and working scroll on the OLD pre-match settings modal,
     since both rules target the same class with equal specificity and
     this one came later - real bug, only found by actually opening that
     modal and seeing it half-transparent with scrolling dead. */
  .settings-group-card{
    background:rgba(255,255,255,.06); border:1px solid rgba(255,255,255,.12); border-radius:16px;
    -webkit-backdrop-filter:blur(14px) saturate(150%); backdrop-filter:blur(14px) saturate(150%);
    overflow:hidden;
  }
  .settings-field-row{ padding:12px 14px; }
  .settings-field-row + .settings-field-row{ border-top:1px solid rgba(255,255,255,.08); }
  .settings-field-label{ display:block; font-size:11px; color:rgba(255,255,255,.5); margin-bottom:5px; }
  .settings-field-inline{ display:flex; gap:8px; }
  .settings-field-col{ flex-direction:column; align-items:stretch !important; gap:8px; }
  .mini-input{
    border:1.5px solid rgba(255,255,255,.2); border-radius:9px; padding:9px 10px;
    font-size:14px; color:#fff; background:rgba(255,255,255,.07); width:100%;
    font-family:inherit;
  }
  .mini-input::placeholder{ color:rgba(255,255,255,.35); }
  .mini-input-abbr{ max-width:74px; text-transform:uppercase; text-align:center; font-weight:700; letter-spacing:.05em; }
  .stepper-row{ display:flex; align-items:center; justify-content:space-between; padding:12px 14px; gap:12px; }
  .stepper-row + .stepper-row{ border-top:1px solid rgba(255,255,255,.08); }
  .stepper-row .settings-row-label{ color:#fff; }
  .toggle-row{ display:flex; align-items:center; justify-content:space-between; padding:13px 14px; gap:12px; }
  .toggle-row + .toggle-row{ border-top:1px solid rgba(255,255,255,.08); }
  .toggle-row-text{ min-width:0; }
  .toggle-row-label{ font-size:13.5px; font-weight:600; color:#fff; margin:0 0 2px; }
  .toggle-row-sub{ font-size:11.5px; color:rgba(255,255,255,.5); line-height:1.4; margin:0; }
  .settings-save-btn{ width:100%; margin-top:6px; }
  .settings-saved-note{ margin:10px 0 0; text-align:center; font-size:12.5px; font-weight:700; color:#32d74b; }
  .settings-storage-note{
    margin:0 0 18px; padding:10px 12px; border-radius:12px;
    background:rgba(255,255,255,.06); border:1px solid rgba(255,255,255,.12);
    font-size:12.5px; line-height:1.5; color:rgba(255,255,255,.65);
  }
  .settings-logout-btn{ display:block; width:100%; text-align:center; margin-top:14px; font-size:12.5px; color:rgba(255,255,255,.4); font-weight:600; }
  /* Same author-display-beats-[hidden] gotcha as elsewhere in this file -
     display:block above has the same specificity as the browser's default
     [hidden]{display:none} and comes later, so it silently won this fight
     and kept "Logg ut" visible (and clickable, doing nothing) while signed
     out - toggled via the `hidden` attribute in updateSettingsStorageNote(). */
  .settings-logout-btn[hidden]{ display:none; }
  .info-label-row{ display:flex; align-items:center; gap:6px; }
  /* Every #endMatchModal action (Kampslutt/Avslutt og nullstill/Forlat
     kampen/Forlat delt økt) is one .stack-btn-sized pill with its own "i"
     info popup living inside it as a small circle, pinned a FIXED distance
     from the right edge (not flex-split, which let the info zone's
     position drift with each button's own text width/length - see the
     mockup this reproduces: right:38px for the divider, right:14px for the
     circle, regardless of how wide the pill or its label is). */
  .stack-btn-split{
    position:relative; width:100%;
    border-radius:var(--radius-sm); background:#f0f1f3;
    /* .5s (not .15s) so the armed->resting fade (see createConfirmArm's
       mutual-exclusivity disarm) reads as a fade, not a flicker, when one
       press-again button bumps another back to resting. */
    transition:background .5s ease;
  }
  /* Same author-display-beats-[hidden] gotcha as elsewhere in this file -
     [hidden] alone can't beat position:relative's own display:block, but
     an explicit override keeps this consistent with the rest of the file. */
  .stack-btn-split[hidden]{ display:none; }
  /* Selector scoped under .stack-btn-split (not a bare .stack-btn-split-main
     rule) so its specificity (0,2,0) beats .modal-actions button's
     (0,1,1) - that rule's padding/border/font-size/font-weight would
     otherwise silently win over this being a <button> in the same
     .modal-actions row (see .modal-actions .info-btn below for the same
     collision on the info circle). */
  .stack-btn-split .stack-btn-split-main{
    display:block; width:100%; border:none; background:none; color:var(--ink);
    font-family:inherit; font-size:15px; font-weight:700; cursor:pointer;
    padding:14px 44px 14px 14px; text-align:center;
    transition:transform .12s ease, color .5s ease;
  }
  .stack-btn-split-main:active{ transform:scale(.97); }
  .stack-btn-split-divider{
    position:absolute; top:10px; bottom:10px; right:38px; width:1px;
    background:rgba(22,32,43,.12);
    transition:background .5s ease;
    pointer-events:none;
  }
  .stack-btn-split .info-btn{
    position:absolute; top:50%; right:14px; transform:translateY(-50%);
    transition:border-color .5s ease, color .5s ease, transform .1s ease;
    outline:none;
  }
  /* Deliberate "pressed into the screen" shrink on tap, replacing the
     browser's own default focus ring (very visible - and ugly - on this
     small a target in a Chromium devtools/webview context; real touch
     taps on iOS/Android never trigger it in the first place). Keep the
     translateY(-50%) centering alongside the scale, or the button jumps
     vertically instead of just shrinking in place. */
  .stack-btn-split .info-btn:active{ transform:translateY(-50%) scale(.82); }
  /* Invisible tap-target enlargement, no visual change - the 16px circle
     was hard to hit precisely. ::before paints nothing (no background/
     border) but still counts for hit-testing, so it silently widens the
     button's clickable box: mostly leftward, past the divider and a bit
     into .stack-btn-split-main's own territory (positioned elements like
     .info-btn always paint above normal-flow siblings, so this still wins
     the tap there), a little rightward toward the pill's corner, and only
     a few px top/bottom so it doesn't reach into the next stack-btn-split
     row (10px .modal-actions gap between them). */
  .stack-btn-split .info-btn::before{
    content:''; position:absolute; top:-4px; bottom:-4px; left:-16px; right:-8px;
  }
  .stack-btn-split.period{ background:rgba(10,132,255,.14); }
  .stack-btn-split.period .stack-btn-split-main{ color:var(--period-blue); }
  .stack-btn-split.period .stack-btn-split-divider{ background:rgba(10,132,255,.3); }
  .stack-btn-split.period .info-btn{ color:var(--period-blue); }
  .stack-btn-split.danger{ background:rgba(230,57,70,.12); }
  .stack-btn-split.danger .stack-btn-split-main{ color:var(--danger); }
  .stack-btn-split.danger .stack-btn-split-divider{ background:rgba(230,57,70,.3); }
  .stack-btn-split.danger .info-btn{ color:var(--danger); }
  /* "Er du sikker?" (see createConfirmArm() in app.js) - the whole pill
     inverts to a solid fill on the first press, same for every .armed
     variant below. */
  .stack-btn-split.danger.armed{ background:var(--danger); }
  .stack-btn-split.danger.armed .stack-btn-split-main{ color:#fff; }
  .stack-btn-split.danger.armed .stack-btn-split-divider{ background:rgba(255,255,255,.4); }
  .stack-btn-split.danger.armed .info-btn{ color:#fff; }
  .stack-btn-split.period.armed{ background:var(--period-blue); }
  .stack-btn-split.period.armed .stack-btn-split-main{ color:#fff; }
  .stack-btn-split.period.armed .stack-btn-split-divider{ background:rgba(255,255,255,.4); }
  .stack-btn-split.period.armed .info-btn{ color:#fff; }
  /* No circle - just the bare glyph ("i" for info, swapped to a bigger "×"
     while armed via .is-cancel below), same borderless language as
     .name-row-clear-btn. */
  .info-btn{
    width:16px; height:16px; flex:0 0 auto;
    border:none; background:none; color:var(--ink-soft);
    font-size:10px; font-weight:700; font-style:italic; line-height:1;
    display:inline-flex; align-items:center; justify-content:center;
    padding:0; cursor:pointer; transition:transform .1s ease;
  }
  .info-btn:active{ transform:scale(.8); }
  /* I-knapp ved en innstilling (Byttetid): en tydelig sirkel, med større trykkflate. */
  .choice-title .info-btn{ position:relative; width:20px; height:20px; border:1.5px solid currentColor; border-radius:50%; color:rgba(255,255,255,.6); font-size:11px; }
  .choice-title .info-btn::before{ content:''; position:absolute; inset:-10px; }
  /* The info-btn repurposed as a cancel "×" while its button is armed
     (createConfirmArm's onArm/onDisarm swap the glyph + this class) -
     bigger/bolder glyph so it still reads as a distinct control, not a
     smaller "i". Scoped under .stack-btn-split (0,3,0) to beat
     .modal-actions .info-btn (0,2,0) below, the same specificity
     collision documented there. */
  .stack-btn-split .info-btn.is-cancel{
    width:20px; height:20px; font-size:18px; font-weight:800; font-style:normal;
    /* Re-centered in the 38px zone left of the divider (right:38px on
       .stack-btn-split-divider) now that it's 20px wide, not 16px -
       (38-20)/2 = 9px on each side, so it sits equidistant from the
       divider and the pill's own right edge instead of hugging the
       divider like the smaller "i" reasonably could. */
    right:9px;
  }
  .info-popup-card{ position:relative; padding-top:46px; }
  .info-popup-close{
    position:absolute; top:14px; right:14px;
    width:30px; height:30px; border-radius:50%; border:none;
    background:#f0f1f3; color:var(--ink-soft);
    font-size:17px; line-height:1; cursor:pointer;
    display:flex; align-items:center; justify-content:center;
    transition:background .12s ease;
  }
  .info-popup-close:active{ background:#e2e4e8; }
  .settings-home-btn{ right:54px; font-size:19px; }
  #infoPopupText{ margin:0; white-space:pre-line; }
  .version-history-list{
    max-height:50vh; overflow-y:auto; touch-action:pan-y;
    display:flex; flex-direction:column; gap:14px;
  }
  .version-history-entry{
    padding-bottom:14px; border-bottom:1px solid #eef0f2;
  }
  .version-history-entry:last-child{ border-bottom:none; padding-bottom:0; }
  .version-history-entry-version{
    margin:0 0 3px; font-size:13px; font-weight:700; color:var(--ink);
    font-variant-numeric:tabular-nums;
  }
  .version-history-entry-text{ margin:0; font-size:13px; color:var(--ink-soft); line-height:1.4; }
  .duration-picker-card{ text-align:center; }
  .duration-picker-wheel{
    margin:22px 0 4px; padding:26px 0;
    border-radius:20px;
    background:linear-gradient(180deg, rgba(15,31,54,.045), rgba(15,31,54,.09));
    touch-action:none; -webkit-user-select:none; user-select:none;
    cursor:grab;
  }
  .duration-picker-wheel:active{ cursor:grabbing; }
  .duration-picker-value{
    font-family:'SF Mono','Courier New',ui-monospace,monospace;
    font-size:60px; font-weight:700; color:var(--navy);
    font-variant-numeric:tabular-nums;
  }
  .duration-picker-unit{
    margin-top:2px; font-size:12px; font-weight:700; color:var(--ink-soft);
    text-transform:uppercase; letter-spacing:.08em;
  }
  /* Global toggle switch - one component, two surface variants. Real
     backdrop-filter glass only reads on the dark/translucent screens
     (.toggle.dark, the new Settings screen) - blurring flat white behind
     flat white does nothing, so the default (light, used by the three
     toggles in the existing white #settingsModal) leans on a soft inset
     track + the same glossy knob gradient instead, to read as the same
     component family without a pointless blur. */
  .toggle{ position:relative; display:inline-block; width:50px; height:29px; flex:0 0 auto; }
  .toggle input{ opacity:0; width:0; height:0; position:absolute; }
  .toggle-slider{
    position:absolute; inset:0; cursor:pointer; border-radius:100px;
    background:#eef0f2; border:1px solid #dde1e6;
    box-shadow:inset 0 1px 2px rgba(15,31,54,.08);
    transition:background .2s ease, border-color .2s ease;
  }
  .toggle-slider::before{
    content:''; position:absolute; left:3px; top:3px;
    width:21px; height:21px; border-radius:50%;
    background:radial-gradient(circle at 32% 28%, #fff, #e7ebef 70%);
    box-shadow:0 2px 5px rgba(0,0,0,.25), inset 0 -2px 3px rgba(0,0,0,.05);
    transition:transform .2s cubic-bezier(.25,.8,.25,1);
  }
  .toggle input:checked + .toggle-slider{ background:#32d74b; border-color:transparent; }
  .toggle input:checked + .toggle-slider::before{ transform:translateX(21px); }
  .toggle input:disabled + .toggle-slider{ opacity:.4; cursor:not-allowed; }

  /* Dark/glass variant - the new Settings screen, and anywhere else on the
     navy background. */
  .toggle.dark .toggle-slider{
    background:rgba(255,255,255,.10); border:1px solid rgba(255,255,255,.22);
    -webkit-backdrop-filter:blur(10px) saturate(160%); backdrop-filter:blur(10px) saturate(160%);
    box-shadow:inset 0 1px 2px rgba(0,0,0,.25);
  }
  .toggle.dark input:checked + .toggle-slider{ background:rgba(50,215,75,.35); border-color:rgba(50,215,75,.55); }

  .export-link-btn{
    margin-top:16px; width:100%; padding:11px;
    background:none; border:1px dashed #c7cdd6;
    color:var(--ink-soft); border-radius:var(--radius-sm);
    font-size:13px; font-weight:600; cursor:pointer;
    transition:transform .12s ease;
  }
  .export-link-btn:active{ transform:scale(.97); }
  /* Confirm-state for the double-press "Overfør økt-eier" button - accent
     (not --danger) since handing off ownership deliberately isn't framed as
     destructive the way "Avslutt og nullstill" is. */
  .export-link-btn.armed{ background:var(--accent); color:var(--accent-ink); border-style:solid; border-color:transparent; }
  #transferOwnerNote{ margin:6px 0 0; text-align:center; }
  /* Generic wrapper for a confirm button that has NO info-btn of its own
     to repurpose as the cancel "×" (contrast Kampslutt/Avslutt og
     nullstill, which reuse .stack-btn-split .info-btn instead - see
     createConfirmArm() in app.js) - #transferOwnerWrap, #newSessionWrap.
     Purely a positioning box: no background/border of its own, so the
     wrapped button keeps its full existing look (dashed pill, glass tile,
     whatever) untouched while unarmed. The divider/× stay `hidden` until
     armed, so they add nothing to the layout normally - same fixed
     right:38px/right:14px geometry as .stack-btn-split, just colored to
     match each button's own armed state via the ID-scoped rules below. */
  .confirm-wrap{ position:relative; width:100%; }
  .confirm-wrap-divider{
    position:absolute; top:10px; bottom:10px; right:38px; width:1px;
    pointer-events:none;
  }
  .confirm-wrap-cancel{
    /* right:9px - same (38-20)/2 centering as .stack-btn-split .info-btn.
       is-cancel above, between .confirm-wrap-divider's right:38px and the
       wrap's own right edge. */
    position:absolute; top:50%; right:9px; transform:translateY(-50%);
    width:20px; height:20px; border:none;
    background:none; color:inherit; font-size:18px; font-weight:800; line-height:1;
    display:inline-flex; align-items:center; justify-content:center;
    padding:0; cursor:pointer; outline:none;
    transition:transform .1s ease;
  }
  .confirm-wrap-cancel:active{ transform:translateY(-50%) scale(.82); }
  /* Same author-display-beats-[hidden] gotcha as elsewhere in this file -
     the unconditional display:inline-flex above needs an explicit
     override, or this stays visible (and unresponsive - createConfirmArm's
     internal armed flag stays false) even while `hidden`. */
  .confirm-wrap-cancel[hidden]{ display:none; }
  /* Same invisible hit-area widening as .stack-btn-split .info-btn::before. */
  .confirm-wrap-cancel::before{
    content:''; position:absolute; top:-4px; bottom:-4px; left:-16px; right:-8px;
  }
  #transferOwnerWrap.armed .confirm-wrap-divider{ background:var(--accent-ink); opacity:.3; }
  #transferOwnerWrap.armed .confirm-wrap-cancel{ color:var(--accent-ink); }
  #newSessionWrap.armed .confirm-wrap-divider{ background:#fff; opacity:.4; }
  #newSessionWrap.armed .confirm-wrap-cancel{ color:#fff; }

  .export-textarea{
    width:100%; min-height:200px; margin:10px 0;
    border:1px solid #dde1e6; border-radius:var(--radius-sm);
    padding:10px 12px; font-size:13px; font-family:inherit;
    background:#fafbfc; color:var(--ink);
    resize:vertical;
    /* body har user-select:none - uten dette kan teksten ikke markeres for hånd på iOS. */
    -webkit-user-select:text; user-select:text;
  }

  /* "Om appen" function guide, above the symbol legend in the same modal -
     one titled block per function rather than the swatch+one-line-text
     rows below (those pair a specific visual symbol with its meaning;
     these explain an action you take, so they get a bit more room). */
  .func-list{ display:flex; flex-direction:column; gap:13px; margin:14px 0 22px; }
  .func-title{ margin:0 0 2px; font-size:14px; font-weight:700; color:var(--ink); }
  .func-text{ margin:0; font-size:13px; line-height:1.42; color:var(--ink-soft); }
  .legend-section-title{
    margin:0 0 2px; font-size:12px; font-weight:700; color:var(--ink-soft);
    text-transform:uppercase; letter-spacing:.04em;
  }
  .legend-divider{ border:none; border-top:1px solid #e7e9ec; margin:18px 0; }

  /* Shared by the "hvem scoret / fjern mål / målskårere" player list
     (goalPlayerModal, three modes - see openGoalPlayerModal) - a plain
     minimalist tap-list, one row per player. */
  .goal-player-list{ display:flex; flex-direction:column; gap:6px; margin:14px 0; max-height:52vh; overflow-y:auto; touch-action:pan-y; }
  /* Separates on-field players (above) from bench/innbyttere (below) in
     the add/remove goal-scorer picker (see renderGoalPlayerList()) - a
     soft line rather than a label, since the row order plus this one
     visual break already reads as "these are the subs" without needing
     to spell it out. Not shown in "view" (top scorers) mode, which
     doesn't group by field/bench at all. */
  .goal-player-divider{ height:1px; background:#e7e9ec; margin:4px 2px; }
  .goal-player-row{
    display:flex; align-items:center; gap:12px; width:100%;
    padding:10px 12px; border:none; border-radius:var(--radius-sm);
    background:#f5f6f7; color:var(--ink);
    font-size:15px; font-weight:600; font-family:inherit; text-align:left;
    cursor:pointer; transition:background .12s ease, transform .12s ease;
  }
  /* Author display:flex above beats the browser's default [hidden]{display:
     none} regardless of specificity - without this, toggling .hidden on
     #goalRemoveLastBtn (add vs. remove mode) has no visible effect. */
  .goal-player-row[hidden]{ display:none; }
  button.goal-player-row:active{ transform:scale(.98); background:#eceef0; }
  button.goal-player-row:disabled{ opacity:.4; cursor:default; }
  button.goal-player-row:disabled:active{ transform:none; background:#f5f6f7; }
  .goal-player-row-avatar{
    width:32px; height:32px; border-radius:50%; flex:0 0 auto;
    display:flex; align-items:center; justify-content:center;
    font-size:12px; font-weight:700; color:#fff;
    background:linear-gradient(160deg,var(--navy-2),var(--navy));
  }
  .goal-player-row-name{ flex:1 1 auto; }
  .goal-player-row-count{ font-size:13px; font-weight:700; color:var(--ink-soft); }
  /* Behind-a-tap timestamp reveal (see openGoalTimesModal) - keeps a
     scorer's row/summary line to one line each even with 7-8 goals,
     instead of spelling every match-time out inline. */
  .goal-time-btn{
    -webkit-appearance:none; appearance:none; flex:0 0 auto;
    width:24px; height:24px; border-radius:50%; border:none;
    background:#eef0f2; color:var(--ink-soft);
    font-size:12px; line-height:1; cursor:pointer;
    display:flex; align-items:center; justify-content:center;
    transition:background .12s ease, transform .12s ease;
  }
  .goal-time-btn:active{ transform:scale(.9); background:#e2e4e8; }
  .goal-player-empty{ margin:14px 0; font-size:13px; color:var(--ink-soft); text-align:center; }
  /* Quick-remove shortcut above the per-player list in 'remove' mode -
     removes whichever goal was actually registered most recently,
     regardless of who it belongs to, so a correction doesn't require
     knowing/remembering which player it was mistakenly attributed to. */
  .goal-quick-remove{
    background:rgba(230,57,70,.1); color:var(--danger);
    margin-top:12px; margin-bottom:2px;
  }
  button.goal-quick-remove:active{ background:rgba(230,57,70,.18); }
  button.goal-quick-remove:disabled{ opacity:.4; cursor:default; }
  .goal-quick-remove-icon{ background:var(--danger); }
  /* Names the player it'll actually affect, so the shortcut is unambiguous
     before you tap it - lighter weight than the label itself so "Fjern
     siste mål" still reads as the primary action, not the name. */
  .goal-quick-remove-target{ font-weight:500; opacity:.8; }
  .legend-list{ display:flex; flex-direction:column; gap:14px; margin:14px 0; }
  .legend-row{ display:flex; align-items:center; gap:12px; }
  .legend-swatch{
    position:relative;
    width:30px; height:30px; border-radius:50%; flex:0 0 auto;
    display:flex; align-items:center; justify-content:center;
    font-size:14px; font-weight:800;
    background:#eef1f0;
  }
  .legend-swatch.warning{ background:var(--orange); color:#fff; }
  /* Mirrors .btn-badge on the real Forslag/multiBytte buttons (see
     .bench-action-btn .btn-icon), scaled down and re-anchored for this
     30px round swatch instead of that button's small rectangular icon
     box - keeps the legend's icons matching what's actually on screen. */
  .legend-badge{
    position:absolute; top:-5px; right:-6px;
    display:flex; align-items:center; justify-content:center;
    min-width:13px; height:13px; padding:0 3px;
    font-size:8.5px; font-weight:800; line-height:1; color:#fff;
    background:var(--navy); border:1.5px solid #fff; border-radius:999px;
    box-shadow:0 1px 2px rgba(15,31,54,.35);
  }
  /* Rank swatches show the same SVG polygon+stroke markup rankBadgeHtml()
     draws on a player's avatar - a navy backdrop (matching the avatar
     itself) so the white outline reads the same way it does in the app,
     rather than a plain colored circle with a unicode arrow. */
  .legend-swatch.rank-most,
  .legend-swatch.rank-most2,
  .legend-swatch.rank-least2,
  .legend-swatch.rank-least{ background:var(--navy); }
  .legend-swatch.check{ background:#32b45a; color:#fff; }
  .legend-swatch.ring-orange{ box-shadow:0 0 0 3px rgba(255,138,61,.65); }
  .legend-swatch.suggest-ring{ box-shadow:0 0 0 3px rgba(50,180,90,.75); }
  .legend-swatch.order{ background:var(--navy); color:#fff; font-size:16px; }
  .legend-swatch.icon-btn-swatch{ background:var(--navy); color:#fff; font-size:16px; }
  .legend-text{ font-size:13px; color:var(--ink); line-height:1.35; }

  .modal-actions{ display:flex; gap:10px; margin-top:22px; }
  .modal-actions button{
    flex:1; padding:13px; border-radius:var(--radius-sm); border:none;
    font-size:15px; font-weight:700; cursor:pointer;
    transition:transform .12s ease;
  }
  .modal-actions button:active{ transform:scale(.97); }
  .modal-actions .secondary{ background:#f0f1f3; color:var(--ink); }
  .modal-actions .primary{ background:var(--accent); color:var(--accent-ink); }
  .modal-actions .primary.danger{ background:var(--danger); color:#fff; }
  .modal-actions-col{ flex-direction:column; }
  .modal-actions-col button{ flex:none; }
  /* .modal-actions button (above) is more specific than .info-btn's own
     rule (class+element beats a single class), so every property they
     both set - padding, border, flex, font-size/weight - was silently
     winning over .info-btn's own styling for any info-btn living inside a
     modal-actions row (e.g. #closeSessionRow, .stack-btn-split). Reassert
     it here rather than patching properties one at a time as each
     collision surfaces. */
  .modal-actions .info-btn{
    flex:0 0 auto; padding:0; border:none;
    font-size:10px; font-weight:700;
  }
  .stack-btn{
    width:100%; padding:14px; border-radius:var(--radius-sm); border:none;
    background:#f0f1f3; color:var(--ink);
    font-size:15px; font-weight:700; cursor:pointer;
    transition:transform .12s ease;
  }
  .stack-btn:active{ transform:scale(.97); }
  .stack-btn:disabled{ opacity:.4; cursor:default; }
  .stack-btn:disabled:active{ transform:none; }
  .stack-btn.period{ background:rgba(10,132,255,.14); color:var(--period-blue); }
  .stack-btn.danger{ background:rgba(230,57,70,.12); color:var(--danger); transition:background .15s ease, color .15s ease; }
  .stack-btn.danger.armed{ background:var(--danger); color:#fff; }
  .shake-btn{ animation:shake .3s; }
  .stack-btn.secondary{ background:#f0f1f3; color:var(--ink); }
  .end-match-summary{
    margin:14px 0; max-height:230px; overflow-y:auto; touch-action:pan-y;
    border-top:1px solid #eef0f2; border-bottom:1px solid #eef0f2;
  }
  /* Tidligere kamper denne økten (se state.matchHistory / endMatchPeriod) -
     each finished match, most recent first, with its own compact per-match
     playtime breakdown so a cup day's several matches stay distinguishable
     from the cross-match cumulative list below (.end-match-summary). Empty
     (no matches ended yet) collapses to nothing via :empty. */
  .match-history-summary:empty{ display:none; }
  .match-history-summary{ margin:14px 0; display:flex; flex-direction:column; gap:10px; }
  .mh-match{ background:#f7f8f9; border-radius:var(--radius-sm); padding:8px 10px; }
  .mh-header{
    display:flex; justify-content:space-between; align-items:baseline;
    font-size:13px; font-weight:700; color:var(--ink); gap:8px;
  }
  .mh-score{ font-variant-numeric:tabular-nums; white-space:nowrap; }
  .mh-players{ margin-top:4px; display:flex; flex-direction:column; gap:2px; }
  .mh-player-row{
    display:flex; justify-content:space-between; gap:8px;
    font-size:12px; color:var(--ink-soft);
  }
  .mh-player-name{ white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
  .mh-player-time{ font-variant-numeric:tabular-nums; white-space:nowrap; }
  .ems-row{
    display:flex; justify-content:space-between; align-items:baseline;
    gap:10px; padding:9px 2px;
    border-bottom:1px solid #f5f6f7;
    font-size:13px;
  }
  .ems-row:last-child{ border-bottom:none; }
  /* min-width:0 is what lets .ems-name ellipsize inside a flex row instead
     of forcing the row wider - the clock button sits outside that
     ellipsis-clipped span so it's never the thing that gets truncated. */
  .ems-name-wrap{ display:flex; align-items:center; gap:4px; min-width:0; }
  .ems-name{ font-weight:700; color:var(--ink); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
  .ems-time{ color:var(--ink-soft); font-variant-numeric:tabular-nums; white-space:nowrap; font-size:12px; }
  #endMatchModal{ z-index:110; }
  /* Åpnes fra Kampslutt-arket («Forlat delt økt»): må ligge over det. */
  #closeSessionConfirmModal{ z-index:115; }
  #exportModal{ z-index:110; }
  /* 320: åpnes også fra Innstillinger-skjermen (.history-screen, z-index:310) - lavere havnet bak den. */
  #legendModal{ z-index:320; }
  #settingsCloseConfirmModal{ z-index:110; }
  #joinCodeModal{ z-index:120; }
  /* Spørres mens launcheren (z-index:300) fortsatt ligger oppå. */
  #joinConfirmModal{ z-index:320; }
  #infoPopupModal{ z-index:130; }
  #goalTimesModal{ z-index:140; }
  /* Opened directly from .launcher-version on .launcher-screen (z-index:300)
     - needs to sit above that, like .coin-flip-screen/.history-screen do,
     not among the other modals above (all opened only once the launcher
     is already hidden, so 100-140 is plenty for them). */
  #versionHistoryModal{ z-index:310; }
  /* Same reasoning as #versionHistoryModal above - "Ny økt" can ask this
     while .launcher-screen is still up (the match being discarded hasn't
     been replaced by a fresh one yet), so this needs to clear it too. */
  #saveHistoryModal{ z-index:310; }
  /* Same reasoning again - opened directly from historyTile on the
     launcher, before .history-screen (also 310) is ever reached. */
  #historyLoginModal{ z-index:310; }
  /* Same reasoning again - opened directly from accountBtn on the
     launcher. Missing this (unlike historyLoginModal above) meant the
     modal rendered fully "open" but completely invisible and unclickable
     behind .launcher-screen - only caught by actually running the app in
     a browser, not by reading the CSS. */
  #accountLoginModal{ z-index:310; }
  /* Opened from inside .history-screen itself (z-index:310, same as
     .launcher-screen) - same reasoning as the two rules above, one level
     higher again since .history-screen is already at 310. Pre-existing bug
     fixed alongside this: #historyDeleteConfirmModal ("Slett valgte") had
     the exact same gap - opened from .history-screen too, but never had an
     override, so it silently rendered underneath. */
  #historyDeleteConfirmModal{ z-index:320; }
  .settings-card{ position:relative; padding-top:50px; }

  .session-code-bar{
    flex:0 0 auto;
    background:rgba(15,31,54,.9);
    color:rgba(255,255,255,.85);
    font-size:11px; font-weight:700; letter-spacing:.3px;
    text-align:center; padding:5px 0;
    cursor:pointer;
  }
  .session-code-bar span{ color:#fff; font-variant-numeric:tabular-nums; }
  .sync-dot{
    display:inline-block; width:7px; height:7px; border-radius:50%;
    margin-left:7px; margin-bottom:1px;
    background:#32d74b; vertical-align:middle;
    transition:background .3s ease;
  }
  .sync-dot.offline{ background:var(--orange); }
  .session-code-bar.offline{ color:rgba(255,197,138,.9); }

  /* Was + 10px - the header/account-btn/coin-flip-close/compact-top formula
     above this got bumped to +26px over several rounds of tuning (see
     header's own long comment), but this one was never brought along, so
     it sat closer to the status-bar overlay than every other top-of-screen
     control and risked the same faded/diffuse look. Matched to the same
     +26px for consistency. */
  .auto-pause-notice{
    position:absolute; left:50%; top:var(--top-clear);
    transform:translate(-50%,-140%);
    z-index:400;
    max-width:86%;
    background:rgba(15,31,54,.82);
    -webkit-backdrop-filter:blur(var(--glass-blur)) saturate(160%);
    backdrop-filter:blur(var(--glass-blur)) saturate(160%);
    color:#fff; font-size:13px; font-weight:600; text-align:center;
    padding:10px 16px; border-radius:999px;
    border:1px solid rgba(255,255,255,.16);
    box-shadow:0 8px 22px rgba(0,0,0,.3);
    opacity:0; pointer-events:none;
    transition:opacity .3s ease, transform .3s ease;
  }
  .auto-pause-notice.show{ opacity:1; transform:translate(-50%,0); }
  /* Longer message than the pill shape above suits (that one's tuned for a
     single short line) - a rounded card reads cleanly across 2-3 lines
     instead of wrapping into an odd near-circular blob. */
  #joinedEmptyReadOnlyNotice{
    max-width:78%; border-radius:var(--radius-md);
    line-height:1.4; text-align:left;
  }

  /* Read-only participant in a shared session: the controls that would
     mutate shared state are visibly inert, not just functionally blocked.
     #undoBtn, #settingsBtn and #endBtn are deliberately NOT in this list -
     opening any of them is pure navigation, not a mutation (undoBtn's "Gå
     tilbake til hovedmeny"; settingsBtn just to look at the current setup;
     endBtn's "Forlat økt" has zero effect on the match either way - see
     the endBtn click handler). Each mutating control inside is
     individually locked instead: undoBtn's "Angre siste"/"Endre tilbake"
     via canEdit(), settings' player names/kampvarighet/byttetid/sharing
     rows via isMaster() (see openSettings), endMatchModal's "Kampslutt"/
     "Avslutt og nullstill" via canEdit()/isMaster() (see the endBtn click
     handler) - disabling the buttons that OPEN these here would leave a
     read-only viewer with no way to reach any of them at all. */
  .view-only #playPauseBtn,
  .view-only #multiSelectBtn,
  .view-only #multiSelectCancelBtn,
  .view-only #swapSuggestionBtn,
  .view-only #swapSuggestionCancelBtn,
  .view-only #homeScoreBtn,
  .view-only #awayScoreBtn,
  .view-only #awayTeamLabel{
    opacity:.35; pointer-events:none;
  }

  .join-code-input{
    width:100%; margin:12px 0; text-align:center;
    font-size:28px; font-weight:700; letter-spacing:8px;
    padding:12px; border:1px solid #dde1e6; border-radius:var(--radius-sm);
    background:#fafbfc; color:var(--ink);
  }
  .join-code-error{ color:var(--danger); font-size:13px; text-align:center; margin:0 0 8px; }

  @media (prefers-reduced-motion: reduce){
    .token-enter{ animation:none; }
    .token.drop-rejected{ animation:none; }
    .shake-btn{ animation:none; }
    .stack-btn.danger{ transition:none; }
    .ghost-token{ transition:none; }
    .avatar{ transition:none; }
    .export-link-btn, .stack-btn, .modal-actions button, .bench-action-btn{ transition:none; }
    .token.suggested-partner .avatar{ animation:none; box-shadow:0 0 0 4px rgba(50,180,90,.75), var(--shadow-sm); }
    .token.swap-flash .avatar{ animation:none; }
    .name-row-enter{ animation:none; }
    .badge-warning.overtime{ animation:none; opacity:1; }
    .multi-cancel-btn{ transition:none; }
  }
