/* ── base.css ───────────────────────────────────────────────────────────────
   Element-level defaults only. Mobile first. No component styles here.
   ───────────────────────────────────────────────────────────────────────── */

html {
  font-size: 62.5%;          /* 1rem = 10px */
  overflow-x: hidden;
}

body {
  padding-top: var(--sheet-top);
  /* Not overflow-x:hidden here — html's own overflow-x:hidden above already
     prevents horizontal bleed, and Safari (iOS and macOS both) has a real,
     well-documented bug where overflow set on body — even just overflow-x —
     breaks position:sticky for every descendant. Chrome and Firefox don't
     have this bug, so it's invisible testing in either of those; nav.css's
     mobile .nav{position:sticky} silently never actually stuck in Safari
     until this was removed. */
  overflow-wrap: break-word;
  background: var(--ground);
  color: var(--fg);
  font-family: var(--font-text);
  font-size: var(--fs-text);
  line-height: var(--lh-text);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1, h2, h3 {
  font-family: var(--font-display);
  line-height: var(--lh-tight);
  letter-spacing: var(--track-tight);
}
h1 { font-size: var(--fs-h1); }
h2 { font-size: var(--fs-h2); }
h3 { font-size: var(--fs-h3); font-family: var(--font-text); line-height: var(--lh-snug); }

p { max-width: var(--measure); }

/* The one shared idiom: metadata set in the data face, uppercase, tracked. */
.label {
  max-width: none;          /* labels are not prose: the 62ch measure would
                               truncate their rules and flex rows */
  font-family: var(--font-data);
  font-size: var(--fs-data);
  letter-spacing: var(--track-data);
  text-transform: uppercase;
  color: var(--fg-faint);
}

/* v3 workstream 6 — bracket framing. An equipment-label device (think a
   panel silkscreen: [ MIX ], [ 4 TOOLS ]) for the shortest, most
   meta-feeling labels only — never body copy. Applied directly to the two
   places that already are this kind of label, rather than a class added at
   every call site. */
.section-meta::before, .p-eyebrow::before { content: "[ "; }
.section-meta::after,  .p-eyebrow::after  { content: " ]"; }

/* Layout helpers — the only two the rebuild needs so far. */
.frame {
  width: 100%;
  max-width: var(--frame);
  margin-inline: auto;
  padding-inline: var(--gutter);
}
.section { padding-block: var(--section); }

/* Focus is the one interaction state kept from the start: it is an
   accessibility requirement, not decoration. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Tap targets on mobile. */
a, button { min-height: 44px; display: inline-flex; align-items: center; }
a.is-inline { min-height: 0; display: inline; }

/* Visually hidden, still announced. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ── Theme crossfade ────────────────────────────────────────────────────────
   Flipping the switch changes every colour on the page at once, and a hard cut
   reads as a glitch. Only three properties transition, and only inside the
   sheet — the lab bar is scaffolding and stays instant. Images and video are
   excluded so nothing repaints them.

   This rebuild has no hover states by design, so a standing transition on
   these properties cannot fire accidentally.
   ───────────────────────────────────────────────────────────────────────── */
@media (prefers-reduced-motion: no-preference) {
  html,
  body,
  .sheet,
  .sheet *:not(img):not(video) {
    transition:
      background-color var(--dur-theme) var(--ease-throw),
      border-color     var(--dur-theme) var(--ease-throw),
      color            var(--dur-theme) var(--ease-throw),
      box-shadow       var(--dur-theme) var(--ease-throw);
  }
}

/* ── Between pages ──────────────────────────────────────────────────────────
   There is no router: every link is a real navigation, which normally means a
   blank flash between pages. Two things fix it.

   1. html carries the ground colour, so the gap is painted in the page's own
      colour rather than the browser's white.
   2. A cross-document view transition crossfades the old page into the new
      one. Browsers without support simply navigate as before — there is no
      fallback to write and no JavaScript involved.
   ───────────────────────────────────────────────────────────────────────── */
html { background: var(--ground); }

@media (prefers-reduced-motion: no-preference) {
  @view-transition { navigation: auto; }
}

/* The nav's Plugins link (#plugins) is the only in-page anchor on the site —
   without this, clicking it (or landing on /#plugins from another page)
   jumps instantly instead of scrolling. Reduced-motion keeps the jump. */
@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
}

