/* ============================================================================
   PixelSchoolWork — Design system
   ============================================================================

   Vanilla CSS, no build step. Layered primitives -> semantic tokens -> themes ->
   base -> components -> utilities; only the semantic layer is ever redefined by
   a theme, and a component variant only remaps local custom properties rather
   than restating a property block.

   Direction "Clair": Lexend everywhere, a pale blue-grey ground, white rounded
   cards, one blue action per screen, and the brand waves (blue, green, night)
   for the moments that matter — loading, the end of a game. Drawn for a phone
   held by someone who does not use computers much: large text, 48-56px
   targets, words next to every icon.

   Contrast: every text/background pair is checked against WCAG. Light and dark
   meet AA (>= 4.5:1 on body text); daltonism meets AAA (>= 7:1) and never
   encodes meaning with hue alone — semantic components always carry an icon.
   ========================================================================= */


/* Lexend, served from static/fonts (SIL Open Font License). Variable: one file
   carries every weight. Local rather than a CDN: the CSP only allows this
   origin, and the page still renders offline. */
@font-face {
  font-family: "Lexend";
  src: url("/fonts/lexend-latin-wght.woff2") format("woff2-variations"),
       url("/fonts/lexend-latin-wght.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}


/* ============================================================================
   1. PRIMITIVES
   ========================================================================= */

:root {
  /* Brand blue. --blue-500 is the logo's #3574f0: it only reaches 4.27:1 under
     white text, so it is decoration (logo, waves) and filled controls use
     --blue-600, which reaches 5.4:1. */
  --blue-50:  #f1f5fe;
  --blue-100: #e6eefd;
  --blue-200: #bcd1fa;
  --blue-300: #8fb2f7;
  --blue-400: #5b8ff3;
  --blue-500: #3574f0;
  --blue-600: #2a63d9;
  --blue-700: #1f4fb5;
  --blue-800: #1d3a7d;
  --blue-900: #14254d;

  /* Brand green. --green-500 is the logo's #5fad65: 2.7:1 on white, so it is
     decoration too; text and fills use --green-700 (5.1:1). */
  --green-50:  #f0f8f1;
  --green-100: #e5f4e6;
  --green-200: #c6e6c8;
  --green-300: #a8d8ab;
  --green-400: #7cc281;
  --green-500: #5fad65;
  --green-600: #3f8f46;
  --green-700: #2f7d36;
  --green-800: #24612a;
  --green-900: #17401b;

  /* The colour halfway between the two brand colours: the middle wave. */
  --teal-500: #4a91a8;

  /* Cool neutrals, tinted towards the brand blue. */
  --neutral-0:   #ffffff;
  --neutral-50:  #f8fafd;
  --neutral-100: #f4f7fc;
  --neutral-150: #eef2f9;
  --neutral-200: #e3e9f3;
  --neutral-300: #dbe2ee;
  --neutral-400: #b5c0d6;
  --neutral-500: #7d8aa3;
  --neutral-600: #4a5670;
  --neutral-700: #333d52;
  --neutral-800: #1c2638;
  --neutral-900: #0f1a2e;
  --neutral-950: #0a1220;

  /* Danger is an orange, not a red: next to the brand green, orange keeps a
     lightness gap that red-green blindness does not erase. */
  --orange-100: #fdeee6;
  --orange-300: #f4a27a;
  --orange-600: #c2410c;
  --orange-700: #a8380a;
  --orange-800: #8a2e08;

  --amber-100: #fdf3e0;
  --amber-300: #f5c26b;
  --amber-600: #7a4f00;
  --amber-700: #6b4400;
  --amber-800: #5c3b00;
  --amber-900: #4d3300;

  /* Magenta exists only for the daltonism theme. A red and a green are the
     same colour to a protanope; shifting danger towards magenta keeps a blue
     component that red-green blindness preserves. */
  --magenta-100: #ffe6f2;
  --magenta-700: #a80060;
  --magenta-800: #8a004e;
  --magenta-900: #6b003c;

  /* ---- Non-colour primitives (shared by every theme) ---- */

  --font-sans: "Lexend", system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-mono: ui-monospace, "Cascadia Code", "SF Mono", Consolas, monospace;

  /* Larger than a desktop product on purpose: read on a phone, by people who
     are not used to screens. Nothing a student reads goes under 15px. */
  --text-xs:   0.8125rem; /* 13px — legal, table captions  */
  --text-sm:   0.9375rem; /* 15px — hints, metadata        */
  --text-base: 1.0625rem; /* 17px — body                   */
  --text-md:   1.125rem;  /* 18px — buttons, inputs        */
  --text-lg:   1.25rem;   /* 20px — card titles            */
  --text-xl:   1.625rem;  /* 26px — page titles, questions */
  --text-2xl:  2rem;      /* 32px — hero, score            */

  --weight-normal: 400;
  --weight-medium: 500;
  --weight-bold:   650;

  --leading-tight: 1.25;
  --leading-base:  1.5;

  /* 4px base scale */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;
  --space-7: 48px;
  --space-8: 64px;

  --radius-sm:   8px;
  --radius-md:   14px;  /* controls */
  --radius-lg:   20px;  /* cards    */
  --radius-pill: 999px;

  --border-width: 1px;
  /* Controls draw a heavier line than cards: an input has to look like a box
     you can type into, a card only has to look like a group. */
  --control-border-width: 2px;

  /* Never under 40px; 48px is the default target, 56px the main action. */
  --control-height-sm: 40px;
  --control-height-md: 48px;
  --control-height-lg: 56px;

  --card-padding: 20px;

  --focus-width:  3px;
  --focus-offset: 2px;

  --transition: 140ms ease-in-out;

  --layout-max: 1128px;
  --layout-narrow: 480px;

  /* The bottom navigation, and the safe area under it on a notched phone. */
  --nav-height: 72px;
  --safe-bottom: env(safe-area-inset-bottom, 0px);
}

@media (min-width: 640px) {
  :root { --card-padding: var(--space-5); }
}


/* ============================================================================
   2 + 3. SEMANTIC TOKENS AND THEMES

   Three mutually exclusive themes. data-theme is written on <html> by theme.js
   before first paint; the light values below are the fallback if JS is off.
   ========================================================================= */

:root,
[data-theme="light"] {
  color-scheme: light;

  --c-bg:            var(--neutral-100);
  --c-surface:       var(--neutral-0);
  --c-surface-sunken: var(--neutral-50);
  --c-surface-hover: var(--neutral-150);
  --c-overlay:       rgb(15 26 46 / 0.55);

  --c-border:        var(--neutral-300);
  /* neutral-500: WCAG asks 3:1 for the outline of an interactive component,
     and it reaches 3.5:1 on white. */
  --c-border-strong: var(--neutral-500);

  --c-text:          var(--neutral-900);
  --c-text-muted:    var(--neutral-600);
  --c-text-inverse:  var(--neutral-0);

  /* Semantic families. Each exposes the same five slots so components can be
     recoloured by swapping one variable instead of restating a rule. */
  --c-primary:          var(--blue-600);   /* filled surface              */
  --c-primary-hover:    var(--blue-700);
  --c-primary-active:   var(--blue-800);
  --c-primary-on:       var(--neutral-0);  /* text/icon on filled surface */
  --c-primary-text:     var(--blue-600);   /* same family, as text/border */
  --c-primary-subtle:   var(--blue-100);   /* tinted background           */

  --c-danger:           var(--orange-600);
  --c-danger-hover:     var(--orange-700);
  --c-danger-active:    var(--orange-800);
  --c-danger-on:        var(--neutral-0);
  --c-danger-text:      var(--orange-700);
  --c-danger-subtle:    var(--orange-100);

  --c-success:          var(--green-700);
  --c-success-hover:    var(--green-800);
  --c-success-active:   var(--green-900);
  --c-success-on:       var(--neutral-0);
  --c-success-text:     var(--green-700);
  --c-success-subtle:   var(--green-100);

  --c-warning:          var(--amber-700);
  --c-warning-hover:    var(--amber-900);
  --c-warning-active:   var(--amber-900);
  --c-warning-on:       var(--neutral-0);
  --c-warning-text:     var(--amber-700);
  --c-warning-subtle:   var(--amber-100);

  --c-info:             var(--blue-700);
  --c-info-hover:       var(--blue-800);
  --c-info-active:      var(--blue-900);
  --c-info-on:          var(--neutral-0);
  --c-info-text:        var(--blue-700);
  --c-info-subtle:      var(--blue-100);

  --c-focus: var(--blue-600);

  /* Brand. Decoration only — the logo, the waves, the night bands — never a
     colour text has to be read in, except white on --c-brand-night. */
  --c-brand-night:   var(--blue-900);
  --c-brand-on-night: #c9d4ec;
  --c-brand-blue:    var(--blue-500);
  --c-brand-teal:    var(--teal-500);
  --c-brand-green:   var(--green-500);
  --c-brand-mist:    var(--blue-200);

  /* The XP bar: green, one step darker than the brand so the fill keeps 3:1
     against its track. */
  --c-xp:            var(--green-600);
  --c-xp-track:      var(--neutral-200);

  --shadow-sm: 0 1px 2px rgb(15 26 46 / 0.08);
  --shadow-md: 0 2px 4px rgb(15 26 46 / 0.08), 0 6px 16px rgb(15 26 46 / 0.06);
  --shadow-lg: 0 10px 32px rgb(15 26 46 / 0.18);
}

[data-theme="dark"] {
  color-scheme: dark;

  --c-bg:            #0c1424;
  --c-surface:       #152036;
  --c-surface-sunken: #0f1a2e;
  --c-surface-hover: #1e2b45;
  --c-overlay:       rgb(0 0 0 / 0.65);

  --c-border:        #283753;
  --c-border-strong: #66779a;

  --c-text:          #eef2fa;
  --c-text-muted:    #a9b5cc;
  --c-text-inverse:  var(--neutral-950);

  /* On dark surfaces the light steps carry the colour and dark text sits on
     the fill. */
  --c-primary:          var(--blue-400);
  --c-primary-hover:    var(--blue-300);
  --c-primary-active:   var(--blue-200);
  --c-primary-on:       var(--neutral-950);
  --c-primary-text:     var(--blue-300);
  --c-primary-subtle:   rgb(91 143 243 / 0.18);

  --c-danger:           var(--orange-300);
  --c-danger-hover:     #f7b896;
  --c-danger-active:    #facdb6;
  --c-danger-on:        var(--neutral-950);
  --c-danger-text:      var(--orange-300);
  --c-danger-subtle:    rgb(244 162 122 / 0.16);

  --c-success:          var(--green-400);
  --c-success-hover:    var(--green-300);
  --c-success-active:   var(--green-200);
  --c-success-on:       var(--neutral-950);
  --c-success-text:     var(--green-400);
  --c-success-subtle:   rgb(124 194 129 / 0.16);

  --c-warning:          var(--amber-300);
  --c-warning-hover:    #f8d18f;
  --c-warning-active:   #fadfb3;
  --c-warning-on:       var(--neutral-950);
  --c-warning-text:     var(--amber-300);
  --c-warning-subtle:   rgb(245 194 107 / 0.16);

  --c-info:             var(--blue-300);
  --c-info-hover:       var(--blue-200);
  --c-info-active:      var(--blue-100);
  --c-info-on:          var(--neutral-950);
  --c-info-text:        var(--blue-300);
  --c-info-subtle:      rgb(143 178 247 / 0.16);

  --c-focus: var(--blue-300);

  --c-brand-night:   #0a1430;
  --c-brand-on-night: #c9d4ec;
  --c-brand-mist:    #2a4680;

  --c-xp:            var(--green-400);
  --c-xp-track:      #283753;

  --shadow-sm: 0 1px 2px rgb(0 0 0 / 0.5);
  --shadow-md: 0 2px 4px rgb(0 0 0 / 0.5), 0 6px 16px rgb(0 0 0 / 0.35);
  --shadow-lg: 0 10px 32px rgb(0 0 0 / 0.6);
}

/* Daltonism / high-contrast theme.
   Two rules drive it.

   1. Every pair clears AAA (7:1).
   2. The semantic families stay apart under protanopia, deuteranopia AND
      tritanopia: danger is a magenta (red-green blindness preserves its blue
      component), success is pushed dark so it separates from warning on the
      lightness axis, warning lighter for the same reason.

   Pale tinted backgrounds cannot be told apart under any simulation, which is
   why every semantic component also carries an icon and a thick border in the
   strong colour. Borders and focus rings are thickened so shape carries
   information too. */
[data-theme="daltonism"] {
  color-scheme: light;

  --c-bg:            var(--neutral-0);
  --c-surface:       var(--neutral-0);
  --c-surface-sunken: #f2f2f2;
  --c-surface-hover: #e6e6e6;
  --c-overlay:       rgb(0 0 0 / 0.7);

  --c-border:        #000000;
  --c-border-strong: #000000;

  --c-text:          #000000;
  --c-text-muted:    #333333;
  --c-text-inverse:  #ffffff;

  /* blue-800 under white: 10.8:1. */
  --c-primary:          var(--blue-800);
  --c-primary-hover:    var(--blue-900);
  --c-primary-active:   #000000;
  --c-primary-on:       #ffffff;
  --c-primary-text:     var(--blue-800);
  --c-primary-subtle:   #e0e9ff;

  --c-danger:           var(--magenta-700);
  --c-danger-hover:     var(--magenta-800);
  --c-danger-active:    var(--magenta-900);
  --c-danger-on:        #ffffff;
  /* Darker than the fill: the fill is only 7.41:1 on white, so text sitting on
     the tinted background needs its own step to stay above 7:1. */
  --c-danger-text:      var(--magenta-800);
  --c-danger-subtle:    var(--magenta-100);

  --c-success:          var(--green-900);
  --c-success-hover:    #0c2a10;
  --c-success-active:   #000000;
  --c-success-on:       #ffffff;
  --c-success-text:     var(--green-900);
  --c-success-subtle:   var(--green-100);

  --c-warning:          var(--amber-600);
  --c-warning-hover:    var(--amber-800);
  --c-warning-active:   var(--amber-900);
  --c-warning-on:       #ffffff;
  --c-warning-text:     var(--amber-800);
  --c-warning-subtle:   #fff2d9;

  --c-info:             var(--blue-900);
  --c-info-hover:       #0b1630;
  --c-info-active:      #000000;
  --c-info-on:          #ffffff;
  --c-info-text:        var(--blue-900);
  --c-info-subtle:      #e0e9ff;

  --c-focus: #000000;

  --c-brand-night:   #000000;
  --c-brand-on-night: #ffffff;

  --c-xp:            var(--green-900);
  --c-xp-track:      #d9d9d9;

  --border-width: 2px;
  --focus-width:  4px;

  --shadow-sm: none;
  --shadow-md: none;
  --shadow-lg: 0 0 0 2px #000000;
}


/* ============================================================================
   4. BASE
   ========================================================================= */

*,
*::before,
*::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--c-bg);
  color: var(--c-text);
  font-family: var(--font-sans);
  font-size: var(--text-base);
  line-height: var(--leading-base);
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4 {
  margin: 0 0 var(--space-3);
  line-height: var(--leading-tight);
  font-weight: var(--weight-bold);
  letter-spacing: -0.01em;
  text-wrap: balance;
}

h1 { font-size: var(--text-xl); }
h2 { font-size: var(--text-lg); }
h3 { font-size: var(--text-md); }
h4 { font-size: var(--text-base); }

p { margin: 0 0 var(--space-3); }

/* Underlined at rest: someone who does not use computers much does not hover
   to find out what is clickable. Components that are links but do not look
   like text (buttons, rows, menu items) remove it themselves. */
a {
  color: var(--c-primary-text);
  font-weight: var(--weight-medium);
  text-decoration: underline;
  text-decoration-thickness: 1.5px;
  text-underline-offset: 3px;
}

a:hover { text-decoration-thickness: 2.5px; }

small { font-size: var(--text-xs); }

code, pre, kbd { font-family: var(--font-mono); }

hr {
  border: 0;
  border-top: var(--border-width) solid var(--c-border);
  margin: var(--space-5) 0;
}

/* Single focus treatment for the whole system. Keyboard only, so pointer users
   never see a ring they did not ask for. */
:focus-visible {
  outline: var(--focus-width) solid var(--c-focus);
  outline-offset: var(--focus-offset);
  border-radius: var(--radius-sm);
}

[hidden] { display: none !important; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}


/* ============================================================================
   5. COMPONENTS
   ========================================================================= */

/* ---------- Layout ---------- */

.page {
  max-width: var(--layout-max);
  margin: 0 auto;
  padding: var(--space-5) var(--space-4) var(--space-8);
}

.page-narrow {
  max-width: var(--layout-narrow);
  margin: var(--space-4) auto var(--space-6);
  padding: 0 var(--space-4);
}

@media (min-width: 640px) {
  .page-narrow { margin-block: var(--space-7); }
}

/* A screen that has several things to show side by side rather than one column
   of them. Still bounded: past this, lines get too long to read. */
.page-wide {
  max-width: 1440px;
  margin: var(--space-7) auto;
  padding: 0 var(--space-4);
}

.stack     { display: flex; flex-direction: column; gap: var(--space-3); }
.stack-lg  { display: flex; flex-direction: column; gap: var(--space-5); }
.row       { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
.row-tight { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.spacer    { flex: 1; }


/* ---------- Icon ----------
   <svg class="icon" aria-hidden="true"><use href="/img/icons.svg#x"></use></svg>
   Sizes follow the text (em), colour follows currentColor. */

.icon {
  width: 1.25em;
  height: 1.25em;
  flex: none;
  vertical-align: -0.2em;
  stroke: currentColor;
  fill: none;
  pointer-events: none;
}

.icon-sm { width: 1em;    height: 1em; }
.icon-lg { width: 1.5em;  height: 1.5em; }
.icon-xl { width: 2.5em;  height: 2.5em; }

/* Multi-colour marks (the Google logo). Same box as .icon, but the symbol
   keeps the fills it declares instead of following currentColor. */
.icon-color {
  width: 1.25em;
  height: 1.25em;
  flex: none;
  vertical-align: -0.2em;
  pointer-events: none;
}


/* ---------- Buttons ----------
   One rule, four local variables. Every variant only remaps the variables,
   which is why there is no duplicated property block below. */

.btn {
  --btn-bg:           var(--c-primary);
  --btn-fg:           var(--c-primary-on);
  --btn-border:       transparent;
  --btn-bg-hover:     var(--c-primary-hover);
  --btn-bg-active:    var(--c-primary-active);
  --btn-border-hover: transparent;

  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: var(--control-height-md);
  padding: 0 var(--space-5);
  border: var(--control-border-width) solid var(--btn-border);
  border-radius: var(--radius-md);
  background: var(--btn-bg);
  color: var(--btn-fg);
  font: inherit;
  font-weight: var(--weight-medium);
  line-height: 1.15;
  text-align: center;
  white-space: nowrap;
  text-decoration: none;
  cursor: pointer;
  transition: background var(--transition), border-color var(--transition), box-shadow var(--transition);
}

.btn:hover {
  background: var(--btn-bg-hover);
  border-color: var(--btn-border-hover);
  text-decoration: none;
  /* Thickens the outline on hover without shifting layout (LinkedIn's trick) */
  box-shadow: inset 0 0 0 1px var(--btn-border-hover);
}

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

.btn:disabled,
.btn[aria-disabled="true"] {
  opacity: 0.45;
  cursor: not-allowed;
  box-shadow: none;
  background: var(--btn-bg);
  border-color: var(--btn-border);
}

/* Outlined: the family colour on the border and the text, transparent fill */
.btn-secondary {
  --btn-bg:           transparent;
  --btn-fg:           var(--c-primary-text);
  --btn-border:       var(--c-primary-text);
  --btn-bg-hover:     var(--c-primary-subtle);
  --btn-bg-active:    var(--c-primary-subtle);
  --btn-border-hover: var(--c-primary-text);
}

/* Looks like a link: for the way out of a form (Annuler), which must stay
   findable without competing with the action next to it. */
.btn-link {
  --btn-bg:           transparent;
  --btn-fg:           var(--c-primary-text);
  --btn-border:       transparent;
  --btn-bg-hover:     var(--c-primary-subtle);
  --btn-bg-active:    var(--c-primary-subtle);
  --btn-border-hover: transparent;

  text-decoration: underline;
  text-decoration-thickness: 1.5px;
  text-underline-offset: 4px;
}

.btn-link:hover { text-decoration: underline; }

/* Quiet: no chrome until hovered */
.btn-tertiary {
  --btn-bg:           transparent;
  --btn-fg:           var(--c-text-muted);
  --btn-border:       transparent;
  --btn-bg-hover:     var(--c-surface-hover);
  --btn-bg-active:    var(--c-border);
  --btn-border-hover: transparent;
}

.btn-danger {
  --btn-bg:           var(--c-danger);
  --btn-fg:           var(--c-danger-on);
  --btn-bg-hover:     var(--c-danger-hover);
  --btn-bg-active:    var(--c-danger-active);
}

.btn-danger-outline {
  --btn-bg:           transparent;
  --btn-fg:           var(--c-danger-text);
  --btn-border:       var(--c-danger-text);
  --btn-bg-hover:     var(--c-danger-subtle);
  --btn-bg-active:    var(--c-danger-subtle);
  --btn-border-hover: var(--c-danger-text);
}

/* Neutral outlined button, for "sign in with Google" style rows */
.btn-neutral {
  --btn-bg:           var(--c-surface);
  --btn-fg:           var(--c-text);
  --btn-border:       var(--c-border-strong);
  --btn-bg-hover:     var(--c-surface-hover);
  --btn-bg-active:    var(--c-border);
  --btn-border-hover: var(--c-border-strong);
}

.btn-sm { min-height: var(--control-height-sm); padding: 0 var(--space-4); font-size: var(--text-sm); }
.btn-lg { min-height: var(--control-height-lg); padding: 0 var(--space-6); font-size: var(--text-md); }

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

/* Icon-only. Requires aria-label — the cross-shaped delete button is the
   canonical case: .btn .btn-icon .btn-danger-outline + #x */
.btn-icon {
  width: var(--control-height-md);
  min-height: var(--control-height-md);
  padding: 0;
}

.btn-icon.btn-sm { width: var(--control-height-sm); min-height: var(--control-height-sm); }
.btn-icon.btn-lg { width: var(--control-height-lg); min-height: var(--control-height-lg); }

/* Loading state: .btn[data-loading] swaps the label for a spinner.
   The button keeps its width, so the row does not reflow. */
.btn > .spinner { display: none; }

.btn[data-loading] {
  position: relative;
  pointer-events: none;
  opacity: 0.7;
}

.btn[data-loading] > :not(.spinner) { visibility: hidden; }
.btn[data-loading] > .spinner { display: block; position: absolute; }


/* ---------- Form fields ---------- */

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

.label,
.field > label {
  display: block;
  margin-bottom: var(--space-2);
  font-size: var(--text-base);
  font-weight: var(--weight-medium);
  color: var(--c-text);
}

.label-optional {
  margin-left: var(--space-1);
  font-weight: var(--weight-normal);
  color: var(--c-text-muted);
}

.input,
.select,
.textarea {
  width: 100%;
  min-height: var(--control-height-md);
  padding: var(--space-2) var(--space-4);
  border: var(--control-border-width) solid var(--c-border-strong);
  border-radius: var(--radius-md);
  background: var(--c-surface);
  color: var(--c-text);
  font: inherit;
  font-size: var(--text-md);
  transition: border-color var(--transition), box-shadow var(--transition);
}

.textarea { min-height: 96px; resize: vertical; }

.input::placeholder,
.textarea::placeholder { color: var(--c-text-muted); }

.input:hover,
.select:hover,
.textarea:hover { border-color: var(--c-text-muted); }

/* The one field a screen is about (a game code): taller and larger. */
.input-lg { min-height: 60px; font-size: var(--text-xl); }

/* Doubling the border on focus rather than only drawing an outline keeps the
   field readable in the daltonism theme, where hue must not be the only cue. */
.input:focus,
.select:focus,
.textarea:focus {
  outline: var(--focus-width) solid var(--c-focus);
  outline-offset: 0;
  border-color: var(--c-focus);
}

.input:disabled,
.select:disabled,
.textarea:disabled {
  background: var(--c-surface-sunken);
  color: var(--c-text-muted);
  cursor: not-allowed;
}

.input[aria-invalid="true"],
.textarea[aria-invalid="true"],
.select[aria-invalid="true"] {
  border-color: var(--c-danger-text);
  box-shadow: inset 0 0 0 1px var(--c-danger-text);
}

/* The native arrow is replaced by the sprite chevron, so the select matches
   every other icon in the system instead of using a CSS-drawn triangle. */
.select { appearance: none; padding-right: var(--space-6); }

/* Control decorated with an icon (search, email, select chevron, password eye).
   The icon sits on top of the control and never intercepts clicks. */
.input-group { position: relative; display: flex; align-items: center; }

.input-group > .icon {
  position: absolute;
  left: var(--space-3);
  color: var(--c-text-muted);
}

.input-group > .icon-trailing { left: auto; right: var(--space-3); }

.input-group > .icon:not(.icon-trailing) ~ .input,
.input-group > .icon:not(.icon-trailing) ~ .textarea { padding-left: calc(var(--space-3) * 2 + 1.25em); }

.input-group > .btn-icon { position: absolute; right: var(--space-1); }
.input-group:has(> .btn-icon) .input { padding-right: calc(var(--control-height-md) + var(--space-1)); }

.hint {
  margin: var(--space-2) 0 0;
  font-size: var(--text-sm);
  line-height: 1.45;
  color: var(--c-text-muted);
}

/* Error text always ships with its icon: colour alone must not carry meaning */
.field-error {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  margin: var(--space-2) 0 0;
  min-height: 1.1em;
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--c-danger-text);
}

.field-error .icon { width: 18px; height: 18px; }

.field-error:empty { display: none; }


/* ---------- Checkbox, radio, switch ---------- */

.choice {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
  font-size: var(--text-base);
  cursor: pointer;
}

.choice input[type="checkbox"],
.choice input[type="radio"] {
  flex: none;
  width: 22px;
  height: 22px;
  margin: 1px 0 0;
  accent-color: var(--c-primary);
  cursor: pointer;
}

.choice:has(input:disabled) { color: var(--c-text-muted); cursor: not-allowed; }

.switch {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  cursor: pointer;
}

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

.switch-track {
  position: relative;
  width: 48px;
  height: 28px;
  flex: none;
  border: var(--border-width) solid var(--c-border-strong);
  border-radius: var(--radius-pill);
  background: var(--c-surface-sunken);
  transition: background var(--transition), border-color var(--transition);
}

.switch-track::after {
  content: "";
  position: absolute;
  top: 3px;
  left: 3px;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--c-text-muted);
  transition: transform var(--transition), background var(--transition);
}

.switch input:checked + .switch-track {
  background: var(--c-primary);
  border-color: var(--c-primary);
}

.switch input:checked + .switch-track::after {
  transform: translateX(20px);
  background: var(--c-primary-on);
}

.switch input:focus-visible + .switch-track {
  outline: var(--focus-width) solid var(--c-focus);
  outline-offset: var(--focus-offset);
}


/* ---------- Card ---------- */

.card {
  background: var(--c-surface);
  border: var(--border-width) solid var(--c-border);
  border-radius: var(--radius-lg);
  padding: var(--card-padding);
  margin-bottom: var(--space-4);
}

.card-narrow { max-width: var(--layout-narrow); margin: var(--space-7) auto; }

.card-header {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin: calc(var(--card-padding) * -1) calc(var(--card-padding) * -1) var(--space-4);
  padding: var(--space-4) var(--card-padding);
  border-bottom: var(--border-width) solid var(--c-border);
}

.card-header > h2, .card-header > h3 { margin: 0; }


/* ---------- Badge ----------
   Same variable-remapping pattern as .btn. */

.badge {
  --badge-bg: var(--c-surface-hover);
  --badge-fg: var(--c-text-muted);

  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  min-height: 28px;
  padding: 2px var(--space-3);
  border: var(--border-width) solid transparent;
  border-radius: var(--radius-pill);
  background: var(--badge-bg);
  color: var(--badge-fg);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  white-space: nowrap;
}

.badge-primary { --badge-bg: var(--c-primary-subtle); --badge-fg: var(--c-primary-text); }
.badge-success { --badge-bg: var(--c-success-subtle); --badge-fg: var(--c-success-text); }
.badge-warning { --badge-bg: var(--c-warning-subtle); --badge-fg: var(--c-warning-text); }
.badge-danger  { --badge-bg: var(--c-danger-subtle);  --badge-fg: var(--c-danger-text); }
.badge-info    { --badge-bg: var(--c-info-subtle);    --badge-fg: var(--c-info-text); }
/* Filled, for the one figure a row is about: a level, a score. */
.badge-strong  { --badge-bg: var(--c-brand-night);    --badge-fg: var(--neutral-0); }
.badge-xp      { --badge-bg: var(--c-success-subtle); --badge-fg: var(--c-success-text); font-weight: var(--weight-bold); }

/* In the daltonism theme the tinted background is too weak on its own, so the
   badge gains a border that reuses its own text colour. */
[data-theme="daltonism"] .badge { border-color: currentColor; }


/* ---------- Chip (removable tag) ---------- */

.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-1) var(--space-1) var(--space-1) var(--space-3);
  border: var(--border-width) solid var(--c-border-strong);
  border-radius: var(--radius-pill);
  background: var(--c-surface);
  font-size: var(--text-sm);
}

