/* ==========================================================================
   LANDING — design system.  Every token is defined once in the :root below,
   in its night-ops form; DAY MODE — which is what the pages ship, further
   down under data-theme="light" — redefines the palette half of them and
   leaves geometry, type and motion alone.
   FLIGHT DECK identity: a night-ops cockpit. Deep-space navy ground with a
   faint blueprint grid, glass panels, an electric-sky accent, Exo 2 for
   display type and instrument numerals, and instrument-style motion (bars
   that sweep in, a radar scan in the hero, numbers that count up).

   White-label contract (UNCHANGED): LandingData.applyTheme overrides exactly
   four tokens — --accent / --accent-dark / --hero-from / --hero-to.
   Everything else is platform-fixed. Signals (--live, --err, --ok, --warn)
   and the platform mark (--brand-ink, --brand-accent) must never move with an
   event's branding: an error stays an error on a blue event, and the way home
   stays recognisable. Because the ground changes with the mode, two DERIVED
   tones follow the accent automatically (no extra token for events to set) —
   each mixed away from whatever ground it sits on:
     --accent-ink   the accent carried off the ground — towards white at
                    night, towards black in daylight — so text and links stay
                    readable whatever colour an event chose, in either mode
     --accent-soft  a translucent wash of the accent for active states

   Print is the exception to everything: paper is light, so @media print
   re-tokens the palette back to ink-on-white.

   Breakpoints (media queries cannot read custom properties, so by convention):
     phone   <= 640px
     tablet  641–900px
     desktop  > 900px   (public nav becomes a floating dock; the console nav
                         becomes a left rail — 901–1350px icons only, wider
                         than 1350px icons + labels)
   Sole exception: the 720/721px view-mode switch — coupled to viewmode.js and
   print behaviour, deliberately kept apart from the three above.
   ========================================================================== */

/* ---------- Exo 2 (vendored, OFL) — display face with Cyrillic cover ------ */
/* Variable weight 400–800: one file per script subset, hashes in
   assets/fonts/README.md. zh falls back to the system stack by design. */
