/* =========================================================================
   LEARNERS — system primitives
   =========================================================================

   Composable pieces built from tokens.css and nothing else. No page styles
   live here, and no value is hard-coded: if you need a number, it belongs in
   tokens.css first.

   The vocabulary is deliberately small. A design system's job is to make the
   obvious thing easy and the inconsistent thing awkward, and every primitive
   added past the point of need makes both harder.
   ========================================================================= */

/* ==========================================================================
   Base
   ========================================================================== */

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

html {
  -webkit-text-size-adjust: 100%;
  /* Scroll behaviour is set here rather than per-page so a reduced-motion
     preference can switch it off in one place. */
  scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: var(--t-body-size);
  line-height: var(--t-body-lh);
  font-weight: var(--w-regular);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  /* Inter's contextual alternates and tabular-friendly defaults. Harmless on
     stacks that do not ship them. */
  font-feature-settings: "cv05" 1, "cv11" 1, "ss01" 1;
}

img, svg, video, canvas { max-width: 100%; display: block; }

::selection { background: var(--accent-soft); color: var(--accent-press); }

/* --- focus -----------------------------------------------------------------
   One ring, everywhere, and never removed. The double shadow means it stays
   visible on white, on glass, and on the inverted petrol ground — a single
   accent ring disappears against accent-soft surfaces. */
:focus { outline: none; }
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--r-xs);
}
.glass:focus-visible,
[data-invert] :focus-visible {
  outline-color: var(--accent);
  box-shadow: 0 0 0 4px rgba(255, 255, 255, .55);
}

/* --- skip link -------------------------------------------------------------
   Present on every page via app.css. Hidden until focused. */
.skip-link {
  position: absolute; left: var(--s-4); top: var(--s-4);
  z-index: var(--z-alert);
  padding: var(--s-3) var(--s-5);
  background: var(--surface); color: var(--ink);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-3);
  font-weight: var(--w-medium);
  transform: translateY(-160%);
  transition: transform var(--t-control);
}
.skip-link:focus-visible { transform: translateY(0); }

/* ==========================================================================
   Type
   ==========================================================================
   Light weight belongs to the two display tiers only. Everything at reading
   size stays at 400 or above, because 300 at 15px fails in the rooms this is
   actually sat in.
   ========================================================================== */

.t-display, .t-title, .t-h1, .t-h2, .t-h3 {
  font-family: var(--font-display);
  color: var(--ink-display);
  margin: 0;
  text-wrap: balance;
}

.t-display {
  font-size: var(--t-display-size);
  line-height: var(--t-display-lh);
  letter-spacing: var(--t-display-track);
  font-weight: var(--w-light);
}
.t-title {
  font-size: var(--t-title-size);
  line-height: var(--t-title-lh);
  letter-spacing: var(--t-title-track);
  font-weight: var(--w-light);
}
.t-h1 {
  font-size: var(--t-h1-size);
  line-height: var(--t-h1-lh);
  letter-spacing: var(--t-h1-track);
  font-weight: var(--w-regular);
}
.t-h2 {
  font-size: var(--t-h2-size);
  line-height: var(--t-h2-lh);
  letter-spacing: var(--t-h2-track);
  font-weight: var(--w-medium);
}
.t-h3 {
  font-size: var(--t-h3-size);
  line-height: var(--t-h3-lh);
  letter-spacing: var(--t-h3-track);
  font-weight: var(--w-semibold);
}

.t-lead {
  font-size: var(--t-lead-size);
  line-height: var(--t-lead-lh);
  letter-spacing: var(--t-lead-track);
  color: var(--ink-secondary);
  font-weight: var(--w-regular);
  margin: 0;
  text-wrap: pretty;
}
.t-body  { font-size: var(--t-body-size);  line-height: var(--t-body-lh); }
.t-small { font-size: var(--t-small-size); line-height: var(--t-small-lh); }

/* The only role with open tracking. Eyebrows, column heads, status labels. */
.t-micro {
  font-size: var(--t-micro-size);
  line-height: var(--t-micro-lh);
  letter-spacing: var(--t-micro-track);
  text-transform: uppercase;
  font-weight: var(--w-semibold);
}

.t-mono { font-family: var(--font-mono); font-size: 0.8125rem; }