.chip .btn-icon {
  width: 20px;
  min-height: 20px;
  border-radius: 50%;
}


/* ---------- Alert ----------
   Always rendered with an icon. In the daltonism theme hue is not a reliable
   channel, so the outline and the icon are what carry the meaning. */

.alert {
  --alert-bg: var(--c-info-subtle);
  --alert-fg: var(--c-info-text);

  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  margin: var(--space-3) 0;
  border: var(--border-width) solid var(--alert-fg);
  border-radius: var(--radius-md);
  background: var(--alert-bg);
  color: var(--alert-fg);
  font-size: var(--text-base);
}

.alert .icon { margin-top: 2px; }
.alert-body { flex: 1; }
.alert-body > :last-child { margin-bottom: 0; }
.alert-title { font-weight: var(--weight-bold); }

.alert.success { --alert-bg: var(--c-success-subtle); --alert-fg: var(--c-success-text); }
.alert.warning { --alert-bg: var(--c-warning-subtle); --alert-fg: var(--c-warning-text); }
.alert.error   { --alert-bg: var(--c-danger-subtle);  --alert-fg: var(--c-danger-text); }
.alert.info    { --alert-bg: var(--c-info-subtle);    --alert-fg: var(--c-info-text); }

[data-theme="daltonism"] .alert { border-color: currentColor; border-width: 3px; }


