/*
 * components.css — layout skeleton (PLAN 3.2) + component states (PLAN 3.3).
 *
 * Every colour, spacing step, type step, radius and duration below is a
 * var() from src/design/tokens.css; the only non-var colour keywords in this
 * file are the CSS system colours inside the forced-colours block, where the
 * operating system's palette is the contract. Rationale and the card register:
 * docs/design-language-2026-09-28.md.
 *
 * Markup contract (the build in Phase 4 emits exactly these classes):
 *   <a class="skip-link" href="#main"> · .site-header · .site-nav
 *   <main id="main" class="site-main"> · .site-footer · .page · .prose
 *   .card-grid > .card > .card__title / .chip-row > .chip / .card__body
 *   / .card__meta / .card__actions > .btn
 *
 * Deliberately absent: iframe/aspect-ratio sizing (Phase 4.2 owns those
 * numbers, and inventing one here would be an off-register value) and any dark
 * or second theme (design language: not in v1).
 *
 * Reading and focus order (WCAG 1.3.2, 2.4.3): nothing in this file reorders
 * content — no `order` property, and the only positioned element is the skip
 * link — so the visual order is the tab order.
 */

/* ---- 1. Base reset -------------------------------------------------------
 * No `outline: none` / `outline: 0` appears anywhere in this file: resetting
 * outlines is W3C failure F78 against SC 2.4.7, and every control here is
 * styled, so the UA-default-focus exemption is not ours to use (SC 1.4.11).
 * :where() keeps these resets at zero specificity so components never need
 * !important to override them. */
*,
*::before,
*::after {
  box-sizing: border-box;
}

:where(html) {
  /* Stops mobile engines re-sizing our type, which is already 18 px at every
     width by design (SC 1.4.4 Resize is met by the scale, not by inflation). */
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0; /* the UA body margin, not an outline */
  background-color: var(--color-page-background);
  color: var(--color-text);
  font-family: var(--font-stack);
  font-size: var(--font-body);
  line-height: var(--font-body-line);
}

/* The type scale, applied to the bare elements so prose needs no class to be on it.
 * tokens.css defines the five steps; until this block existed nothing consumed them,
 * which is why the page rendered in the UA's serif at UA sizes (measured in
 * test/evidence/site-desktop.png, 2026-09-29). :where() keeps every rule at zero
 * specificity, so .page-title / .card__title / .btn still win without !important.
 * A size outside these five steps is a design-language defect (D-014). */
:where(h1) {
  font-size: var(--font-display);
  line-height: var(--font-display-line);
}

:where(h2) {
  font-size: var(--font-heading-l);
  line-height: var(--font-heading-l-line);
}

:where(h3) {
  font-size: var(--font-heading-m);
  line-height: var(--font-heading-m-line);
}

:where(h4, h5, h6) {
  font-size: var(--font-heading-s);
  line-height: var(--font-heading-s-line);
}

:where(h1, h2, h3, h4, h5, h6) {
  font-weight: var(--font-weight-bold);
  overflow-wrap: break-word;
}

/* Closer to what a heading introduces than to what came before it. */
:where(h2, h3, h4) {
  margin-block-start: var(--space-6);
  margin-block-end: var(--space-3);
}

:where(p) {
  margin-block: var(--space-4);
  max-inline-size: var(--prose-max-width);
}

:where(ul, ol) {
  margin-block: var(--space-4);
  padding-inline-start: var(--space-5);
}

:where(li) {
  margin-block: var(--space-1);
}

/* Links are always underlined (design language); colour is never the only signal. */
:where(a) {
  color: var(--color-link);
  text-decoration: underline;
  text-underline-offset: var(--text-underline-offset);
}

:where(a:visited) {
  color: var(--color-link-visited);
}

:where(a:hover) {
  color: var(--color-link-hover);
}

:where(code) {
  font-family: var(--font-mono);
  font-size: var(--font-meta);
  overflow-wrap: break-word;
}