/* Icons from design/icons.js. Inline-block rather than block so they sit in a
   line of text, and `flex: none` so they never shrink inside a flex row. They
   inherit colour and stroke from the parent, which is why there is no dark
   variant of the icon set. */
.icon { display: inline-block; vertical-align: -0.125em; flex: none; }

/* Numerals that must not shuffle: scores, timers, counts, durations. */
.t-num { font-variant-numeric: tabular-nums; font-feature-settings: "tnum" 1; }

.t-muted     { color: var(--ink-muted); }
.t-secondary { color: var(--ink-secondary); }
.t-accent    { color: var(--accent); }
.measure     { max-width: var(--measure); }

a { color: var(--accent); text-decoration-color: var(--accent-line);
    text-underline-offset: 2px; transition: color var(--t-control); }
a:hover { color: var(--accent-hover); text-decoration-color: currentColor; }

/* ==========================================================================
   Layout
   ========================================================================== */

.container {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
}
.container--narrow { max-width: var(--container-narrow); }
.container--wide   { max-width: 88rem; }

.section { padding-block: var(--section-y); }

/* Vertical rhythm without margin collapse surprises. */
.stack   { display: flex; flex-direction: column; }
.stack-2 { gap: var(--s-2); } .stack-3 { gap: var(--s-3); }
.stack-4 { gap: var(--s-4); } .stack-6 { gap: var(--s-6); }
.stack-8 { gap: var(--s-8); } .stack-12{ gap: var(--s-12); }

/* Horizontal group that wraps rather than overflowing. */
.cluster {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-3);
}
.cluster--between { justify-content: space-between; }

.grid { display: grid; gap: var(--s-6); }
.grid--auto { grid-template-columns: repeat(auto-fit, minmax(19rem, 1fr)); }
.grid--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
@media (max-width: 52rem) { .grid--2, .grid--3 { grid-template-columns: 1fr; } }

.divider { height: 1px; background: var(--line); border: 0; margin: 0; }

/* ==========================================================================
   Atmosphere
   ==========================================================================
   Environmental light. Always behind content, never interactive, and never
   animated on a timer — it exists to give glass something to refract.
   ========================================================================== */

.atmosphere {
  position: fixed; inset: 0;
  z-index: var(--z-atmosphere);
  pointer-events: none;
  background-image: var(--aura-petrol), var(--aura-sand);
  background-repeat: no-repeat;
}

/* ==========================================================================
   Glass
   ==========================================================================
   Four layers, always: a gradient fill (light falls unevenly across a pane),
   a blur that also saturates (glass concentrates colour rather than washing
   it out), a specular top edge, and a contact shadow so it sits in the scene.
   Drop any one and it reads as a filter instead of a material.
   ========================================================================== */

.glass {
  position: relative;
  background-image: var(--glass-fill-light);
  background-color: rgba(255, 255, 255, .01);
  border: 1px solid var(--glass-border);
  border-radius: var(--r-lg);
  box-shadow: var(--glass-inner), var(--shadow-1);
}
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  .glass {
    -webkit-backdrop-filter: blur(var(--glass-blur-md)) saturate(var(--glass-sat));
    backdrop-filter: blur(var(--glass-blur-md)) saturate(var(--glass-sat));
  }
}
/* No backdrop-filter: fall back to an opaque panel rather than a washed-out
   translucent one, which is unreadable over content. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .glass { background-image: none; background-color: var(--surface); }
}

.glass--control {
  background-image: var(--glass-fill-control);
  border-radius: var(--r-pill);
  box-shadow: var(--glass-inner), var(--shadow-1);
}
.glass--floating {
  background-image: var(--glass-fill-floating);
  border-radius: var(--r-pill);
  box-shadow: var(--glass-inner), var(--shadow-2);
  -webkit-backdrop-filter: blur(var(--glass-blur-lg)) saturate(var(--glass-sat));
  backdrop-filter: blur(var(--glass-blur-lg)) saturate(var(--glass-sat));
}
.glass--elevated {
  background-image: var(--glass-fill-elevated);
  border-radius: var(--r-xl);
  box-shadow: var(--glass-inner), var(--shadow-3);
  -webkit-backdrop-filter: blur(var(--glass-blur-lg)) saturate(var(--glass-sat));
  backdrop-filter: blur(var(--glass-blur-lg)) saturate(var(--glass-sat));
}
.glass--dark {
  background-image: var(--glass-fill-dark);
  border-color: var(--glass-border-dark);
  box-shadow: var(--glass-inner-dark), var(--shadow-2);
  color: var(--c-white);
}

/* The bright arc across the top of a large pane. Optional, and worth it only
   on surfaces big enough to show a gradient across their width. */