/* ---------- Toast ---------- */

.toast-stack {
  position: fixed;
  left: var(--space-4);
  right: var(--space-4);
  bottom: calc(var(--space-4) + var(--safe-bottom));
  z-index: 60;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

/* Clear of the bottom navigation, which would otherwise sit on top of it. */
.has-app-nav .toast-stack { bottom: calc(var(--nav-height) + var(--space-3) + var(--safe-bottom)); }

@media (min-width: 640px) {
  .toast-stack { left: auto; max-width: 400px; }
}

.toast {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  border: var(--border-width) solid var(--c-border);
  border-radius: var(--radius-md);
  background: var(--c-surface);
  color: var(--c-text);
  box-shadow: var(--shadow-lg);
  align-items: center;
}

.toast .icon { color: var(--c-primary-text); margin-top: 2px; }
.toast.error .icon { color: var(--c-danger-text); }


/* ---------- Table ---------- */

.table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-sm);
}

.table th,
.table td {
  text-align: left;
  padding: var(--space-3);
  border-bottom: var(--border-width) solid var(--c-border);
}

.table th {
  color: var(--c-text-muted);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
}

.table tbody tr:hover { background: var(--c-surface-hover); }

/* A table wider than the screen scrolls itself. Without this the whole page
   scrolls sideways, and the header goes with it. */