/* Long URLs and package filenames must break rather than create a horizontal
   scroll container at 320 px wide (SC 1.4.10 Reflow). */
:where(header, nav, main, footer) {
  overflow-wrap: break-word;
}

:where(button, input, select, textarea) {
  font: inherit; /* controls inherit the type scale, not the UA's 13 px */
  color: inherit;
}

/* ---- 2. The one focus rule -----------------------------------------------
 * SC 2.4.7 Focus Visible + SC 1.4.11 Non-text contrast, built as W3C technique
 * C40 (a two-colour indicator). It applies to EVERY focusable thing. A
 * component may change outline-color (see .u-focus-on-fill, .btn) but never
 * removes the outline or its offset. */
:focus-visible {
  outline: var(--focus-ring-width) solid var(--color-focus-ring);
  outline-offset: var(--focus-ring-offset);
}

/* ---- 3. Skip link (SC 2.4.1 Bypass Blocks) -------------------------------
 * First focusable element in the document, markup
 * <a class="skip-link" href="#main">Skip to main content</a>, a direct child of
 * <body>. Hidden by moving it above the viewport: display:none or
 * visibility:hidden would make it unfocusable, which is not the same thing. */
.skip-link {
  position: absolute;
  top: var(--space-2);
  inset-inline-start: var(--space-2);
  z-index: 1; /* the only stacking layer authored here */
  padding: var(--space-2) var(--space-3);
  background-color: var(--color-surface);
  color: var(--color-text);
  border: var(--border-width-ui) solid var(--color-border);
  border-radius: var(--radius-control);
  font-weight: var(--font-weight-bold);
  text-decoration: underline;
  /* -100% clears its own box; the extra grid step clears the top offset. */
  transform: translateY(calc(-100% - var(--space-2)));
}

/* :focus, not :focus-visible: a skip link is reachable only by keyboard, and
 * some engines skip the :focus-visible heuristic for programmatically focused
 * links. The ring itself still comes from rule 2. */
.skip-link:focus {
  transform: none;
}

/* ---- 4. Landmarks, page wrapper, prose measure ---------------------------
 * Landmarks are full-bleed bands; .page inside each one carries the measure. */
.page {
  max-inline-size: var(--page-max-width);
  margin-inline: auto;
  padding-inline: var(--space-3);
  /* Query target for the container twin of section 12 (@supports block): the
     measured size is the content band, so stage thresholds sit in band px. */
  container: page-band / inline-size;
}

/* Prose sits narrower than the page on purpose: ~75 characters per line. */
.prose {
  max-inline-size: var(--prose-max-width);
}

.site-header {
  padding-block: var(--space-3);
  background-color: var(--color-surface);
  border-block-end: var(--border-width-ui) solid var(--color-border-decorative);
}

.site-main {
  padding-block: var(--space-5) var(--space-6);
  /* The skip target: keeps the heading below the viewport edge after the jump. */
  scroll-margin-block-start: var(--space-4);
}

.site-nav ul {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-3);
  margin-block: var(--space-3);
  padding-inline-start: 0;
  list-style: none;
}

.site-footer {
  padding-block: var(--space-4);
  background-color: var(--color-surface);
  border-block-start: var(--border-width-ui) solid var(--color-border-decorative);
  color: var(--color-text-secondary);
  font-size: var(--font-meta);
  line-height: var(--font-meta-line);
}

/* ---- 5. Type and links --------------------------------------------------- */
:where(h1, h2, h3, h4, h5, h6) {
  margin-block: 0 var(--space-3);
  color: var(--color-text);
  font-weight: var(--font-weight-bold);
}

:where(h1) {
  font-size: var(--font-display);
  line-height: var(--font-display-line);
}

:where(h2) {
  font-size: var(--font-heading-l);
  line-height: var(--font-heading-l-line);
  margin-block: 0 var(--space-4);
}