@font-face{
  font-family:'Exo 2';font-style:normal;font-weight:400 800;font-display:swap;
  src:url(../fonts/exo2-latin.woff2) format('woff2');
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face{
  font-family:'Exo 2';font-style:normal;font-weight:400 800;font-display:swap;
  src:url(../fonts/exo2-latin-ext.woff2) format('woff2');
  unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face{
  font-family:'Exo 2';font-style:normal;font-weight:400 800;font-display:swap;
  src:url(../fonts/exo2-cyrillic.woff2) format('woff2');
  unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;
}
@font-face{
  font-family:'Exo 2';font-style:normal;font-weight:400 800;font-display:swap;
  src:url(../fonts/exo2-cyrillic-ext.woff2) format('woff2');
  unicode-range:U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;
}

/* The hidden attribute has to win. The browser's own [hidden]{display:none}
   has almost no specificity, so ANY class that sets display puts the element
   back on screen — which has now happened four times here: a dropdown menu, a
   role chip and two form fields all sat visible while el.hidden read true.
   One rule, rather than remembering to patch every class that sets display. */
[hidden]{display:none !important}

:root{
  /* brand tokens (the ONLY four applyTheme may override, per event) */
  --accent:#38b6ff;
  --accent-dark:#1d8fe0;
  --hero-from:#0d2138;
  --hero-to:#060b14;

  /* Derived from the accent, so they follow an event's brand automatically —
     and so the SAME brand colour works on both the dark deck and the light
     one. --accent stays exactly what the event set (rails, dots, borders);
     these three are the readable/contrasting forms of it:
       --accent-ink    text and links on the page ground
       --accent-solid  a filled surface carrying WHITE text (buttons, badges)
       --accent-deep   the far end of a filled gradient
     The light theme redefines all four mixes towards black instead of white;
     nothing else in the file has to know which theme is on. */
  --accent-ink:#7ecbff;                                     /* fallback for the line below */
  --accent-ink:color-mix(in srgb,var(--accent) 55%,#fff);
  --accent-soft:rgba(56,182,255,.14);                       /* fallback for the line below */
  --accent-soft:color-mix(in srgb,var(--accent) 16%,transparent);
  --accent-solid:var(--accent);
  --accent-deep:var(--accent-dark);

  /* platform mark — never touched by applyTheme, so the way home stays stable */
  --brand-ink:#10233c;
  --brand-accent:#38b6ff;

  /* palette — the deck */
  --bg:#060b14;
  --card:#0e1a2c;
  --glass:rgba(13,23,40,.82);
  --text:#e7eefb;
  --muted:#8fa1b8;
  --line:#1e3049;
  --ok:#4ed492;
  --ok-bg:rgba(64,205,138,.13);
  --ok-line:rgba(64,205,138,.36);
  --warn:#f4bf62;
  --warn-bg:rgba(244,191,98,.13);
  --warn-line:rgba(244,191,98,.36);
  /* signals — platform-fixed, deliberately NOT per-event brandable */
  --live:#ff5257;
  --err:#ff8177;
  --err-bg:rgba(255,110,97,.13);
  --err-line:rgba(255,110,97,.38);

  /* neutral surfaces the palette above cannot reach from components */
  --chip-bg:#182740;
  --hover-bg:#152337;
  --surface-2:#0a1424;
  --line-strong:#35507a;
  --focus-border:#52709c;
  --focus-ring:rgba(56,182,255,.25);                        /* fallback for the line below */
  --focus-ring:color-mix(in srgb,var(--accent) 26%,transparent);

  /* The run list's three states — see the .row-done / .row-next block, which
     is where the reasoning for having them as tokens at all is written.

     The live row's FILL is cool here and warm in daylight, and that is not an
     oversight: green over navy stays green, but any warm colour laid over this
     deck at a low alpha pulls all three channels to the middle and comes out
     khaki. So the night deck says "up next" with the rail and the two
     hairlines, which are amber and stay amber, and lets the fill do what it
     can actually do on a dark ground — lift the row off it. */
  --run-done-bg:rgba(78,212,146,.055);
  --run-done-rail:#4ed492;
  --run-next-bg:rgba(150,195,245,.13);
  --run-next-edge:rgba(250,199,110,.42);
  --run-next-rail:#f6c66c;
  --run-wait-rail:var(--line-strong);

  /* type scale — 16px inputs stay >=16 or iOS zooms the field on focus */
  --fs-2xs:11px;
  --fs-xs:12px;
  --fs-sm:13px;
  --fs-base:14px;
  --fs-md:15px;
  --fs-lg:16px;
  --fs-xl:18px;
  --fs-2xl:21px;
  --fs-3xl:24px;
  --fs-4xl:28px;
  --fs-display:34px;
  --track-caps:.08em;
  /* the one uppercase micro-label treatment (nine components share it) */
  --caps-size:12px;
  --caps-track:.08em;
  --caps-weight:700;

  /* spacing — 4px base */
  --sp-1:4px;
  --sp-2:8px;
  --sp-3:12px;
  --sp-4:16px;
  --sp-5:20px;
  --sp-6:24px;
  --sp-8:32px;
  --sp-10:40px;

  /* radius ramp */
  --radius-lg:18px;
  --radius-md:12px;
  --radius-xs:8px;
  --radius-pill:999px;
  --radius:var(--radius-lg);
  --radius-sm:var(--radius-md);

  /* elevation — night ink at four strengths, plus the accent glow */
  --shadow-1:0 1px 2px rgba(1,6,16,.5);
  --shadow-2:0 8px 24px -10px rgba(1,6,16,.6);
  --shadow-3:0 18px 48px -12px rgba(1,6,16,.7);
  --shadow-up:0 -8px 24px rgba(1,6,16,.5);
  --shadow:var(--shadow-2);
  --shadow-lg:var(--shadow-3);
  --glow:0 0 22px -4px rgba(56,182,255,.5);                 /* fallback for the line below */
  --glow:0 0 22px -4px color-mix(in srgb,var(--accent) 50%,transparent);

  --maxw:960px;
  --font:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif;
  --font-display:'Exo 2',system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif;
  --font-mono:ui-monospace,"SF Mono",Menlo,Consolas,monospace;
}

/* ==========================================================================
   DAY MODE — the same deck under daylight, and what the platform SHIPS.
   Every page carries data-theme="light" in its markup, so this is the ground
   before a line of script runs; theme.js leaves the stamp on unless this
   browser has chosen night mode, and the toggle in the top bar remembers
   that choice. Night mode is the :root above, reached by taking the stamp
   off — which is why the palette is still written dark-first in this file.

   Only TOKENS are redefined here, plus the handful of rules whose colour is
   baked into a gradient or a literal. The four brand tokens are untouched —
   an event's accent is its accent in both themes; what changes is how the
   readable forms of it are mixed (towards black rather than towards white).
   ========================================================================== */
:root[data-theme="light"]{
  --accent-ink:#0b5f92;                                     /* fallback for the line below */
  --accent-ink:color-mix(in srgb,var(--accent) 72%,#000);
  --accent-soft:rgba(56,182,255,.16);                       /* fallback for the line below */
  --accent-soft:color-mix(in srgb,var(--accent) 15%,transparent);
  --accent-solid:#1183cc;                                   /* fallback for the line below */
  --accent-solid:color-mix(in srgb,var(--accent) 84%,#000);
  --accent-deep:#0d6ba8;                                    /* fallback for the line below */
  --accent-deep:color-mix(in srgb,var(--accent) 62%,#000);

  --bg:#eef2f7;
  --card:#ffffff;
  --glass:rgba(255,255,255,.86);
  --text:#0f1b2b;
  --muted:#586a7e;
  --line:#dce4ee;
  --ok:#15803d;
  --ok-bg:#ecfdf3;
  --ok-line:#bbf7d0;
  --warn:#a16207;
  --warn-bg:#fffbeb;
  --warn-line:#fde68a;
  --live:#d92d20;
  --err:#b42318;
  --err-bg:#fef3f2;
  --err-line:#fecdca;

  --chip-bg:#e7eef7;
  --hover-bg:#f1f5fa;
  --surface-2:#f7fafc;
  --line-strong:#b4c3d4;
  --focus-border:#89a0b9;
  --focus-ring:rgba(56,182,255,.28);                        /* fallback for the line below */
  --focus-ring:color-mix(in srgb,var(--accent) 28%,transparent);

  /* Daylight's own three, not a formula's guess at them. --ok and --warn are
     nearly black down here so that they can be READ as text; six per cent of
     either mixed into white is mud, which is exactly what the run list used
     to be. These are the colours a tint wants: clean green, clean amber. */
  --run-done-bg:rgba(22,163,74,.055);
  --run-done-rail:#16a34a;
  --run-next-bg:rgba(245,158,11,.13);
  --run-next-edge:rgba(245,158,11,.45);
  --run-next-rail:#f59e0b;
  --run-wait-rail:var(--line-strong);

  --shadow-1:0 1px 2px rgba(16,24,40,.05);
  --shadow-2:0 6px 18px -8px rgba(16,24,40,.22);
  --shadow-3:0 16px 40px -14px rgba(16,24,40,.3);
  --shadow-up:0 -6px 18px rgba(16,24,40,.09);
  --glow:0 4px 14px -6px rgba(16,24,40,.35);

  color-scheme:light;
}
/* the atmosphere inverts: dark ink on a pale ground, same geometry */
:root[data-theme="light"] body::before{
  background-image:
    radial-gradient(1100px 520px at 82% -12%,rgba(56,182,255,.13),transparent 60%),
    radial-gradient(900px 620px at -12% 108%,rgba(56,182,255,.08),transparent 62%),
    repeating-linear-gradient(0deg,transparent 0 47px,rgba(31,58,92,.05) 47px 48px),
    repeating-linear-gradient(90deg,transparent 0 47px,rgba(31,58,92,.05) 47px 48px);
}
/* panels are paper, not glass — the gradients above are written in dark ink */
:root[data-theme="light"] .card,
:root[data-theme="light"] .plat-feature{
  background:var(--card);
  box-shadow:var(--shadow);
}
:root[data-theme="light"] .modal{background:var(--card)}
:root[data-theme="light"] .modal-backdrop{background:rgba(15,27,43,.42)}
:root[data-theme="light"] input,
:root[data-theme="light"] select,
:root[data-theme="light"] textarea,
:root[data-theme="light"] .pick-btn{color-scheme:light;background:#fff}
:root[data-theme="light"] .ev-go{color:#9fb0c4}
:root[data-theme="light"] .btn-ok{color:#fff}
:root[data-theme="light"] ::selection{background:rgba(56,182,255,.28);color:#06263c}
/* the deck's own scrollbars go back to the platform default */
:root[data-theme="light"] ::-webkit-scrollbar-thumb{background:var(--line-strong)}

*{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{
  margin:0;
  font-family:var(--font);
  background:var(--bg);
  color:var(--text);
  line-height:1.5;
}
/* The atmosphere: two faint instrument glows and a blueprint grid over the
   base navy. Painted on body so every page carries the deck, and kept to
   whisper-level alphas so a branded event's colours still lead. */
body::before{
  content:"";position:fixed;inset:0;z-index:-1;pointer-events:none;
  background-image:
    radial-gradient(1100px 520px at 82% -12%,rgba(56,182,255,.09),transparent 60%),
    radial-gradient(900px 620px at -12% 108%,rgba(56,182,255,.05),transparent 62%),
    repeating-linear-gradient(0deg,transparent 0 47px,rgba(126,164,206,.045) 47px 48px),
    repeating-linear-gradient(90deg,transparent 0 47px,rgba(126,164,206,.045) 47px 48px);
}
::selection{background:rgba(56,182,255,.32);color:#fff}
a{color:var(--accent-ink);text-decoration:none}
a:hover{text-decoration:underline}
img{max-width:100%;height:auto}
h1,h2,h3{font-family:var(--font-display);letter-spacing:.01em}

/* dark chrome for the scrollbars the console's capped lists show */
*{scrollbar-width:thin;scrollbar-color:var(--line-strong) transparent}
::-webkit-scrollbar{width:10px;height:10px}
::-webkit-scrollbar-thumb{background:var(--line-strong);border-radius:999px;border:2px solid var(--bg)}
::-webkit-scrollbar-track{background:transparent}

/* ---------- motion primitives ------------------------------------------- */
/* Panels rise into place. [hidden] toggles restart CSS animations, so the
   console's tab switches replay this for free — that is the point. */
@keyframes deck-rise{from{opacity:0;transform:translateY(9px)}to{opacity:1;transform:none}}
@keyframes deck-fade{from{opacity:0}to{opacity:1}}
.card,.plat-feature,.ev-card{animation:deck-rise .4s cubic-bezier(.22,.9,.3,1) backwards}
.ev-grid > :nth-child(2),.plat-features > :nth-child(2){animation-delay:.05s}
.ev-grid > :nth-child(3),.plat-features > :nth-child(3){animation-delay:.1s}
.ev-grid > :nth-child(4){animation-delay:.15s}
.ev-grid > :nth-child(5){animation-delay:.2s}
.ev-grid > :nth-child(6){animation-delay:.25s}
@media (prefers-reduced-motion:reduce){
  .card,.plat-feature,.ev-card{animation:none}
}

/* ---------- cross-document transitions ----------------------------------- */
/* Same-origin navigations CROSSFADE instead of hard-cutting (Chrome/Safari;
   a browser without support simply navigates). The chrome — top bar and the
   nav — carries its own transition name, so it holds still while the page
   content fades beneath it. The pages seed themselves from the session stash
   (util.js U.stash) before first paint, so the incoming frame already shows
   real content in the event's own theme rather than skeletons. */
@view-transition{navigation:auto}
::view-transition-old(root),::view-transition-new(root){animation-duration:.18s}
.topbar{view-transition-name:deck-topbar}
.tabbar{view-transition-name:deck-nav}
@media (prefers-reduced-motion:reduce){
  @view-transition{navigation:none}
}

.wrap{max-width:var(--maxw);margin:0 auto;padding:18px 14px 40px}

/* The console is a working surface, not a column of prose. 960px is the right
   measure for a page someone reads; a console row carries a name, a date, a
   count, a status and the buttons that act on it, and at 960px they do not all
   fit — the Users table was giving an e-mail address less width than one
   address takes, so every address broke across two lines mid-word. Only
   admin.html carries this class, so the public pages keep the reading measure.
   The bar and the content share the token, so their edges stay aligned.
   1240 is not a taste: the Users table's own content asks for 1114px, and this
   is the first round number that clears it with room for a longer address than
   the ones on the platform today. Below a laptop width the rows grow taller
   instead, and below 720px they stop being rows at all. */
body.console{--maxw:1240px}

/* ---------- top bar ------------------------------------------------------ */
/* Full-width sticky glass strip: the one piece of chrome every page shares. */
.topbar{
  position:sticky;top:0;z-index:45;
  display:flex;align-items:center;justify-content:space-between;gap:12px;
  margin:0;padding:11px 20px;
  background:var(--glass);
  -webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px);
  border-bottom:1px solid var(--line);
}
.brand{display:flex;align-items:center;gap:10px;font-weight:800;color:var(--text);font-family:var(--font-display);letter-spacing:.06em}
.brand img{height:34px;width:auto}

/* The top bar doubles as the breadcrumb: LANDING is the way home from every
   public page, and the event's own name follows it. Only the trail's LAST item
   is allowed to shrink — the way back must never be the part that vanishes. */
.brandbar{display:flex;align-items:center;gap:9px;min-width:0;flex:1 1 auto}
.brandbar .brand{flex:none;min-width:0}
.brandbar .brand span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.brand-sep{color:var(--muted);font-weight:700;opacity:.55;flex:none}
.brand-event{color:var(--muted);font-weight:700;min-width:0;flex:0 1 auto}
a.brand-event:hover{color:var(--text);text-decoration:none}
.brand-event img{height:26px}
@media (max-width:640px){
  .brandbar{gap:7px}
  .brand-event{font-size:var(--fs-sm)}
  .brand-event img{height:22px}
}

/* Language switcher. However many languages an event offers, the header shows
   at most two: the event's default, plus the chosen one when that is something
   else. The rest open from the ▾ button. .lang is only the anchor — the pill
   clips the buttons to its rounded ends, so the menu has to sit outside it. */
.lang{display:inline-flex;position:relative;max-width:100%}
.lang-pill{
  display:inline-flex;border:1px solid var(--line);border-radius:var(--radius-pill);
  overflow:hidden;background:var(--card);max-width:100%
}
.lang button{
  border:0;background:transparent;padding:7px 11px;font-weight:700;font-size:var(--fs-sm);cursor:pointer;color:var(--muted);
  white-space:nowrap;flex:none
}
.lang-pill button.active{background:var(--accent-solid);color:#fff}
.lang-pill .lang-more{font-size:var(--fs-2xs);line-height:1;padding:7px 10px;border-left:1px solid var(--line)}
.lang-pill .lang-more:hover{background:var(--hover-bg);color:var(--text)}

.lang-menu{
  position:absolute;top:calc(100% + 6px);right:0;z-index:60;
  min-width:190px;max-height:min(60vh,340px);overflow-y:auto;
  background:var(--card);border:1px solid var(--line);border-radius:12px;
  box-shadow:var(--shadow-lg);padding:6px;
  display:flex;flex-direction:column;gap:2px
}
.lang-menu button{
  display:flex;align-items:center;justify-content:space-between;gap:12px;
  width:100%;text-align:left;border-radius:var(--radius-xs);padding:9px 12px;
  font-weight:600;font-size:var(--fs-base);color:var(--text)
}
.lang-menu button:hover{background:var(--hover-bg)}
.lang-menu .l-code{font-size:var(--fs-2xs);font-weight:800;letter-spacing:.04em;color:var(--muted)}
@media (max-width:640px){ .lang button{padding:7px 9px;font-size:var(--fs-xs)} }

/* ---------- the account menu -------------------------------------------- */
/* The avatar and the panel it opens. Built like .lang above: the wrapper is
   only an anchor, so the panel can hang outside the round button.
   [hidden] is spelled out on both, because a `display` rule on a class beats
   the browser's own `[hidden] { display:none }` — a bug this project has
   already shipped once, with a language menu that would not close. */
.usermenu{display:inline-flex;position:relative}
.usermenu[hidden]{display:none}
.avatar-btn{
  display:inline-flex;align-items:center;justify-content:center;align-self:center;
  width:34px;height:34px;padding:0;border:0;border-radius:var(--radius-pill);cursor:pointer;
  background:var(--accent-solid);color:#fff;font:inherit;font-weight:800;font-size:var(--fs-xs);
  letter-spacing:.02em;line-height:1;
}
.avatar-btn:hover{filter:brightness(1.12)}
.avatar-btn[aria-expanded="true"]{box-shadow:0 0 0 3px var(--bg),0 0 0 4px var(--accent)}

.usermenu-pop{
  position:absolute;top:calc(100% + 8px);right:0;z-index:60;
  min-width:240px;max-width:min(88vw,320px);
  background:var(--card);border:1px solid var(--line);border-radius:var(--radius-md);
  box-shadow:var(--shadow-lg);padding:8px;
  display:flex;flex-direction:column;gap:2px;text-align:left;
}
.usermenu-pop[hidden]{display:none}
.usermenu-head{display:flex;align-items:center;gap:11px;padding:8px 10px 10px}
.avatar-lg{
  display:inline-flex;align-items:center;justify-content:center;flex:none;
  width:42px;height:42px;border-radius:var(--radius-pill);
  background:var(--accent-solid);color:#fff;font-weight:800;font-size:var(--fs-md);line-height:1;
}
/* An address is one long word and the panel is narrow: let it break rather
   than push the panel wider than the screen. */
.usermenu-who{display:flex;flex-direction:column;gap:1px;min-width:0}
.usermenu-who b{font-size:var(--fs-base);line-height:1.3;overflow-wrap:anywhere}
.usermenu-who span{font-size:var(--fs-xs);color:var(--muted);overflow-wrap:anywhere}
.usermenu-badges{display:flex;flex-wrap:wrap;gap:6px;padding:0 10px 4px}
.usermenu-badges:empty{display:none}
.usermenu-scope{padding:2px 10px 8px;font-size:var(--fs-xs);color:var(--muted)}
.usermenu-sep{height:1px;background:var(--line);margin:4px 2px 6px}
.usermenu-item{
  display:block;width:100%;text-align:left;border:0;background:transparent;
  border-radius:var(--radius-xs);padding:10px 12px;cursor:pointer;
  font:inherit;font-weight:600;font-size:var(--fs-base);color:var(--text);
}
.usermenu-item:hover{background:var(--hover-bg)}
.usermenu-item.danger{color:var(--accent-ink)}

/* ---------- day / night switch ------------------------------------------ */
/* Sits at the head of the top bar's controls, before the language pill —
   both are "how this page is shown to me". One button, two glyphs: the one
   on screen is the mode you would switch TO, which is what a single-button
   switch has to show to be readable at a glance. */
.theme-toggle{
  display:inline-flex;align-items:center;justify-content:center;align-self:center;flex:none;
  width:34px;height:34px;padding:0;border:1px solid var(--line);border-radius:var(--radius-pill);
  background:var(--card);color:var(--muted);cursor:pointer;
  transition:color .15s,border-color .15s;
}
.theme-toggle:hover{color:var(--text);border-color:var(--line-strong)}
.theme-toggle svg{width:17px;height:17px;display:block}
/* dark deck on screen -> offer the sun; day mode on -> offer the moon */
.theme-toggle .ico-sun{display:block}
.theme-toggle .ico-moon{display:none}
:root[data-theme="light"] .theme-toggle .ico-sun{display:none}
:root[data-theme="light"] .theme-toggle .ico-moon{display:block}

/* round icon-only control (admin bar: home / sign out) */
.icon-btn{
  display:inline-flex;align-items:center;justify-content:center;align-self:center;
  width:34px;height:34px;padding:0;border:1px solid var(--line);border-radius:var(--radius-pill);
  background:var(--card);color:var(--muted);cursor:pointer;
}
.icon-btn:hover{background:var(--hover-bg);color:var(--text);text-decoration:none}
.icon-btn svg{width:17px;height:17px;display:block}

/* ---------- navigation -------------------------------------------------- */
/* One markup, three placements. Phones (public and console): a full-width
   glass bar along the bottom, thumb-reach. Desktop public: a floating glass
   DOCK, centred over the foot of the page. Desktop console: a fixed left rail
   — icons only up to 1350px, icons + labels beyond. */
.tabbar{
  position:fixed;left:0;right:0;bottom:0;z-index:40;
  background:var(--glass);
  -webkit-backdrop-filter:blur(16px);backdrop-filter:blur(16px);
  border-top:1px solid var(--line);box-shadow:var(--shadow-up);
  padding-bottom:env(safe-area-inset-bottom);
}
.tabbar-inner{max-width:var(--maxw);margin:0 auto;display:grid;grid-template-columns:repeat(5,1fr)}
.tabbar .tab{
  display:flex;flex-direction:column;align-items:center;justify-content:flex-end;gap:4px;
  min-height:58px;min-width:0;padding:8px 3px 7px;
  font-family:var(--font-display);
  font-size:var(--fs-2xs);font-weight:700;line-height:1.15;text-align:center;color:var(--muted);
}
.tabbar .tab:hover{color:var(--text);text-decoration:none}
.tabbar .tab.active{color:var(--accent-ink)}
.tabbar .tab-ico{width:22px;height:22px;display:block}
/* button tabs (admin) must match the anchor tabs (public) — reset the button
   chrome but keep the .tab font size/weight (never inherit the body font). */
.tabbar button.tab{border:0;background:none;cursor:pointer;font-family:var(--font-display);font-size:var(--fs-2xs);font-weight:700;line-height:1.15}
.tabbar .tab span{max-width:100%;overflow:hidden;text-overflow:ellipsis}
/* The last tab carries the CLUB'S NAME, and a name is several words where
   every other label is one. Left to wrap it made that tab two lines tall, and
   the grid made all five match — a 79px bar under a body padded for 66px, so
   the foot of every page was covered. It ellipsises on one line instead. */
.tabbar .tab [data-club-name]{white-space:nowrap}

/* 3rd tab is the primary action — raised above the bar */
.tabbar .tab-cta{color:var(--accent-ink)}
.tab-fab{
  display:inline-flex;align-items:center;justify-content:center;
  width:52px;height:52px;margin-top:-24px;border-radius:var(--radius-pill);
  background:linear-gradient(160deg,var(--accent-solid),var(--accent-deep));color:#fff;border:4px solid var(--card);
  box-shadow:0 6px 18px rgba(56,182,255,.4);                /* fallback for the line below */
  box-shadow:0 6px 18px color-mix(in srgb,var(--accent) 42%,transparent);
}
.tab-fab svg{width:24px;height:24px;display:block}
.tabbar .tab-cta:hover .tab-fab{filter:brightness(1.1)}
.tabbar .tab-cta:active .tab-fab{transform:translateY(1px)}

/* keep page content clear of the fixed bar */
body:has(.tabbar){padding-bottom:calc(66px + env(safe-area-inset-bottom))}

/* desktop PUBLIC: the floating dock */
@media (min-width:901px){
  body:not(.console) .tabbar{
    left:50%;right:auto;bottom:16px;transform:translateX(-50%);
    width:auto;max-width:calc(100vw - 32px);
    border:1px solid var(--line);border-top:1px solid var(--line);border-radius:var(--radius-pill);
    box-shadow:var(--shadow-lg);
    padding:5px 8px;
  }
  body:not(.console) .tabbar-inner{display:flex;gap:2px;max-width:none}
  body:not(.console) .tabbar .tab{
    flex-direction:row;gap:8px;justify-content:center;
    min-height:0;padding:10px 15px;border-radius:var(--radius-pill);
    font-size:var(--fs-sm);
  }
  body:not(.console) .tabbar .tab.active{background:var(--accent-soft)}
  body:not(.console) .tabbar .tab-ico{width:18px;height:18px}
  /* The raised action reads like every other item on the dock: mark first,
     then its name. It keeps the filled disc — that is what marks it as the
     primary action — but not a different word order from its neighbours. */
  body:not(.console) .tab-fab{width:34px;height:34px;margin:-4px 0 -4px -5px;border:0}
  body:not(.console) .tab-fab svg{width:18px;height:18px}
  body:not(.console) .tabbar .tab-cta{padding-left:9px}
  body:not(.console):has(.tabbar){padding-bottom:96px}
}

/* desktop CONSOLE: the left rail (only admin.html carries body.console) */
@media (min-width:901px){
  body.console .tabbar{
    top:0;bottom:0;left:0;right:auto;width:74px;height:auto;
    border-top:0;border-right:1px solid var(--line);box-shadow:none;
    padding:64px 10px 14px;overflow-y:auto;
  }
  body.console .tabbar-inner{display:flex;flex-direction:column;gap:4px;max-width:none;grid-template-columns:none !important}
  body.console .tabbar .tab{
    flex-direction:row;justify-content:center;gap:11px;
    min-height:0;padding:12px 10px;border-radius:var(--radius-md);
    font-size:var(--fs-base);text-align:left;
  }
  body.console .tabbar .tab.active{background:var(--accent-soft)}
  body.console .tabbar .tab-ico{width:20px;height:20px;flex:none}
  /* only the LABEL span hides on the narrow rail — the raised action and the
     pending-review count are spans too, and must not vanish with it */
  body.console .tabbar .tab > span:not(.tab-fab):not(.tab-n){display:none}
  body.console .tabbar .tab-cta .tab-fab{display:inline-flex;width:36px;height:36px;margin:0;border:0;flex:none}
  body.console .tabbar .tab-cta{padding:8px}
  /* the pending-review count rides the icon in the rail */
  body.console .tabbar .tab-n{top:4px;left:auto;right:8px;margin-left:0}
  /* #adminTabbar, not .tabbar: the competition dock below is a .tabbar too,
     and it is not what the page has to leave a left margin for. */
  body.console:has(#adminTabbar:not([hidden])){padding-left:74px;padding-bottom:0}
}
@media (min-width:1351px){
  body.console #adminTabbar{width:216px;padding:64px 12px 16px}
  body.console #adminTabbar .tab{justify-content:flex-start;padding:12px 13px}
  body.console #adminTabbar .tab > span:not(.tab-fab):not(.tab-n){display:inline;overflow:hidden;text-overflow:ellipsis}
  /* the count sits at the far end of the row, after the label */
  body.console #adminTabbar .tab-n{position:static;order:9;margin-left:auto}
  body.console:has(#adminTabbar:not([hidden])){padding-left:216px}
}

/* ---- the console's second bar: one competition's own menu --------------- */
/* The rail above is the platform (dashboard, competitions, users, clubs,
   public site) and stays put. THIS is the competition you are inside, and it
   wears exactly what the public pages wear: a floating dock on a desktop,
   the bottom bar on a phone. Two bars never share a screen edge — on a phone
   the platform bar stands down (body.in-event) and the top bar's back arrow
   is the way up a level. */
@media (min-width:901px){
  body.console .tabbar-event{
    top:auto;bottom:16px;height:auto;overflow:visible;
    left:calc(50% + 37px);right:auto;transform:translateX(-50%);
    width:auto;max-width:calc(100vw - 120px);
    border:1px solid var(--line);border-right:1px solid var(--line);
    border-radius:var(--radius-pill);box-shadow:var(--shadow-lg);
    padding:5px 8px;
  }
  body.console .tabbar-event .tabbar-inner{
    display:flex;flex-direction:row;gap:2px;max-width:none;grid-template-columns:none !important;
  }
  body.console .tabbar-event .tab{
    flex-direction:row;gap:8px;justify-content:center;
    min-height:0;padding:10px 15px;border-radius:var(--radius-pill);
    font-size:var(--fs-sm);text-align:center;
  }
  body.console .tabbar-event .tab > span:not(.tab-fab):not(.tab-n){display:inline}
  body.console .tabbar-event .tab.active{background:var(--accent-soft)}
  body.console .tabbar-event .tab-ico{width:18px;height:18px}
  body.console .tabbar-event .tab-fab{width:34px;height:34px;margin:-4px 0 -4px -5px;border:0}
  body.console .tabbar-event .tab-fab svg{width:18px;height:18px}
  body.console .tabbar-event .tab-cta{padding-left:9px}
  body.console .tabbar-event .tab-n{top:2px;left:auto;right:6px;margin-left:0}
  /* The dock floats over the page, so the page owes it clearance. The id in
     the :has() is what outranks the rail's own padding-bottom:0 above. */
  body.console.in-event:has(#eventTabbar:not([hidden])){padding-bottom:92px}
  /* ...and it is centred on the content, not on the window: the rail owns the
     left 74px (216px on a wide screen), so the dock's midpoint shifts by half */
  body.console.in-event .tabbar-event{left:calc(50% + 37px)}
}
@media (min-width:1351px){
  body.console.in-event .tabbar-event{left:calc(50% + 108px);max-width:calc(100vw - 260px)}
}
/* phones: one bar at the bottom, whichever level you are on */
@media (max-width:900px){
  body.console.in-event #adminTabbar{display:none}
}

/* ---------- hero -------------------------------------------------------- */
/* The faint horizontal line at 68% is the horizon: a second gradient screened
   over the brand gradient, with a whisper of "ground" tint below it. White-
   alpha only, so it reads over ANY event's hero colours. The horizon layer is
   listed first — layers stack top-down, and under the opaque brand gradient
   it would never show. */
.hero{
  position:relative;overflow:hidden;
  background-image:
    linear-gradient(180deg,transparent calc(68% - 1px),rgba(255,255,255,.13) 68%,rgba(255,255,255,.03) calc(68% + 1px),rgba(255,255,255,.025) 100%),
    linear-gradient(145deg,var(--hero-from),var(--hero-to));
  background-blend-mode:screen,normal;
  color:#fff;border-radius:var(--radius);padding:26px 22px;
  border:1px solid var(--line);box-shadow:var(--shadow-lg);
}
.hero .eyebrow{display:flex;align-items:center;flex-wrap:wrap;gap:8px;font-family:var(--font-display);font-size:var(--fs-xs);letter-spacing:.14em;text-transform:uppercase;opacity:.78;margin-bottom:8px}
.hero .eyebrow:empty{display:none}
.hero .eyebrow .disc-ico{opacity:.92}
.eyebrow-sep{opacity:.5}
.eyebrow-who{min-width:0;overflow:hidden;text-overflow:ellipsis}
.hero h1{margin:0;font-size:var(--fs-4xl);line-height:1.08;letter-spacing:.2px}
.hero .venue{margin-top:8px;font-size:var(--fs-md);opacity:.9}
.hero .tagline{margin-top:14px;font-size:var(--fs-base);opacity:.82}

/* ---------- cards ------------------------------------------------------- */
/* Glass panels: translucent navy over the deck grid, hairline border, a
   1px light catch along the top edge. */
.card{
  background:linear-gradient(180deg,rgba(21,35,58,.88),var(--glass));
  -webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);
  border:1px solid var(--line);
  border-radius:var(--radius);padding:20px;margin-top:16px;
  box-shadow:inset 0 1px 0 rgba(255,255,255,.045),var(--shadow);
}
.card h2{font-size:var(--fs-xl);margin:0 0 14px}
.card h2:first-child{margin-top:0}
.small{color:var(--muted);font-size:var(--fs-sm)}

/* detail rows */
.details{display:grid;grid-template-columns:1fr 1fr;gap:14px}
.detail{display:flex;flex-direction:column;gap:2px}
.detail .k{font-size:var(--caps-size);font-weight:var(--caps-weight);text-transform:uppercase;letter-spacing:var(--caps-track);color:var(--muted)}
.detail .v{font-size:var(--fs-lg);font-weight:650}

/* ---------- forms ------------------------------------------------------- */
.grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}
.field{display:flex;flex-direction:column;gap:6px}
/* checkbox rows (eligibility criteria) */
/* A TICK BOX IS A TARGET, not a bullet point. These are pressed with a thumb
   on a phone at a kitchen table, one per line, by somebody reading the
   sentence beside them — so the box is finger-sized, the words are set at
   reading size, and consecutive rows are far enough apart that a mis-tap
   lands on nothing rather than on the wrong answer.

   align-items:flex-start, not center: a criterion that wraps to two lines
   must keep its box beside the FIRST line, not floating in the middle of
   the paragraph. The small top offset optically centres it on that line. */
:root{--check-box:22px}
.check{
  display:flex;flex-direction:row;align-items:flex-start;gap:12px;
  font-weight:500;cursor:pointer;font-size:var(--fs-md);line-height:1.5;
  padding:7px 0;
}
.check + .check{margin-top:2px}
.check input{
  width:var(--check-box);height:var(--check-box);flex:none;margin:1px 0 0;
  cursor:pointer;accent-color:var(--accent);
}
/* The whole row lights on hover, so it is obvious the words are the target
   too and not just the box. */
.check:hover{color:var(--text)}
@media (max-width:720px){ :root{--check-box:24px} .check{padding:9px 0;gap:13px} }
.field.invalid{border-left:3px solid var(--err);padding-left:12px;border-radius:6px}
/* document photo upload (registration + admin) + lightbox */
.doc-up{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.doc-btn{display:inline-flex;align-items:center;gap:8px;cursor:pointer}
.doc-thumb{height:54px;width:auto;max-width:96px;object-fit:cover;border-radius:var(--radius-xs);border:1px solid var(--line)}
/* The competitor, rather than a photographed piece of paper. Square and
   round-cornered so a head-and-shoulders picture is not cropped to a letterbox
   the way a licence card wants to be. */
.doc-thumb-face{width:54px;max-width:54px;border-radius:var(--radius-pill)}
.doc-view{cursor:zoom-in}
.detail-list .doc-view{max-width:170px;max-height:120px;border-radius:var(--radius-xs);border:1px solid var(--line)}
#imgLightbox{z-index:60;align-items:center;padding:20px}
#imgLightbox .lightbox-inner{max-width:min(94vw,940px);max-height:90vh}
#imgLightbox img{max-width:100%;max-height:90vh;border-radius:10px;display:block;margin:auto;box-shadow:var(--shadow-lg)}
.cam-modal video{width:100%;max-height:56vh;background:#000;border-radius:10px;display:block;margin-bottom:12px}
.field.full{grid-column:1 / -1}
label{font-weight:650;font-size:var(--fs-base)}
input,select,textarea{
  width:100%;border:1px solid var(--line);border-radius:var(--radius-sm);
  padding:14px 13px;font-size:var(--fs-lg);background:var(--surface-2);color:var(--text);outline:none;
  color-scheme:dark;
}
input::placeholder,textarea::placeholder{color:var(--muted);opacity:.8}
textarea{font-family:inherit;resize:vertical;min-height:72px;line-height:1.4}
input[type=color]{padding:4px;height:46px;cursor:pointer}
input:focus,select:focus,textarea:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--focus-ring)}
input:disabled,select:disabled,textarea:disabled{background:var(--chip-bg);color:var(--muted)}
.hint{color:var(--muted);font-size:var(--fs-xs)}

/* Searchable picker — THE drop-down. Every <select> on the platform is turned
   into one of these by pick.js: the select keeps the value and the id, this
   keeps the eyes and the keyboard. Only an entry from the list can become the
   value, so a half-typed word leaves the field as it was rather than becoming
   an answer nobody meant.

   The trigger deliberately shares the plain input's border, radius, padding and
   type size, because it is standing in a row of them. */
.pick{position:relative}
/* The select itself. Kept in the DOM — it is still the value, still the id,
   still the thing every listener is attached to — and simply not drawn. */
.pick-native{display:none}
.pick-btn{
  display:flex;align-items:center;justify-content:space-between;gap:10px;
  width:100%;border:1px solid var(--line);border-radius:var(--radius-sm);
  padding:14px 13px;font:inherit;font-size:var(--fs-lg);background:var(--surface-2);color:var(--text);
  text-align:left;cursor:pointer
}
.pick-btn:focus-visible{border-color:var(--accent);box-shadow:0 0 0 3px var(--focus-ring);outline:none}
/* Open reads like focus, because it is: the keyboard is in the menu, and the
   field it belongs to has to stay obviously the field it belongs to. */
.pick-btn.is-open{border-color:var(--accent);box-shadow:0 0 0 3px var(--focus-ring)}
.pick-btn:disabled{background:var(--chip-bg);color:var(--muted);cursor:default}
.pick-btn .pick-value{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.pick-btn .pick-value:empty::before{content:attr(data-placeholder);color:var(--muted)}
/* Masked SVG, not a ▾: at caret size the glyph is a one-pixel dash on Windows
   and a solid triangle on a Mac, which makes one control look like two. Same
   technique, and the same reason, as .search-box::before. */
.pick-caret{
  flex:none;width:15px;height:15px;background:var(--muted);transition:transform .12s ease;
  -webkit-mask:url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'/></svg>") center/contain no-repeat;
  mask:url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'/></svg>") center/contain no-repeat;
}
.pick-btn.is-open .pick-caret{transform:rotate(180deg);background:var(--accent-solid)}

/* The menu is a single element at the end of <body>, positioned `fixed` by
   pick.js against the trigger's rectangle. It is NOT inside the field, and that
   is the whole point: .card carries backdrop-filter, which makes every card its
   own stacking context, so a menu positioned inside one was painted under the
   NEXT card whatever its z-index claimed. Nothing an ancestor does can clip
   this one, and place() keeps it inside the viewport — flipping above the field
   when there is more room up than down, and never taller than the room it has.
   left/top/width/max-height are all written by place(); what is here is only
   what does not depend on where it opened. */
.pick-menu{
  position:fixed;z-index:200;
  display:flex;flex-direction:column;gap:6px;
  padding:6px;
  background:var(--card);border:1px solid var(--line-strong);border-radius:12px;box-shadow:var(--shadow-lg);
  animation:pick-in .13s ease-out
}
@keyframes pick-in{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}
.pick-menu.is-up{animation-name:pick-in-up}
@keyframes pick-in-up{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
@media (prefers-reduced-motion:reduce){ .pick-menu{animation:none} }
/* A short list has no search box, so open() puts the keyboard on the menu
   itself — and the browser then draws its own focus ring right over the
   hairline border. Which row the keyboard is on is said by .here, on the row;
   a ring around the whole sheet says nothing and looked like a bug. */
.pick-menu:focus,.pick-menu:focus-visible{outline:none}
.pick-searchwrap{flex:none}
.pick-search{font-size:var(--fs-md);padding:9px 11px;flex:none}
.pick-list{overflow-y:auto;display:flex;flex-direction:column;gap:2px}
.pick-list button{
  border:0;background:none;font:inherit;font-size:var(--fs-md);text-align:left;cursor:pointer;
  padding:9px 11px;border-radius:var(--radius-xs);color:var(--text);white-space:nowrap;
  /* A WHOLE number of pixels, deliberately. The inherited 1.5 against a
     15px font gives a 22.5px line box and a 40.5px row, and a half pixel
     does not survive device-pixel snapping the same way down a list of
     249: neighbouring rows land a whole pixel apart. 22px makes every
     option exactly 40px, which is what a list of identical rows is
     supposed to be, and tests/layout_check.js measures. */
  line-height:22px;
  overflow:hidden;text-overflow:ellipsis;
  /* flex:none is load-bearing, not tidiness. These are flex items in a column
     whose height is capped, so by default they shrink to share it — and a flex
     item's automatic minimum size, the thing that normally stops that, is only
     the content's height while overflow is visible. The ellipsis above sets
     overflow:hidden, which drops that minimum to zero. With 249 countries the
     deficit is enormous, every option collapsed to 18px of a 41px line, and the
     same overflow:hidden then clipped the text to a sliver of itself. */
  flex:none;
}
.pick-list button:hover,.pick-list button.here{background:var(--hover-bg)}
.pick-list button[aria-selected="true"]{font-weight:800}
.pick-list button[disabled]{color:var(--muted);cursor:default;background:none}
/* An <optgroup> — the event's own shortlist of countries above the other two
   hundred, and anything else a select groups. Drawn only while the list is in
   its own order: a search reorders it by relevance, and a heading across that
   would separate nothing, so pick.js drops them while searching. */
.pick-list .pick-group{
  flex:none;padding:9px 11px 4px;
  font-size:var(--caps-size);font-weight:var(--caps-weight);
  text-transform:uppercase;letter-spacing:var(--caps-track);color:var(--muted);
}
.pick-list .pick-group ~ .pick-group{border-top:1px solid var(--line-strong);margin-top:5px;padding-top:10px}
/* Where a group's rows end and the ungrouped ones begin — see draw(). */
.pick-list button.after-group{border-top:1px solid var(--line-strong);margin-top:5px;padding-top:9px}
.pick-none{color:var(--muted);font-size:var(--fs-base);padding:9px 11px}

/* Where a rule was written for the <select> that used to be here, the trigger
   that replaced it needs the same one. pick.js copies the select's own classes
   onto the trigger, so only the rules written against the ELEMENT need a
   companion — these. */
.disc-pick .pick-btn{flex:1;min-width:0}
.ro-row .pick-btn{width:auto;min-width:190px;flex:0 1 auto}
@media (max-width:640px){ .ro-row .pick-btn{min-width:0;width:100%} }
@media (max-width:720px){ .off-row > .pick-btn{grid-column:1 / -1} }
@media (hover:none){ .pick-btn{min-height:48px} }

/* A drop-down you may also TYPE into — the club box and the flight log's
   instructor box. Same menu, same rows, same placement as every other picker;
   the difference is above the menu, where the trigger is the field itself
   rather than a button standing in for a hidden <select>. Nothing may be typed
   into a picker; anything may be typed into one of these, and what is typed is
   the answer whether the list has heard of it or not.

   The wrapper exists for the caret to be positioned against and for nothing
   else. .field is a flex column, so it is simply one more item in it. */
.pick-combo{position:relative;display:flex;align-items:center;min-width:0}
.pick-combo > input{flex:1;min-width:0;padding-right:36px}
/* Open reads like focus on the field, exactly as .pick-btn.is-open does on the
   trigger it replaced — and it IS focus here: the keyboard never leaves the box. */
.pick-combo.is-open > input{border-color:var(--accent);box-shadow:0 0 0 3px var(--focus-ring)}
/* top, not translateY: .pick-caret spends its transform on the flip below. */
.pick-combo-caret{position:absolute;right:12px;top:calc(50% - 7px);cursor:pointer}
.pick-combo.is-open .pick-caret{transform:rotate(180deg);background:var(--accent-solid)}
.pick-combo > input:disabled ~ .pick-combo-caret{cursor:default;opacity:.45}
@media (hover:none){ .pick-combo > input{min-height:48px} }

/* Keep what was typed. The row is the typed text offered back, so it has to
   read as an action rather than as one more suggestion — hence the accent and
   the +. It sits at the top, and the list under it is separated by a rule in
   the same weight the group boundaries use. */
.pick-list button.pick-free{display:flex;align-items:center;gap:9px;font-weight:700;color:var(--accent-solid)}
.pick-list button.pick-free .pf-t{min-width:0;overflow:hidden;text-overflow:ellipsis}
.pick-list button.pick-free + button,.pick-list button.pick-free + .pick-group{
  border-top:1px solid var(--line-strong);margin-top:5px;padding-top:9px
}
.pick-plus{
  flex:none;width:14px;height:14px;background:var(--accent-solid);
  -webkit-mask:url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.6' stroke-linecap='round'><line x1='12' y1='5' x2='12' y2='19'/><line x1='5' y1='12' x2='19' y2='12'/></svg>") center/contain no-repeat;
  mask:url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.6' stroke-linecap='round'><line x1='12' y1='5' x2='12' y2='19'/><line x1='5' y1='12' x2='19' y2='12'/></svg>") center/contain no-repeat;
}

/* password field with reveal toggle */
.pw-wrap{position:relative;display:flex}
.pw-wrap input{padding-right:48px}
.pw-toggle{
  position:absolute;top:50%;right:6px;transform:translateY(-50%);
  display:inline-flex;align-items:center;justify-content:center;
  width:36px;height:36px;padding:0;border:0;border-radius:var(--radius-pill);
  background:transparent;color:var(--muted);cursor:pointer;
}
.pw-toggle:hover{background:var(--chip-bg);color:var(--text)}
.pw-toggle svg{width:19px;height:19px;display:block}
.pw-toggle .eye-off{display:none}
.pw-toggle.revealed .eye{display:none}
.pw-toggle.revealed .eye-off{display:block}
.req{color:var(--err)}
.invalid{
  border-color:var(--err) !important;
  box-shadow:0 0 0 3px rgba(255,110,97,.15) !important;     /* fallback for the line below */
  box-shadow:0 0 0 3px color-mix(in srgb,var(--err) 15%,transparent) !important;
}

/* image upload preview + event banner. Logos are drawn for light paper, so
   their preview plates stay white on purpose. */
.upload{display:flex;flex-wrap:wrap;align-items:center;gap:10px}
.img-preview{max-height:64px;max-width:180px;border:1px solid var(--line);border-radius:10px;background:#fff;object-fit:contain;padding:4px}
.img-preview-wide{max-width:100%;max-height:120px}
.event-banner{width:100%;max-height:340px;object-fit:cover;border-radius:var(--radius);box-shadow:var(--shadow);display:block;margin-bottom:16px}

/* admin menu tabs: icon + label */
.subtab{display:inline-flex;align-items:center;gap:8px}
.subtab svg{width:17px;height:17px;flex:0 0 auto}

/* ---------- buttons ----------------------------------------------------- */
.btn{
  display:inline-flex;align-items:center;justify-content:center;gap:8px;
  border:1px solid var(--line-strong);background:var(--card);color:var(--text);
  border-radius:var(--radius-md);padding:13px 18px;font-size:var(--fs-md);font-weight:700;cursor:pointer;
  font-family:var(--font-display);letter-spacing:.015em;
}
.btn:hover{background:var(--hover-bg);text-decoration:none}
.btn-primary{
  background:linear-gradient(160deg,var(--accent-solid),var(--accent-deep));
  border-color:transparent;color:#fff;box-shadow:var(--glow);
}
.btn-primary:hover{filter:brightness(1.1)}
.btn-primary:active{transform:translateY(1px)}
.btn-block{width:100%}
.btn-lg{padding:16px 20px;font-size:17px;font-weight:800;border-radius:16px}
.actions{display:flex;flex-wrap:wrap;gap:10px;margin-top:16px}

/* one focus ring for the interactive set that had none — keyboard users get
   the same ring everywhere instead of the browser's default or nothing */
:is(.btn,.chip,.icon-btn,.lang button,.tabbar .tab,.viewtoggle button,.pw-toggle,.avatar-btn,.result-btn,.instr-btn,.land-sq,.scope-item):focus-visible{
  outline:2px solid var(--accent);outline-offset:2px;
}

/* spacing utilities — the 4px scale, for markup that would otherwise carry
   inline margins */
.mt-2{margin-top:var(--sp-2)}
.mt-3{margin-top:var(--sp-3)}
.mt-4{margin-top:var(--sp-4)}
/* single-purpose cards (login, forced password change) read best on a short measure */
.card-narrow{max-width:440px;margin-left:auto;margin-right:auto}
/* visually hidden, still read aloud — for text that only assistive tech needs */
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}

/* ---- one entry, as a page ------------------------------------------------ */
/* THE PAGE SAYS EACH THING ONCE. It used to say the money three times — a
   chip in the header, the callout under it, and a card at the foot — and the
   status twice, and the club, the country and the category both in the line
   under the name and again as rows in the cards below. Every repetition is a
   place a reader has to check against another place. So: the head is who they
   are, the callouts are where they stand and what they owe (all of it,
   including what the money is FOR), the cards are the facts nothing above has
   already given, and the photographs are one strip at the foot.

   Spacing is one scale, not six: --sp-4 (16px) between blocks and inside a
   card's rows, --sp-2 (8px) between things that belong to each other. */

/* The head: a face, a name, and the one line that places them. */
.en-head{display:flex;align-items:center;gap:var(--sp-4);flex-wrap:wrap;padding:var(--sp-4)}
.en-face{
  flex:none;display:inline-flex;align-items:center;justify-content:center;
  width:64px;height:64px;border-radius:50%;overflow:hidden;
  background:var(--chip-bg);border:1px solid var(--line);color:var(--muted);
}
.en-face img{width:100%;height:100%;object-fit:cover;display:block}
.en-face svg{width:36px;height:36px;display:block}
.en-who{flex:1 1 220px;min-width:0}
.en-who h2{margin:0;font-size:var(--fs-2xl);line-height:1.15;overflow-wrap:anywhere}
.en-sub{margin-top:2px;color:var(--muted);font-size:var(--fs-base)}

/* WHERE THIS ENTRY STANDS, and what it owes. Two boxes, and between them they
   are the ONLY place either answer is written. The payment one carries its own
   breakdown, which is why the boxes size to their contents instead of being
   stretched to match: a status box padded out to the height of a five-line
   price list is empty space pretending to be information. */
.en-callouts{
  display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));
  gap:var(--sp-4);margin:var(--sp-4) 0;align-items:start;
}
.en-callout{
  display:flex;flex-direction:column;gap:2px;
  padding:var(--sp-4);border-radius:var(--radius);
  border:1px solid var(--line-strong);background:var(--card);
  border-left-width:5px;
}
.en-callout.is-ok{border-left-color:var(--ok);background:var(--ok-bg)}
.en-callout.is-wait{border-left-color:var(--warn);background:var(--warn-bg)}
.en-callout.is-no{border-left-color:var(--err);background:var(--err-bg)}
.en-c-k{font-size:var(--fs-xs);text-transform:uppercase;letter-spacing:.05em;color:var(--muted)}
.en-c-v{
  font-family:var(--font-display);font-size:var(--fs-xl);font-weight:800;line-height:1.15;
  font-variant-numeric:tabular-nums;color:var(--text);
}
.en-c-n{font-size:var(--fs-sm);color:var(--muted)}
/* What the money is for, under what it comes to. One line per item, the
   amount right-aligned against it — the shape of a bill, because that is
   what it is. */
.en-c-lines{
  list-style:none;margin:var(--sp-3) 0 0;padding:var(--sp-3) 0 0;
  border-top:1px solid var(--line-strong);
  display:flex;flex-direction:column;gap:var(--sp-1);
}
.en-c-lines li{display:flex;align-items:baseline;justify-content:space-between;gap:var(--sp-3);font-size:var(--fs-sm)}
.en-c-lines li b{flex:none;font-variant-numeric:tabular-nums}
.en-c-note{margin:var(--sp-2) 0 0;font-size:var(--fs-sm);color:var(--muted)}

/* EVERYTHING YOU CAN DO, GROUPED BY WHAT IT ACTS ON. Seven buttons on one
   line, in one colour scheme, is a line nobody reads: it is scanned for the
   word you came for, and Delete sat two along from Edit. Three groups, each
   named for what it touches — the desk's decisions, the paper that leaves the
   room, and the row itself — and the two irreversible ones are alone at the
   end under a heading that says so. */
.en-actcard{padding:var(--sp-4)}
.en-acts{display:flex;flex-wrap:wrap;gap:var(--sp-4) var(--sp-8)}
.en-act-grp{display:flex;flex-direction:column;gap:var(--sp-2);min-width:0}
.en-act-k{
  font-size:var(--caps-size);font-weight:var(--caps-weight);letter-spacing:var(--caps-track);
  text-transform:uppercase;color:var(--muted);
}
.en-act-btns{display:flex;flex-wrap:wrap;gap:var(--sp-2)}

/* THE FACTS, PACKED. A grid laid these out in rows, and every row was as tall
   as the tallest card in it: a medical with three blank lines left a band of
   empty page under it, and the row below started beneath the longest card
   rather than beneath its own. Columns pack instead — each card is as tall as
   its own contents and the next one begins directly under it. */
/* The cards carry no top margin — the first in each column has to start at
   the top of it — so the block itself keeps the page rhythm. */
#enCards{columns:290px 3;column-gap:var(--sp-4);margin-top:var(--sp-4)}
#enCards .card{margin:0 0 var(--sp-4);padding:var(--sp-4);break-inside:avoid;-webkit-column-break-inside:avoid}
#enCards h2{margin:0 0 var(--sp-3);font-size:var(--fs-lg)}
#enCards .details{gap:var(--sp-4) var(--sp-3)}
/* A fact nobody filled in is still worth SHOWING — an accreditation desk has
   to see that the licence number is missing — but it is not worth reading at
   the weight of one that is there. Every blank used to be set in the same
   bold as the answers, so a card of three blanks shouted three times and the
   two real values on the page had to be hunted for. */
#enCards .detail.is-none .v{font-weight:500;color:var(--muted)}
/* Two facts side by side, and one of them labelled "Flight hours in the last
   2 years": the long label wraps, and its value drops a line below the value
   beside it. The cells are the same height (the grid stretches them), so
   settling the contents to the BOTTOM of the cell puts every value in a row
   on one line and every label directly above its own value. */
#enCards .detail{justify-content:flex-end}
@supports (grid-template-rows:subgrid){
  /* ...and where subgrid is understood, both halves line up: the pair takes
     two rows of the card grid, so the label row is as tall as the taller of
     the two labels and BOTH labels and BOTH values start on one line. */
  /* justify-* are reset: the flex fallback above pushes the COLUMN down,
     and the same property on a grid would push its columns right instead. */
  #enCards .detail{
    display:grid;grid-template-rows:subgrid;grid-row:span 2;row-gap:2px;
    justify-content:normal;justify-items:start;
  }
}

/* THE PHOTOGRAPHS, at the foot, as thumbnails. The competitor's face used to
   be a card of its own carrying the full-size image — a column of the page
   spent on one picture, while the two documents were single rows lost among
   the facts. They are one strip now: same size, same frame, captioned, and
   any of them opens full size on a click (.doc-view, the lightbox). A slot
   the event asks for and nobody filled keeps its place as an empty frame,
   because "no medical on file" is something a desk has to be able to see. */
#enPhotos .card{margin-top:var(--sp-4);padding:var(--sp-4)}
#enPhotos h2{margin:0 0 var(--sp-3);font-size:var(--fs-lg)}
.en-shots{display:flex;flex-wrap:wrap;gap:var(--sp-4)}
.en-shot{margin:0;display:flex;flex-direction:column;gap:var(--sp-2);width:150px}
.en-shot img,.en-shot .en-shot-none{
  width:150px;height:100px;border-radius:var(--radius-xs);
  border:1px solid var(--line);background:var(--chip-bg);
}
.en-shot img{object-fit:cover;display:block}
.en-shot-face{width:100px}
.en-shot-face img,.en-shot-face .en-shot-none{border-radius:50%;width:100px;height:100px}
.en-shot-none{
  display:flex;align-items:center;justify-content:center;
  border-style:dashed;color:var(--muted);font-size:var(--fs-lg);
}
/* Set to be read, not to shout: three lines of spaced capitals under a
   picture is a label competing with the thing it labels. */
.en-shot figcaption{font-size:var(--fs-sm);color:var(--muted);line-height:1.3}
@media print{ .en-callout{background:none} }

/* ---- the competitor's face ---------------------------------------------- */
/* A round frame beside a name, on the public start list and the standings.
   THE FRAME IS ALWAYS THERE — an entry with no photograph gets the same
   circle with a silhouette in it, because a column of faces with holes in it
   reads as broken and a start list has to line up down the page. */
.face{
  flex:none;display:inline-flex;align-items:center;justify-content:center;
  width:34px;height:34px;border-radius:50%;overflow:hidden;
  background:var(--chip-bg);border:1px solid var(--line);
}
.face img{width:100%;height:100%;object-fit:cover;display:block}
.face.is-none{color:var(--muted)}
.face.is-none svg{width:22px;height:22px;display:block}
/* Face then name, side by side — in a WRAPPER inside the cell, never on the
   cell itself. A <td> given display:flex stops being a table cell: it leaves
   the column grid and every column after it drifts out of line. */
.who-face{display:flex;align-items:center;gap:10px;min-width:0}
.who-face > b{min-width:0;overflow-wrap:anywhere}

/* ---- one order for three lists ------------------------------------------ */
/* The sort picker rides in the toolbar beside the view switch. It is a plain
   select, so pick.js gives it the platform's own drop-down like every other
   one on the site. */
/* On a toolbar this is a CONTROL among controls, so it takes a width and
   holds it. .pick-btn is width:100% — right for a field in a form, wrong
   here: inside a shrink-to-fit group it asked for the whole line, so the
   group measured narrower than its own contents and the print button and
   the view switch dropped to a second row beneath it. */
.sortpick{flex:0 0 200px;width:200px;max-width:200px}
/* ...and it stands in a row of chips, so it is sized like one rather than
   like the tall field it is on a form. */
.toolbar-filters .sortpick{padding:9px 13px;font-size:var(--fs-md)}
@media (max-width:520px){ .sortpick{flex:1 1 100%;width:auto;max-width:none} }
/* How many rows a container is showing, in its heading — the answer to
   "did my search find anything" without counting. */
.count-pill{
  display:inline-block;padding:3px 11px;border-radius:var(--radius-pill);
  background:var(--chip-bg);border:1px solid var(--line);
  font-weight:700;font-size:var(--fs-sm);font-variant-numeric:tabular-nums;color:var(--muted);
}
/* The people inside a club or a team. One per line, and never stretched to
   the full width of a wide screen — a list of names is read down. */
.club-who{line-height:1.75;max-width:34ch}
body.view-list .club-who{max-width:none}
/* A member's start number, ahead of their name, in the width of two digits so
   the names below each other start in the same place. */
.tm-n{
  display:inline-block;min-width:2.2em;color:var(--muted);
  font-variant-numeric:tabular-nums;font-weight:700;
}
/* ---- what a printed sheet says it is ------------------------------------
   A page of names that does not name the competition is a page of names.
   This line rides in the table's HEADER GROUP, which browsers repeat at the
   top of every page a table runs onto — so a start list that takes three
   sheets is identified on all three, not just the first. On screen it is
   not there at all. .sheet-line is the same line for a container that has
   no table to put it in, because it has nothing to list. */
.rtable tr.sheet-row,.sheet-line{display:none}

/* ---- the result, drawn --------------------------------------------------
   The figure is the answer; the bar beside it is how far off the leader it
   is, read without arithmetic. Penalty points, so the SHORTEST bar leads —
   the same scale the top-ten chart above the boards used to draw, now in
   the row it belongs to instead of in a card of its own repeating ten of
   them. Decoration: the number is written next to it, and the bar is
   aria-hidden. */
.rankbar{
  display:block;height:5px;min-width:78px;margin-top:6px;overflow:hidden;
  border-radius:99px;background:var(--chip-bg);
}
.rankbar i{display:block;height:100%;border-radius:99px;background:var(--accent-solid)}
body.view-list .rankbar{min-width:96px}

/* ---- how much of a long list is on the page ------------------------------
   One row above each board: how many, where you are, and the way on. It
   does not print, because what prints is the rows. */
.pager{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:0 0 12px}
.pager-lab{
  font-size:var(--fs-sm);text-transform:uppercase;letter-spacing:var(--caps-track);
  color:var(--muted);font-weight:var(--caps-weight);
}
/* Sized like the sort picker, and for the same reason: on a toolbar a
   drop-down is a control, and .pick-btn is width:100%. */
.pager-size{flex:0 0 104px;width:104px;max-width:104px;padding:8px 12px;font-size:var(--fs-md)}
.pager-at{font-size:var(--fs-sm);color:var(--muted);font-variant-numeric:tabular-nums}
.pager-nav{display:inline-flex;gap:6px;margin-left:auto}
.pager-nav .btn{min-width:40px;font-size:var(--fs-lg);line-height:1;padding:7px 11px}

/* A stamped time on the public board reads as a clock, not as prose. */
td.clock{font-variant-numeric:tabular-nums;white-space:nowrap}

/* ---------- notices ----------------------------------------------------- */
.notice{margin-top:16px;padding:15px;border-radius:var(--radius-md);font-weight:600;display:none}
.notice.show{display:block}
.notice-ok{background:var(--ok-bg);border:1px solid var(--ok-line);color:var(--ok)}
.notice-warn{background:var(--warn-bg);border:1px solid var(--warn-line);color:var(--warn)}
.notice-err{background:var(--err-bg);border:1px solid var(--err-line);color:var(--err)}

/* small inline banner (e.g. preview mode) */
.banner{
  max-width:var(--maxw);margin:8px auto 0;padding:10px 14px;border-radius:12px;
  background:var(--warn-bg);border:1px solid var(--warn-line);color:var(--warn);font-size:var(--fs-sm);
}

/* ---------- tables ------------------------------------------------------ */
.tablewrap{overflow-x:auto}
table{width:100%;border-collapse:collapse;font-size:var(--fs-base)}
th,td{padding:10px 12px;text-align:left;border-bottom:1px solid var(--line);white-space:nowrap}
th{font-size:var(--caps-size);font-weight:var(--caps-weight);text-transform:uppercase;letter-spacing:var(--caps-track);color:var(--muted)}
td.rank{font-weight:800;color:var(--accent-ink);font-family:var(--font-display);font-variant-numeric:tabular-nums}
.badge{display:inline-block;padding:4px 9px;border-radius:var(--radius-pill);background:var(--chip-bg);font-weight:700;font-size:var(--fs-sm);white-space:nowrap}

/* ---------- what sport a competition is --------------------------------- */
/* One mark and one name, wherever a competition is named: a card in the public
   directory, the breadcrumb on every page inside an event, the hero's eyebrow,
   the printed QR poster, and the console's own rows and settings.
   assets/js/discipline.js owns the list and emits all of these — this is only
   how they sit. Every mark is stroke-only and inherits its colour, so the same
   markup takes the accent on a card, white in a hero and ink on paper without
   a second rule anywhere. */
.disc-ico{flex:none;display:block;vertical-align:-.18em}
.disc-tag{display:inline-flex;align-items:center;gap:6px;min-width:0;white-space:nowrap}
.disc-name{overflow:hidden;text-overflow:ellipsis}
/* On a card and in the console's rows it is a chip beside the status badges,
   sized to match .badge in .ev-head so it cannot make that row taller. */
.disc-chip{
  flex:none;padding:3px 9px 3px 7px;border-radius:var(--radius-pill);
  background:var(--chip-bg);border:1px solid var(--line);
  color:var(--accent-ink);font-weight:700;font-size:var(--fs-xs);line-height:1.35;
}
/* A phone shows the mark alone. The words are HIDDEN, not dropped: the chip's
   title and the icon's accessible name still say the sport, so nothing is left
   depending on a reader recognising a 15-pixel drawing. */
@media (max-width:640px){
  .disc-chip{padding:3px 6px}
  .disc-chip .disc-name{display:none}
}
/* Top bar, between the separator and the event's name: "LANDING / ✈ Blanik Kup". */
.brand-disc{display:inline-flex;align-items:center;flex:none;color:var(--accent-ink)}
.brand-disc[hidden]{display:none}
/* The console's picker, showing the mark it is choosing. */
.disc-pick{display:flex;align-items:center;gap:12px}
.disc-pick select{flex:1;min-width:0}
.disc-mark{
  flex:none;display:inline-flex;align-items:center;justify-content:center;
  width:46px;height:46px;border-radius:var(--radius-xs);
  background:var(--chip-bg);border:1px solid var(--line);color:var(--accent-ink);
}
/* The printed poster: the mark leads the competition's name. */
.poster-disc{display:inline-block;vertical-align:-.22em;margin-right:8px;color:var(--accent-ink)}

/* ---------- documents and the gallery ----------------------------------- */
/* One set of rules for both surfaces: the public event page draws these bare,
   the console draws the same markup with controls inside it. Nothing here is
   console-only except .filedrop and the two .*-acts groups. */

/* ---- a document, as a row ---- */
.file-list{display:grid;gap:8px}
/* The shell exists so the console's buttons can sit BESIDE the download link
   rather than inside it: a button nested in an anchor is not a thing a
   browser, a screen reader or a middle-click agree about. */
.file-item{
  display:flex;align-items:center;gap:8px;
  border:1px solid var(--line);border-radius:var(--radius-sm);background:var(--surface-2);
  padding-right:8px;transition:border-color .14s,background .14s;
}
.file-item:hover{border-color:var(--line-strong);background:var(--hover-bg)}
.file-row{
  flex:1;min-width:0;display:flex;align-items:center;gap:12px;
  padding:12px 4px 12px 13px;color:inherit;text-decoration:none;
}
.file-row:hover{text-decoration:none}
.file-item:focus-within{border-color:var(--accent)}
.file-mark{flex:none;display:flex;color:var(--accent-ink)}
.file-main{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
.file-name{font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.file-meta{
  font-size:var(--fs-xs);color:var(--muted);
  letter-spacing:.04em;text-transform:uppercase;font-variant-numeric:tabular-nums;
}
.file-go{flex:none;display:flex;align-items:center;color:#40546e;transition:color .14s,transform .14s}
.file-go svg{width:17px;height:17px}
.file-item:hover .file-go{color:var(--accent-ink);transform:translateY(1px)}
.file-acts{flex:none;display:flex;align-items:center;gap:4px}
/* Half-size sibling of .icon-btn, for controls that live inside a row rather
   than in a bar. Same shape, same states — only smaller. */
.icon-btn-sm{width:30px;height:30px}
.icon-btn-sm svg{width:15px;height:15px}
.icon-btn[disabled]{opacity:.35;cursor:default}
.icon-btn[disabled]:hover{background:var(--card);color:var(--muted)}

/* ---------- waiting ------------------------------------------------------ */
/* One answer to "is it me, or is it stuck", in three places at once: a bar
   along the top of the window (busy.js), the pointer, and the button that was
   pressed. See the head of assets/js/busy.js for why it counts fetches and why
   it waits before showing anything at all. */

/* Above the picker's menu (200), the modals (50) and the top bar (45): if the
   page is waiting, that is the most important thing on it. */
.busybar{
  position:fixed;top:0;left:0;right:0;height:3px;z-index:300;
  pointer-events:none;opacity:0;transition:opacity .22s ease;
}
.busybar.on{opacity:1}
.busybar-fill{
  display:block;height:100%;width:0;
  background:linear-gradient(90deg,var(--accent-deep),var(--accent));
  box-shadow:0 0 10px var(--accent);
  /* Slower than the 220ms creep step, so the fill glides between steps instead
     of ticking from one width to the next. */
  transition:width .3s cubic-bezier(.25,.8,.3,1);
}
@media (prefers-reduced-motion:reduce){ .busybar-fill{transition-duration:.6s} }
/* A printed page is never waiting for anything. */
@media print{ .busybar{display:none} }

/* The pointer says it too, and only once the bar has decided the wait is real —
   busy.js stamps the attribute at the same moment it draws. `progress` rather
   than `wait`: the page is still usable, it is just working. */
:root[data-busy], :root[data-busy] a, :root[data-busy] button,
:root[data-busy] input, :root[data-busy] select, :root[data-busy] .pick-btn{
  cursor:progress;
}

/* The button that started it. withBusy() has always disabled these — it is what
   stops a double-click submitting twice — but until now that was invisible, so
   a slow save looked like a button that had not registered the press. The label
   stays put and the spinner joins it: .btn is a flex row with a gap, so the
   pseudo-element lands beside the text with the spacing already right. */
.btn.busy,.icon-btn.busy{pointer-events:none}
.btn.busy::after{
  content:"";width:14px;height:14px;flex:none;
  border:2px solid currentColor;border-right-color:transparent;border-radius:50%;
  animation:busy-spin .7s linear infinite;
}
/* An icon button has no label to keep, so the spinner takes the icon's place
   rather than crowding in beside it in 34 pixels. */
.icon-btn.busy svg{display:none}
.icon-btn.busy::after{
  content:"";width:15px;height:15px;
  border:2px solid currentColor;border-right-color:transparent;border-radius:50%;
  animation:busy-spin .7s linear infinite;
}
@keyframes busy-spin{to{transform:rotate(360deg)}}
/* Still turning, because a stopped spinner reads as a hung page — just slowly
   enough not to be the thing you are looking at. */
@media (prefers-reduced-motion:reduce){
  .btn.busy::after,.icon-btn.busy::after{animation-duration:2.4s}
}
/* Disabled normally means "you may not"; here it means "not yet". The busy
   button keeps its own colours so it still reads as the button you pressed. */
.btn.busy:disabled,.icon-btn.busy:disabled{opacity:1}
/* A file kept off the public pages is dimmed but never hidden from the
   organizer: "why is nobody downloading the rules" has to be answerable on
   the screen that could answer it. */
.file-item.is-hidden,.gal-cell.is-hidden{opacity:.62}
.file-hidden{flex:none;font-size:var(--fs-2xs);padding:2px 8px}

/* ---- what a duplicate carries over ---- */
.clone-head{display:flex;align-items:baseline;justify-content:space-between;gap:10px}
/* The section label is a grid child elsewhere on the page; here it is a flex
   one, and its grid-column would stretch it across the whole head. */
.clone-head .section-label{grid-column:auto;margin:14px 0 6px}
.clone-opts{display:grid;gap:2px}
.clone-opt{
  display:flex;align-items:flex-start;gap:10px;cursor:pointer;
  padding:8px 10px;border-radius:var(--radius-sm);border:1px solid transparent;
}
.clone-opt:hover{background:var(--hover-bg)}
.clone-opt input{width:17px;height:17px;margin:2px 0 0;flex:none;cursor:pointer;accent-color:var(--accent)}
.clone-main{display:flex;flex-direction:column;gap:1px;min-width:0}
.clone-name{font-weight:700;font-size:var(--fs-sm)}
.clone-hint{font-size:var(--fs-xs);font-weight:400;color:var(--muted);line-height:1.4}
/* Results and start numbers belong to the pilots above them, and are indented
   to say so rather than explained to say so. */
.clone-opt.is-sub{margin-left:26px}
/* Off because what it depends on is off — not disabled by an error, so it is
   dimmed rather than struck out, and it comes back when its parent does. */
.clone-opt.is-off{opacity:.45}
.clone-opt.is-off:hover{background:none}

/* ---- upload progress ---- */
/* --accent, not --ok: this is work in progress on THIS competition, and the
   accent is the competition's own colour. Green would say "finished" while it
   is still going. */
.upl{margin:0 0 12px}
.upl-head{display:flex;align-items:baseline;justify-content:space-between;gap:10px;margin:0 0 6px}
.upl-what{
  font-size:var(--fs-sm);color:var(--muted);min-width:0;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.upl-pct{flex:none;font-size:var(--fs-sm);font-weight:700;font-variant-numeric:tabular-nums}
.upl-track{
  height:6px;border-radius:99px;overflow:hidden;
  background:var(--surface-2);border:1px solid var(--line);
}
.upl-bar{height:100%;width:0;border-radius:99px;background:var(--accent);transition:width .18s ease-out}
/* Indeterminate: bytes are moving but none have been reported yet. A sliver
   that travels says "working"; a track at zero says "stuck". */
.upl-track.is-waiting .upl-bar{width:32%;animation:upl-slide 1.15s ease-in-out infinite}
@keyframes upl-slide{
  0%{transform:translateX(-110%)}
  100%{transform:translateX(320%)}
}
@media (prefers-reduced-motion:reduce){
  .upl-bar{transition:none}
  /* No travelling sliver, but not a dead bar either: a still band at the
     start of the track, which is true — some of it is under way. */
  .upl-track.is-waiting .upl-bar{animation:none;width:12%}
}

/* ---- choosing several at once ---- */
/* The bar over each list. It is a row of the list's own width so the tick-all
   box lines up under nothing and over the first file's tick box. */
.file-bulk{display:flex;align-items:center;gap:10px;margin:0 0 8px;min-height:34px}
.file-bulk > .file-pick{padding-left:12px}   /* = the row's 1px border + 11px */
.bulk-label{flex:1;min-width:0;font-size:var(--fs-sm);color:var(--muted)}
.file-pick{flex:none;display:flex;align-items:center;justify-content:center;cursor:pointer;padding:0 2px}
.file-pick input{width:17px;height:17px;margin:0;cursor:pointer;accent-color:var(--accent)}
.file-item > .file-pick{padding-left:11px}
.file-item.is-picked{border-color:var(--accent);background:var(--hover-bg)}
/* In the gallery the box rides ON the picture, so it needs its own ground to
   stay visible over a bright photograph — the same plate the row buttons use. */
.gal-cell > .file-pick{
  position:absolute;top:8px;left:8px;z-index:2;width:26px;height:26px;
  border-radius:6px;padding:0;
  background:rgba(8,17,32,.82);border:1px solid rgba(255,255,255,.2);
}
.gal-cell.is-picked{border-color:var(--accent);box-shadow:inset 0 0 0 2px var(--accent)}

/* ---- the drop target ---- */
/* It is the empty state as well: an organizer with nothing uploaded sees the
   thing to drop onto, not a sentence about emptiness. */
.filedrop{
  display:flex;align-items:center;justify-content:center;gap:10px;
  margin:0 0 12px;padding:20px 16px;cursor:pointer;
  border:1.5px dashed var(--line-strong);border-radius:var(--radius-sm);
  background:var(--surface-2);color:var(--muted);font-size:var(--fs-sm);text-align:center;
  transition:border-color .14s,background .14s,color .14s;
}
.filedrop svg{width:22px;height:22px;flex:none}
.filedrop:hover{border-color:var(--accent);color:var(--text)}
.filedrop.is-over{border-color:var(--accent);background:var(--hover-bg);color:var(--text)}
.filedrop.is-busy{opacity:.55;pointer-events:none}

/* ---- the gallery ---- */
/* auto-FILL, not auto-fit, unlike the event grid: a gallery of three photos
   should keep them at thumbnail size on a wide screen, not stretch each one
   across a third of the page. */
.gal-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(190px,100%),1fr));gap:10px}
/* One ratio for every cell, not each photograph's own: a grid whose rows are
   as tall as their tallest picture leaves the landscape shots beside a
   portrait one floating in a band of empty card. The pictures are cropped to
   fit (object-fit:cover) and shown whole in the lightbox. */
.gal-cell{
  position:relative;margin:0;overflow:hidden;aspect-ratio:4 / 3;
  border:1px solid var(--line);border-radius:var(--radius-sm);background:var(--surface-2);
}
.gal-open{
  display:block;width:100%;height:100%;padding:0;border:0;background:none;cursor:zoom-in;
}
.gal-open img{width:100%;height:100%;object-fit:cover;display:block}
.gal-cell:hover{border-color:var(--line-strong)}
.gal-cell:focus-within{border-color:var(--accent)}
.gal-open:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
/* The caption rides over the foot of the picture, on a gradient rather than a
   bar: a solid strip crops the photograph, and a caption over bare image is
   unreadable on half of them. */
.gal-cell figcaption{
  position:absolute;left:0;right:0;bottom:0;padding:18px 10px 8px;
  background:linear-gradient(180deg,transparent,rgba(4,10,20,.82));
  color:#fff;font-size:var(--fs-xs);line-height:1.3;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;pointer-events:none;
}
.gal-cell .file-hidden{position:absolute;top:40px;left:8px}
.gal-acts{position:absolute;top:8px;right:8px;display:flex;gap:4px}
.gal-acts .icon-btn{background:rgba(8,17,32,.82);border-color:rgba(255,255,255,.2);color:#dbe7f5}
.gal-acts .icon-btn:hover{background:rgba(8,17,32,.95);color:#fff}
@media (max-width:640px){
  .gal-grid{grid-template-columns:repeat(auto-fill,minmax(min(140px,100%),1fr));gap:8px}
}

/* ---- the lightbox's own additions ---- */
.lightbox-n{color:#cfe0f2;font-size:var(--fs-sm);font-variant-numeric:tabular-nums;opacity:.8}
.lightbox-cap{margin:10px auto 0;max-width:80ch;text-align:center;color:#e8eff8;font-size:var(--fs-sm)}
.lightbox-nav{display:flex;justify-content:center;gap:12px;margin-top:12px}
.lightbox-nav .icon-btn{background:rgba(8,17,32,.7);border-color:rgba(255,255,255,.22);color:#dbe7f5}
.lightbox-nav .icon-btn:hover{background:rgba(8,17,32,.92);color:#fff}


/* overview / dashboard cards */
.card-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:12px}
.card-head-btns{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
/* A part of the entry form, as a row you can arrange: the heading an entrant
   will read, with the buttons that move and rename it. Heavier than the field
   rows under it, because it is what they are grouped by. */
.ro-sec{
  display:flex;align-items:center;gap:10px;flex-wrap:wrap;
  margin:16px 0 6px;padding:11px 12px;border-radius:var(--radius-sm);
  /* DARKER than the rows it groups, and deliberately so: --surface-2 is what
     the field rows are painted in, and a heading in the same tone as its own
     contents groups nothing. --chip-bg is the platform tone for "this is the
     furniture, not the content" and it is a step down in both themes. */
  background:var(--chip-bg);border:1px solid var(--line-strong);
}
.ro-sec:first-child{margin-top:2px}
.ro-sec-name{
  flex:1 1 auto;min-width:0;font-size:var(--caps-size);text-transform:uppercase;
  letter-spacing:var(--caps-track);color:var(--text);font-weight:var(--caps-weight);
}
.ro-sec-tag{flex:none;font-size:var(--fs-2xs);color:var(--muted);font-weight:700;text-transform:uppercase;letter-spacing:var(--caps-track)}
.ro-sec .rowbtns{flex:none}
/* The part's own switch: show it or hide it, drawn as the control the field
   rows carry so that a part's answer and a field's answer sit in one column all
   the way down. It is pushed right, before the arrange buttons. */
.ro-sec .seg3-all{flex:none;margin-left:auto}
/* A part that is switched off stays exactly where it is — this list is where it
   gets switched back on — but its heading and everything it governs go quiet,
   so the list reads as what the entry form will actually ask. The fields keep
   their own answers and stay editable; they simply do not apply right now. */
.ro-sec.ro-sec-off .ro-sec-name{color:var(--muted)}
.ro-row.is-quiet{opacity:.55}
@media (max-width:640px){
  .ro-sec-name{flex:1 0 100%}
  .ro-sec .seg3-all{margin-left:0}
}
.card-head h2{margin:0}
.mini-row{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:11px 0;border-bottom:1px solid var(--line)}
.mini-row:last-child{border-bottom:0}
.mini-row .who b{font-size:var(--fs-md)}
.mini-rank{display:inline-flex;align-items:center;justify-content:center;min-width:26px;height:26px;border-radius:var(--radius-xs);background:var(--chip-bg);font-weight:800;color:var(--accent-ink);margin-right:6px;font-family:var(--font-display)}
.mini-row .score{font-weight:800;font-family:var(--font-display);font-variant-numeric:tabular-nums}

/* ---- the days a competition runs ----------------------------------------
   Two lists of the same thing: .sched is the read-only one on both
   Information pages, .sched-edit the arrangement of it in Settings. A day
   that is FLOWN carries the accent down its left edge — on a five-day
   meeting with two competition days, that edge is the whole answer, readable
   before a single word of it has been.
   -------------------------------------------------------------------------- */
.sched{list-style:none;margin:0;padding:0}
.sched-day{
  display:flex;align-items:center;gap:14px;
  padding:11px 0 11px 14px;border-bottom:1px solid var(--line);
  border-left:3px solid transparent;
}
.sched-day:last-child{border-bottom:0}
.sched-day.is-comp{border-left-color:var(--accent)}
.sched-day.is-today{background:var(--hover-bg)}
/* the weekday over the date, in the tear-off shape the event cards use */
.sd-when{
  flex:none;display:flex;flex-direction:column;gap:1px;min-width:104px;
  font-variant-numeric:tabular-nums;
}
.sd-when b{
  font-size:var(--fs-2xs);text-transform:uppercase;letter-spacing:var(--caps-track);
  color:var(--muted);font-weight:800;
}
.sd-when span{font-size:var(--fs-md);font-weight:650}
/* what the day holds: one line per thing that happens on it, and a line that
   opens with a time is set out as a programme — the times in their own column
   so they read down the page. */
.sd-what{flex:1 1 auto;min-width:0;font-size:var(--fs-md);display:flex;flex-direction:column;gap:4px}
.sd-l{display:block;overflow-wrap:anywhere}
.sd-l.is-at{display:flex;gap:10px;align-items:baseline;font-size:var(--fs-sm);font-weight:500}
.sd-at{
  flex:none;min-width:62px;font-variant-numeric:tabular-nums;
  font-size:var(--fs-2xs);font-weight:800;letter-spacing:var(--caps-track);
  color:var(--muted);
}
.sd-l.is-at span{min-width:0;overflow-wrap:anywhere}
/* a day with a programme in it starts at the top of its date, not halfway down */
.sched-day.is-multi{align-items:flex-start}
.sched-day.is-comp .sd-what{font-weight:700}
.sched-day.is-comp .sd-l.is-at{font-weight:600}
.sd-today{
  flex:none;font-size:var(--fs-2xs);font-weight:800;text-transform:uppercase;
  letter-spacing:var(--caps-track);padding:3px 9px;border-radius:999px;
  background:var(--accent-soft);color:var(--accent-ink);
}
/* a day that is not flown is quieter, not hidden — it is still the event */
.sched-day:not(.is-comp) .sd-what{color:var(--muted)}

/* ---- and the same list, arranged ---- */
.sched-sum{display:flex;flex-wrap:wrap;align-items:center;gap:8px 12px;margin:14px 0 2px}
.sched-sum .ss-n{
  font-size:var(--fs-2xs);font-weight:800;text-transform:uppercase;
  letter-spacing:var(--caps-track);padding:5px 11px;border-radius:999px;
  background:var(--chip-bg);color:var(--muted);
}
.sched-sum .ss-comp{background:var(--accent-soft);color:var(--accent-ink)}
.sched-sum .ss-warn{
  flex:1 1 100%;font-size:var(--fs-sm);font-weight:600;line-height:1.45;
  padding:9px 12px;border-radius:var(--radius-sm);
  background:var(--warn-bg);border:1px solid var(--warn-line);color:var(--warn);
}
.sched-sum .ss-warn code{font-family:var(--font-mono);font-size:var(--fs-xs);word-break:break-all}
.sched-edit{margin-top:10px}
.sched-row{
  display:flex;align-items:flex-start;gap:10px;
  padding:8px 0 8px 14px;border-bottom:1px solid var(--line);
  border-left:3px solid transparent;
}
.sched-row .sd-when{padding-top:7px}
.sched-row:last-child{border-bottom:0}
.sched-row.is-comp{border-left-color:var(--accent)}
.sched-row .sd-kind{flex:0 1 250px;min-width:0}
/* the box the programme is typed into: as tall as one line to begin with, and
   it grows with what is written (schedGrow() in admin.js) rather than making
   the organizer drag a corner to see what they typed. */
.sched-row .sd-label{
  flex:1 1 240px;min-width:0;min-height:0;height:auto;
  padding:11px 12px;font-size:var(--fs-md);line-height:1.5;overflow:auto;
}
@media (max-width:640px){
  .sched-row{flex-wrap:wrap;gap:8px}
  .sched-row .sd-when{min-width:100%}
  .sched-row .sd-kind,.sched-row .sd-label{flex:1 1 100%}
  .sd-when{min-width:92px}
}

/* ---- the countries an event draws from ---- */
.fav-list{max-height:46vh;overflow:auto;border:1px solid var(--line);border-radius:var(--radius-sm);background:var(--surface-2)}
.fav-row{
  display:flex;align-items:center;gap:10px;padding:7px 12px;cursor:pointer;
  border-bottom:1px solid var(--line);
}
.fav-row:last-child{border-bottom:0}
.fav-row:hover{background:var(--hover-bg)}
.fav-row input{width:16px;height:16px;margin:0;flex:none;accent-color:var(--accent)}
.fav-code{
  flex:none;width:2.4em;font-size:var(--fs-2xs);font-weight:800;
  letter-spacing:.06em;color:var(--muted);font-variant-numeric:tabular-nums;
}
.fav-name{flex:1 1 auto;min-width:0;font-size:var(--fs-sm);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.fav-row .rowbtns{flex:none}
/* The chosen band is the thing being built, so it is marked as one rather than
   just happening to be at the top. */
.fav-band{background:var(--chip-bg);border-bottom:2px solid var(--accent)}
.fav-band .fav-row{background:none}
.fav-row.is-on .fav-name{font-weight:700}

/* ---- the toolbar over a list ---------------------------------------------
   Search, the filters and the buttons that act on the whole list. It used to
   be five stacked bands — search, a caps label, its chips, another label, its
   chips, then a row of buttons — which on a laptop pushed the list itself
   below the fold on the two pages people spend the most time on.
   Now: ONE line for search + actions + the view switch, and ONE line for the
   filters — every group on it, each label beside its own chips rather than
   over them. The filters wrap only when the chips genuinely outrun the
   column, which on a 960px public page they sometimes do.

   Same markup on every page that has these, public and console alike, so the
   registrations page and the standings page are laid out by the same rules
   rather than by two similar-looking sets. */
.toolbar{display:flex;flex-direction:column;gap:10px}
.toolbar-main{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.toolbar-main .search-box{flex:1 1 240px;min-width:0;margin:0}
/* The buttons keep to the right of the search on a wide screen and drop under
   it on a narrow one, rather than each taking a line of its own. */
.toolbar-actions{display:flex;align-items:center;gap:8px;flex:0 0 auto;flex-wrap:wrap;margin:0}
/* The filters share ONE line wherever they fit. "Championship" and "Class"
   are two short questions about the same list, not two bands of chrome, and a
   line each pushed the board itself down the page. They wrap to the next line
   only when the chips genuinely run out of room. The wide column gap is what
   separates one question from the next — its label is the marker. */
.toolbar-filters{display:flex;flex-wrap:wrap;align-items:center;gap:10px 26px}
.toolbar-filters .filter-group{flex:0 1 auto}

.chips{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:0}
/* Kept: pages not yet moved to .toolbar still use it, and it behaves the same. */
.toolbar-row{display:flex;align-items:flex-start;justify-content:space-between;gap:10px 16px;flex-wrap:wrap}
.toolbar-row .filter-group{flex:1 1 auto;min-width:0}
.toolbar-row .viewtoggle{flex:none}
/* The label sits BESIDE its chips. Above them it cost a whole line each, and
   said less: "Status" over a row of statuses is not information. */
.filter-group{display:flex;align-items:center;gap:10px;flex-wrap:wrap;min-width:0}
.filter-label{
  flex:none;margin:0;font-size:var(--fs-sm);text-transform:uppercase;
  letter-spacing:var(--caps-track);color:var(--muted);font-weight:var(--caps-weight);
}
/* Sized to read as a control rather than a caption: one notch under .btn,
   which is the button standing beside it on the same toolbar. */
.chip{border:1px solid var(--line-strong);background:var(--card);border-radius:var(--radius-pill);padding:9px 16px;font-weight:700;font-size:var(--fs-md);cursor:pointer;color:var(--text)}
.chip.active{background:var(--text);color:var(--bg);border-color:var(--text)}
.chip .n{opacity:.75;margin-left:6px;font-variant-numeric:tabular-nums}
.chip.active .n{opacity:.85}
.badge.st-pending{background:var(--warn-bg);color:var(--warn)}
.badge.st-confirmed{background:var(--ok-bg);color:var(--ok)}
.badge.st-rejected{background:var(--err-bg);color:var(--err)}

/* admin: small buttons, modal (the sub-tab strip itself is styled further down) */
.btn-sm{padding:8px 11px;font-size:var(--fs-sm);border-radius:10px}
.btn-ok{background:var(--ok);border-color:var(--ok);color:#04240f}
.btn-ok:hover{filter:brightness(1.07)}
.btn-danger{background:var(--err-bg);border-color:var(--err-line);color:var(--err)}
.btn-danger:hover{
  background:rgba(255,110,97,.22);                          /* fallback for the line below */
  background:color-mix(in srgb,var(--err) 22%,transparent);
}
/* Saved. Slides down from under the top bar, holds, and goes. Fixed rather
   than in the flow: it has to be the same thing in the same place whichever
   page saved, and it must not move the page it is confirming. --topbar-h is
   measured by admin.js (syncTopbar), because the bar wraps on a narrow
   screen. */
.toast{
  position:fixed;z-index:60;
  top:calc(var(--topbar-h, 58px) + 14px);left:50%;
  display:flex;align-items:center;gap:11px;
  max-width:calc(100vw - 28px);
  padding:13px 22px;border-radius:var(--radius-pill);
  border:1px solid var(--ok-line);background:var(--ok-bg);color:var(--ok);
  font-family:var(--font-display);font-weight:800;font-size:var(--fs-lg);
  box-shadow:var(--shadow);
  -webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);
  cursor:pointer;                                   /* click it away early */
  transform:translate(-50%,-190%);opacity:0;
  transition:transform .3s cubic-bezier(.2,.85,.3,1),opacity .3s ease;
}
.toast.show{transform:translate(-50%,0);opacity:1}
.toast-ico{width:20px;height:20px;flex:none;display:block}
/* Which glyph is drawn follows the strip's KIND, in CSS: a tick for a save, an
   "i" for anything else. Not in script — .hidden does nothing to an <svg>,
   which is an SVGElement and has no such property. */
.toast .ic-info{display:none}
.toast-info .ic-ok,.toast-warn .ic-ok,.toast-err .ic-ok{display:none}
.toast-info .ic-info,.toast-warn .ic-info,.toast-err .ic-info{display:block}
/* A save says one word; "there is no start list yet" is a sentence, and a
   sentence has to be allowed to reach its full stop. */
.toast-txt{min-width:0;overflow-wrap:anywhere}
.toast-info .toast-txt{font-size:var(--fs-md);font-weight:700;line-height:1.35}
/* the same strip, for the things that are not a save */
.toast-warn{border-color:var(--warn-line);background:var(--warn-bg);color:var(--warn)}
/* nothing happened, and that is not a failure — the calm one of the three */
.toast-info{border-color:var(--line-strong);background:var(--surface-2);color:var(--text)}
.toast-err{border-color:var(--err-line);background:var(--err-bg);color:var(--err)}
@media (prefers-reduced-motion:reduce){
  /* no slide — but it still has to stay centred, which is the same transform */
  .toast{transform:translate(-50%,0);transition:opacity .2s ease}
}
@media print{.toast{display:none}}

/* The last result entered. Sticky to the top of the competition pane: the
   list is long and the strip has to still be there when the next pilot's row
   is the one on screen. Coloured as a confirmation, not a warning — nothing
   is wrong, something was recorded. */
.flash{
  position:sticky;top:calc(var(--topbar-h, 58px) + 10px);z-index:7;
  display:flex;align-items:center;gap:12px;
  margin:0 0 14px;padding:12px 16px;border-radius:var(--radius-md);
  border:1px solid var(--ok-line);background:var(--ok-bg);color:var(--text);
  box-shadow:var(--shadow);
  -webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);
}
.flash-txt{flex:1 1 auto;min-width:0;font-size:var(--fs-md)}
.flash-what{color:var(--ok);font-weight:700}
.flash-btns{flex:none;display:flex;gap:8px}
/* the clock: a ring that fills over the ten seconds the undo is offered */
.flash-ring{flex:none;width:24px;height:24px}
.flash-ring svg{width:24px;height:24px;display:block;transform:rotate(-90deg)}
.flash-ring circle{fill:none;stroke-width:2.5}
.flash-ring circle.track{stroke:var(--ok-line)}
.flash-ring circle.fill{stroke:var(--ok);stroke-dasharray:56.5;stroke-dashoffset:56.5}
.flash-ring.go circle.fill{animation:flash-fill 10s linear forwards}
@keyframes flash-fill{from{stroke-dashoffset:56.5}to{stroke-dashoffset:0}}
@media (prefers-reduced-motion:reduce){
  .flash-ring.go circle.fill{animation:none;stroke-dashoffset:28}
}
@media (max-width:560px){
  .flash{flex-wrap:wrap}
  .flash-txt{flex:1 1 100%;order:2}
}

/* "Not saved yet." Sticky to the bottom of the settings panel, because on a
   pane the length of Registration the control you changed and the button that
   saves it are rarely on screen together. Thin and warn-coloured: it is
   stating a fact about the form, not reporting an error. */
.dirtyline{
  /* --topbar-h is measured and set by admin.js: the bar wraps on a narrow
     screen, and a guessed offset would leave the line half under the glass. */
  position:sticky;top:calc(var(--topbar-h, 58px) + 10px);z-index:8;
  display:flex;align-items:center;gap:10px;
  margin:0 0 14px;padding:10px 16px;border-radius:var(--radius-pill);
  border:1px solid var(--warn-line);background:var(--warn-bg);color:var(--warn);
  font-size:var(--fs-sm);font-weight:700;
  box-shadow:var(--shadow);
  -webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);
}
/* the dot is the only ornament: it marks the line as a state, not a message */
.dirtyline::before{
  content:"";flex:none;width:8px;height:8px;border-radius:50%;
  background:var(--warn);
}
@media print{.dirtyline{display:none}}

/* A card whose contents cannot be undone. Outlined in the error colour
   rather than tinted: the fence has to read at a glance while scrolling past,
   but the card still has to be readable in both themes. */
.card-danger{
  border-color:var(--err-line);                             /* fallback for the line below */
  border-color:color-mix(in srgb,var(--err) 52%,transparent);
  box-shadow:inset 0 0 0 1px var(--err-bg),var(--shadow);
}
.card-danger h2{color:var(--err)}
/* The entry form is a column of parts, and an event arranges them: order is
   set on the cards by layoutSections(). Without a flex container the property
   would be inert and the arrangement would only exist in the console. */
#regForm{display:flex;flex-direction:column}
/* ...and so is the console's copy of it, for the same reason. Its cards are
   flex items ordered by layoutEditSections() and the space between them is
   the container's, so an .is-off card leaves no gap behind it.

   The console's entry form IS the public form, and it is a PAGE now rather
   than a dialog — so its cards are register.html's cards with nothing taken
   off them: the same glass, the same shadow, the same heading size. The
   `.modal .card` overrides that used to strip all three went with the dialog,
   and no modal on the platform carries a .card any more. */
#editForm{display:flex;flex-direction:column;gap:14px}
/* Save, and under it the way out: the public form's one big block button,
   with a second and quieter one beneath. .form-actions is order 999 on both
   forms, so it stays at the foot however the sections are arranged. */
#editForm .form-actions{display:flex;flex-direction:column;gap:10px}

/* One action per line: the button first, then what it does. The buttons form
   a column down the left so they can be read as a list of what this card can
   do, and the sentence beside each one is set at reading size rather than at
   hint size — it is the thing that stops the wrong button being pressed. */
.danger-list{display:flex;flex-direction:column}
.danger-row{
  display:flex;align-items:center;gap:18px;
  flex-wrap:wrap;padding:12px 0;border-top:1px solid var(--err-line);
}
.danger-row:first-child{border-top:0;padding-top:2px}
.danger-row:last-child{padding-bottom:2px}
.danger-what{flex:1 1 280px;min-width:0}
.danger-what .hint{display:block;font-size:var(--fs-md);line-height:1.45;color:var(--text)}
.danger-n{display:block;margin-top:4px;font-size:var(--fs-base);font-weight:700;color:var(--text)}
.danger-act{flex:none;display:flex;align-items:center;gap:10px}
.danger-act .btn{min-width:240px}
/* the card's opening line is the frame for all of them, so it stays quieter
   than the sentences beside the buttons — but not smaller than body text */
.card-danger > .hint{font-size:var(--fs-base);line-height:1.45}
@media (max-width:560px){
  .danger-act{width:100%}
  .danger-act .btn{flex:1 1 auto;min-width:0}
}

/* the word that the button alone cannot say */
.danger-tag{
  flex:none;padding:4px 10px;border-radius:var(--radius-pill);
  background:var(--err-bg);border:1px solid var(--err-line);color:var(--err);
  font-size:var(--caps-size);font-weight:var(--caps-weight);
  text-transform:uppercase;letter-spacing:var(--caps-track);
}

/* big square landing buttons (Valid / Fault) side by side */
/* Both pairs on the run list are this grid — the stamps on one side, the
   landing buttons on the other — so a floor as well as a ceiling keeps the two
   halves of the line the same width as each other and as the result button
   that eventually replaces one of them. */
.land-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;min-width:200px;max-width:230px}
/* The flight cell stacks one ROW PER FLIGHT instead, because the row is two
   squares wide until the flight has both times and three once it does — see
   timesCell(). Everything already written against .land-grid (the width floor
   that lines this band up with the score band facing it) still applies. */
.land-grid.is-rows{display:flex;flex-direction:column}
.land-row{display:grid;grid-template-columns:1fr 1fr;gap:8px}
.land-row + .land-row{margin-top:8px}
.land-row.has-dur{grid-template-columns:1fr 1fr 1fr}
/* Three across the same 230px is a narrower square, so the clock inside it
   steps down rather than wrapping mid-number. */
.land-row.has-dur .land-sq{padding:8px 4px}
.land-row.has-dur .land-sq.stamped b,.land-row.has-dur .land-sq.is-dur b{font-size:var(--fs-md)}
/* Arithmetic, not a record: the quieter of the three, and dashed like every
   other box on this platform that is derived rather than entered. */
.land-sq.is-dur{border-style:dashed;font-variant-numeric:tabular-nums}
.land-sq{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;min-height:78px;border-radius:var(--radius-md);border:1px solid var(--line-strong);background:var(--card);cursor:pointer;font-weight:800;font-size:var(--fs-sm);color:var(--text);padding:8px;line-height:1.15;text-align:center}
.land-sq svg{width:26px;height:26px}
.land-sq.good{background:var(--ok-bg);border-color:var(--ok-line);color:var(--ok)}
.land-sq.good:hover{
  background:rgba(64,205,138,.22);                          /* fallback for the line below */
  background:color-mix(in srgb,var(--ok) 22%,transparent);
}
.land-sq.fault{background:var(--err-bg);border-color:var(--err-line);color:var(--err)}
.land-sq.fault:hover{
  background:rgba(255,110,97,.22);                          /* fallback for the line below */
  background:color-mix(in srgb,var(--err) 22%,transparent);
}
/* ---- the run list's three states -----------------------------------------
   A competitor on the Competition tab is in one of exactly three states, and
   the screen has to say which at a glance: FLOWN (a result is in), UP NEXT
   (the first without one — the person to act on), and WAITING.

   Each is a rail down the left edge and a tint over the surface, and both come
   from TOKENS written once per theme (--run-* in the two :root blocks). That
   is the whole point of them. This block used to mix --ok and --warn into the
   ground with ONE formula for both decks, and one formula cannot serve both:
   in daylight --warn is a near-black ochre because it has to be readable as
   text, and nine per cent of it over white is beige; on the night deck any
   low-alpha warm colour pulls all three channels towards the middle and lands
   on grey. So the row the entire screen exists to point at — UP NEXT — came
   out a smudge in both themes. Written per theme they are what they should
   have been all along: a clean cream and a clean amber.

   The three are deliberately NOT weighted equally. UP NEXT is the loudest
   thing on the table, because it is the only row anybody is about to touch,
   and it is the only one that closes itself off top and bottom. FLOWN is the
   quietest: it is a record, and eight of them down a column must not shout
   over the one that is live. WAITING is the plain surface — a row with
   nothing to say about it says nothing.

   Green for flown and amber for next, never --accent: an event brands
   --accent (Blanik's is red), and whether a pilot has flown yet is not part
   of anyone's branding. The same argument the live badge makes further down.*/
body:not(.view-list) tr.row-done > td{background:var(--run-done-bg)}
body:not(.view-list) tr.row-next > td{background:var(--run-next-bg)}
/* The hairlines above and below the live row. A rail alone says where a row
   BEGINS; across a dozen columns the eye also needs to see where it ends, and
   this is what lets "up next" be found without a louder fill. They are inset
   shadows rather than borders because the table is border-collapse: a border
   on this row's top edge loses the collapse to the row above it, every time.
   The first cell carries the rail in the same shadow list, since one element
   gets one box-shadow property and the rail must not be dropped for them. */
body:not(.view-list) tr.row-next > td{
  box-shadow:inset 0 1px 0 var(--run-next-edge),inset 0 -1px 0 var(--run-next-edge);
}
body:not(.view-list) tr.row-done > td:first-child{box-shadow:inset 3px 0 0 var(--run-done-rail)}
body:not(.view-list) tr.row-next > td:first-child{
  box-shadow:inset 3px 0 0 var(--run-next-rail),
             inset 0 1px 0 var(--run-next-edge),inset 0 -1px 0 var(--run-next-edge);
}
body:not(.view-list) tr.row-wait > td:first-child{box-shadow:inset 3px 0 0 var(--run-wait-rail)}

/* card view: one rail, one surface, no stacking. The tint arrives as an image
   layer rather than a background COLOUR, because the card's own --surface-2 is
   underneath and has to stay there — a translucent colour would replace it and
   leave the row see-through to the card behind. */
body.view-list table.comp-run tbody tr{
  box-shadow:inset 3px 0 0 var(--run-wait-rail);            /* waiting, the default */
}
body.view-list table.comp-run tbody tr.row-done{
  background-image:linear-gradient(var(--run-done-bg),var(--run-done-bg));
  box-shadow:inset 3px 0 0 var(--run-done-rail);
}
body.view-list table.comp-run tbody tr.row-next{
  background-image:linear-gradient(var(--run-next-bg),var(--run-next-bg));
  box-shadow:inset 3px 0 0 var(--run-next-rail);
  border-color:var(--run-next-edge);
}
.res-flight{display:grid;grid-template-columns:1fr 1fr auto;gap:10px;align-items:end;margin-bottom:12px}
.res-flight .field{margin:0}
.res-flight .rm-fault{align-self:end;height:42px}

/* ---- the flight log ------------------------------------------------------
   A flight is two stamps and they are drawn as two squares the size of the
   landing buttons facing them across the card, in the same .land-grid. An
   unstamped square is the ACTION; a stamped one shows the time and opens the
   popup. Tabular figures throughout, so a column of times lines up under
   itself, and --muted for the second line because "Take-off" and the duration
   are what the number IS, not something to read. */
.log-cell{display:flex;align-items:center;gap:8px}
.log-cell .land-grid{flex:1 1 auto}
.log-cell .log-edit{flex:none}
/* the two states of a stamp square (the box itself is .land-sq, above) */
.land-sq.stamp.go{background:var(--surface-2)}
.land-sq.stamp.go:hover{border-color:var(--accent);background:var(--hover-bg)}
/* Land is the one button on this pane waiting on a glider in the air, so it is
   the one that looks like it is waiting. */
.land-sq.stamp.land{
  background:rgba(244,191,98,.12);                          /* fallback for the line below */
  background:color-mix(in srgb,var(--warn) 12%,var(--card));
  border-color:#5a4a2a;                                     /* fallback for the line below */
  border-color:color-mix(in srgb,var(--warn) 45%,var(--line));
  color:var(--warn);
}
.land-sq.stamp.land:hover{
  background:rgba(244,191,98,.22);                          /* fallback for the line below */
  background:color-mix(in srgb,var(--warn) 22%,var(--card));
}
/* A landing with no take-off yet: still pressable — it opens the popup, which
   is the only way to write down a flight nobody was standing by for. */
.land-sq.stamp.wait{background:var(--card);color:var(--muted);border-style:dashed}
.land-sq.stamp.wait:hover{color:var(--text);border-color:var(--line-strong)}
/* A square carrying a NUMBER rather than an invitation: a stamped time, or the
   duration between two of them. They look alike because they read alike; only
   .stamped means a time was actually recorded here, which is what the console
   and the tests count. */
.land-sq.stamped,.land-sq.is-dur{
  background:var(--surface-2);border-color:var(--line-strong);color:var(--text);gap:3px;
}
.land-sq.stamped:hover,.land-sq.is-dur:hover{border-color:var(--accent);background:var(--hover-bg)}
.land-sq.stamped b,.land-sq.is-dur b{
  font-family:var(--font-display);font-size:var(--fs-2xl);line-height:1;
  font-variant-numeric:tabular-nums;
}
.land-sq.stamped span,.land-sq.is-dur span{font-weight:600;font-size:var(--fs-xs);color:var(--muted)}
/* Airborne: the elapsed time ticks, so it is the one number that moves. */
.land-sq.stamped .sq-el{color:var(--warn);font-weight:800;font-variant-numeric:tabular-nums}

/* ---- the result, once there is one ---------------------------------------
   Replaces the two landing buttons in place and at the same size: the total in
   the size it deserves, and under it, small, how it was arrived at. A score
   you cannot check is a score somebody has to open a popup to trust. */
.result-btn{
  display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;
  /* Floor as well as ceiling: this button shares a line with the stamp squares
     and has to match them, and inside a flex cell a bare width:100% resolves
     against a container that is itself sizing to this button — which collapsed
     it to 26px wide and 169 tall in table view. */
  width:100%;min-width:200px;max-width:230px;min-height:78px;flex:none;align-self:center;
  padding:8px 12px;
  border:1px solid var(--line-strong);border-radius:var(--radius-md);
  background:var(--surface-2);color:var(--text);cursor:pointer;text-align:center;
}
.result-btn:hover{border-color:var(--accent);background:var(--hover-bg)}
.result-btn .rb-tot{
  font-family:var(--font-display);font-size:var(--fs-3xl);font-weight:800;line-height:1;
  font-variant-numeric:tabular-nums;
}
.result-btn .rb-sub{
  display:inline-flex;align-items:center;gap:5px;
  font-size:var(--fs-sm);font-weight:600;color:var(--muted);font-variant-numeric:tabular-nums;
}
.result-btn .rb-sub svg{width:15px;height:15px;flex:none}
.result-btn.fault{background:var(--err-bg);border-color:var(--err-line);color:var(--err)}
.result-btn.fault .rb-sub{color:var(--err)}

/* ---- who sat in the back, on the name line -------------------------------
   Two controls: the name, and the button that brings down the name from the
   pilot above — which is who it usually is, and on a busy grid is the button
   pressed twenty times. */
/* align-items:stretch, so the copy-down button is exactly as tall as the name
   beside it however tall that gets — the two are one control to the eye. */
.instr-cell{display:flex;align-items:stretch;gap:6px;min-width:0}
.instr-btn{
  display:inline-flex;align-items:center;gap:8px;cursor:pointer;min-width:0;
  border:1px solid var(--line-strong);border-radius:var(--radius-pill);
  background:var(--card);color:var(--text);padding:9px 15px;line-height:1.2;
  /* A finger-sized target: it is pressed once per pilot on a tablet held at
     the launch point, and it now carries a whole word rather than 'Instr.' */
  min-height:44px;
}
.instr-btn:hover{border-color:var(--accent);background:var(--hover-bg)}
.instr-btn .ib-k{
  flex:none;font-size:var(--caps-size);font-weight:var(--caps-weight);
  letter-spacing:var(--caps-track);text-transform:uppercase;color:var(--muted);
}
.instr-btn .ib-v{
  font-weight:700;font-size:var(--fs-sm);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.instr-btn.is-set{border-color:var(--line-strong);background:var(--surface-2)}
.instr-cell .icon-btn{align-self:stretch;width:44px;height:auto;flex:none}
.instr-cell .icon-btn[disabled]{opacity:.35;cursor:default}
.instr-cell .icon-btn[disabled]:hover{background:transparent;color:var(--muted)}
/* the modal: one block per flight, take-off over landing, then who was in the back */
.log-flight{border:1px solid var(--line);border-radius:var(--radius-md);padding:12px;margin-bottom:12px}
.log-flight > .lf-head{font-size:var(--caps-size);font-weight:var(--caps-weight);
  text-transform:uppercase;letter-spacing:var(--caps-track);color:var(--muted);margin-bottom:10px}
.log-line{display:grid;grid-template-columns:minmax(0,1.4fr) minmax(0,1fr) auto;gap:8px;align-items:end;margin-bottom:10px}
.log-line .field{margin:0}
.log-line .lf-now{align-self:end;height:42px;white-space:nowrap}
.log-sum{display:flex;justify-content:space-between;align-items:baseline;gap:10px;
  border-top:1px solid var(--line);padding-top:9px;margin-top:2px}
.log-sum .k{color:var(--muted);font-size:var(--fs-sm)}
.log-sum .v{font-family:var(--font-display);font-weight:800;font-variant-numeric:tabular-nums}
.log-sum .v.bad{color:var(--err)}
.lf-instr{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px;align-items:end;margin-top:10px}
.lf-instr .field{margin:0}
.lf-instr .lf-same{align-self:end;height:42px;white-space:nowrap}
@media (max-width:520px){
  .log-line{grid-template-columns:1fr auto}
  .log-line .field:first-child{grid-column:1 / -1}
}

/* ---- arranging the start order -------------------------------------------
   A list you pick up and put down. The handle is the only thing that starts a
   drag (touch-action:none, or a phone scrolls the modal instead of moving the
   row), and every move the handle can make has a button beside it too: an
   organizer wearing gloves on a winter airfield is not going to drag. */
.ordermodes{display:inline-flex;gap:0;border:1px solid var(--line-strong);border-radius:var(--radius-pill);overflow:hidden}
.ordermodes .btn{border:0;border-radius:0;background:transparent}
.ordermodes .btn.active{background:var(--accent-solid);color:#fff}
/* What swap mode is about to do, said before it does it. It sits between the
   mode buttons and the list, in place of the hint line — in swap mode the hint
   IS this, and it gets more useful as the two are chosen. */
.swapbar{
  display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;
  margin:8px 0 10px;padding:10px 12px;border-radius:var(--radius-sm);
  background:var(--chip-bg);border:1px solid var(--line-strong);
}
.swapbar-txt{flex:1 1 220px;min-width:0;font-size:var(--fs-base);color:var(--text)}
.swapbar-btns{flex:none;display:flex;align-items:center;gap:8px}
@media (max-width:520px){ .swapbar-btns{flex:1 0 100%} .swapbar-btns .btn{flex:1} }
.orderlist{max-height:min(52vh,460px);overflow:auto;-webkit-overflow-scrolling:touch;
  border:1px solid var(--line);border-radius:var(--radius-md);padding:6px}
.order-row{
  display:grid;grid-template-columns:auto auto minmax(0,1fr) auto;gap:10px;align-items:center;
  padding:9px 10px;border:1px solid var(--line);border-radius:var(--radius-md);
  background:var(--surface-2);margin-bottom:6px;
}
.order-row:last-child{margin-bottom:0}
.order-row .oh{
  display:flex;align-items:center;justify-content:center;cursor:grab;
  width:30px;height:34px;color:var(--muted);border-radius:8px;
  touch-action:none;                 /* the drag is ours, not the scroller's */
  border:1px solid transparent;background:transparent;
}
.order-row .oh:hover{color:var(--text);border-color:var(--line-strong)}
.order-row .oh svg{width:18px;height:18px;pointer-events:none}
.order-row .on-pos{
  width:62px;text-align:center;font-family:var(--font-display);font-weight:800;
  font-variant-numeric:tabular-nums;padding:7px 4px;
}
.order-row .on-who{min-width:0}
.order-row .on-who b{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.order-row .on-who span{display:block;color:var(--muted);font-size:var(--fs-sm);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.order-row .on-btns{display:flex;gap:4px}
.order-row .on-btns .icon-btn{width:34px;height:34px}
/* Moved: the row says where it came from, so the list can be checked against
   the one on the noticeboard before it is saved. */
.order-row.is-moved{
  border-color:#5a4a2a;                                     /* fallback for the line below */
  border-color:color-mix(in srgb,var(--warn) 45%,var(--line));
}
.order-row .on-was{color:var(--warn);font-size:var(--fs-sm);font-weight:700;white-space:nowrap}
/* Picked, in swap mode: the first of the two. */
.order-row.is-picked{
  border-color:var(--accent);
  background:var(--hover-bg);
  box-shadow:inset 3px 0 0 var(--accent);
}
.order-row.is-drag{
  opacity:.92;box-shadow:var(--shadow-lg);border-color:var(--accent);
  position:relative;z-index:2;cursor:grabbing;
}
.orderlist.is-swap .order-row{cursor:pointer}
.orderlist.is-swap .order-row .oh,
.orderlist.is-swap .order-row .on-pos,
.orderlist.is-swap .order-row .on-btns{opacity:.35;pointer-events:none}
/* A pilot with no start number yet — a late entry, or everybody at all, when
   the list is being written by hand for the first time. */
.order-row.is-new .on-pos{color:var(--muted)}
@media (max-width:520px){
  .order-row{grid-template-columns:auto auto minmax(0,1fr);row-gap:8px}
  .order-row .on-btns{grid-column:1 / -1;justify-content:flex-end}
}

/* ---- the officials roster ------------------------------------------------- */
.offlist{display:flex;flex-direction:column;gap:8px}
.off-row{display:grid;grid-template-columns:minmax(0,1.2fr) minmax(0,1fr) minmax(0,1fr) auto;
  gap:8px;align-items:center}
.off-row.off-one{grid-template-columns:minmax(0,1fr) auto}
/* A price is a WIDE label and a NARROW number, which is not the three even
   columns a referee row wants. Same row furniture, different proportions. */
.off-row.fee-row{grid-template-columns:minmax(0,1fr) minmax(0,170px) auto}
/* The currency rides inside the amount box rather than beside it, so a row
   of prices reads as prices and not as numbers with a code after them. */
.fee-amt{position:relative;display:block;min-width:0}
.fee-amt input{width:100%;padding-right:52px;text-align:right;font-variant-numeric:tabular-nums}
.fee-cur{
  position:absolute;right:11px;top:50%;transform:translateY(-50%);pointer-events:none;
  font-size:var(--fs-xs);font-weight:700;letter-spacing:.04em;color:var(--muted);
}
.off-row .icon-btn{width:38px;height:38px}
@media (max-width:720px){
  .off-row{grid-template-columns:minmax(0,1fr) auto}
  .off-row.fee-row{grid-template-columns:minmax(0,1fr) auto}
  .off-row.fee-row > input:first-child{grid-column:1 / -1}
  .off-row > input:first-child,.off-row > select{grid-column:1 / -1}
  .off-row.off-one > input:first-child{grid-column:1}
}
.off-empty{color:var(--muted);font-size:var(--fs-sm);margin:0}
.rowbtns{display:flex;gap:6px;flex-wrap:wrap}
/* ---- what a competitor is paying ---------------------------------------- */
/* One row per line of the price list: a tick, what it is, what it costs. The
   tick is a radio when the competition charges for one item and a checkbox
   when it charges for several — same row either way, because it is the same
   question with a different answer. */
/* The submit row of the entry form, in a block of its own rather than
   inside whichever section happens to be last. layoutSections() gives it
   order 999. */
.form-actions{order:999}
.pay-list{display:flex;flex-direction:column;gap:2px;margin:0 0 4px}
/* On a FORM the fee rows are choices somebody is making, not a summary
   somebody is reading — so they get the tick-box target size and the
   framing of a real option list. */
#feeCard .pay-item,#e_feeCard .pay-item{border-color:var(--line);margin-bottom:6px}
#feeCard .pay-item input,#e_feeCard .pay-item input{
  width:var(--check-box);height:var(--check-box);flex:none;margin:0;
  cursor:pointer;accent-color:var(--accent);
}
.pay-item{
  display:flex;align-items:center;gap:10px;
  padding:9px 11px;border-radius:var(--radius-sm);border:1px solid transparent;
}
.pay-item:hover{background:var(--hover-bg);border-color:var(--line)}
.pay-item .pay-what{flex:1 1 auto;min-width:0}
.pay-item .pay-amt{flex:none;font-weight:700;font-variant-numeric:tabular-nums}
/* A line recorded before the organizer deleted it from the price list. It
   still shows, still counts and still prints — it says what was actually
   paid — and this is the only mark that it is no longer on offer. */
.pay-gone{
  font-size:var(--fs-xs);font-weight:700;text-transform:uppercase;letter-spacing:.05em;
  color:var(--warn);margin-left:6px;
}
.pay-total{
  display:flex;align-items:baseline;justify-content:space-between;gap:12px;
  margin:10px 0 14px;padding:11px 12px;border-radius:var(--radius-sm);
  background:var(--chip-bg);border:1px solid var(--line-strong);
  font-size:var(--fs-sm);text-transform:uppercase;letter-spacing:.05em;color:var(--muted);
}
.pay-total b{
  font-family:var(--font-display);font-size:var(--fs-xl);letter-spacing:normal;
  text-transform:none;color:var(--text);font-variant-numeric:tabular-nums;
}
/* The chip beside an entry's status in the list: green for money that has
   arrived, amber for money that has not. It carries the AMOUNT rather than a
   word, because "how much" is the question being asked of a list of forty. */
.badge-paid{background:var(--ok-bg);color:var(--ok);font-variant-numeric:tabular-nums}
.badge-due{background:var(--warn-bg);color:var(--warn);font-variant-numeric:tabular-nums}

.modal-backdrop{position:fixed;inset:0;background:rgba(2,6,13,.7);-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);display:flex;align-items:flex-start;justify-content:center;padding:24px 14px;overflow:auto;z-index:50}
.modal{
  background:linear-gradient(180deg,#15233a,var(--card));
  border:1px solid var(--line-strong);
  border-radius:var(--radius);padding:20px;max-width:680px;width:100%;
  box-shadow:inset 0 1px 0 rgba(255,255,255,.05),var(--shadow-lg);
  animation:deck-rise .28s cubic-bezier(.22,.9,.3,1) backwards;
}
@media (prefers-reduced-motion:reduce){ .modal{animation:none} }
.modal h2{margin-top:0}
/* modal skeleton — Phase 3 moved the dialogs' markup onto these */
.modal-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:14px}
.modal-head h3{margin:0;font-size:var(--fs-xl);font-family:var(--font-display)}
/* WRAPS. The details panel of one entry carries eight actions on a
/* sibling fields share one rhythm; inline margin-top attributes come off */
.modal .field + .field{margin-top:var(--sp-3)}
/* ...but not inside a .grid, which owns its own spacing. The console's entry
   form is the public form and lays its fields out in two columns; this margin
   landed on every field but the first and staggered the columns against each
   other, so "Last name" sat lower than "First name" beside it. */
.modal .grid > .field + .field{margin-top:0}
/* Settings groups its language fields under these too, not only the modals. */
.section-label{font-size:var(--caps-size);text-transform:uppercase;letter-spacing:var(--caps-track);color:var(--muted);font-weight:var(--caps-weight);margin:14px 0 2px;grid-column:1/-1}

/* An ordered list you rearrange: the languages an event offers, and the custom
   fields it adds. Both are sequences a visitor sees in order, so both are shown
   as a numbered list with arrows rather than as an unordered set. */
.sort-rows{display:grid;gap:8px;margin-top:6px}
.sort-row{
  display:flex;align-items:center;gap:12px;flex-wrap:wrap;
  border:1px solid var(--line);border-radius:12px;padding:10px 12px;background:var(--surface-2)
}
/* A row can carry a name, a note, a badge and three buttons. On a phone that is
   more than one line's worth, so the buttons take their own rather than pushing
   the page sideways. */
@media (max-width:640px){
  .sort-row .rowbtns{flex:1 1 100%;display:flex;justify-content:flex-end}
  .sort-row .spacer{display:none}
}
.sort-row.is-default{border-color:var(--accent)}
.sort-row .l-pos{
  flex:none;width:26px;height:26px;border-radius:var(--radius-pill);background:var(--chip-bg);
  display:flex;align-items:center;justify-content:center;
  font-size:var(--fs-xs);font-weight:800;color:var(--muted);font-variant-numeric:tabular-nums
}
.sort-row .l-name{font-weight:800;flex:1 1 auto;min-width:0}
.sort-row .l-code{font-size:var(--fs-2xs);font-weight:800;letter-spacing:.04em;color:var(--muted);flex:none}
.sort-row .rowbtns{flex:none}
.sort-row .spacer{flex:1 1 auto}

.lang-add{display:flex;flex-wrap:wrap;gap:8px;margin-top:6px}
.lang-add button{
  display:inline-flex;align-items:center;gap:8px;cursor:pointer;
  border:1px dashed var(--line-strong);border-radius:var(--radius-pill);padding:8px 14px;
  background:var(--card);color:var(--text);font-size:var(--fs-sm);font-weight:600
}
.lang-add button:hover{border-color:var(--accent);border-style:solid}
.lang-add .l-code{font-size:var(--fs-2xs);font-weight:800;letter-spacing:.04em;color:var(--muted)}
.lang-add .l-none{color:var(--muted);font-size:var(--fs-base)}

/* admin portal chrome */
.admin-tag{display:inline-block;margin-left:8px;padding:2px 8px;border-radius:var(--radius-pill);background:var(--accent-solid);color:#fff;font-size:var(--fs-2xs);font-weight:800;letter-spacing:.04em;vertical-align:middle}
/* min-width:0 lets the language pill scroll inside this row instead of growing
   it: a flex item will not shrink below its content without it. */
.topbar-controls{display:flex;align-items:center;gap:8px;flex-wrap:wrap;min-width:0}
.topbar-controls .lang{flex:0 1 auto;min-width:0}

/* read-only detail grid (review) */
.detail-list{display:grid;grid-template-columns:1fr 1fr;gap:12px 18px}
.detail-list .di{display:flex;flex-direction:column;gap:2px;min-width:0}
.detail-list .di.full{grid-column:1/-1}
.detail-list .dk{font-size:var(--fs-xs);text-transform:uppercase;letter-spacing:.05em;color:var(--muted)}
.detail-list .dv{font-size:var(--fs-md);font-weight:600;word-break:break-word}
@media (max-width:640px){ .detail-list{grid-template-columns:1fr} }

/* ---- responsive data tables: never scroll sideways --------------------- */
/* Applied to <table class="rtable">. Cells wrap on all sizes; on phones each
   row becomes a labeled card (td[data-label]) so nothing scrolls. */
/* One markup, two layouts. Phones always get the LIST (card) layout; from
   tablet up the reader chooses with the .viewtoggle switch and viewmode.js
   puts `view-list` on <body>. Printing always uses the real table.

   Row anatomy — every renderer marks its cells up the same way:
     .cell-num / .rank  leading number (row number or place)
     .who               primary line (competitor, team, category name)
     .meta              subtitle: the same values the columns hold, joined and
                        UNLABELLED — this is what makes a card read as a card
     .score             trailing number   .tag  trailing badge
     .cell-actions      buttons, full width at the foot of the card
     .keep-label        keeps its column name in card form (score inputs)
   An unmarked cell is hidden in card form: its value belongs in .meta. */
table.rtable th, table.rtable td{white-space:normal;word-break:break-word}
table.rtable th{word-break:normal}
table.rtable td.meta, table.rtable th.meta{display:none}    /* card-only cell */
.tie{color:var(--accent-ink);font-weight:800;margin-left:2px}

body.view-list table.rtable thead{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0)}
body.view-list table.rtable,
body.view-list table.rtable tbody{display:block;width:100%}
body.view-list table.rtable tbody tr{
  display:grid;grid-template-columns:auto 1fr auto;column-gap:12px;row-gap:2px;
  width:100%;border:1px solid var(--line);border-radius:var(--radius-md);
  margin-bottom:10px;padding:12px 14px;background:var(--surface-2);
}
body.view-list table.rtable td{display:none;border:0;padding:0;text-align:left}
body.view-list table.rtable td::before{display:none}
body.view-list table.rtable td.rank,
body.view-list table.rtable td.cell-num,
body.view-list table.rtable td.who,
body.view-list table.rtable td.meta,
body.view-list table.rtable td.score,
body.view-list table.rtable td.tag,
body.view-list table.rtable td.cell-actions,
body.view-list table.rtable td.cell-log,
body.view-list table.rtable td.cell-instr,
body.view-list table.rtable td.keep-label{display:block}
body.view-list table.rtable td.rank,
body.view-list table.rtable td.cell-num{
  grid-column:1;grid-row:1 / span 2;align-self:center;
  min-width:1.5em;text-align:center;font-size:20px;font-weight:800;color:var(--muted);
  font-family:var(--font-display);font-variant-numeric:tabular-nums;
}
body.view-list table.rtable td.who{grid-column:2;grid-row:1;font-size:var(--fs-lg)}
body.view-list table.rtable td.meta{grid-column:2;grid-row:2;font-size:var(--fs-sm);color:var(--muted)}
body.view-list table.rtable td.score{
  grid-column:3;grid-row:1 / span 2;align-self:center;text-align:right;font-size:20px;font-weight:800;
  font-family:var(--font-display);font-variant-numeric:tabular-nums;
}
body.view-list table.rtable td.tag{grid-column:3;grid-row:1 / span 2;align-self:center;text-align:right}
body.view-list table.rtable td.keep-label{
  grid-column:1 / -1;display:flex;justify-content:space-between;align-items:center;gap:14px;
  padding:7px 0;border-top:1px solid var(--line);
}
body.view-list table.rtable td.keep-label::before{
  content:attr(data-label);display:block;font-weight:700;color:var(--muted);font-size:var(--fs-sm);
}
body.view-list table.rtable td.cell-actions{
  grid-column:1 / -1;border-top:1px solid var(--line);margin-top:6px;padding-top:10px;
}
/* WHO IS IN IT. A team and a club are groups, and a card that gives the
   count without the names answers the smaller half of the question — the
   table has said who all along and the card dropped it on the floor,
   because an unmarked cell is hidden in card form. A band of its own,
   under the name, with its column's label above it. */
body.view-list table.rtable td.club-who{
  display:block;grid-column:1 / -1;
  margin-top:9px;padding-top:9px;border-top:1px solid var(--line);
  font-size:var(--fs-sm);line-height:1.7;
}
body.view-list table.rtable td.club-who::before{
  content:attr(data-label);display:block;margin-bottom:3px;
  font-size:var(--fs-xs);text-transform:uppercase;letter-spacing:var(--caps-track);
  color:var(--muted);font-weight:var(--caps-weight);
}
/* The run list's card had TWO full-width bands stacked under the name — the
   result on one, the total on the next — each with its own rule above it. They
   are one line of thought ("what was flown, and what that comes to"), so they
   are now one band: result on the left, total on the right, one rule. */
/* ---- the run list's card -------------------------------------------------
   Three bands, and each answers a different question:

     1  who        the start number, the name, the club line — and, on the
                   right of that same line, who is in the back seat, because
                   that is a fact about the PERSON, not about the score.
     2  the score  left, where the eye lands and the hand travels least: the
                   two landing buttons until there is a result, and then one
                   button of the same size carrying the total. Recording it is
                   what this screen is opened to do.
     3  the flight facing it across the card — the two stamps, the occasional
                   job beside the constant one.

   The total has no line of its own any more — it IS the result button, which
   is why keep-label goes. The table puts these two in the same order, for the
   same reason: a reader moving between the views is not learning it twice. */
body.view-list table.comp-run tbody tr{row-gap:0}
body.view-list table.comp-run td.cell-instr{
  grid-column:3;grid-row:1 / span 2;align-self:center;justify-self:end;
  /* width:max-content, or the name goes to "Ibr…" with half the card empty
     beside it: an item hugging the right edge is shrink-to-fit, and a child
     that may ellipsize reports a min-content width of nothing, so the box
     collapses to the label. Natural width first, then the cap.
     The cap is in PIXELS on purpose. A percentage here resolves against this
     item's grid AREA — the third column, which the result button already holds
     at 230px — so "55%" quietly meant 126px and truncated every name. */
  width:max-content;max-width:320px;
}
/* THE RULE ABOVE THE BOTTOM BAND IS THE ROW'S, NOT THE CELLS'.
   The score band and the flight band are both shrink-to-fit — one hugs the
   left edge, the other the right — so a border-top on each drew two short
   lines with a gap between them wherever the card was wider than their
   contents. A rule that stops in the middle of a card reads as a mistake,
   because it is one.

   So the rule gets a grid row of its own: 1px tall, every column, drawn by
   the row itself. The two bands move down to row 4 and carry no border. It
   is the row's rule now, which is what it always was. */
body.view-list table.comp-run tbody tr::after{
  content:'';grid-column:1 / -1;grid-row:3;
  height:1px;background:var(--line);margin-top:10px;
}
body.view-list table.comp-run td.cell-actions{
  grid-column:1 / 3;grid-row:4;align-self:center;justify-self:start;
  /* The generic .rtable rule above gives every card's action band a rule of its
     own. This band no longer draws one — the ROW does, full width — and equal
     specificity means the earlier rule wins unless this one says so. */
  border-top:0;margin-top:0;
  /* Shrink-to-fit either way it is aligned, and its contents change shape:
     two landing buttons until there is a result, then one button in their
     place. Without a floor those would be different widths from each other
     and from the stamps facing them. Hold the line at the ceiling instead. */
  min-width:230px;
  padding-top:9px;
}
body.view-list table.comp-run td.cell-log{
  grid-column:3;grid-row:4;align-self:center;justify-self:end;
  padding-top:9px;
}
/* The stamps pair and the result button face each other across the card and
   have to be the same width, or the band reads as two unrelated things. Both
   cells are shrink-to-fit — one hugs the left edge, the other the right — so
   each carries the floor itself rather than inheriting one from the column it
   happens to sit in. .land-grid caps at 230, so this pins it exactly there,
   which is what .cell-actions min-width holds the result button to. */
body.view-list table.comp-run td.cell-log .land-grid{min-width:230px}
/* With no flight cell to share the line with, the score band takes the width
   rather than sitting against an otherwise empty rule. It keeps its contents
   at the left, which is where they now live. */
body.view-list table.comp-run:not(.has-log) td.cell-actions{
  grid-column:1 / -1;justify-self:stretch;
}
/* A phone has no room for two bands side by side: four squares at 165px each
   is narrower than any phone in circulation, so they stack instead — in the
   order they read across on a wider screen, score first and flight under it,
   and the instructor on a line of its own rather than squeezed against the
   name. */
@media (max-width:640px){
  /* Stacked, every band IS the width of the card, so each draws its own rule
     again and the row-wide one above would be a second line in mid-air. */
  body.view-list table.comp-run tbody tr::after{display:none}
  body.view-list table.comp-run td.cell-instr{
    grid-column:1 / -1;grid-row:3;justify-self:start;max-width:100%;
    margin-top:10px;padding-top:10px;border-top:1px solid var(--line);
  }
  body.view-list table.comp-run td.cell-actions,
  body.view-list table.comp-run:not(.has-log) td.cell-actions{
    grid-column:1 / -1;grid-row:4;justify-self:stretch;border-top:0;padding-top:0;
  }
  body.view-list table.comp-run td.cell-log{
    /* justify-self as well as the column: stacked, this band is the width of
       the card like the one above it, not a 230px block still hugging the
       right edge it was pushed to when the two sat side by side. */
    grid-column:1 / -1;grid-row:5;justify-self:stretch;border-top:0;padding-top:0;
  }
  /* Stacked, the rule belongs to whichever band is first, and the score band
     is first unless the instructor is above it. (It used to be written for the
     narrower case of no flight AND no instructor, which was right only while
     the flight band came first.) */
  body.view-list table.comp-run:not(.has-instr) td.cell-actions{
    border-top:1px solid var(--line);padding-top:10px;
  }
  .log-cell .land-grid,.result-btn{max-width:none}
}

/* ---- the run list as a TABLE ---------------------------------------------
   Same components, so a reader moving between the two views is not learning
   the screen twice: the stamps and the score keep their size, and the two
   columns that carry them are the only wide ones. The rest of the row is short
   text and gives up its width willingly. */
/* The shared .cell-actions rule above turns that cell into a flex box, which
   suits a row of small buttons and not this: here the cell holds exactly one
   child — the pair of landing buttons, or the result that replaces them — and
   as a flex item it collapsed to the width of its own text. Back to a cell. */
body:not(.view-list) table.comp-run td.cell-actions{display:table-cell}
body:not(.view-list) table.comp-run td.cell-log,
body:not(.view-list) table.comp-run td.cell-instr,
body:not(.view-list) table.comp-run td.cell-actions{vertical-align:middle}
body:not(.view-list) table.comp-run td.cell-instr{white-space:nowrap;width:1%}
/* The two wide columns take what they need; the short text columns give it up. */
body:not(.view-list) table.comp-run td.cell-log,
body:not(.view-list) table.comp-run td.cell-actions{width:1%}
/* ...but width:1% means "as narrow as your min-content", and every cell in
   this table breaks words, which puts min-content near zero. The flight cell
   survives it because .log-cell is a flex box and its .land-grid floor carries
   up; the result cell is a plain table-cell, so it shrank to 24px and its
   button hung outside it — far enough that the row's own tint stopped a
   column short of the row. The cell is given the button's floor plus the
   12px it is padded by on each side. */
body:not(.view-list) table.comp-run td.cell-actions{min-width:224px}
/* The short text columns keep a floor of their own, so the three button
   columns cannot squeeze them until a club name breaks mid-word. Past that the
   .tablewrap scrolls, which is the honest answer to a table with more in it
   than the screen has room for. */
body:not(.view-list) table.comp-run td.who,
body:not(.view-list) table.comp-run th:nth-child(2){min-width:190px}
body:not(.view-list) table.comp-run td.col-txt{min-width:104px}
/* The standings run to twelve columns once an event records its take-offs,
   its landings and who was in the back seat. Same floor, same reason: past
   it the .tablewrap scrolls, which is honest, where squeezing turns
   'AK Prijedor' into 'AK Prijedo r', which is not. */
body:not(.view-list) table.ranklist td.col-txt{min-width:110px;word-break:normal;overflow-wrap:normal}
body:not(.view-list) table.ranklist td.who{min-width:170px}
body:not(.view-list) table.ranklist td.score{min-width:104px}
/* Club and team ride under the competitor's name instead of taking a column
   each. The row already carries a flight, an instructor and a score, all three
   of which want real width, and the text columns beside them pushed the table
   past the width of the page at every viewport there is — so the scroller was
   never not scrolling. The card answers the same question with .meta and keeps
   it, which is why this line is table-only: neither view says it twice. */
table.comp-run td.who .who-sub{
  display:block;margin-top:2px;font-weight:400;line-height:1.35;
  font-size:var(--fs-sm);color:var(--muted);
}
body.view-list table.comp-run td.who .who-sub{display:none}
body.view-list table.clickable tbody tr:hover{background:var(--hover-bg)}

/* the switch — phones have no choice to make, so it appears from tablet up */
.viewtoggle{display:none}
.viewtoggle button{
  border:0;background:transparent;padding:7px 10px;cursor:pointer;color:var(--muted);
  display:inline-flex;align-items:center;justify-content:center;
}
.viewtoggle button.active{background:var(--accent-solid);color:#fff}
@media (max-width:520px){
  .filter-group{align-items:flex-start;flex-direction:column;gap:4px}
}
.viewtoggle svg{width:17px;height:17px;display:block}
@media (min-width:721px){
  .viewtoggle{display:inline-flex;border:1px solid var(--line);border-radius:var(--radius-pill);overflow:hidden;background:var(--card)}
}

/* clickable rows (open details) */
table.clickable tbody tr{cursor:pointer;transition:background .12s ease}
table.clickable tbody tr:hover{background:var(--hover-bg)}

/* search box */
.search-box{position:relative;margin-bottom:12px}
.toolbar .search-box{margin-bottom:0}
/* masked SVG, not an emoji: tintable, and the same glyph on every OS */
.search-box::before{
  content:"";position:absolute;left:12px;top:50%;transform:translateY(-50%);
  width:16px;height:16px;background:var(--muted);pointer-events:none;
  -webkit-mask:url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'><circle cx='11' cy='11' r='7'/><line x1='21' y1='21' x2='16.5' y2='16.5'/></svg>") center/contain no-repeat;
  mask:url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'><circle cx='11' cy='11' r='7'/><line x1='21' y1='21' x2='16.5' y2='16.5'/></svg>") center/contain no-repeat;
}
.search-box input{padding-left:40px}
.modal-sm{max-width:440px}

.footer-note{text-align:center;color:var(--muted);font-size:var(--fs-xs);margin-top:20px;padding:0 12px}
/* subtle "Powered by TimeLine" credit (logo recoloured via CSS mask) */
.powered{display:flex;align-items:center;justify-content:center;gap:7px;margin:12px 0 4px;color:var(--muted);font-size:var(--fs-xs);letter-spacing:.02em;text-decoration:none;transition:color .15s}
.powered:hover{color:var(--text)}
.powered-logo{display:inline-block;width:78px;height:13px;background-color:currentColor;-webkit-mask:url(../img/timeline-logo.svg) center/contain no-repeat;mask:url(../img/timeline-logo.svg) center/contain no-repeat}

/* ---------- responsive -------------------------------------------------- */
/* phones: single column, wrapping header, full-width tap targets */
@media (max-width:640px){
  .wrap{padding:14px 12px 34px}
  .grid,.details{grid-template-columns:1fr}
  .field.full{grid-column:auto}
  .topbar{gap:8px;padding:10px 12px}
  .brand span{font-size:var(--fs-md)}
  .hero{padding:20px 16px}
  .hero h1{font-size:var(--fs-3xl)}
  .card{padding:16px}
  .btn-lg{width:100%}
  .actions .btn{width:100%}
  /* result popup: result+penalty side by side, fault button full-width below */
  .res-flight{grid-template-columns:1fr 1fr}
  .res-flight .rm-fault{grid-column:1 / -1;height:auto;padding:10px}
}

/* tablets: two-column stays, just a touch tighter */
@media (min-width:641px) and (max-width:900px){
  .wrap{padding:16px 16px 36px}
}

/* larger tap targets on touch devices (phones/tablets) */
@media (hover:none){
  .btn{min-height:46px}
  .icon-btn{width:40px;height:40px}
  .tabbar .tab{min-height:62px}
  input,select{min-height:48px}
}

.muted{color:var(--muted)}

/* ---------- registered clubs list -------------------------------------- */
.clubs-list{display:flex;flex-wrap:wrap;gap:8px}
.club-chip{border:1px solid var(--line-strong);border-radius:var(--radius-pill);padding:8px 13px;font-weight:700;font-size:var(--fs-base);display:inline-flex;gap:8px;align-items:center;background:var(--card);color:var(--text)}
.club-chip .n{opacity:.55;font-variant-numeric:tabular-nums;font-weight:600}
/* printable clubs block: a body-level node shown only while printing clubs */
#clubsPrint{display:none}
/* ONE printed board header — the page-local .board-print-head / #startPrintHead
   copies migrate onto this, and the clubs variant below aliases it */
.print-head,.clubs-print-head{margin-bottom:14px;padding-bottom:8px;border-bottom:2px solid #000}
.print-head .ev-name,.clubs-print-head .ev-name{display:block;font-size:20px;font-weight:800}
.print-head .ev-sub,.clubs-print-head .ev-sub{font-size:var(--fs-xs)}
/* the QR poster prints with the same metrics from public and admin alike.
   The plate stays white on screen too — a QR must be dark-on-light to scan. */
.qr-box{width:300px;max-width:100%;background:#fff;border-radius:12px;padding:12px}
.qr-url{font-family:var(--font-mono);font-size:var(--fs-sm);word-break:break-all;text-align:center}
#clubsPrint ol{margin:0;padding-left:26px;columns:2;column-gap:32px}
#clubsPrint li{margin:0 0 7px;font-size:var(--fs-base);break-inside:avoid}
#clubsPrint li .n{color:#555}

/* ---- one entry, on paper ------------------------------------------------ */
/* The same body-level trick the club list uses: a node that is invisible on
   screen and is the ONLY thing on the page while printing (body.print-doc,
   in the print block below). It carries the registration and, underneath it,
   the receipt for whatever that competitor paid. */
#regPrint{display:none}
#regPrint .sheet-who{display:flex;align-items:center;gap:16px;margin:0 0 16px}
#regPrint .sheet-who h2{margin:0;font-size:22px;font-weight:800}
#regPrint .sheet-face img{
  width:74px;height:74px;object-fit:cover;border-radius:8px;border:1px solid #999;display:block;
}
/* Two columns of facts, because a sheet handed across a desk is read down a
   column and not across a page. */
#regPrint .sheet-facts{display:grid;grid-template-columns:1fr 1fr;gap:7px 26px;margin:0 0 20px}
#regPrint .sf{display:flex;gap:8px;justify-content:space-between;break-inside:avoid;
  border-bottom:1px dotted #bbb;padding-bottom:3px}
#regPrint .sf-k{color:#555;font-size:12px}
#regPrint .sf-v{font-weight:700;font-size:12px;text-align:right;word-break:break-word}
/* The receipt is a block of its own with a rule above it: it is a second
   document on the same sheet, and it has to look like one. */
#regPrint .sheet-rc{border:1px solid #444;border-radius:6px;padding:14px 16px;break-inside:avoid}
#regPrint .sheet-rc h3{
  margin:0 0 10px;font-size:13px;font-weight:800;text-transform:uppercase;letter-spacing:.09em;
}
#regPrint .rc-line{display:flex;justify-content:space-between;gap:16px;padding:4px 0;font-size:13px}
#regPrint .rc-line b{font-variant-numeric:tabular-nums}
#regPrint .rc-total{border-top:1px solid #444;margin-top:6px;padding-top:8px;font-weight:800;font-size:15px}
#regPrint .rc-state{margin-top:9px;font-weight:700;font-size:13px}
#regPrint .rc-note{margin-top:5px;font-size:12px;color:#444}
/* Somebody took the money and somebody handed the sheet over. A line to say
   who — a receipt from a desk has always had one. */
#regPrint .rc-sign{margin-top:26px;font-size:11px;color:#555}
#regPrint .rc-sign span{display:block;border-top:1px solid #777;padding-top:4px;width:210px}

/* ---------- print ------------------------------------------------------- */
/* Paper is light: re-token the whole deck back to ink-on-white. The four
   brand tokens are left alone — a printed board may keep its event colour. */
@media print{
  /* Every mark prints as ink: the accent is a screen colour and greys out. */
  .disc-ico,.brand-disc,.poster-disc{color:inherit}
  /* A document list prints as a list; a gallery and a drop target do not
     print at all — one wastes a ream, the other is a control. */
  .filedrop,.gal-grid,.gal-cell{display:none !important}
  .file-item{border-color:#999;background:none;break-inside:avoid}
  .file-mark,.file-go{color:inherit}
  .disc-chip{background:none;border-color:#999;color:inherit}
  :root{
    --bg:#fff;--card:#fff;--glass:#fff;--text:#111721;--muted:#525e6c;
    --line:#c9d2dc;--line-strong:#9aa8b7;
    --chip-bg:#edf1f5;--hover-bg:#f2f5f8;--surface-2:#f6f8fa;
    --accent-ink:#0f5f8f;--accent-soft:rgba(3,105,161,.1);
    --ok:#166534;--ok-bg:#ecfdf3;--ok-line:#bbf7d0;
    --warn:#92400e;--warn-bg:#fffbeb;--warn-line:#fde68a;
    --err:#b42318;--err-bg:#fef3f2;--err-line:#fecdca;--live:#d92d20;
    --shadow-1:none;--shadow-2:none;--shadow-3:none;--shadow-up:none;--glow:none;
  }
  .no-print{display:none !important}
  /* Three lists, three sheets. The FIRST one does not break, or the print
     starts with a page carrying nothing but the banner — which is exactly
     what `.card + .printpage` did: the filter card is a .card, and
     display:none does not take an element out of a sibling selector. Only
     a printpage that FOLLOWS a printpage breaks. */
  .printpage{break-inside:auto}
  .printpage + .printpage{break-before:page;page-break-before:always}
  /* The header every sheet carries, repeated by the browser at the top of
     each page its table runs onto. Set flat against the paper: a table
     header cell is small, grey and upper-cased, and this is a title. */
  .rtable tr.sheet-row{display:table-row}
  .rtable tr.sheet-row th{
    padding:0 0 5px;border:0;border-bottom:1.5px solid #555;text-align:left;
    text-transform:none;letter-spacing:0;color:#111;
  }
  .sheet-line{display:block;padding:0 0 5px;margin-bottom:9px;border-bottom:1.5px solid #555}
  .sh-ev{font-weight:800;font-size:12.5px;color:#111}
  .sh-what{font-weight:600;color:#333}
  .sh-ev + .sh-what::before{content:" · ";font-weight:400;color:#777}
  .sh-sub{float:right;font-weight:400;color:#333}
  /* A face survives the trip to paper — it is why the sheet is worth
     printing at an accreditation desk — but not in colour it cannot hold. */
  .face{border-color:#999;background:#f2f2f2;print-color-adjust:exact;-webkit-print-color-adjust:exact}
  .rankbar{background:none;border:1px solid #999;height:6px}
  .rankbar i{background:#555;print-color-adjust:exact;-webkit-print-color-adjust:exact}
  body.print-clubs > *{display:none !important}
  body.print-clubs #clubsPrint{display:block !important}
  body.print-doc > *{display:none !important}
  body.print-doc #regPrint{display:block !important}
  body{background:#fff;color:#111}
  body::before{display:none}
  body:has(.tabbar){padding-bottom:0}   /* the fixed bar is hidden — reclaim its space */
  *{animation:none !important;-webkit-backdrop-filter:none !important;backdrop-filter:none !important}
  .card{box-shadow:none;border:1px solid var(--line);background:#fff}
  .hero{background:none;color:#000;box-shadow:none;border:0;padding:0 0 10px}
  /* no horizontal scroll in print: drop the scroll container and let cells wrap */
  .wrap{max-width:none;padding:0}
  .topbar{position:static;background:none;border-bottom:0}
  .tablewrap{overflow:visible !important}
  table{font-size:var(--fs-xs);width:100%}
  th,td{white-space:normal;word-break:break-word;padding:6px 8px}
}

/* ==========================================================================
   Platform home — the welcome page and the event directory. It is the only
   public page with no event context, so it keeps the default theme rather than
   any event's branding, and it runs wider than the event pages: a directory of
   competitions wants two columns, a registration form does not.
   ========================================================================== */
body.plat{--maxw:1120px}
/* the hero's "browse competitions" is an in-page jump to the directory */
html:has(body.plat){scroll-behavior:smooth;scroll-padding-top:14px}
@media (prefers-reduced-motion:reduce){html:has(body.plat){scroll-behavior:auto}}

/* ---- welcome hero ------------------------------------------------------- */
.plat-hero{
  position:relative;overflow:hidden;
  /* same horizon-line layering as .hero above */
  background-image:
    linear-gradient(180deg,transparent calc(68% - 1px),rgba(255,255,255,.13) 68%,rgba(255,255,255,.03) calc(68% + 1px),rgba(255,255,255,.025) 100%),
    linear-gradient(145deg,var(--hero-from),var(--hero-to));
  background-blend-mode:screen,normal;
  color:#fff;border-radius:var(--radius);border:1px solid var(--line);box-shadow:var(--shadow-lg);
  padding:46px 42px 0;
}
/* The sport's own mark — the concentric rings of a landing target — with a
   radar sweep turning over it. The rings are rotation-symmetric, so animating
   the element's rotation moves ONLY the conic sweep layer. */
.plat-hero::before{
  content:"";position:absolute;right:-90px;top:-110px;width:460px;height:460px;
  border-radius:50%;pointer-events:none;
  background-image:
    conic-gradient(from 0deg,transparent 0 78%,rgba(56,182,255,.16) 97%,rgba(255,255,255,.22) 100%),
    radial-gradient(circle,
      transparent 0 16px, rgba(255,255,255,.20) 16px 17px,
      transparent 17px 62px, rgba(255,255,255,.13) 62px 63px,
      transparent 63px 122px, rgba(255,255,255,.09) 122px 123px,
      transparent 123px 196px, rgba(255,255,255,.06) 196px 197px, transparent 197px);
  animation:radar-turn 9s linear infinite;
}
@keyframes radar-turn{to{transform:rotate(360deg)}}
/* accent glow, low and left, so the hero is not a flat slab */
.plat-hero::after{
  content:"";position:absolute;left:-140px;bottom:-190px;width:430px;height:430px;
  border-radius:50%;pointer-events:none;opacity:.26;
  background:radial-gradient(circle,var(--accent),transparent 70%);
}
/* a paper-dart glider riding a long approach across the hero, over everything */
.hero-glider{
  position:absolute;inset:0;z-index:2;pointer-events:none;display:none;
}
@supports (offset-path:path('M0 0 L10 10')){ .hero-glider{display:block} }
.hero-glider svg{
  width:30px;height:30px;color:rgba(255,255,255,.5);
  offset-path:path('M -60 60 C 260 150, 520 20, 900 120 C 1000 145, 1080 150, 1220 130');
  offset-rotate:auto;
  animation:glide-by 17s linear infinite;
}
@keyframes glide-by{
  0%{offset-distance:0%;opacity:0}
  6%{opacity:.9}
  86%{opacity:.9}
  96%,100%{offset-distance:100%;opacity:0}
}
@media (prefers-reduced-motion:reduce){
  .plat-hero::before{animation:none}
  .hero-glider{display:none}
}
.plat-hero-in{position:relative;z-index:1;max-width:660px}
.plat-eyebrow{
  display:inline-flex;align-items:center;gap:9px;margin-bottom:16px;
  padding:5px 12px 5px 10px;border-radius:var(--radius-pill);
  border:1px solid rgba(255,255,255,.18);background:rgba(255,255,255,.06);
  font-family:var(--font-display);
  font-size:var(--fs-xs);font-weight:700;letter-spacing:.13em;text-transform:uppercase;opacity:.9;
}
.plat-eyebrow .dot{
  width:7px;height:7px;border-radius:50%;flex:none;
  background:var(--accent);box-shadow:0 0 0 3px rgba(255,255,255,.14);
}
.plat-hero h1{margin:0;font-size:var(--fs-display);line-height:1.14;letter-spacing:-.2px;font-weight:800}
.plat-lead{margin:15px 0 0;font-size:var(--fs-md);line-height:1.62;opacity:.84}
.plat-cta{display:flex;flex-wrap:wrap;gap:10px;margin-top:24px}
.btn-ghost{background:rgba(255,255,255,.08);border-color:rgba(255,255,255,.24);color:#fff}
.btn-ghost:hover{background:rgba(255,255,255,.16);color:#fff}

/* Live counts, sitting on the hero's own footing rather than in a card of
   their own — a visitor should see the platform is in use before scrolling. */
.plat-stats{
  position:relative;z-index:1;display:grid;grid-template-columns:repeat(4,1fr);gap:18px;
  margin:34px -42px 0;padding:19px 42px;
  border-top:1px solid rgba(255,255,255,.13);background:rgba(255,255,255,.035);
}
.plat-stat .n{font-family:var(--font-display);font-size:var(--fs-4xl);font-weight:800;line-height:1;letter-spacing:-.5px;font-variant-numeric:tabular-nums}
.plat-stat .k{display:block;margin-top:7px;font-size:var(--caps-size);letter-spacing:var(--caps-track);text-transform:uppercase;opacity:.66}
/* How many of the upcoming competitions can be entered right now. It hangs
   under the label it qualifies rather than taking a tile of its own, and it
   is the only green on the hero — the same green the cards use for
   "Registration open", so the two read as one fact in two places. */
.plat-stat .stat-open{
  display:inline-flex;align-items:center;gap:7px;margin-top:9px;
  padding:4px 10px;border-radius:var(--radius-pill);
  background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.16);
  font-size:var(--fs-xs);font-weight:700;line-height:1.2;white-space:nowrap;
  font-variant-numeric:tabular-nums;
}
.plat-stat .stat-open .dot{
  width:7px;height:7px;border-radius:50%;flex:none;background:#4ed492;
  box-shadow:0 0 0 3px rgba(78,212,146,.16);
}
@media (max-width:640px){
  .plat-stat .stat-open{font-size:var(--fs-2xs);padding:3px 8px;gap:5px;margin-top:7px}
}

/* ---- a club's own page -------------------------------------------------- */
/* Same hero as the platform home, identifying the club instead: its mark beside
   its name, and whichever of country / website / e-mail it has filled in. The
   chips sit ON the dark panel, so they are drawn in its own light-on-dark set
   rather than the card palette the rest of the page uses. */
.club-id{display:flex;align-items:center;gap:14px}
.club-logo{
  width:56px;height:56px;flex:none;object-fit:contain;
  border-radius:12px;background:rgba(255,255,255,.92);padding:6px;
}
.club-meta{display:flex;flex-wrap:wrap;gap:8px;margin-top:16px}
.club-fact{
  display:inline-flex;align-items:center;gap:7px;padding:5px 12px;border-radius:var(--radius-pill);
  border:1px solid rgba(255,255,255,.18);background:rgba(255,255,255,.06);
  font-size:var(--fs-sm);color:#fff;max-width:100%;
}
.club-fact svg{width:15px;height:15px;flex:none;opacity:.75}
a.club-fact:hover{background:rgba(255,255,255,.16);color:#fff;text-decoration:none}
a.club-fact:focus-visible{outline:2px solid #fff;outline-offset:2px}

/* ---- what the platform does (the short version) ------------------------- */
.plat-features{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:16px}
.plat-feature{
  background:linear-gradient(180deg,rgba(21,35,58,.88),var(--glass));
  border:1px solid var(--line);border-radius:var(--radius);
  padding:20px;box-shadow:inset 0 1px 0 rgba(255,255,255,.045),var(--shadow);
  transition:border-color .18s,transform .18s;
}
.plat-feature:hover{border-color:var(--line-strong);transform:translateY(-2px)}
.pf-ico{
  display:inline-flex;align-items:center;justify-content:center;
  width:38px;height:38px;border-radius:11px;margin-bottom:13px;color:var(--accent-ink);
  background:rgba(56,182,255,.12);                          /* fallback for the line below */
  background:color-mix(in srgb,var(--accent) 13%,transparent);
  box-shadow:var(--glow);
}
.pf-ico svg{width:20px;height:20px}
.plat-feature h2{margin:0 0 6px;font-size:var(--fs-lg);letter-spacing:.1px}
.plat-feature p{margin:0;color:var(--muted);font-size:var(--fs-base);line-height:1.58}

/* ---- directory toolbar -------------------------------------------------- */
.plat-toolbar{display:flex;align-items:center;justify-content:space-between;gap:14px;flex-wrap:wrap;margin-bottom:16px}
.plat-toolbar h2{margin:0;font-size:19px}
.plat-tools{display:flex;align-items:center;justify-content:flex-end;gap:10px;flex:1 1 auto;flex-wrap:wrap}
.seg{display:inline-flex;background:var(--surface-2);border:1px solid var(--line);border-radius:12px;padding:3px;gap:2px}
.seg button{
  border:0;background:transparent;color:var(--muted);font:inherit;font-size:var(--fs-sm);font-weight:700;
  padding:8px 13px;border-radius:var(--radius-xs);cursor:pointer;white-space:nowrap;
}
.seg button:hover{color:var(--text)}
.seg button.active{background:var(--chip-bg);color:var(--text);box-shadow:var(--shadow-1)}
.seg button .n{margin-left:7px;opacity:.55;font-variant-numeric:tabular-nums}
/* The home has a search box of its own: an icon ELEMENT rather than the
   ::before emoji the admin tables use, so it lines up with the segment beside it. */
.plat-search{position:relative;display:flex;align-items:center;flex:none;width:280px}
.plat-search svg{position:absolute;left:13px;width:17px;height:17px;color:var(--muted);pointer-events:none}
.plat-search input{padding:11px 13px 11px 39px;border-radius:12px;font-size:var(--fs-base)}

/* ---- event cards -------------------------------------------------------- */
.ev-group + .ev-group{margin-top:24px}
.ev-group-head{display:flex;align-items:center;gap:10px;margin:0 0 11px}
.ev-group-head h3{margin:0;font-size:var(--caps-size);font-weight:var(--caps-weight);letter-spacing:var(--caps-track);text-transform:uppercase;color:var(--muted)}
.ev-group-head .n{font-size:var(--fs-xs);font-weight:700;color:var(--muted);background:var(--chip-bg);border-radius:var(--radius-pill);padding:2px 8px;font-variant-numeric:tabular-nums}
/* runway centerline, not a plain hairline */
.ev-group-head .rule{flex:1;height:2px;background:repeating-linear-gradient(90deg,var(--line-strong) 0 16px,transparent 16px 28px);opacity:.5}
/* each group's own way through to the full list, already filtered */
.ev-group-head .see-all-link{flex:none;font-size:var(--fs-xs)}
.ev-group-head .see-all-link .n{background:none;padding:0 0 0 1px;color:inherit;opacity:.65;font-size:var(--fs-xs)}
/* auto-FIT, not auto-fill: a directory with one event should show one wide row,
   not a narrow card marooned beside two empty tracks. */
.ev-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(360px,100%),1fr));gap:12px}
/* The console's competition lists: one per line, always. These cards carry
   controls as well as content, and side by side neither half had room.
   Equal specificity with .ev-grid above, so it must stay AFTER it. */
.ev-grid-rows{grid-template-columns:1fr}
.ev-card{
  display:flex;align-items:stretch;overflow:hidden;color:inherit;text-decoration:none;
  border:1px solid var(--line);border-radius:var(--radius-md);background:var(--card);
  transition:border-color .14s,box-shadow .14s,transform .14s;
}
.ev-card:hover{text-decoration:none;border-color:var(--line-strong);box-shadow:var(--shadow-2),var(--glow);transform:translateY(-2px)}
.ev-card:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.ev-rail{flex:none;width:4px;background:var(--accent)}
.ev-date{
  flex:none;display:flex;flex-direction:column;align-items:center;justify-content:center;
  width:74px;padding:14px 5px;background:var(--surface-2);border-right:1px solid var(--line);line-height:1.05;
}
.ev-date .d{font-family:var(--font-display);font-size:var(--fs-3xl);font-weight:800;font-variant-numeric:tabular-nums;letter-spacing:-.5px}
.ev-date .m{margin-top:4px;font-size:var(--fs-2xs);font-weight:800;letter-spacing:.09em;text-transform:uppercase;color:var(--accent-ink)}
.ev-date .y{margin-top:3px;font-size:var(--fs-2xs);color:var(--muted);font-variant-numeric:tabular-nums}
.ev-date.range .d{font-size:17px}
.ev-date.tbd .d{font-size:20px;color:var(--muted)}
.ev-main{flex:1;min-width:0;display:flex;flex-direction:column;gap:3px;padding:14px}
/* The status badge shares the title's line and is pushed to the far edge, so a
   wide row stays two lines tall. It wraps under the title when there is no room. */
.ev-head{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;flex-wrap:wrap}
.ev-title{font-weight:800;font-size:var(--fs-lg);line-height:1.3;color:var(--text);font-family:var(--font-display)}
.ev-head .badge{flex:none;margin-top:1px;font-size:var(--fs-xs);padding:3px 9px}
.ev-sub,.ev-org{font-size:var(--fs-sm);color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ev-org{font-weight:700;font-size:var(--fs-xs)}
.ev-go{flex:none;display:flex;align-items:center;padding-right:12px;color:#40546e;transition:color .14s,transform .14s}
.ev-go svg{width:18px;height:18px}
.ev-card:hover .ev-go{color:var(--accent-ink);transform:translateX(2px)}
.badge-ok{background:var(--ok-bg);color:var(--ok);border:1px solid var(--ok-line)}
.badge-muted{background:var(--chip-bg);color:var(--muted)}

/* .empty is the shared empty-state card util.js emits (glide-path svg + h3 +
   optional p); .plat-empty predates it and keeps working unchanged.
   .load-error is the same card for a failed read — its retry button keeps
   normal .btn chrome. */
.plat-empty,.empty{text-align:center;padding:36px 16px;color:var(--muted)}
.plat-empty h3,.empty h3{margin:0 0 6px;font-size:var(--fs-lg);color:var(--text)}
.plat-empty p,.empty p{margin:0;font-size:var(--fs-base)}
/* the dashed descending glide path, ending on an accent touchdown dot */
.empty .empty-glide{display:block;margin:0 auto 10px;color:var(--muted)}
.empty .btn{margin-top:var(--sp-3)}
/* Placeholder cards while the directory loads: an empty panel that then fills
   reads as "nothing here", which is the one thing this page must never say. */
.plat-skeleton{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(360px,100%),1fr));gap:12px}
.plat-skeleton span{
  display:block;height:88px;border-radius:var(--radius-md);
  background:linear-gradient(100deg,var(--chip-bg) 30%,var(--hover-bg) 50%,var(--chip-bg) 70%);background-size:220% 100%;
  animation:plat-shimmer 1.3s linear infinite;
}
@keyframes plat-shimmer{from{background-position:180% 0}to{background-position:-40% 0}}
/* util.js's U.skeleton emits childless .plat-skeleton divs on the non-platform
   pages; without spans the grid is 0px tall, so an empty one shimmers itself */
.plat-skeleton:empty{
  display:block;height:56px;margin-top:10px;border-radius:var(--radius-md);
  background:linear-gradient(100deg,var(--chip-bg) 30%,var(--hover-bg) 50%,var(--chip-bg) 70%);background-size:220% 100%;
  animation:plat-shimmer 1.3s linear infinite;
}
@media (prefers-reduced-motion:reduce){.plat-skeleton span,.plat-skeleton:empty{animation:none}}

/* ---- "run a competition on LANDING" (the contact form) ------------------ */
/* The ask sits on the hero-dark panel — the page opens and closes on the same
   note — and the other half holds nothing but the fields. */
.contact-card{padding:0;overflow:hidden}
.contact-split{display:grid;grid-template-columns:minmax(0,340px) minmax(0,1fr)}
.contact-aside{
  position:relative;overflow:hidden;padding:28px 26px;color:#fff;
  background:linear-gradient(160deg,var(--hero-from),var(--hero-to));
  display:flex;flex-direction:column;
}
/* the landing target again, smaller, low in the corner */
.contact-aside::before{
  content:"";position:absolute;right:-120px;bottom:-140px;width:300px;height:300px;
  border-radius:50%;pointer-events:none;
  background:radial-gradient(circle,
    transparent 0 12px, rgba(255,255,255,.16) 12px 13px,
    transparent 13px 48px, rgba(255,255,255,.11) 48px 49px,
    transparent 49px 96px, rgba(255,255,255,.07) 96px 97px, transparent 97px);
}
.contact-aside > *{position:relative;z-index:1}
/* the panel is a flex column, and a flex child stretches across the cross axis
   unless told otherwise — which turned the eyebrow pill into a full-width bar */
.contact-aside .plat-eyebrow{align-self:flex-start}
.contact-aside h2{margin:0 0 10px;font-size:var(--fs-2xl);line-height:1.2;letter-spacing:-.2px;color:#fff}
.contact-aside p{margin:0;font-size:var(--fs-base);line-height:1.6;opacity:.82}
.ct-steps{list-style:none;margin:22px 0 0;padding:0;display:grid;gap:13px}
.ct-steps li{display:flex;align-items:flex-start;gap:11px;font-size:var(--fs-sm);line-height:1.45;opacity:.92}
.ct-steps .n{
  flex:none;width:22px;height:22px;border-radius:50%;
  display:inline-flex;align-items:center;justify-content:center;
  background:rgba(255,255,255,.12);border:1px solid rgba(255,255,255,.2);
  font-size:var(--fs-xs);font-weight:800;font-variant-numeric:tabular-nums;
}
/* margin-top:auto pins this to the foot of the panel, so a tall form does not
   leave the dark half half-empty */
.ct-note{
  margin:24px 0 0 !important;padding-top:16px;
  border-top:1px solid rgba(255,255,255,.13);
  font-size:var(--fs-xs);opacity:.6 !important;
}
@media (min-width:901px){ .ct-note{margin-top:auto !important} }
.contact-body{padding:24px 26px}
.ct-foot{display:flex;align-items:center;gap:16px;flex-wrap:wrap;margin-top:18px}
@media (max-width:900px){
  .contact-split{grid-template-columns:1fr}
  .contact-aside{padding:24px 20px}
  .contact-aside::before{right:-150px;bottom:-170px}
  .contact-body{padding:20px}
  .ct-foot .btn{width:100%}
}
.seg-wide{display:flex;flex-wrap:wrap;width:100%}
.seg-wide button{flex:1 1 auto;text-align:center}
/* On a phone, three options with labels this long share a row at about eleven
   pixels each. One per line is plainer and readable. */
@media (max-width:640px){
  .seg-wide{flex-direction:column;gap:3px}
  .seg-wide button{width:100%;padding:11px 12px}
}
/* The honeypot must be reachable by a bot reading the DOM and invisible to a
   person — display:none is the one thing that would tip a good bot off. */
.hp-field{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}
#contactForm .grid{gap:14px}
#contactForm .hint{display:block;margin-top:6px}
#contactForm .notice{margin-top:16px}
/* the "what do you need?" switch leads the form, so it gets room of its own */
#contactForm > .field.full:first-child{margin-bottom:18px}
#contactForm > .field.full:first-child > label{margin-bottom:8px;display:block}
.top-scope{
  display:inline-block;margin:-8px 0 12px;padding:3px 10px;border-radius:var(--radius-pill);
  background:var(--chip-bg);color:var(--muted);font-weight:700;font-size:var(--fs-xs);
}
/* the latest competition's podium: individual and team side by side */
/* min() so the track can never be wider than the column it sits in: at
   320px a bare minmax(300px,1fr) plus the page padding overflowed, and took
   the whole page sideways with it. */
.podium-cols{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(300px,100%),1fr));gap:14px 28px}
.podium-col h3{margin:0 0 8px;font-size:var(--fs-base);letter-spacing:.02em}
.podium-col .top-scope{margin:0 0 8px}
@media (min-width:901px){
  /* a hairline between the two boards, but only when both are on screen */
  .podium-col + .podium-col{border-left:1px solid var(--line);padding-left:28px}
}

.plat-foot{margin-top:26px;text-align:center;color:var(--muted);font-size:var(--fs-xs)}

/* ---- live now ----------------------------------------------------------- */
/* A competition running today is the one thing on this page worth interrupting
   for, so it gets the only saturated badge and the only moving pixel. */
/* flex:0 1 auto, NOT flex:none. A live card carries two badges, and in Russian
   that is "Идёт сейчас" beside "Регистрация закрыта" — with flex-shrink at 0 the
   pair could not be squeezed, so its own flex-wrap never fired and the second
   badge ran off under the card's overflow:hidden on a phone. */
.ev-badges{display:flex;align-items:center;gap:6px;flex-wrap:wrap;flex:0 1 auto;min-width:0}
/* --live, not --accent: an event card overrides --accent with its own brand
   colour, which turned "Live" blue on one card and red on the next. Whether a
   competition is running is a platform status, not part of anyone's branding,
   so it has to look the same on every card. */
.badge-live{
  display:inline-flex;align-items:center;gap:6px;
  background:var(--live);border:1px solid var(--live);color:#fff;
}
.badge-live .dot{
  width:6px;height:6px;border-radius:50%;background:#fff;flex:none;
  animation:live-pulse 1.6s ease-in-out infinite;
}
@keyframes live-pulse{0%,100%{opacity:1}50%{opacity:.25}}
.ev-card.is-live{
  border-color:#2c5a7f;                                 /* fallback for the line below */
  border-color:color-mix(in srgb,var(--accent) 40%,var(--line));
}
.ev-card.is-live .ev-date{
  background:rgba(56,182,255,.08);
  background:color-mix(in srgb,var(--accent) 8%,transparent);
}
.ev-group.is-live .ev-group-head h3{color:var(--accent-ink)}
/* the live count on the hero, and the live tab in the filter */
.plat-stat.is-live .k::before,.seg button.has-live .dot{
  content:"";display:inline-block;width:6px;height:6px;border-radius:50%;
  background:var(--live);margin-right:7px;vertical-align:middle;
  animation:live-pulse 1.6s ease-in-out infinite;
}
@media (prefers-reduced-motion:reduce){
  .badge-live .dot,.plat-stat.is-live .k::before,.seg button.has-live .dot{animation:none}
}

/* ---- "see all", and the page it leads to -------------------------------- */
.see-all-link{display:inline-flex;align-items:center;gap:5px;font-size:var(--fs-sm);font-weight:700;color:var(--accent-ink)}
.see-all-link:hover{text-decoration:none;filter:brightness(1.15)}
.see-all-link svg{width:15px;height:15px}
.see-all-btn{
  display:flex;align-items:center;justify-content:center;gap:9px;
  margin-top:14px;font-size:var(--fs-base);color:var(--text);
}
.see-all-btn:hover{border-color:var(--line-strong);text-decoration:none}
.see-all-btn .n{
  font-size:var(--fs-xs);font-weight:800;color:var(--muted);background:var(--chip-bg);
  border-radius:var(--radius-pill);padding:2px 8px;font-variant-numeric:tabular-nums;
}
.see-all-btn svg{width:16px;height:16px;color:var(--muted)}
.see-all-btn:hover svg{color:var(--accent-ink)}

.page-head{margin:0}
.page-head h1{margin:0;font-size:var(--fs-4xl);line-height:1.15;letter-spacing:-.3px}
.page-head p{margin:6px 0 0;font-size:var(--fs-sm)}

/* the brand mark: chevron on final over the landing target */
.brand .brand-mark{width:27px;height:27px;flex:none;display:block}

@media (max-width:900px){
  .plat-hero{padding:34px 26px 0}
  .plat-hero h1{font-size:var(--fs-4xl)}
  .plat-stats{margin:26px -26px 0;padding:17px 26px}
  .plat-features{grid-template-columns:1fr}
}
@media (max-width:640px){
  .plat-hero{padding:26px 18px 0;border-radius:16px}
  .plat-hero h1{font-size:var(--fs-3xl)}
  .plat-lead{font-size:var(--fs-base)}
  .plat-hero::before{right:-170px;top:-150px}
  .plat-stats{grid-template-columns:1fr 1fr;gap:16px;margin:22px -18px 0;padding:15px 18px}
  .plat-stat .n{font-size:var(--fs-3xl)}
  .plat-toolbar{align-items:flex-start}
  .plat-tools{width:100%}
  .plat-search{width:100%}
  .seg{width:100%}
  .seg button{flex:1;text-align:center;padding:9px 6px}
  .ev-grid,.plat-skeleton{grid-template-columns:1fr}
}

/* ==========================================================================
   Console identity — scope picker and the forced password change.
   ========================================================================== */
/* minmax(0,1fr), not the implicit 1fr: a grid track's automatic minimum is its
   content, so one long line (a competitor, a club and a timestamp on one row)
   widened the track, the row and the whole page with it. */
.scope-list{display:grid;grid-template-columns:minmax(0,1fr);gap:8px;margin-top:6px}
.scope-item{
  display:flex;align-items:center;justify-content:space-between;gap:12px;width:100%;
  min-width:0;max-width:100%;
  padding:12px 14px;border:1px solid var(--line);border-radius:12px;
  background:var(--surface-2);cursor:pointer;text-align:left;font:inherit;color:inherit;
}
.scope-item > .badge{flex:none}
.scope-item:hover{border-color:var(--accent)}
/* A row holds a name, an address and a role, and on a phone the three of them
   are wider than the screen. A flex item will not shrink below its content
   without min-width:0 — without it the whole row grew past the viewport and
   took the page's horizontal scrollbar with it. */
.scope-item > span:first-child{min-width:0;flex:1 1 auto}
.scope-item .s-name,.scope-item .s-meta{
  display:block;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.scope-item .s-name{font-weight:800}
.scope-item .s-meta{font-size:var(--fs-sm);color:var(--muted)}
.scope-role{font-size:var(--fs-2xs);font-weight:800;letter-spacing:.04em;text-transform:uppercase;color:var(--muted);white-space:nowrap;flex:none}
/* the role chips a club's people list carries, on the same row */
.scope-item .grant-chips{flex:0 1 auto;min-width:0}
/* ---- the console two levels -------------------------------------------- */
/* ADMIN level: three segments of five rows, over the same .scope-item rows the
   competition picker used. EVENT level: the numbers of one competition. */
/* ---- the console's masthead ----------------------------------------------
   The public pages' hero, on every page of the console, so a screen inside
   a competition says which competition without being asked. The badges, the
   facts and the actions under it belong to the Information page alone —
   they were a card directly under the header saying the same thing. */
.adm-hero{margin-bottom:16px}
.adm-hero .dash-who{color:#fff;opacity:.86}
.adm-hero .hero-badges{display:flex;flex-wrap:wrap;gap:8px;margin-top:14px}
.adm-hero .details{
  margin-top:16px;gap:12px 20px;
  grid-template-columns:repeat(auto-fit,minmax(min(190px,100%),1fr));
}
.adm-hero .detail .k{color:rgba(255,255,255,.7)}
.adm-hero .detail .v{color:#fff;font-size:var(--fs-md)}
.adm-hero .plat-cta{margin-top:18px}
/* On paper it is a heading, not a banner: the competition's name at the top
   of whatever was printed, in ink. */
@media print{
  .adm-hero{background:none;color:#111;border:0;box-shadow:none;padding:0 0 10px}
  .adm-hero .detail .k{color:#555}
  .adm-hero .detail .v{color:#111}
}

.dash-who{margin-top:7px}
.dash-seg{margin-top:16px}
/* the numbers, then the two panels, then the lists */
.dash-stats.kpi-row{margin-bottom:14px;grid-template-columns:repeat(auto-fit,minmax(min(172px,100%),1fr))}
/* The chart is the working half and gets the room; the calendar is a glance —
   a month fits in a narrow column and reads better tight than stretched. */
.dash-two{display:grid;grid-template-columns:minmax(0,1fr) minmax(272px,340px);gap:12px;align-items:start}
/* nothing to chart (one competition, or none with entries) — the month takes
   the full width rather than leaving a hole where the bars would be */
.dash-two:has(> [hidden]){grid-template-columns:1fr}
@media (max-width:900px){ .dash-two{grid-template-columns:1fr} }

/* ---- a competition, as a card, in the console --------------------------- */
/* Same anatomy as the public directory's card (rail, tear-off date, title
   line, badges) — the difference is that this one is acted on rather than
   followed, so the title is a button and the controls live in the card. */
.ev-card-admin{cursor:default}
.ev-card-admin:hover{transform:none;box-shadow:var(--shadow-2)}
.ev-card-admin .ev-main{gap:5px;padding:13px 14px}
.ev-card-admin .ev-title{
  border:0;background:none;padding:0;margin:0;cursor:pointer;text-align:left;
  font-family:var(--font-display);font-weight:800;font-size:var(--fs-lg);line-height:1.3;color:var(--text);
}
.ev-card-admin .ev-title:hover{color:var(--accent-ink);text-decoration:underline}
.ev-card-admin .ev-title:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:4px}
.ev-acts{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px;padding-top:9px;border-top:1px solid var(--line)}
.ev-acts .btn{flex:0 0 auto}
/* a status badge and a live badge can share the line; keep them off the title */
.ev-card-admin .ev-badges{justify-content:flex-end}

/* month view: a marked day is a competition, and the names sit under it */
.cal-card{display:flex;flex-direction:column}
.cal-head{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}
.cal-nav{display:flex;align-items:center;gap:4px}
.cal-title{min-width:9.5em;text-align:center;font-weight:800;font-size:var(--fs-base);font-family:var(--font-display)}
.cal-nav .icon-btn{width:30px;height:30px}
.cal-nav .icon-btn svg{width:15px;height:15px}
/* Compact by design: a month is a glance, not a workspace. The cells drop the
   1:1 aspect ratio (which was what made the grid as tall as it was wide) and
   sit on a fixed, readable line height instead. */
.cal-grid{display:grid;grid-template-columns:repeat(7,1fr);gap:2px;margin-top:10px}
.cal-dow{
  text-align:center;font-size:10px;font-weight:800;letter-spacing:.04em;
  color:var(--muted);padding-bottom:2px;text-transform:uppercase;
}
.cal-day{
  position:relative;display:flex;align-items:center;justify-content:center;
  height:28px;border-radius:var(--radius-xs);border:0;background:none;
  font:inherit;font-size:var(--fs-xs);color:var(--text);
  font-variant-numeric:tabular-nums;
}
.cal-day.is-out{visibility:hidden}
.cal-day.is-today{background:var(--chip-bg);font-weight:800}
.cal-day.has-ev{cursor:pointer;font-weight:800;color:var(--accent-ink)}
.cal-day.has-ev:hover{background:rgba(56,182,255,.12);background:color-mix(in srgb,var(--accent) 12%,transparent)}
.cal-day.has-ev:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
.cal-dot{position:absolute;bottom:2px;left:50%;margin-left:-2px;width:4px;height:4px;border-radius:var(--radius-pill);background:var(--accent)}
.cal-list{display:grid;gap:1px;margin-top:10px;padding-top:8px;border-top:1px solid var(--line)}
.cal-list:empty{display:none}
.cal-card .cal-title{min-width:8em;font-size:var(--fs-sm)}
.cal-item{
  display:flex;align-items:center;gap:10px;width:100%;text-align:left;
  padding:7px 8px;border:0;border-radius:var(--radius-xs);background:none;font:inherit;color:inherit;cursor:pointer;
}
.cal-item:hover{background:var(--hover-bg)}
.cal-item:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
.cal-d{
  flex:none;min-width:26px;height:26px;display:inline-flex;align-items:center;justify-content:center;
  border-radius:var(--radius-xs);background:var(--chip-bg);color:var(--accent-ink);font-weight:800;font-size:var(--fs-xs);
}
.cal-n{font-weight:700;font-size:var(--fs-sm);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.cal-item .s-meta{margin-left:auto;flex:none;font-size:var(--fs-xs)}
.seg-actions{display:flex;align-items:center;gap:12px;flex-wrap:wrap}
.seg-stats{display:flex;flex-wrap:wrap;gap:7px;margin:0 0 14px}
.seg-stats:empty{display:none}
.seg-stat{
  display:inline-flex;align-items:center;gap:7px;padding:5px 11px;border-radius:var(--radius-pill);
  background:var(--surface-2);font-size:var(--fs-xs);font-weight:700;color:var(--muted);
}
.seg-stat .n{font-size:var(--fs-sm);font-weight:800;color:var(--text);font-family:var(--font-display)}
/* the roles a user holds, in the cell that keeps its label as a card */
.grant-chips{display:flex;flex-wrap:wrap;gap:6px;justify-content:flex-end}
/* ...but a table column is read down from its heading, which is on the left.
   Only the card, where the label sits opposite the value, wants them right. */
body:not(.view-list) table.rtable .grant-chips{justify-content:flex-start}
/* Row actions wrap rather than demand a column of their own: a cell that
   refuses to wrap takes its width from the cells that can, and the e-mail
   column ends up four characters wide. */
body:not(.view-list) table.rtable td.cell-actions{display:flex;flex-wrap:wrap;gap:6px}
/* the status badges stay on one line; a grant names a competition and cannot */
.grant-chips .badge{white-space:normal}
/* the name in a row is the way into the row: a link, not a whole clickable
   band that opens a page when you meant to press Archive. */
.rowlink{
  border:0;background:none;padding:0;font:inherit;font-weight:800;
  color:var(--text);cursor:pointer;text-align:left;
}
.rowlink:hover{color:var(--accent-ink);text-decoration:underline}
.rowlink:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:4px}
.back-link svg{width:15px;height:15px}
/* who the row is about gets the width it needs before anything else */
table.rtable td.who{min-width:14ch}
.info-badges{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
/* what is waiting for review, on the tab that deals with it */
.tabbar .tab{position:relative}
.tab-n{
  position:absolute;top:5px;left:50%;margin-left:4px;
  min-width:17px;padding:0 5px;border-radius:var(--radius-pill);
  background:var(--accent-solid);color:#fff;font-size:10px;font-weight:800;line-height:17px;
}
.dash-group{margin-top:22px}
.dash-group > h3{
  margin:0 0 10px;font-size:var(--caps-size);font-weight:var(--caps-weight);letter-spacing:var(--caps-track);
  text-transform:uppercase;color:var(--muted);
}

/* the numbers under the shortcuts: two meters and one bar chart. Marks stay
   thin and the value is written beside every one of them, so nothing has to be
   read off a colour or a length alone. */
.dash-h{display:flex;align-items:center;justify-content:space-between;gap:12px}
button.see-all-link{border:0;background:none;padding:0;cursor:pointer;font-family:inherit}
.dash-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(248px,100%),1fr));gap:12px}
.dash-stat,.dash-chart{display:flex;flex-direction:column;padding:16px}
.dash-chart{grid-column:1 / -1}
.ds-k{font-size:var(--caps-size);font-weight:var(--caps-weight);letter-spacing:var(--caps-track);text-transform:uppercase;color:var(--muted)}
.ds-v{margin-top:7px;font-size:var(--fs-4xl);font-weight:800;line-height:1.05;letter-spacing:-.5px;font-family:var(--font-display);font-variant-numeric:tabular-nums}
.ds-of{font-size:var(--fs-lg);font-weight:700;color:var(--muted);margin-left:3px}
.ds-note{margin-top:9px;font-size:var(--fs-xs);color:var(--muted)}
.meter{display:block;height:8px;margin-top:12px;border-radius:var(--radius-pill);background:var(--chip-bg);overflow:hidden}
.meter-fill{
  display:block;height:100%;border-radius:0 999px 999px 0;
  background:linear-gradient(90deg,var(--accent-deep),var(--accent-solid));
  box-shadow:var(--glow);transition:width .3s;
}
/* the plot stops where reading stops: a 1200px-long bar is not more legible */
.barchart{display:grid;gap:8px;margin-top:14px;max-width:680px}
.bc-row{
  display:grid;grid-template-columns:minmax(58px,22%) 1fr auto;align-items:center;gap:12px;
  width:100%;padding:3px 0;border:0;background:none;font:inherit;color:inherit;text-align:left;cursor:pointer;
}
.bc-row:hover .bc-fill{filter:brightness(1.15)}
.bc-row:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:6px}
.bc-label{font-size:var(--fs-sm);color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.barchart.wide .bc-row{grid-template-columns:minmax(90px,40%) 1fr auto}
.bc-bar{display:block;height:10px}
.bc-fill{
  display:block;height:100%;min-width:3px;border-radius:0 5px 5px 0;
  background:linear-gradient(90deg,var(--accent-deep),var(--accent-solid));
  transform-origin:left;animation:lc-grow .7s cubic-bezier(.25,.9,.3,1) backwards;
}
.bc-n{font-size:var(--fs-sm);font-weight:800;font-variant-numeric:tabular-nums;min-width:1.6em;text-align:right;font-family:var(--font-display)}
/* one card per category, each holding the individual and team boards side
   by side — the same two columns the public podium has, so they keep the
   hairline between them. */
#dashTopCats{display:grid;gap:12px}
.dash-cat-h{
  margin:0 0 14px;padding-bottom:10px;border-bottom:1px solid var(--line);
  font-size:var(--fs-md);font-weight:800;
}
/* NOTE the :not(td). This rule was written for the console's scope line and the
   mini-rows, but ".who" is also the class every table gives its competitor /
   team / category cell — so it was landing on <td> as well, in six renderers
   across the public site and the console.
   display:flex on a table cell takes that cell out of the table's layout: it
   stops stretching to the row's height, so its border-bottom is painted right
   under the text instead of along the bottom of the row. One column's underline
   sat a few pixels high in every table on the site. It was also making the
   competitor's name muted grey at 13px, since ".who strong" does not match the
   <b> the renderers emit. */
.who:not(td){display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:var(--fs-sm);color:var(--muted)}
.who:not(td) strong{color:var(--text)}

/* the small "x" that removes a role chip in the users list */
.linkish{
  border:0;background:none;padding:0 0 0 4px;margin:0;cursor:pointer;
  font:inherit;font-weight:800;line-height:1;color:var(--muted);
}
.linkish:hover{color:var(--accent-ink)}
.linkish:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:3px}

/* ---- user management ---------------------------------------------------
   Password fields reuse the login page's .pw-wrap / .pw-toggle pattern; these
   are the extras: generate/copy row, strength read-out, and the scope checklist
   that replaces a single-select when a role can apply to several places. */
.pw-actions{display:flex;align-items:center;gap:8px;margin-top:8px;flex-wrap:wrap}
.pw-meter{font-size:var(--fs-xs);font-weight:700;color:var(--muted)}
.pw-meter.weak{color:var(--err)}
.pw-meter.ok{color:var(--warn)}
.pw-meter.strong{color:var(--ok)}
.btn.copied{background:var(--ok-bg);border-color:var(--ok-line);color:var(--ok)}

.scope-check-head{display:flex;gap:12px;margin:2px 0 6px}
.linkish-btn{
  border:0;background:none;padding:0;margin:0;cursor:pointer;
  font:inherit;font-size:var(--fs-xs);font-weight:700;color:var(--accent-ink);text-decoration:underline;
  text-underline-offset:2px;
}
.linkish-btn:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.scope-checks{
  border:1px solid var(--line);border-radius:10px;
  max-height:230px;overflow-y:auto;padding:4px;background:var(--surface-2);
}
.scope-checks .grp{
  font-size:var(--caps-size);font-weight:var(--caps-weight);letter-spacing:var(--caps-track);text-transform:uppercase;
  color:var(--muted);padding:8px 8px 4px;
}
.scope-checks label{
  display:flex;align-items:center;gap:9px;padding:7px 8px;border-radius:var(--radius-xs);cursor:pointer;
}
.scope-checks label:hover{background:var(--hover-bg)}
.scope-checks input{width:17px;height:17px;flex:none;margin:0;accent-color:var(--accent)}
.scope-checks .s-sub{color:var(--muted);font-size:var(--fs-xs)}
.scope-checks .empty{padding:10px;color:var(--muted);font-size:var(--fs-sm)}

.handover{
  background:var(--surface-2);border:1px solid var(--line);border-radius:10px;
  padding:12px 14px;font-size:var(--fs-sm);line-height:1.7;white-space:pre-wrap;word-break:break-word;margin:0;
}

/* A field switched off for this event. A class, not [hidden]: .field sets its
   own display, which would win over the hidden attribute. */
.is-off{display:none !important}

/* The registration form before its eligibility criteria are answered: the
   criteria card is all there is, and the rest of the form arrives with it. */
#regForm.is-gated > section.card:not(#eligCard){display:none}
#regForm:not(.is-gated) > section.card:not(#eligCard){animation:gate-open .22s ease-out}
@keyframes gate-open{from{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:none}}
@media (prefers-reduced-motion:reduce){
  #regForm:not(.is-gated) > section.card:not(#eligCard){animation:none}
}

/* Registration options: a label and its control on one line, wrapping on narrow
   screens rather than squashing the select down to nothing. */
.ro-fields,.ro-elig{display:grid;gap:6px;margin-top:8px}
.ro-row{
  display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;
  padding:8px 10px;border:1px solid var(--line);border-radius:10px;background:var(--surface-2);
}
/* Name over what the field IS. The key and the type are the two facts that
   decide what a CSV column means six months later, and they used to be
   visible only for a custom field, and only inside a dialog. */
.ro-row .ro-main{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:1px}
.ro-row .ro-name{font-weight:600;min-width:0}
.ro-row .ro-meta{
  font-size:var(--fs-2xs);color:var(--muted);
  letter-spacing:.04em;text-transform:uppercase;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.ro-row select,.ro-row input[type=text]{width:auto;min-width:190px;flex:0 1 auto}
/* An eligibility criterion is named by its own text box — empty means "use the
   built-in wording", which is what the placeholder shows. */
.ro-row .ro-name-in{flex:1 1 220px;font-weight:600}
/* Asked on every entry: nothing to switch, so it reads quieter. */
.ro-row.is-fixed{background:var(--chip-bg)}
.ro-row.is-fixed .ro-name{flex:1 1 auto;min-width:0;color:var(--muted)}
/* A field the event added itself, sitting among the standard ones. */
.ro-row.is-custom{border-style:dashed}
.ro-row.is-custom .ro-name{flex:1 1 auto;min-width:0}
.ro-row .l-code{font-size:var(--fs-2xs);font-weight:800;letter-spacing:.04em;color:var(--muted);flex:none}
.ro-row .rowbtns{flex:none}

/* Three-way toggle (.seg3, NOT .seg — that name belongs to the public
   directory filter, and the two definitions used to fight in the cascade):
   one tap sets a field, and the colour of the chosen segment says what it
   means, so a whole form's worth of rules reads at a glance — grey off, light
   optional, accent required. */
.seg3{display:inline-flex;border:1px solid var(--line-strong);border-radius:var(--radius-pill);overflow:hidden;background:var(--card);flex:0 0 auto}
.seg3 button{
  border:0;background:transparent;cursor:pointer;color:var(--muted);
  font-family:inherit;font-weight:700;font-size:var(--fs-xs);padding:7px 13px;white-space:nowrap
}
.seg3 button + button{border-left:1px solid var(--line)}
.seg3 button:hover{background:var(--hover-bg);color:var(--text)}
.seg3 button.on{background:var(--text);color:var(--bg)}
/* #64748b keeps white 12px/700 text at 4.5:1 */
.seg3 button.on[data-mode="hidden"]{background:#64748b;color:#fff}
.seg3 button.on[data-mode="required"]{background:var(--accent-solid);color:#fff}
.seg3 button:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}

@media (max-width:640px){
  .ro-row{align-items:stretch}
  .ro-row select,.ro-row input[type=text]{min-width:0;width:100%}
  .seg3{width:100%}
  .seg3 button{flex:1 1 0;padding:8px 6px;text-align:center}
}

/* Sub-tabs inside a panel (Settings). A scrolling strip rather than a wrapping
   grid, so the row keeps its shape on a phone instead of becoming two ragged
   lines. */
.subtabs{
  display:flex;gap:4px;overflow-x:auto;margin:0 0 14px;padding:4px;
  background:var(--surface-2);border:1px solid var(--line);border-radius:12px;
  -webkit-overflow-scrolling:touch;scrollbar-width:none;
}
.subtabs::-webkit-scrollbar{display:none}
.subtab{
  flex:0 0 auto;border:0;background:none;cursor:pointer;
  padding:9px 14px;border-radius:var(--radius-xs);
  font:inherit;font-size:var(--fs-base);font-weight:700;color:var(--muted);white-space:nowrap;
}
.subtab:hover{background:var(--hover-bg);color:var(--text)}
.subtab.active{background:var(--accent-solid);color:#fff}
.subtab:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
/* A pane with unsaved changes carries a star, so it can be found from any
   other tab. ::after and not appended text: the labels are rewritten on every
   language switch, which would take an appended star straight off again. */
.subtab.is-dirty::after{content:"*";margin-left:5px;color:var(--warn);font-weight:900}
.subtab.is-dirty.active::after{color:#fff}

/* ==========================================================================
   Instrument charts (LandingCharts, assets/js/charts.js). Pure CSS bars: the
   value is always written beside the mark, so nothing is read off a length
   alone. The sweep-in is the same motion the console's .bc-fill uses.
   ========================================================================== */
.lc{display:grid;gap:9px;margin-top:6px}
.lc-row{display:grid;grid-template-columns:minmax(110px,26%) 1fr auto;align-items:center;gap:12px}
.lc-label{
  display:flex;align-items:baseline;gap:8px;min-width:0;
  font-size:var(--fs-sm);color:var(--text);
}
.lc-label .lc-who{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.lc-rank{flex:none;color:var(--accent-ink);font-weight:800;font-family:var(--font-display);font-variant-numeric:tabular-nums}
.lc-track{height:12px;border-radius:var(--radius-pill);background:var(--chip-bg);overflow:hidden}
.lc-fill{
  display:block;height:100%;min-width:4px;border-radius:inherit;
  background:linear-gradient(90deg,var(--accent-deep),var(--accent-solid));
  transform-origin:left;animation:lc-grow .8s cubic-bezier(.25,.9,.3,1) backwards;
}
.lc-row:first-child .lc-fill{box-shadow:var(--glow)}
.lc-n{
  font-family:var(--font-display);font-weight:700;font-variant-numeric:tabular-nums;
  font-size:var(--fs-sm);text-align:right;min-width:2.4em;
}
@keyframes lc-grow{from{transform:scaleX(0)}}
/* the little "TOP 10" tag beside a chart heading */
.lc-cap{
  flex:none;padding:3px 10px;border-radius:var(--radius-pill);
  border:1px solid var(--line-strong);color:var(--muted);
  font-family:var(--font-display);font-size:var(--fs-2xs);font-weight:800;letter-spacing:.09em;
}
@media (max-width:640px){
  .lc-row{grid-template-columns:1fr auto;row-gap:2px}
  .lc-label{grid-column:1 / -1}
}
@media (prefers-reduced-motion:reduce){
  .lc-fill,.bc-fill{animation:none}
}

/* ==========================================================================
   A COMPETITION'S OWN FRONT PAGE — event.html.

   The public event page used to be a stack of identical cards: a control
   panel a visitor happened to be allowed to read. It is the competition's
   web site now — a hero that answers what / where / when / can I still
   enter, the live numbers on its footing, and a table of contents down the
   page — so everything below is about making one event read as a place
   rather than as a screen.

   It borrows the platform home's language deliberately (the .plat-stat
   tiles, the pill call-to-action row, the glass cards): a visitor moving
   from the directory into one competition must not feel they changed
   products halfway.
   ========================================================================== */

/* ---- the hero, with room under it for the numbers ----------------------- */
.ev-hero{padding:34px 30px 0}
.ev-hero-in{position:relative;z-index:1;max-width:720px}
.ev-hero .eyebrow{margin-bottom:12px}
.ev-hero h1{font-size:var(--fs-display);line-height:1.12;letter-spacing:-.2px}
.ev-hero .tagline{margin-top:12px;max-width:62ch}
.ev-hero .plat-cta{margin-top:22px}

/* What state the competition is in, and whether the door is still open.
   --live, not --accent: whether a competition is running is a platform
   status, not part of anyone's branding — the same argument .badge-live
   makes on the directory cards, where an event's own accent would have
   turned "Live" blue on one card and red on the next. */
.hero-badges{display:flex;flex-wrap:wrap;gap:8px;margin-top:14px}
.hero-badges:empty{display:none}
.hb{
  display:inline-flex;align-items:center;gap:7px;
  padding:5px 12px;border-radius:var(--radius-pill);
  border:1px solid rgba(255,255,255,.2);background:rgba(255,255,255,.08);color:#fff;
  font-family:var(--font-display);
  font-size:var(--fs-xs);font-weight:700;letter-spacing:.08em;text-transform:uppercase;white-space:nowrap;
}
.hb .dot{width:7px;height:7px;border-radius:50%;flex:none;background:currentColor}
.hb.is-live{background:var(--live);border-color:var(--live)}
.hb.is-live .dot{animation:live-pulse 1.6s ease-in-out infinite}
.hb.is-open .dot{background:#4ed492;box-shadow:0 0 0 3px rgba(78,212,146,.16)}
.hb.is-shut,.hb.is-done{opacity:.82}
@media (prefers-reduced-motion:reduce){ .hb.is-live .dot{animation:none} }

/* The poster facts: where, when, and which days are flown — said once, at
   the top, the way a competition's own poster says them. */
.hero-facts{display:flex;flex-wrap:wrap;gap:8px;margin-top:16px}
.hero-facts:empty{display:none}
.hero-fact{
  display:inline-flex;align-items:center;gap:8px;max-width:100%;
  padding:6px 13px;border-radius:var(--radius-pill);
  border:1px solid rgba(255,255,255,.18);background:rgba(255,255,255,.06);
  font-size:var(--fs-sm);color:#fff;
}
.hero-fact svg{width:15px;height:15px;flex:none;opacity:.75}
.hero-fact span{min-width:0;overflow:hidden;text-overflow:ellipsis}

/* The live numbers, on the hero's own footing rather than in a card of
   their own — the same tiles the platform home uses, so the directory and
   one competition count in one voice. .plat-stats carries the grid; only
   the bleed out to the hero's own padding is this page's. */
.ev-stats{margin:30px -30px 0;padding:18px 30px}

/* ---- the page's own table of contents ----------------------------------- */
/* Sticky under the top bar. It is what turns eight stacked cards into a
   site: a visitor sees at a glance that this competition HAS a schedule,
   documents and a gallery without scrolling the page to find out. Built
   from the sections actually on the page — see renderNav() in eventhome.js.
   A hidden section must never be linked to. */
.ev-nav{
  /* --topbar-h and --jump-pad are MEASURED off the drawn page (see
     measureBars in eventhome.js): the top bar is shorter on a phone and the
     contents list is two lines tall in a language with long words, so a
     hand-kept number here goes stale the first time either changes. The
     fallbacks are what those two measure to at desktop width. */
  position:sticky;top:var(--topbar-h,57px);z-index:30;
  display:flex;gap:4px;margin:16px 0 0;padding:6px;
  border:1px solid var(--line);border-radius:var(--radius-pill);
  background:var(--glass);
  -webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px);
  box-shadow:var(--shadow-1);
  /* The row must be ALLOWED to be narrower than the links it holds, or it
     is laid out at its content width and takes the whole page sideways —
     the trap .chips fell into on a phone. overflow-x does the scrolling;
     max-width is what lets it happen. */
  overflow-x:auto;scrollbar-width:none;max-width:100%;
}
.ev-nav::-webkit-scrollbar{display:none}
.ev-nav[hidden]{display:none}
.ev-nav a{
  flex:0 0 auto;padding:9px 15px;border-radius:var(--radius-pill);white-space:nowrap;
  font-family:var(--font-display);font-size:var(--fs-sm);font-weight:700;color:var(--muted);
}
.ev-nav a:hover{background:var(--hover-bg);color:var(--text);text-decoration:none}
.ev-nav a.is-here{background:var(--accent-soft);color:var(--accent-ink)}
/* A jump has to land under BOTH sticky bars, not behind them. */
html:has(.ev-nav){scroll-behavior:smooth;scroll-padding-top:var(--jump-pad,126px)}
@media (prefers-reduced-motion:reduce){ html:has(.ev-nav){scroll-behavior:auto} }
.ev-sec{scroll-margin-top:var(--jump-pad,126px)}

/* ---- inside a section --------------------------------------------------- */
/* Two things side by side wherever there is room for two: the newest
   entries beside the clubs they came from, the individual podium beside
   the team one. Below 900px it is one column, and the rule between them
   would be a line drawn across the page, so it goes. */
.ev-split{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(300px,100%),1fr));gap:18px 28px}
@media (min-width:901px){
  .ev-split > * + *{border-left:1px solid var(--line);padding-left:28px}
}
.ev-subhead{margin:0 0 10px;font-size:var(--fs-base);letter-spacing:.02em}
/* A sub-head that shares its row with a button is already spaced by the
   row; a second bottom margin under it would open a gap the card head
   does not have anywhere else on the site. */
.card-head .ev-subhead{margin:0}
/* The scope chip hangs under the heading here rather than pulling up under
   a card title — the same correction .podium-col makes. */
.ev-split .top-scope{margin:0 0 8px}
.ev-sec .section-label:first-child{margin-top:0}
.detail.full{grid-column:1 / -1}
/* The organizer's own words are a different KIND of thing from the grid
   above them, so they are separated the way two cards would be — without
   spending a second card on four lines of prose. */
#aboutBlock{margin-top:18px;padding-top:16px;border-top:1px solid var(--line)}
.detail .v .badge + .badge{margin-left:6px}
.detail .v .disc-tag{font-size:var(--fs-lg);font-weight:650}

/* The rule book, read out as a grid rather than as the form that set it. */
.rule-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(230px,100%),1fr));gap:14px 24px}

/* ---- who runs the competition ------------------------------------------- */
/* A name and the job it holds. Nothing here is an account or a person the
   platform knows — it is the jury list a competition posts on its
   noticeboard, which is exactly what db/migrations/014_officials.sql
   stores and what public_events publishes. */
.off-list{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(230px,100%),1fr));gap:10px}
.off-person{
  display:flex;align-items:center;gap:12px;min-width:0;
  padding:11px 13px;border:1px solid var(--line);border-radius:var(--radius-md);background:var(--surface-2);
}
.off-ini{
  flex:none;width:38px;height:38px;border-radius:50%;
  display:inline-flex;align-items:center;justify-content:center;
  background:var(--accent-soft);color:var(--accent-ink);
  font-family:var(--font-display);font-size:var(--fs-sm);font-weight:800;letter-spacing:.04em;
}
.off-who{display:flex;flex-direction:column;gap:1px;min-width:0}
.off-who b{font-size:var(--fs-md);overflow-wrap:anywhere}
.off-sub{margin:20px 0 10px;font-size:var(--fs-base)}

@media (max-width:900px){
  .ev-hero{padding:28px 22px 0}
  .ev-stats{margin:24px -22px 0;padding:16px 22px}
}
@media (max-width:640px){
  .ev-hero{padding:22px 16px 0;border-radius:16px}
  .ev-hero h1{font-size:var(--fs-3xl)}
  /* The way in takes a row of its own; the two secondary ways share the
     next one, rather than three full-width buttons filling the fold. */
  .ev-hero .plat-cta{gap:8px}
  .ev-hero .plat-cta .btn-primary{width:100%}
  /* ...and at 15px rather than 17px, so "Registration QR" is one line
     in a half-width button instead of two. */
  .ev-hero .plat-cta .btn-ghost{flex:1 1 140px;font-size:var(--fs-md);padding:14px 12px}
  .ev-stats{grid-template-columns:1fr 1fr;gap:16px;margin:20px -16px 0;padding:15px 16px}
}

/* On paper the shared print block turns the hero black-on-white, so
   everything that was drawn white-on-dark has to turn with it. These rules
   sit after that block and win on document order. */
@media print{
  .ev-hero{padding:0 0 10px}
  .ev-stats{margin:12px 0 0;padding:12px 0 0;border-top:1px solid var(--line);background:none}
  .hb,.hero-fact{color:#111;background:none;border-color:var(--line-strong)}
  .hb.is-live{color:var(--live);border-color:var(--live)}
  .ev-split > * + *{border-left:0;padding-left:0}
}

/* ==========================================================================
   PHONE POLISH — last in the file on purpose.
   The platform is used at an airfield, on a phone, by somebody wearing
   gloves. These rules come after everything they correct, so they win
   without needing !important, and they are grouped here rather than
   scattered so the mobile shape of the site can be read in one place.
   ========================================================================== */
@media (max-width:640px){
  /* The role this session holds is already in the account menu, and in the
     bar it was the one item that would not shrink — so it took the whole
     page sideways rather than being squeezed. */
  .topbar .admin-tag{display:none}

  /* The console's measure is a desktop one; a phone gets the reading one. */
  body.console{--maxw:100%}

  /* Headline numbers: two per row rather than one long column. */
  .dash-stats,.dash-stats.kpi-row{grid-template-columns:1fr 1fr;gap:8px}
  .dash-stat,.dash-chart{padding:13px}
  .ds-v{font-size:var(--fs-3xl);margin-top:5px}
  .ds-note{margin-top:6px}

  /* A card head that carries a title AND an action stacks rather than
     squeezing the button into two words per line. */
  .card-head{flex-wrap:wrap}
  .card-head .seg-actions{width:100%}
  .card-head .seg-actions .btn{flex:1 1 auto}

  /* Competition cards: the date chip narrows, the actions go full width. */
  .ev-date{width:60px;padding:11px 4px}
  .ev-date .d{font-size:var(--fs-2xl)}
  .ev-card-admin .ev-main{padding:11px 12px}
  .ev-card-admin .ev-badges{justify-content:flex-start}
  .ev-acts .btn{flex:1 1 auto;justify-content:center}

  /* Console rows stack their chips under the name instead of beside it. */
  .scope-item{flex-wrap:wrap}
  .scope-item .grant-chips{width:100%;justify-content:flex-start}

  /* Dialogs behave like sheets: full width, and clear of the notch. */
  .modal-backdrop{padding:12px 10px calc(12px + env(safe-area-inset-bottom))}
  .modal{padding:16px;border-radius:16px}
  .modal-foot{flex-direction:column-reverse;gap:8px}
  .modal-foot .btn{width:100%}

  /* Five tabs on a 390px bar: let the labels use the room they have. */
  .tabbar .tab{padding:7px 2px 6px;font-size:10px}
  .tabbar .tab-ico{width:21px;height:21px}

  /* A filter row scrolls rather than wrapping into four ragged lines. */
  .chips{flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none;padding-bottom:2px}
  .chips::-webkit-scrollbar{display:none}
  .chips .chip{flex:0 0 auto}

  /* ...and the scroller has to be ALLOWED to be narrower than the chips it
     holds. Uncapped it is laid out at its CONTENT width — 368px inside a
     332px column — so the row built to scroll sideways pushed the whole PAGE
     7px wide instead, putting a horizontal scrollbar under every list on a
     phone. max-width is what does it, and the obvious flex answers do not:
     min-width:0 and flex:1 1 0 both leave the box sized by its content here,
     because what is over-wide is the flex LINE, not this item's share of it.
     overflow-x:auto above is untouched, so the chips still scroll. */
  .chips{max-width:100%}

  /* The results toolbar wraps its buttons. It could not: flex:0 0 auto sizes
     the box to its own widest line — 385px inside a 390px viewport — and a
     box sized to its own content never runs out of room, so flex-wrap had
     nothing to react to and "Results QR code" hung 24px off the right edge.
     Let it shrink and the wrap it already asks for does the rest. Both flex
     ancestors kept min-width:auto, which would hold the row open again. */
  .toolbar-actions{flex:0 1 auto;min-width:0}
  .toolbar-main,.toolbar-filters{min-width:0}


  /* Two big touch targets, not two small ones. */
  .land-grid{max-width:none}
  .land-sq{min-height:88px}

  /* The account/settings toolbars wrap instead of overflowing. */
  .pw-actions .btn{flex:1 1 auto}

  /* THE TOP BAR MUST NOT WRAP. It is one line by construction — brand and
     trail on the left, controls on the right — and letting the controls wrap
     dropped the language pill and the avatar on top of the competition's
     name. The controls keep their size; the trail is what gives way, which
     is right: it is the part that can be shortened and still be read. */
  .topbar{flex-wrap:nowrap;padding:9px 12px}
  .topbar-controls{flex:none;flex-wrap:nowrap;gap:6px}
  .brandbar{flex:1 1 auto;min-width:0;overflow:hidden}
  .brandbar .brand span{max-width:100%}
  .theme-toggle,.icon-btn,.avatar-btn{width:32px;height:32px}
}
/* Very narrow AND showing a trail: the wordmark stands down so the thing you
   are actually looking at has the room. The mark stays — it is the way home.
   With no trail (the platform home, the directory) the wordmark is all there
   is, so it is left alone. */
@media (max-width:420px){
  .brandbar:has(.brand-event:not([hidden])) > .brand:first-child > span{display:none}
  .brandbar:has(.brand-event:not([hidden])) .brand-sep{display:none}
}

/* Tablets: the dock is the public nav here too, and the console's KPI row
   fits four across without becoming unreadable. */
@media (min-width:641px) and (max-width:900px){
  .dash-stats.kpi-row{grid-template-columns:repeat(auto-fit,minmax(150px,1fr))}
}