/* Positioned, so it is also the box an absolutely placed child (a .sr-only
   header) is clipped to: without it that child escapes the scroll and
   widens the whole page. */
.table-scroll { position: relative; overflow-x: auto; }

/* Written as .table th/.table td so it outranks the alignment set above,
   instead of leaning on an !important in a utility class. */
.table th.actions,
.table td.actions { text-align: right; white-space: nowrap; }


/* ---------- Tabs ---------- */

.tabs {
  display: flex;
  gap: var(--space-5);
  border-bottom: var(--border-width) solid var(--c-border);
  margin-bottom: var(--space-4);
  /* Eight classes is eight tabs: the strip scrolls rather than wrapping into
     two rows whose underline no longer reads as one line. */
  overflow-x: auto;
}

.tab {
  /* inline-flex so a tab can carry a count next to its name without the
     strip's own gap pushing the two apart. */
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  flex: none;
  white-space: nowrap;
  padding: var(--space-3) 0;
  border: 0;
  min-height: var(--control-height-md);
  border-bottom: 3px solid transparent;
  background: none;
  color: var(--c-text-muted);
  font: inherit;
  font-weight: var(--weight-bold);
  cursor: pointer;
}

.tab:hover { color: var(--c-text); }

.tab[aria-selected="true"] {
  color: var(--c-primary-text);
  border-bottom-color: var(--c-primary-text);
}