:where(h3) {
  font-size: var(--font-heading-m);
  line-height: var(--font-heading-m-line);
}

:where(h4, h5, h6) {
  font-size: var(--font-heading-s);
  line-height: var(--font-heading-s-line);
}

:where(p, ul, ol, pre, dl) {
  margin-block: 0 var(--space-3);
}

:where(ul, ol) {
  padding-inline-start: var(--space-4);
}

:where(img, svg, video, iframe, table) {
  max-inline-size: 100%; /* no sideways page (SC 1.4.10) */
}

/* A frame's boundary is structural, so it is ink (>=3:1, in scope for 1.4.11),
   not the decorative hairline used for card outlines. */
:where(iframe) {
  border: var(--border-width-ui) solid var(--color-border);
}

/* Always underlined: W3C notes the default underline inherits the text colour
   and therefore always clears 1.4.11, so the marker is never colour alone. */
:where(a[href]) {
  color: var(--color-link);
  text-decoration: underline;
  /* Keeps the rule clear of descenders; a grid step, not a new value. */
  text-underline-offset: var(--space-1);
}

:where(a[href]:hover) {
  color: var(--color-link-hover);
}

:where(a[href]:visited) {
  color: var(--color-link-visited);
}

/* ---- 6. Card register ----------------------------------------------------
 * The shape every item takes (design language "Card register"). Nothing here
 * knows how many items exist — no :nth-child, no count-specific rule (D-014). */
.card-grid {
  display: grid;
  /* One column below 40rem (see section 13). The 0 minimum is deliberate: an `auto`
     track minimum (what plain `1fr` gives) is the card's min-content, which measured
     322 px inside a 288 px column at 400 % zoom and forced sideways scrolling. */
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-4);
}

.card {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-4);
  background-color: var(--color-surface);
  /* Decorative outline: carries no meaning, so 1.4.11 does not apply to it. */
  border: var(--border-width-ui) solid var(--color-border-decorative);
  border-radius: var(--radius-card);
}

/* Author rules beat the UA's `[hidden] { display: none }`, and `.card` sets display,
   so filtering must restate it: without this line a hidden card stays on screen. */
.card[hidden] {
  display: none;
}

.card__title {
  margin-block: 0;
  font-size: var(--font-heading-s);
  line-height: var(--font-heading-s-line);
  font-weight: var(--font-weight-semibold);
}

/* The title link keeps the link treatment from section 5: it names the item
   (SC 2.4.4) and looks exactly like every other link on the site. */
.card__body {
  margin-block: 0;
}

.card__meta {
  margin-block: 0;
  color: var(--color-text-meta);
  font-size: var(--font-meta);
  line-height: var(--font-meta-line);
}

.card__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  /* Pinned to the end so actions line up across a row of uneven cards. */
  margin-block-start: auto;
  padding-block-start: var(--space-3);
}

/* ---- 7. Chips ------------------------------------------------------------
 * Markup: <ul class="chip-row"><li class="chip">Live</li></ul> (or a span when
 * the row is inline). A chip's meaning is always spelled in its text; the
 * semantic fill is reinforcement only, never the carrier (SC 1.4.1).
 * Chips are rounded but are not controls, so the 44 px target size does not
 * apply to them (design language, note on 2.5.8). */
.chip-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-block: 0;
  padding-inline-start: 0;
  list-style: none;
}

.chip {
  padding: var(--space-1) var(--space-2);
  background-color: var(--color-surface-muted);
  color: var(--color-text);
  border: var(--border-width-ui) solid var(--color-border-decorative);
  border-radius: var(--radius-chip);
  font-size: var(--font-meta);
  line-height: var(--font-meta-line);
  font-weight: var(--font-weight-semibold);
}

.chip--accent {
  background-color: var(--color-accent-soft);
}

.chip--info {
  background-color: var(--color-info-background);
  color: var(--color-info);
}

.chip--success {
  background-color: var(--color-success-background);
  color: var(--color-success);
}

