/* ==========================================================================
   BASE — ground, focus, motion, utilities
   ========================================================================== */

html {
  background-color: var(--color-bg-base);
  color-scheme: dark;
  scroll-behavior: smooth;

  /* The guard sits on BOTH html and body deliberately. <body>'s overflow
     normally propagates to the viewport and <body> itself computes to
     visible, so the body declaration alone should be enough — but that
     propagation only happens while html's own overflow is `visible`, and it
     is one `overflow: hidden` on html away from silently stopping. Declaring
     it here removes the dependency.

     `clip`, never `hidden`: `hidden` makes the element a scroll container,
     and a scroll container between the viewport and a `position: sticky`
     element stops sticky working. §10's stack depends on it. */
  overflow-x: hidden;
  overflow-x: clip;
}

body {
  background-color: var(--color-bg-base);
  color: var(--color-text-body);
  font-family: var(--font-family-base);
  font-size: var(--font-size-body);
  font-weight: var(--font-weight-regular);
  line-height: var(--line-height-body);
  letter-spacing: var(--letter-spacing-body);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  min-height: 100vh;

  /* ⚠⚠ `clip`, NOT `hidden` — and this is a fix, not a preference.
     `overflow-x: hidden` on <body> makes the body a SCROLL CONTAINER, and a
     scroll container between the viewport and a `position: sticky` element is
     the classic reason sticky silently stops working. §10's speaker stack and
     its sticky heading are both descendants of <body> and nothing else
     between them clips, so this one declaration was disabling the page's
     scroll-stacking signature interaction.

     `overflow-x: clip` prevents the same horizontal overflow without creating
     a scroll container, so sticky keeps working. `hidden` is left first as the
     fallback for engines that do not know `clip` — they behave exactly as
     they did before, which is to say sticky is broken there too and was
     already. */
  overflow-x: hidden;
  overflow-x: clip;

  /* The guard of last resort for horizontal overflow. Prose wraps at spaces;
     a long unbroken token — an email typed into the form, a URL, an
     institution name run together — has no space to wrap at and will push a
     flex or grid track wider than its container. A flex item's automatic
     minimum is its min-content width, so that push reaches the page.
     break-word lets the token break rather than the layout. */
  overflow-wrap: break-word;
}

/* THE DOCK'S FOOTPRINT IS RESERVED, not overlaid. The sticky CTA is fixed, so
   it is out of flow and would otherwise cover the last of the page — the
   footer sits directly under it. The space is held for the whole range the
   dock can appear in rather than toggled with it: adding padding at the
   moment the bar arrives would shift the page under the reader's thumb, which
   is worse than a reserved strip they never see at the top of the page.

   52 (--control-height) + 16 above + 16 below, plus whatever the home
   indicator takes. */
@media (max-width: 991px) {
  body {
    padding-block-end: calc(
      var(--control-height) + (var(--space-sm) * 2) + env(safe-area-inset-bottom, 0px)
    );
  }
}

/* Scroll lock, applied by JS while the modal or mobile menu is open.
   Compensates for the removed scrollbar so the page does not shift. */
html.is-scroll-locked {
  overflow: hidden;
  padding-right: var(--scrollbar-width, 0px);
}

/* --------------------------------------------------------------------------
   FOCUS
   2px --color-focus-ring at 2px offset. Never clipped.

   ⚠⚠ "NEVER REMOVED" WAS TRUE UNTIL R21t AND IS NOT ANY MORE. form.css now
   sets `outline: none` on .input, .select, .textarea and .field__digits by
   direction — a known WCAG 2.4.7 failure, amended in design-rules.md §9 and
   logged in CLAUDE.md §12. EVERY OTHER FOCUSABLE THING STILL TAKES THE RING
   from this rule; do not extend the exception to reach them.
   -------------------------------------------------------------------------- */
:focus-visible {
  outline: var(--focus-ring-width) solid var(--color-focus-ring);
  outline-offset: var(--focus-ring-offset);
}

:focus:not(:focus-visible) { outline: none; }

/* --------------------------------------------------------------------------
   SELECTION
   -------------------------------------------------------------------------- */
::selection {
  background-color: var(--color-accent-action);
  color: var(--color-on-action);
}

/* --------------------------------------------------------------------------
   UTILITIES
   -------------------------------------------------------------------------- */
.visually-hidden:not(:focus):not(:active) {
  clip-path: inset(50%);
  height: 1px;
  overflow: hidden;
  position: absolute;
  white-space: nowrap;
  width: 1px;
}

.skip-link {
  position: absolute;
  top: var(--space-sm);
  left: var(--space-sm);
  z-index: var(--layer-modal-panel);
  background-color: var(--color-accent-action);
  color: var(--color-on-action);
  font-size: var(--font-size-meta);
  font-weight: var(--font-weight-semibold);
  text-transform: uppercase;
  letter-spacing: var(--letter-spacing-caps);
  padding: var(--space-xs) var(--space-sm);
  border-radius: var(--radius-control);
  transform: translateY(-200%);
}

.skip-link:focus { transform: none; }

/* Grain. Only ever layered over a gradient.

   `isolation: isolate` — R6j. Two reasons, and the second is the reason it
   was worth doing:
     · CORRECTNESS. Without it, `mix-blend-mode: overlay` blends against
       everything beneath in the nearest stacking context, not just the
       gradient it belongs to. The rule says grain appears only where gradient
       appears; this is what enforces it in the compositor rather than by
       convention.
     · COST. A blend that reaches outside its parent forces the browser to
       keep the backdrop beneath it available and re-composite it. Isolating
       confines that work to the gradient box. */
.u-grain { position: relative; isolation: isolate; }
.u-grain::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image: var(--texture-grain);
  opacity: var(--texture-grain-opacity);
  pointer-events: none;
  mix-blend-mode: overlay;
}

/* --------------------------------------------------------------------------
   REVEAL
   Visible by default. The offset state is applied ONLY when JS is present
   AND motion is allowed — so content can never be invisible if either the
   script fails or the user prefers reduced motion.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: no-preference) {
  .js .reveal {
    opacity: 0;
    transform: translateY(var(--reveal-distance));
    transition:
      opacity   var(--duration-reveal) var(--ease-reveal),
      transform var(--duration-reveal) var(--ease-reveal);
    /* R18e — every entrance on the page waits --delay-reveal-lead before it
       starts. ADDED to --reveal-delay rather than replacing it, so the
       stagger and sequence utilities still compose on top. */
    transition-delay: calc(var(--reveal-delay, 0ms) + var(--delay-reveal-lead));
  }
  .js .reveal.is-revealed {
    opacity: 1;
    transform: none;
  }
}

/* --------------------------------------------------------------------------
   REDUCED MOTION
   Content renders in its final state. Nothing becomes invisible.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