/* ---------- Avatar and user chip ---------- */

.avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  flex: none;
  border-radius: 50%;
  background: var(--c-primary-subtle);
  color: var(--c-primary-text);
  font-weight: var(--weight-bold);
  overflow: hidden;
}

.avatar-sm { width: 36px; height: 36px; font-size: var(--text-sm); }
.avatar-lg { width: 64px; height: 64px; font-size: var(--text-xl); }
.avatar img { width: 100%; height: 100%; object-fit: cover; }

.user-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  color: var(--c-text-muted);
  font-size: var(--text-sm);
}


/* ---------- Topbar ---------- */

.topbar {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  min-height: 64px;
  padding: 0 var(--space-3) 0 var(--space-4);
  background: var(--c-surface);
  border-bottom: var(--border-width) solid var(--c-border);
}

@media (min-width: 640px) {
  .topbar { gap: var(--space-3); padding: 0 var(--space-5); }
}

/* ---------- Brand ----------
   The pixel-wave mark and the wordmark, both symbols of the icon sprite:
     <a class="brand" href="/">
       <svg class="brand-mark"><use href="/img/icons.svg#brand-mark"></use></svg>
       <svg class="brand-word" role="img" aria-label="PixelSchoolWork"><use href="/img/icons.svg#brand-word"></use></svg>
     </a>
   The word is drawn in currentColor with "School" in --brand-accent, so it
   follows the theme like text does. Below 375px only the mark is shown: the
   header would not hold both and its menus. */
.brand {
  --brand-accent: var(--c-primary-text);

  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--control-height-md);
  color: var(--c-text);
  text-decoration: none;
}

.brand-mark { width: 32px; height: 32px; flex: none; }

/* 10.54 is the wordmark's own aspect ratio (see the symbol's viewBox). */
.brand-word { display: none; height: 16px; width: calc(16px * 10.54); flex: none; }

@media (min-width: 375px) {
  .brand-word { display: block; }
}

@media (min-width: 640px) {
  .brand-word { height: 20px; width: calc(20px * 10.54); }
}


/* ---------- Modal ---------- */

.modal::backdrop { background: var(--c-overlay); }

/* On a phone a dialog rises from the bottom of the screen, as tall as what it
   holds and never taller than the screen: a two-line confirmation stays a
   small sheet under the thumb, a long form fills the screen and scrolls its
   body, with the actions pinned at the bottom either way. */
.modal {
  width: 100vw;
  max-width: 100vw;
  /* fit-content, not auto: an open dialog is pinned to both edges of the
     screen, and an auto height between two pinned edges fills the screen. */
  height: fit-content;
  max-height: 100dvh;
  margin: auto 0 0;
  padding: 0;
  border: 0;
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  background: var(--c-surface);
  color: var(--c-text);
  box-shadow: var(--shadow-lg);
}