.chip--warning {
  background-color: var(--color-warning-background);
  color: var(--color-warning);
}

.chip--error {
  background-color: var(--color-error-background);
  color: var(--color-error);
}

/* ---- 8. Controls ----------------------------------------------------------
 * .btn IS the primary control (accent fill, inverse text); .btn--secondary is
 * the outline variant. Both are 44 px on both axes: W3C 2.5.8 asks for 24 px
 * and explicitly recommends the stricter 2.5.5 for important controls, and a
 * Play button is an important control. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-inline-size: var(--control-min-width);
  min-block-size: var(--control-min-height);
  padding-inline: var(--space-4);
  padding-block: var(--space-2);
  border: var(--border-width-ui) solid var(--color-accent);
  border-radius: var(--radius-control);
  background-color: var(--color-accent);
  color: var(--color-inverse-text);
  font-family: inherit;
  font-size: var(--font-body);
  line-height: var(--font-body-line);
  font-weight: var(--font-weight-semibold);
  /* A button-shaped control is identified by its 2 px boundary, not by an
     underline; this rule never carries meaning that colour alone would lose. */
  text-decoration: none;
  appearance: none;
  cursor: pointer;
  transition: background-color var(--motion-fast) ease-out,
    border-color var(--motion-fast) ease-out, color var(--motion-fast) ease-out;
}

.btn:hover {
  background-color: var(--color-accent-hover);
  border-color: var(--color-accent-hover);
}

/* Pressed state is visible as a boundary that no longer matches the fill, so it
   survives without a new colour and without motion. */
.btn:active {
  background-color: var(--color-accent-hover);
  border-color: var(--color-accent);
}

/* The focus swap: ink (#111314) on teal measures 2.522:1 and would fail 1.4.11,
   so any control whose own fill is the accent takes the yellow focus ink. */
.btn:focus-visible {
  outline-color: var(--color-focus-fill);
}

.btn--secondary {
  background-color: var(--color-surface);
  color: var(--color-text);
  border-color: var(--color-border);
}

.btn--secondary:hover {
  background-color: var(--color-accent-soft);
  border-color: var(--color-border);
}

.btn--secondary:active {
  background-color: var(--color-accent);
  color: var(--color-inverse-text);
  border-color: var(--color-accent);
}

/* Disabled stays readable: the label keeps the full-contrast text token, the
   boundary turns dashed so the state has a shape as well as a tone (1.4.1), and
   nothing here may be the only signal — the markup still names the reason.
   [aria-disabled="true"] mirrors :disabled for <a class="btn">, which cannot
   carry the disabled attribute at all (SC 4.1.2). */
.btn:disabled,
.btn[aria-disabled="true"],
.btn--secondary:disabled,
.btn--secondary[aria-disabled="true"] {
  background-color: var(--color-surface-muted);
  color: var(--color-text);
  border-style: dashed;
  cursor: not-allowed;
}

/* ---- 9. Notices -----------------------------------------------------------
 * Markup requires .notice__title to state the status in words — "Error",
 * "Saved", "Warning", "Note" — before any body text. The paired fill+text
 * tokens below are reinforcement only (SC 1.4.1 Use of Colour); per the design
 * language an error notice explains, it never just colourises. */
.notice {
  max-inline-size: var(--prose-max-width);
  margin-block: var(--space-4);
  padding: var(--space-3);
  border: var(--border-width-ui) solid var(--color-border);
  border-radius: var(--radius-control);
}

.notice__title {
  margin-block: 0 var(--space-2);
  font-size: var(--font-heading-s);
  line-height: var(--font-heading-s-line);
  font-weight: var(--font-weight-bold);
}

.notice--error {
  background-color: var(--color-error-background);
  color: var(--color-error);
  border-color: var(--color-error);
}

.notice--success {
  background-color: var(--color-success-background);
  color: var(--color-success);
  border-color: var(--color-success);
}