.glass--specular::before {
  content: "";
  position: absolute; inset: 0 0 auto 0; height: 40%;
  border-radius: inherit;
  background: linear-gradient(180deg, rgba(255, 255, 255, .45), transparent);
  pointer-events: none;
}

/* ==========================================================================
   Panels
   ========================================================================== */

.panel {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: var(--s-6);
}
.panel--flush   { padding: 0; }
.panel--roomy   { padding: var(--s-8); }
.panel--sunken  { background: var(--surface-sunken); border-color: transparent; }
.panel--raised  { box-shadow: var(--shadow-2); border-color: transparent; }
.panel--accent  { background: var(--accent-soft); border-color: var(--accent-line); }

/* ==========================================================================
   Buttons
   ==========================================================================
   Pill geometry throughout. The press is a scale, not a colour jump: physical
   response reads as quality, and a 2% squash is felt more than seen.
   ========================================================================== */

.btn {
  --btn-bg: transparent;
  --btn-fg: var(--ink);
  --btn-border: transparent;
  --btn-shadow: none;

  display: inline-flex; align-items: center; justify-content: center;
  gap: var(--s-2);
  min-height: 2.75rem;              /* 44px — the touch floor, everywhere */
  padding: 0 var(--s-5);
  border-radius: var(--r-pill);
  border: 1px solid var(--btn-border);
  background: var(--btn-bg);
  color: var(--btn-fg);
  box-shadow: var(--btn-shadow);
  font: inherit;
  font-size: var(--t-body-size);
  font-weight: var(--w-medium);
  letter-spacing: -0.005em;
  text-decoration: none;
  cursor: pointer;
  white-space: nowrap;
  transition:
    background-color var(--t-control),
    border-color var(--t-control),
    color var(--t-control),
    box-shadow var(--t-control),
    transform var(--d-instant) var(--ease-spring);
}
.btn:active:not(:disabled) { transform: scale(0.975); }
.btn:disabled { opacity: .45; cursor: not-allowed; }

.btn--primary {
  --btn-bg: var(--accent);
  --btn-fg: var(--ink-on-accent);
  --btn-shadow: var(--shadow-accent);
}
.btn--primary:hover:not(:disabled) { --btn-bg: var(--accent-hover); }
.btn--primary:active:not(:disabled) { --btn-bg: var(--accent-press); }

.btn--secondary {
  --btn-bg: var(--surface);
  --btn-fg: var(--ink);
  --btn-border: var(--line-strong);
  --btn-shadow: var(--shadow-1);
}
.btn--secondary:hover:not(:disabled) { --btn-border: var(--ink-faint); }

.btn--ghost { --btn-fg: var(--accent); }
.btn--ghost:hover:not(:disabled) { --btn-bg: var(--accent-soft); }

.btn--quiet { --btn-fg: var(--ink-muted); }
.btn--quiet:hover:not(:disabled) { --btn-fg: var(--ink); --btn-bg: var(--surface-sunken); }

/* Glass control. For surfaces floating over content or over a live camera. */
.btn--glass {
  --btn-fg: var(--ink);
  --btn-border: var(--glass-border);
  background-image: var(--glass-fill-control);
  box-shadow: var(--glass-inner), var(--shadow-1);
  -webkit-backdrop-filter: blur(var(--glass-blur-md)) saturate(var(--glass-sat));
  backdrop-filter: blur(var(--glass-blur-md)) saturate(var(--glass-sat));
}

.btn--danger { --btn-bg: var(--danger); --btn-fg: var(--c-white); }

.btn--lg { min-height: 3.25rem; padding: 0 var(--s-8); font-size: 1rem; }
.btn--sm { min-height: 2.25rem; padding: 0 var(--s-4); font-size: var(--t-small-size); }
.btn--block { display: flex; width: 100%; }

.btn--icon { width: 2.75rem; min-height: 2.75rem; padding: 0; border-radius: var(--r-pill); }