.modal > form {
  display: flex;
  flex-direction: column;
  max-height: 100dvh;
}

.modal-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; }

@media (min-width: 640px) {
  .modal {
    width: min(560px, calc(100vw - var(--space-6)));
    max-height: calc(100dvh - var(--space-7));
    margin: auto;
    border: var(--border-width) solid var(--c-border);
    border-radius: var(--radius-lg);
  }

  .modal > form { max-height: calc(100dvh - var(--space-7)); }
}

.modal-header {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-5);
  border-bottom: var(--border-width) solid var(--c-border);
}

.modal-header h2 { margin: 0; flex: 1; }
.modal-body { padding: var(--space-5); }

.modal-footer {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4) calc(var(--space-3) + var(--safe-bottom));
  border-top: var(--border-width) solid var(--c-border);
}

/* On a phone the button that validates takes the remaining width. */
.modal-footer > .btn:last-child { flex: 1; }

@media (min-width: 640px) {
  .modal-footer { padding: var(--space-4) var(--space-5); }
  .modal-footer > .btn:last-child { flex: none; }
}


/* ---------- Dropdown ---------- */

.menu {
  min-width: 200px;
  padding: var(--space-1);
  margin: 0;
  list-style: none;
  border: var(--border-width) solid var(--c-border);
  border-radius: var(--radius-md);
  background: var(--c-surface);
  box-shadow: var(--shadow-md);
}

.menu-item {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  min-height: var(--control-height-md);
  padding: var(--space-2) var(--space-3);
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--c-text);
  font: inherit;
  font-weight: var(--weight-normal);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}

.menu-item:hover { background: var(--c-surface-hover); text-decoration: none; }
.menu-item.danger { color: var(--c-danger-text); }
.menu-separator { height: var(--border-width); margin: var(--space-1) 0; background: var(--c-border); }

/* A .menu on its own is a list; hanging it off a trigger needs a positioning
   context, which is all .dropdown is. The menu itself is unchanged. */
.dropdown { position: relative; display: inline-flex; }

.dropdown-menu {
  position: absolute;
  top: calc(100% + var(--space-1));
  right: 0;
  z-index: 40;
}

/* Anchored to the trigger's left edge instead, for a menu that would
   otherwise run off the left of the viewport. */
.dropdown-menu.align-left { right: auto; left: 0; }

/* Non-interactive first row of a menu: who is signed in, which role. */
.menu-heading {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  max-width: 240px;
  padding: var(--space-2) var(--space-3);
}

/* An entry that exists but is not available yet. Kept in the menu on purpose:
   removing it would hide that the feature is coming. */
.menu-item[aria-disabled="true"] { color: var(--c-text-muted); cursor: not-allowed; }
.menu-item[aria-disabled="true"]:hover { background: none; }


/* ---------- Breadcrumb and pagination ---------- */

.breadcrumb {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  padding: 0;
  margin: 0 0 var(--space-4);
  list-style: none;
  font-size: var(--text-sm);
  color: var(--c-text-muted);
}

.breadcrumb .icon { color: var(--c-text-muted); }
.breadcrumb [aria-current="page"] { color: var(--c-text); font-weight: var(--weight-bold); }

.pagination { display: flex; align-items: center; gap: var(--space-1); }

.pagination .page-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--control-height-sm);
  height: var(--control-height-sm);
  padding: 0 var(--space-2);
  border-radius: var(--radius-sm);
  color: var(--c-text);
  font-weight: var(--weight-normal);
  text-decoration: none;
}

.pagination .page-link:hover { background: var(--c-surface-hover); text-decoration: none; }

.pagination .page-link[aria-current="page"] {
  background: var(--c-primary);
  color: var(--c-primary-on);
  font-weight: var(--weight-bold);
}


/* ---------- Spinner, progress, skeleton ---------- */

.spinner {
  display: inline-block;
  width: 1.25em;
  height: 1.25em;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: spin 700ms linear infinite;
}

@keyframes spin { to { transform: rotate(360deg); } }

.progress {
  width: 100%;
  height: 10px;
  overflow: hidden;
  border-radius: var(--radius-pill);
  background: var(--c-surface-hover);
  border: var(--border-width) solid transparent;
}

.progress-bar { height: 100%; background: var(--c-primary); }

.skeleton {
  border-radius: var(--radius-sm);
  background: linear-gradient(90deg, var(--c-surface-hover) 25%, var(--c-border) 37%, var(--c-surface-hover) 63%);
  background-size: 400% 100%;
  animation: skeleton 1.4s ease infinite;
}

@keyframes skeleton { 0% { background-position: 100% 50%; } 100% { background-position: 0 50%; } }


/* ---------- Tooltip ---------- */

.tooltip { position: relative; display: inline-flex; }

.tooltip::after {
  content: attr(data-tooltip);
  position: absolute;
  bottom: calc(100% + var(--space-2));
  left: 50%;
  transform: translateX(-50%);
  z-index: 50;
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-sm);
  background: var(--c-text);
  color: var(--c-bg);
  font-size: var(--text-xs);
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--transition);
}

.tooltip:hover::after,
.tooltip:focus-within::after { opacity: 1; }


/* ---------- Info tip ---------- */

/* The (i) itself. Green rather than the muted grey of a tertiary button: it
   offers something to read, and nobody hovers what they cannot see. */
.info-tip { color: var(--c-success-text); }

.info-tip:hover,
.info-tip:focus-visible { color: var(--c-success-hover); }

/* The bubble the (i) opens. A popover, so it is drawn in the top layer: no
   overflow and no stacking context can clip it, which is what an absolutely
   positioned tooltip cannot promise inside a scrolling <dialog>.
   :popover-open is what makes it visible; position and inset are written by
   InfoTip, in viewport coordinates. */
.tip-bubble {
  position: fixed;
  /* The user-agent sheet pins a popover to inset: 0 and centres it with auto
     margins; both have to go before top/left can mean anything. */
  inset: auto;
  margin: 0;
  z-index: 60;
  max-width: min(24rem, calc(100vw - var(--space-6)));
  padding: var(--space-2) var(--space-3);
  border: none;
  border-radius: var(--radius-sm);
  background: var(--c-text);
  color: var(--c-bg);
  font-size: var(--text-xs);
  line-height: 1.45;
  text-align: left;
  /* A sentence, not a label: it wraps. pre-line rather than normal so a tip
     made of several lines keeps its breaks — a list of reasons run together
     into one paragraph is a list nobody counts. */
  white-space: pre-line;
  overflow-wrap: anywhere;
  box-shadow: var(--shadow-lg);
  pointer-events: none;
}


/* ---------- Empty state and divider ---------- */

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

.empty-state .icon-xl { color: var(--c-border-strong); margin-bottom: var(--space-3); }

.divider {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin: var(--space-4) 0;
  color: var(--c-text-muted);
  font-size: var(--text-xs);
}

.divider::before,
.divider::after {
  content: "";
  flex: 1;
  height: var(--border-width);
  background: var(--c-border);
}


/* ---------- Code block ---------- */

.code-box {
  padding: var(--space-3);
  border: var(--border-width) solid var(--c-border);
  border-radius: var(--radius-sm);
  background: var(--c-surface-sunken);
  color: var(--c-text);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  white-space: pre-wrap;
  word-break: break-word;
}