.notice--warning {
  background-color: var(--color-warning-background);
  color: var(--color-warning);
  border-color: var(--color-warning);
}

.notice--info {
  background-color: var(--color-info-background);
  color: var(--color-info);
  border-color: var(--color-info);
}

/* ---- 10. States: loading, empty, live status ----------------------------- */
/* Opacity, not colour: the pulse needs no new token, and a skeleton holds no
   text, so it makes no contrast claim. */
@keyframes skeleton-pulse {
  from {
    opacity: 1;
  }
  to {
    opacity: 0.5;
  }
}

/* Replaces real content while a card or SCORM package loads. Its height is one
   body line so swapping in the real thing does not move the page (CLS budget
   in docs/research-design-andragogy-2026-09-28.md R2). */
.skeleton {
  min-block-size: calc(var(--font-body) * var(--font-body-line));
  background-color: var(--color-surface-muted);
  border-radius: var(--radius-control);
  animation: skeleton-pulse var(--motion-slow) ease-in-out infinite alternate;
}

.skeleton--media {
  min-block-size: var(--space-8);
}

/* Not a fourth surface: the card's own surface and radius, with a dashed edge
   to read as "nothing here yet". Must still carry a heading and a control, per
   the design language rule that every feature is a card, chip, notice or
   control. Empty states are a real case — filters can legitimately match zero
   of 30 items (D-014). */
.state-empty {
  max-inline-size: var(--prose-max-width);
  padding: var(--space-4);
  background-color: var(--color-surface);
  border: var(--border-width-ui) dashed var(--color-border);
  border-radius: var(--radius-card);
}

.state-empty__title {
  margin-block: 0 var(--space-2);
  font-size: var(--font-heading-s);
  line-height: var(--font-heading-s-line);
  font-weight: var(--font-weight-semibold);
}

/* For <p class="status" role="status" aria-live="polite">: the size is reserved
   so the message appearing does not shift what is below it, and the region is
   never display:none, which would make it unannounced (SC 4.1.3). */
.status {
  min-block-size: calc(var(--font-meta) * var(--font-meta-line));
  margin-block: var(--space-2);
  color: var(--color-text-secondary);
  font-size: var(--font-meta);
  line-height: var(--font-meta-line);
}

/* ---- 11. Utilities -------------------------------------------------------- */
/* The standard visually-hidden recipe: content stays in the accessibility tree
   and stays a focus target, so it must never carry display/visibility:none.
   The 1 px box carries no meaning, so the "nothing load-bearing is 1 px" rule
   does not apply to it. */
.u-visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Marker utility: adds nothing itself, it only swaps the focus ring to the
   yellow ink for any control you place on an accent or dark fill (ink-on-teal
   fails 1.4.11). Do not use it on a chip — a chip is not a control. */
.u-focus-on-fill:focus-visible {
  outline-color: var(--color-focus-fill);
}

/* ---- 12. Responsive -------------------------------------------------------
 * 40rem switches the non-grid chrome (page padding, nav gaps) to its roomier
 * step; the GRID steps later, at 57.5rem/85rem, because its numbers come from
 * the playable-width floor, not from typography (D-030). */
@media (min-width: 40rem) {
  .page {
    padding-inline: var(--space-5);
  }

  .site-nav ul {
    gap: var(--space-2) var(--space-4);
  }
}