/* Inline text action. Used where a button would be visually too loud. */
.link-btn {
  /* Reset the box as well as the paint — a page that also styles bare
     `button` would otherwise leave a pill and a shadow behind a text link. */
  display: inline; min-height: 0; border-radius: 0; box-shadow: none;
  background: none; border: 0; padding: 0;
  color: var(--accent); font: inherit; font-size: var(--t-small-size);
  font-weight: var(--w-medium); cursor: pointer;
  text-decoration: underline; text-underline-offset: 2px;
  text-decoration-color: var(--accent-line);
  transition: color var(--t-control);
}
.link-btn:hover { color: var(--accent-hover); text-decoration-color: currentColor; }

/* ==========================================================================
   Fields
   ==========================================================================
   Tight radii, deliberately. Pill-shaped inputs read as search boxes; a form
   field should look like somewhere to put an answer.
   ========================================================================== */

.field-label {
  display: block;
  font-size: var(--t-small-size);
  font-weight: var(--w-medium);
  color: var(--ink-secondary);
  margin-bottom: var(--s-2);
}

.field {
  width: 100%;
  min-height: 2.75rem;
  padding: var(--s-3) var(--s-4);
  background: var(--surface);
  color: var(--ink);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-md);
  font: inherit;
  font-size: var(--t-body-size);
  transition: border-color var(--t-control), box-shadow var(--t-control);
}
.field::placeholder { color: var(--ink-faint); }
.field:hover { border-color: var(--ink-faint); }
.field:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}
.field[aria-invalid="true"] { border-color: var(--danger); }

/* ==========================================================================
   Chips & badges
   ========================================================================== */

.chip {
  display: inline-flex; align-items: center; gap: var(--s-1);
  padding: var(--s-1) var(--s-3);
  border-radius: var(--r-pill);
  background: var(--surface-sunken);
  color: var(--ink-secondary);
  font-size: var(--t-small-size);
  font-weight: var(--w-medium);
  line-height: 1.4;
}
.chip--accent  { background: var(--accent-soft); color: var(--accent-press); }
.chip--ok      { background: var(--ok-soft);     color: var(--ok); }
.chip--warn    { background: var(--warn-soft);   color: var(--warn); }
.chip--danger  { background: var(--danger-soft); color: var(--danger); }
.chip--outline { background: transparent; border: 1px solid var(--line-strong); }

/* Eyebrow above a section headline. */
.eyebrow {
  display: inline-flex; align-items: center; gap: var(--s-2);
  font-size: var(--t-micro-size);
  letter-spacing: var(--t-micro-track);
  text-transform: uppercase;
  font-weight: var(--w-semibold);
  color: var(--accent);
}
.eyebrow::before {
  content: ""; width: 1.25rem; height: 1px; background: var(--accent-line);
}

/* Status dot that is never colour-only: pair with a text label. */
.dot {
  width: .5rem; height: .5rem; border-radius: var(--r-pill);
  background: var(--ink-faint); flex: none;
}
.dot--live { background: var(--danger); }
.dot--ok   { background: var(--ok); }
.dot--warn { background: var(--warn); }

/* ==========================================================================
   Notices
   ==========================================================================
   Semantic tone carried by a left rule and a soft ground, never by colour
   alone — each variant also gets an icon slot in the markup.
   ========================================================================== */

.notice {
  display: flex; gap: var(--s-3);
  padding: var(--s-4) var(--s-5);
  border-radius: var(--r-md);
  border: 1px solid var(--line);
  border-left: 3px solid var(--ink-faint);
  background: var(--surface-sunken);
  color: var(--ink-secondary);
  font-size: var(--t-small-size);
  line-height: 1.55;
}
.notice strong { color: var(--ink); font-weight: var(--w-semibold); }
.notice--info   { border-left-color: var(--accent); background: var(--accent-soft);
                  border-color: var(--accent-line); }
.notice--ok     { border-left-color: var(--ok);     background: var(--ok-soft);     border-color: transparent; }
.notice--warn   { border-left-color: var(--warn);   background: var(--warn-soft);   border-color: transparent; }
.notice--danger { border-left-color: var(--danger); background: var(--danger-soft); border-color: transparent; }

/* ==========================================================================
   Meter
   ==========================================================================
   One horizontal measurement. The fill animates from zero via a CSS custom
   property so the reveal is declarative and a reduced-motion user simply gets
   the final width instantly.
   ========================================================================== */