/* ---------- Range ----------
   A slider whose value is shown by whatever sits next to it, never by the
   control: browsers give a range no readable value at all. Pair it with a
   .field-inline label and a .field-value caption.

   The three vendor pseudo-elements are written out separately on purpose.
   Grouping them in one selector list is not a style choice here — an engine
   that does not know one of them drops the whole rule, so the repetition is
   what makes the slider look the same everywhere. */

.range {
  appearance: none;
  -webkit-appearance: none;
  width: 100%;
  height: var(--space-6);
  background: transparent;
  cursor: pointer;
}

.range::-webkit-slider-runnable-track {
  height: 6px;
  border-radius: var(--radius-pill);
  background: var(--c-border);
}

.range::-moz-range-track {
  height: 6px;
  border-radius: var(--radius-pill);
  background: var(--c-border);
}

.range::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 24px;
  height: 24px;
  /* Half the thumb minus half the track: what centres it on the line. */
  margin-top: -9px;
  border: var(--border-width) solid var(--c-surface);
  border-radius: var(--radius-pill);
  background: var(--c-primary);
}

.range::-moz-range-thumb {
  width: 24px;
  height: 24px;
  border: var(--border-width) solid var(--c-surface);
  border-radius: var(--radius-pill);
  background: var(--c-primary);
}

/* The ring goes around the thumb, not around the full-width track. */
.range:focus-visible { outline: none; }

.range:focus-visible::-webkit-slider-thumb {
  outline: var(--focus-width) solid var(--c-focus);
  outline-offset: var(--focus-offset);
}

.range:focus-visible::-moz-range-thumb {
  outline: var(--focus-width) solid var(--c-focus);
  outline-offset: var(--focus-offset);
}

.range:disabled { cursor: not-allowed; opacity: 0.6; }


/* ---------- Colour input ----------
   The native picker, reduced to a swatch that matches the other controls.
   Always give it a <label>: the colour alone never says what it paints. */

.color-input {
  appearance: none;
  -webkit-appearance: none;
  width: 100%;
  height: var(--control-height-sm);
  padding: 0;
  border: var(--border-width) solid var(--c-border);
  border-radius: var(--radius-sm);
  background: var(--c-surface);
  cursor: pointer;
}

.color-input::-webkit-color-swatch-wrapper { padding: 3px; }
.color-input::-webkit-color-swatch { border: none; border-radius: 2px; }
.color-input::-moz-color-swatch { border: none; border-radius: 2px; }

.color-input:hover { border-color: var(--c-text-muted); }

.color-input:focus-visible {
  outline: var(--focus-width) solid var(--c-focus);
  outline-offset: var(--focus-offset);
}


/* ---------- Compact field rows ----------
   For panels of settings, where a stacked .field per control would run the
   list off the screen: the label keeps its line, the control takes what is
   left, and .field-value reports what a control cannot say by itself. */

.field-inline {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}

/* A swatch in a compact row keeps a swatch's width: stretched across a
   panel it reads as a coloured bar, not as a control. */
.field-inline > .color-input { flex: 0 0 88px; }

.field-inline > label,
.field-inline > .label {
  flex: 1;
  min-width: 0;
  margin-bottom: 0;
}

.field-value {
  font-size: var(--text-xs);
  color: var(--c-text-muted);
  /* Digits of equal width: a value that changes as a slider moves must not
     make the row jitter. */
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* ---------- App navigation ----------
   The two or three places someone signed in moves between. A bar at the
   bottom of a phone, under the thumb, with a word under every icon; part of
   the top bar from a tablet up. One element, two layouts: the markup does not
   change with the screen.
     <nav class="app-nav"><a class="app-nav-link" aria-current="page">…</a></nav>
   The page carrying it gets .has-app-nav on <body> (header.js sets it), which
   keeps the last line of content and the toasts clear of the bar. */

.app-nav {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 30;
  display: grid;
  grid-auto-columns: minmax(0, 1fr);
  grid-auto-flow: column;
  height: calc(var(--nav-height) + var(--safe-bottom));
  padding-bottom: var(--safe-bottom);
  background: var(--c-surface);
  border-top: var(--border-width) solid var(--c-border);
}

.app-nav-link {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
  color: var(--c-text-muted);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  text-decoration: none;
}

/* The pill behind the icon. Active is marked by the pill AND the weight, not
   by the colour alone. */
.app-nav-pill {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 56px;
  height: 32px;
  border-radius: var(--radius-pill);
}

.app-nav-link[aria-current="page"] {
  color: var(--c-primary-text);
  font-weight: var(--weight-bold);
}

.app-nav-link[aria-current="page"] .app-nav-pill { background: var(--c-primary-subtle); }
.app-nav-link:hover { color: var(--c-text); text-decoration: none; }
.app-nav-link:hover .app-nav-pill { background: var(--c-surface-hover); }
.app-nav-link[aria-current="page"]:hover { color: var(--c-primary-text); }

[data-theme="daltonism"] .app-nav-link[aria-current="page"] .app-nav-pill { outline: 2px solid currentColor; }

.has-app-nav { padding-bottom: calc(var(--nav-height) + var(--safe-bottom)); }

@media (min-width: 900px) {
  .app-nav {
    position: static;
    display: flex;
    gap: var(--space-1);
    height: auto;
    padding: 0;
    background: none;
    border: 0;
  }

  .app-nav-link {
    flex-direction: row;
    gap: var(--space-2);
    min-height: var(--control-height-md);
    padding: 0 var(--space-3) 0 var(--space-1);
    border-radius: var(--radius-pill);
    font-size: var(--text-base);
  }

  .app-nav-pill { width: 40px; }
  .app-nav-link[aria-current="page"] { background: var(--c-primary-subtle); }
  .app-nav-link[aria-current="page"] .app-nav-pill { background: none; }
  .has-app-nav { padding-bottom: 0; }
}


/* ---------- Waves ----------
   The brand's one ornament: three bands of colour, blue, teal-green and night,
   rolling over each other. Two uses, and only these two — a wave everywhere
   is a wave nobody sees.

   1. .hero-waves: a night band closing on still waves, for the moment a
      screen exists for (the end of a game, the landing page).
   2. .loader: the same waves, moving, while something loads.

   The wave itself is a mask over a background colour, so every band is a
   token and follows the theme. The shape is one period of a curve, 600 units
   wide, whose two ends meet: repeated along x it never shows a seam. */

.hero-waves {
  position: relative;
  overflow: hidden;
  padding: var(--space-6) var(--space-5) var(--space-8);
  border-radius: var(--radius-lg);
  background: var(--c-brand-night);
  color: var(--neutral-0);
}

.hero-waves h1,
.hero-waves h2 { color: var(--neutral-0); margin: 0; }
.hero-waves p { color: var(--c-brand-on-night); margin: 0; }

.hero-waves::before,
.hero-waves::after,
.wave {
  -webkit-mask: var(--wave-shape) repeat-x 0 0 / var(--wave-width, 600px) 100%;
          mask: var(--wave-shape) repeat-x 0 0 / var(--wave-width, 600px) 100%;
}

.hero-waves::before,
.hero-waves::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  pointer-events: none;
}

.hero-waves::before { height: 56px; background: var(--c-brand-blue); --wave-width: 520px; }
.hero-waves::after  { height: 36px; background: var(--c-bg); --wave-width: 440px; -webkit-mask-position: 120px 0; mask-position: 120px 0; }

/* The middle band. A pseudo-element short, so it is the one wave that needs
   its own (empty, aria-hidden) element: <span class="hero-waves-green"></span>. */