/* ── Page-transition morph — shared plugin shot ────────────────────────────
   The default cross-document transition above crossfades the whole page.
   A plugin's own shot is given the same view-transition-name on both ends
   (homepage card / product hero) via js/plugins.js and js/product.js, so the
   browser morphs that one element's position and size across the navigation
   instead of crossfading it — the shot itself is the "shared border-box."
   Timing switches from the UA default (0.25s ease) to this system's own
   draw/ease tokens, so the morph reads as part of the same vocabulary as
   stroke-draw rather than a generic browser default. */
@media (prefers-reduced-motion: no-preference) {
  ::view-transition-group(*) {
    animation-duration: var(--dur-draw);
    animation-timing-function: var(--ease-reveal);
  }
}

/* ── Page-transition pin — nav and footer ──────────────────────────────────
   Same shared-element mechanism as the plugin shot above, given to the nav
   and footer on every page. Their markup is now identical across the whole
   site (baked into each page's HTML, see js/shell.js), so the browser has
   nothing to crossfade between old and new — it just holds them in place,
   which is what makes navigating feel like the chrome never left, only the
   content between them changed. A footer whose position shifts because the
   page above it is a different height still gets the same smooth morph the
   shot does, rather than a jump. */
@media (prefers-reduced-motion: no-preference) {
  #nav { view-transition-name: site-nav; }
  .footer { view-transition-name: site-footer; }
}

/* ── Page-transition content — hand off to the page's own reveal ──────────
   Everything between nav and footer that isn't a named plugin shot falls
   into the implicit "root" group, which by default crossfades: the old
   page's content fades out while the new page's fades in, retimed above to
   460ms/--ease-reveal. But the new page's titles/lead copy now stroke-draw
   and whisper-fade themselves in on load (motion.js, gated by
   [data-stroke-draw]'s own opacity:0 start state) — layering that on top of
   a second, independent opacity crossfade read as two competing fades
   rather than one clean build-in. Turning off root's own crossfade
   animation leaves the incoming page's JS-driven reveal as the only motion
   anyone sees: the new page's content starts exactly as it does on a first
   load (invisible, per that same opacity:0 rule) and stroke-draws in from
   there; the old page's content just cuts away rather than fading, same
   instant-hold treatment nav/footer already get above. */
@media (prefers-reduced-motion: no-preference) {
  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation: none;
  }
}


/* ── Motion system v2 — stroke-draw / whisper-fade primitive ──────────────
   js/motion.js builds the SVG overlay; this is the shared shell every
   [data-stroke-draw] element gets regardless of page. Content starts hidden
   and only becomes visible once the draw has finished (transition-delay
   equal to the draw duration) — structure leads, content settles after,
   never simultaneous. prefers-reduced-motion shows content immediately with
   no overlay ever built, unconditionally — motion.js doesn't even run its
   observer in that case, this rule is the belt to that suspenders.
   ───────────────────────────────────────────────────────────────────────── */
[data-stroke-draw] { position: relative; }
[data-stroke-draw] > :not(.stroke-draw-svg) { opacity: 0; }
.stroke-draw-svg {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  overflow: visible;
  pointer-events: none;
}
.stroke-draw-path { fill: none; stroke: var(--rule); stroke-width: 1; }
[data-stroke-draw].is-drawing > :not(.stroke-draw-svg),
[data-stroke-draw].is-drawn > :not(.stroke-draw-svg) {
  opacity: 1;
  transition: opacity var(--dur-whisper) ease-out var(--dur-draw-sm);
}
@media (prefers-reduced-motion: reduce) {
  [data-stroke-draw] > :not(.stroke-draw-svg) { opacity: 1 !important; transition: none !important; }
}

/* ── [data-fade-in] — whisper-fade on its own, no stroke-draw precursor ────
   Page titles/lead copy settling in on load: no line, no box, nothing
   drawn — just the opacity half of the vocabulary, staggered via --i so a
   page's intro block reads as one top-to-bottom wave. Always above the
   fold at load, so this runs as a plain CSS animation on paint — no JS,
   no IntersectionObserver needed. */
@media (prefers-reduced-motion: no-preference) {
  [data-fade-in] {
    opacity: 0;
    animation: fade-in-whisper var(--dur-whisper) ease-out both;
    animation-delay: calc(var(--i, 0) * 80ms);
  }
}
@keyframes fade-in-whisper { to { opacity: 1; } }