@media (min-width: 57.5rem) {
  .card-grid {
    /* D-030 (supersedes D-029's stage points+cap; supersedes the pre-D-029
       auto-fill, which measured-plateaued at 336 px inner and inverted at 1080).
       Track arithmetic, all values on the token scale: border-box .page =
       min(96rem, vw); content band = page − 2×32 px pad; track =
       (band − (n−1)×24 gap)/n; playable inner = track − 2×24 pad − 2×2 border.
       The 2-column stage opens only where the floor is already met:
       n=2 ⇒ band ≥ 2×416+24 = 856 ⇒ vw ≥ 57.5rem (920 px): track 416, inner
       364 px ≥ the 360 px operator floor for embedded games (2026-09-30,
       "columns should never get so narrow that the game's title text is
       clipped"; docs/responsive-layout-research-2026-09-30.md).
       The 3-column stage opens the same way at 85rem (1360 px vw), and the
       96rem cap caps columns at 3 (a 4th would need a 1736 px band, impossible
       under the cap): across 960–1536 px the inner width runs 364→422 px
       monotone-with-two-arithmetic-dips, never below the floor — no plateau,
       no inversion. A single track below 57.5rem is full-band by construction,
       so phone/tablet portrait gives the game maximum width, not a column cut.
       minmax(0, …) stays: an `auto` track minimum (plain 1fr) is the card's
       min-content — 322 px inside a 288 px column at 400 % zoom forced sideways
       page scrolling (SC 1.4.10). Games inside iframes may 2-D-scroll
       internally (W3C Understanding 1.4.10, games exception); the page may not.
       No rule here assumes an item count (D-014). */
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (min-width: 85rem) {
  .card-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

@supports (width: 100cqi) {
  .card-grid {
    /* Container-query twin of the two media stages above: the page width IS the
       container's own width, so breakpoints move to the content-band measure —
       immune if .page ever grows side rails. (MDN: a container cannot style
       itself, hence the media fallback stays.) Widely available since 2023. */
    @container page-band (min-width: 53.5rem) {
      .card-grid {
        grid-template-columns: repeat(2, minmax(0, 1fr));
      }
    }

    @container page-band (min-width: 81rem) {
      .card-grid {
        grid-template-columns: repeat(3, minmax(0, 1fr));
      }
    }
  }
}

/* ---- 13. Reduced motion ---------------------------------------------------
 * tokens.css already sets --motion-fast/--motion-slow to 0ms here, which is why
 * no component needs its own guard and why nothing here needs !important (a
 * universal `* { transition: none }` would lose to a class-level shorthand and
 * would have to shout to win — so it is not written). These are the explicit
 * belts for the components in this file: the skeleton's infinite shimmer is the
 * one thing a zeroed duration does not obviously cancel. */
@media (prefers-reduced-motion: reduce) {
  .skip-link,
  .card,
  .chip,
  .btn,
  .notice,
  .state-empty,
  .status,
  .skeleton {
    animation: none;
    transition: none;
  }
}

/* ---- 14. Forced colours / High Contrast ----------------------------------
 * SC 1.4.1 in the mode the user chose, and a named Phase 5 check. The OS owns
 * the palette, so anything whose visibility is load-bearing is re-declared in
 * system colours instead of relying on ours surviving. No !important is needed:
 * the system colours beat our custom properties by cascade origin, not force. */
@media (forced-colors: active) {
  /* The two-ink construction collapses to the system ink, which is guaranteed
     to contrast with the system canvas. */
  :focus-visible {
    outline: var(--focus-ring-width) solid CanvasText;
    outline-offset: var(--focus-ring-offset);
  }

  /* Controls the OS paints with its own fill take the selection colour, which
     stays visible against ButtonFace and Canvas alike. */
  .btn:focus-visible,
  .u-focus-on-fill:focus-visible {
    outline-color: Highlight;
  }

  .card,
  .notice,
  .chip,
  .state-empty,
  .skip-link {
    border-color: CanvasText;
  }

  .btn {
    border-color: ButtonText;
  }

  /* GrayText is the OS's own disabled ink; the dashed boundary from section 8
     still carries the state, so it is not colour-only here either. */
  .btn:disabled,
  .btn[aria-disabled="true"],
  .btn--secondary:disabled,
  .btn--secondary[aria-disabled="true"] {
    color: GrayText;
    border-color: GrayText;
  }

  /* A placeholder whose only signal was a fill would vanish once fills go. */
  .skeleton {
    border: var(--border-width-ui) dashed CanvasText;
  }
}