.hero-waves-green {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 46px;
  background: var(--c-brand-green);
  -webkit-mask: var(--wave-shape) repeat-x 260px 0 / 480px 100%;
          mask: var(--wave-shape) repeat-x 260px 0 / 480px 100%;
  pointer-events: none;
}

/* The ground wave goes over the other two, the content over everything. */
.hero-waves::after { z-index: 1; }
.hero-waves > :not(.hero-waves-green) { position: relative; z-index: 2; }

:root {
  --wave-shape: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 600 100' preserveAspectRatio='none'%3E%3Cpath d='M0 50C100 10 200 10 300 50S500 90 600 50V100H0Z'/%3E%3C/svg%3E");
}


/* ---------- Loader ----------
   Built by Loader in ui.js; shown only when a wait outlasts 300ms, so a fast
   answer never flashes it. The sentence says WHAT is loading: waves alone do
   not tell a stuck screen from a slow one. Reduced motion stops the waves
   (see Base), the sentence stays. */

.loader {
  position: fixed;
  inset: 0;
  z-index: 90;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-4);
  padding-bottom: 30vh;
  background: color-mix(in srgb, var(--c-bg) 78%, transparent);
  color: var(--c-text);
  text-align: center;
  animation: loader-in 200ms ease-out;
}

.loader-mark { width: 64px; height: 64px; }

.loader-text {
  margin: 0;
  padding: 0 var(--space-5);
  font-size: var(--text-lg);
  font-weight: var(--weight-medium);
}

.loader-waves {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 38vh;
  pointer-events: none;
}

.wave {
  position: absolute;
  left: 0;
  right: 0;
  /* Below the edge: the swell lifts each band, and the screen's bottom edge
     must not show through. */
  bottom: -24px;
  animation:
    wave-roll var(--wave-speed, 1.8s) linear infinite,
    wave-swell var(--wave-swell, 2.4s) ease-in-out infinite;
}

.wave-night { height: 100%; background: var(--c-brand-night); --wave-width: 900px; --wave-speed: 2.6s; --wave-swell: 3s; animation-direction: reverse, normal; opacity: 0.92; }
.wave-blue  { height: 80%;  background: var(--c-brand-blue);  --wave-width: 640px; --wave-speed: 1.8s; --wave-swell: 2.4s; opacity: 0.9; }
.wave-green { height: 60%;  background: var(--c-brand-green); --wave-width: 720px; --wave-speed: 1.3s; --wave-swell: 1.9s; animation-direction: reverse, normal; opacity: 0.92; }
.wave-mist  { height: 36%;  background: var(--c-brand-mist);  --wave-width: 520px; --wave-speed: 1s; opacity: 0.6; }

@keyframes wave-roll {
  to {
    -webkit-mask-position: var(--wave-width) 0;
            mask-position: var(--wave-width) 0;
  }
}

@keyframes wave-swell {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-14px); }
}

@keyframes loader-in { from { opacity: 0; } }


/* ---------- Sweep ----------
   The waves crossing the whole screen, between two screens that have nothing
   in common (entering a game, handing it in). Built and moved by sweep.js;
   this only draws it. Three bands rise one under the other — green, blue,
   then night, whose trailing edge is a wave too — and the night band carries
   the sentence saying what is on its way. */

.sweep {
  position: fixed;
  inset: 0;
  z-index: 95;
  overflow: hidden;
}

.sweep-track {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  will-change: transform;
}

.sweep-band {
  position: absolute;
  left: 0;
  right: 0;
  height: 130vh;
}

/* The leading wave of each band, rolling sideways as it climbs. */
.sweep-band::before,
.sweep-night::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  height: 64px;
  background: inherit;
  -webkit-mask: var(--wave-shape) repeat-x 0 0 / var(--wave-width) 100%;
          mask: var(--wave-shape) repeat-x 0 0 / var(--wave-width) 100%;
  animation: wave-roll 0.9s linear infinite;
}

.sweep-band::before { bottom: calc(100% - 1px); }

.sweep-green { top: 0;    background: var(--c-brand-green); --wave-width: 520px; }
.sweep-blue  { top: 10vh; background: var(--c-brand-blue);  --wave-width: 640px; }
.sweep-night { top: 20vh; height: 140vh; background: var(--c-brand-night); --wave-width: 720px; }

.sweep-blue::before { animation-direction: reverse; }

/* The night band's trailing edge: the same wave, upside down. */
.sweep-night::after { top: calc(100% - 1px); transform: scaleY(-1); animation-direction: reverse; }

.sweep-caption {
  position: absolute;
  left: 0;
  right: 0;
  top: 45vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
  padding: 0 var(--space-5);
  color: var(--neutral-0);
  text-align: center;
}

.sweep-mark { width: 56px; height: 56px; }

.sweep-text { margin: 0; font-size: var(--text-lg); font-weight: var(--weight-medium); }

/* Arriving under a curtain, before it can be drawn: the page is the colour of
   the curtain, so nothing of it flashes between the two screens. */
html.sweep-pending { background: var(--c-brand-night); }
html.sweep-pending body { visibility: hidden; }


/* ---------- Action bar ----------
   The buttons a screen is for, kept under the thumb: on a phone they are
   fixed to the bottom of the screen whatever the length of the content, and
   the block that holds them (.has-action-bar) keeps its last line clear of
   them. From a tablet up they sit at the end of the content. */

.action-bar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 20;
  display: flex;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4) calc(var(--space-4) + var(--safe-bottom));
  background: var(--c-surface);
  border-top: var(--border-width) solid var(--c-border);
}

.action-bar > .btn:last-child { flex: 1; }

.has-action-bar { padding-bottom: calc(var(--control-height-lg) + var(--space-7) + var(--safe-bottom)); }

@media (min-width: 640px) {
  .action-bar {
    position: static;
    margin-top: var(--space-5);
    padding: 0;
    background: none;
    border: 0;
    justify-content: flex-end;
  }

  .action-bar > .btn:last-child { flex: none; min-width: 200px; }
  .has-action-bar { padding-bottom: 0; }
}


/* ============================================================================
   6. UTILITIES
   ========================================================================= */

.muted     { color: var(--c-text-muted); }

/* Semantic text colours, for the times an icon or a word carries the meaning
   without a whole .alert or .badge around it. */
.text-danger  { color: var(--c-danger-text); }
.text-warning { color: var(--c-warning-text); }
.text-success { color: var(--c-success-text); }
.text-info    { color: var(--c-info-text); }

.text-xs   { font-size: var(--text-xs); }
.text-sm   { font-size: var(--text-sm); }
.text-lg   { font-size: var(--text-lg); }
.bold      { font-weight: var(--weight-bold); }
.center    { text-align: center; }
.right     { text-align: right; }
.truncate  { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.full      { width: 100%; }
.no-margin { margin: 0; }

.mt-1 { margin-top: var(--space-1); }
.mt-2 { margin-top: var(--space-2); }
.mt-3 { margin-top: var(--space-3); }
.mt-4 { margin-top: var(--space-4); }
.mt-5 { margin-top: var(--space-5); }
.mt-6 { margin-top: var(--space-6); }
.mb-0 { margin-bottom: 0; }
.mb-3 { margin-bottom: var(--space-3); }
.mb-5 { margin-bottom: var(--space-5); }

/* Visible to screen readers only */
.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;
}