.meter-row {
  display: grid;
  grid-template-columns: minmax(6rem, 10rem) 1fr auto;
  align-items: center;
  gap: var(--s-4);
  padding-block: var(--s-2);
}
.meter-track {
  height: .375rem;
  background: var(--surface-sunken);
  border-radius: var(--r-pill);
  overflow: hidden;
}
.meter-fill {
  display: block; height: 100%;
  width: var(--meter-value, 0%);
  background: var(--viz-1);
  border-radius: inherit;
  transition: width var(--d-scene) var(--ease-out);
}
.meter-value { font-variant-numeric: tabular-nums; font-size: var(--t-small-size);
               color: var(--ink-secondary); white-space: nowrap; }
@media (max-width: 40rem) {
  .meter-row { grid-template-columns: 1fr auto; }
  .meter-row .meter-track { grid-column: 1 / -1; order: 3; }
}

/* ==========================================================================
   Motion utilities
   ==========================================================================
   Driven by design/motion.js, which adds [data-inview] when an element enters
   the viewport. The CSS owns the look of the transition; the script owns only
   when it fires. An element with no JS still ends up visible — the hidden
   state is applied by the script, never by the stylesheet alone.
   ========================================================================== */

[data-reveal] {
  opacity: 1;
  transform: none;
}
/* Applied by motion.js only once it has confirmed it will observe the element,
   so a failed script can never leave content invisible. */
[data-reveal][data-reveal-armed] {
  opacity: 0;
  transform: translate3d(0, var(--reveal-y, 1.25rem), 0) scale(var(--reveal-scale, 1));
  transition:
    opacity var(--d-scene) var(--ease-out),
    transform var(--d-scene) var(--ease-out),
    filter var(--d-scene) var(--ease-out);
  transition-delay: var(--reveal-delay, 0ms);
  will-change: opacity, transform;
}
[data-reveal][data-inview] {
  opacity: 1;
  transform: translate3d(0, 0, 0) scale(1);
  filter: none;
}
[data-reveal="soft"]  { --reveal-y: .5rem; }
[data-reveal="rise"]  { --reveal-y: 2rem; }
[data-reveal="scale"] { --reveal-y: 1rem; --reveal-scale: .97; }

@media (prefers-reduced-motion: reduce) {
  [data-reveal][data-reveal-armed] { transform: none; }
}

/* ==========================================================================
   Scene transitions
   ==========================================================================
   For swapping one full-screen step for another inside a single-page flow —
   the assessment runner's question-to-question change. Fast, because this is
   not a moment to admire: it is the seam between two questions.
   ========================================================================== */

.scene { animation: scene-in var(--d-slow) var(--ease-out) both; }
@keyframes scene-in {
  from { opacity: 0; transform: translate3d(0, .75rem, 0); }
  to   { opacity: 1; transform: none; }
}
.scene--leaving { animation: scene-out var(--d-fast) var(--ease-in-out) both; }
@keyframes scene-out {
  from { opacity: 1; transform: none; }
  to   { opacity: 0; transform: translate3d(0, -.5rem, 0); }
}
@media (prefers-reduced-motion: reduce) {
  .scene, .scene--leaving { animation: none; }
}

/* ==========================================================================
   Utilities
   ========================================================================== */

/* ==========================================================================
   Hidden
   ==========================================================================
   THE USER AGENT'S `[hidden] { display: none }` LOSES TO ANY CLASS.

   It is an element-level rule with zero specificity, so the moment a component
   sets `display: grid` or `display: block` on a class, `el.hidden = true` stops
   hiding anything. That has now bitten this codebase twice - the console's
   sign-in gate kept rendering underneath a signed-in console, and a camera tile
   kept showing a stale picture under its "no signal" placeholder.

   `!important` is the right tool exactly here: `hidden` is not a style opinion
   a component should be able to overrule, it is a statement that the element is
   not in the page. Anything that needs to animate something out should not be
   setting `hidden` in the first place.
   ========================================================================== */

[hidden] { display: none !important; }

.hidden { display: none !important; }

/* Present for assistive technology, absent from the page. */
.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
}

.no-scroll { overflow: hidden; }
.full-bleed { width: 100vw; margin-inline: calc(50% - 50vw); }
