/* ==========================================================================
   3D Scroll Prompts - the four prompts behind the OPAL demo
   https://3d-scroll-prompts.vikataylor.com

   Same hand as opal.vikataylor.com, different job. OPAL is a showcase built
   around a scroll-pinned stage; this page is a reading and copying surface,
   so the pin machinery (.flow / .seq / .stage, the drifting studio blobs, the
   sheen keyframes) is deliberately absent. What carries over is the language:
   the token block, the fixed scanline texture, the eyebrow with its hairline
   rule, the .btn sweep and the .btn--ghost hairline, very light Bodoni set
   large and tightly leaded, 10-12px uppercase labels on wide tracking, the
   glowing footer wordmark, hairline rules, and the script-applied reveal.

   Dark only. No theme toggle - the sibling site has one, OPAL does not, and
   mixing the two would read as two studios.

   Contrast: every text colour below carries its computed WCAG 2.1 ratio
   against the page background #05080F. Nothing used for text is under
   4.5:1. --accent-deep #485671 is 2.71:1 and is decoration only, never text.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Tokens

   Palette and families are OPAL's, unchanged. Everything added here is
   layout rhythm for a long-form page: a reading measure, a shell width, the
   height of the sticky nav (anchors scroll past it) and the pre/figure cap.
   -------------------------------------------------------------------------- */

:root {
  --ink: #05080F;                 /* page background                          */
  --ink-scrim: #04070D;           /* inset panels: prompt bodies, video frame */
  --accent: #A8BBD6;              /* 10.25:1 on --ink                         */
  --accent-mid: #7E96BC;          /*  6.66:1 on --ink                         */
  --accent-deep: #485671;         /*  2.71:1 - hairlines and marks only       */

  --paper: rgba(255, 255, 255, 0.86);   /* #DCDCDD -> 14.67:1  running prose  */
  --paper-mid: rgba(255, 255, 255, 0.72); /* #B9BABC -> 10.30:1  quiet labels */
  --paper-low: rgba(255, 255, 255, 0.62); /* #A0A1A4 ->  7.76:1  fine print   */

  --line: rgba(168, 187, 214, 0.18);      /* hairline rules, decorative       */
  --line-soft: rgba(168, 187, 214, 0.11); /* inset panel edges, decorative    */
  --hairline-strong: rgba(255, 255, 255, 0.4); /* #696B6F -> 3.74:1, clears
                                     WCAG 1.4.11 for the ghost button edge    */

  --serif: 'Bodoni Moda', 'Times New Roman', serif;
  --sans: 'Space Grotesk', 'Helvetica Neue', Helvetica, sans-serif;
  --mono: ui-monospace, 'SF Mono', SFMono-Regular, Menlo, Consolas,
          'Liberation Mono', monospace;

  --gutter: max(20px, 4vw);
  --shell: 1080px;          /* content column; prose is narrower inside it    */
  --pre-max: 760px;         /* prompt body + its fade share this exact cap    */
  --nav-h: 66px;            /* one row; the narrow block below overrides it   */
  --pipe-n-w: clamp(48px, 6vw, 76px);

  --ease-out: cubic-bezier(0.16, 0.84, 0.24, 1);
}

/* --------------------------------------------------------------------------
   2. Reset and base
   -------------------------------------------------------------------------- */

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

html {
  -webkit-text-size-adjust: 100%;
  background: var(--ink);
  /* The nav is sticky, so anchor jumps have to clear it. */
  scroll-padding-top: calc(var(--nav-h) + 14px);
  scroll-behavior: smooth;
}

body {
  margin: 0;
  background: var(--ink);
  color: var(--paper);                 /* 14.67:1 */
  font-family: var(--sans);
  font-size: 16px;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  text-wrap: pretty;
  /* html keeps overflow:visible, so this propagates to the viewport and the
     used overflow of body stays visible: position:sticky on the nav is
     unaffected. It is a backstop only - nothing here is designed to overflow. */
  overflow-x: hidden;
}

main { display: block; }

img,
picture,
svg,
video,
iframe { max-width: 100%; }

img { height: auto; }

figure,
dl,
dd,
blockquote { margin: 0; }

/* Structural guarantee, not decoration: no preformatted block on this page
   may ever scroll sideways, with or without its class hook. */
pre {
  margin: 0;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

a { color: #FFFFFF; text-decoration: none; }        /* 20.04:1 */

button { font-family: var(--sans); }

::selection { background: var(--accent); color: var(--ink); }  /* 10.25:1 */

/* Anchor targets, including a card deep link. Mirrors scroll-padding-top for
   engines that honour scroll-margin but not scroll-padding. */
#top,
#main,
#watch,
#prompts,
#result,
.card,
[data-prompt] { scroll-margin-top: calc(var(--nav-h) + 14px); }

/* --------------------------------------------------------------------------
   3. Scanline texture

   OPAL draws this with a <div class="scanlines">. That element is not in the
   DOM contract for this page, so the texture is painted by body::after and
   depends on no markup at all. If a .scanlines element is emitted anyway it
   is neutralised, so the texture can never end up drawn twice.
   -------------------------------------------------------------------------- */

body::after {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 90;
  pointer-events: none;
  opacity: 0.28;
  background: repeating-linear-gradient(0deg,
    rgba(255, 255, 255, 0.022) 0px,
    rgba(255, 255, 255, 0.022) 1px,
    rgba(0, 0, 0, 0) 1px,
    rgba(0, 0, 0, 0) 3px);
}

.scanlines { display: none; }

/* --------------------------------------------------------------------------
   4. Shell and section rhythm

   No wrapper class exists in the contract, so the column is made by the
   section itself: a single centred track of at most --shell, with a zero min
   so a long unbroken string can never widen it. Every direct child lands in
   that track whatever the markup shape, and no child's own margins are
   touched, which a `> *` max-width rule would have fought over.
   -------------------------------------------------------------------------- */

main > section,
main > article,
main > div > section,
.hero {
  display: grid;
  grid-template-columns: minmax(0, var(--shell));
  justify-content: center;
  padding: clamp(58px, 8vw, 116px) var(--gutter);
}

/* The hero opens the page, so it sits tighter under the nav. */
.hero { padding-top: clamp(40px, 6vw, 84px); }

/* Section heads. The contract gives no hook for them, so the element
   selectors carry the treatment and the block hooks refine it. */
main h2 {
  margin: 0 0 clamp(16px, 1.8vw, 26px);
  font-family: var(--serif);
  font-weight: 200;               /* variable axis floor is 400; clamps there */
  font-size: clamp(30px, 4.6vw, 64px);
  line-height: 1.0;
  letter-spacing: 0.01em;
  text-transform: uppercase;
  color: #FFFFFF;                 /* 20.04:1 */
}

main h3 {
  margin: 0 0 12px;
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(21px, 2.4vw, 30px);
  line-height: 1.18;
  color: #FFFFFF;                 /* 20.04:1 */
}

main h4 {
  margin: 0 0 10px;
  font-family: var(--sans);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.26em;
  text-transform: uppercase;
  color: var(--accent);           /* 10.25:1 */
}

main p {
  margin: 0 0 clamp(12px, 1.2vw, 18px);
  max-width: 62ch;                /* Space Grotesk: ~68 characters per line   */
}

main p:last-child { margin-bottom: 0; }

main strong { color: #FFFFFF; font-weight: 500; }   /* 20.04:1 */

/* In-flow links inside running copy. Buttons and label links keep their own
   treatment, so the underline is opted into by container, never globally. */
.hero__stand a,
.hero__meta a,
.pipe__body a,
.card__why a,
.card__meta a,
.result a:not(.btn),
.result__note a,
.vid__caption a,
.vid__note a,
.vid__link,
.footer__note a {
  color: var(--accent);           /* 10.25:1 */
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
  text-decoration-color: rgba(168, 187, 214, 0.45);
  transition: color 300ms linear, text-decoration-color 300ms linear;
}

.hero__stand a:hover,
.hero__meta a:hover,
.pipe__body a:hover,
.card__why a:hover,
.card__meta a:hover,
.result a:not(.btn):hover,
.result__note a:hover,
.vid__caption a:hover,
.vid__note a:hover,
.vid__link:hover,
.footer__note a:hover {
  color: #FFFFFF;                 /* 20.04:1 */
  text-decoration-color: #FFFFFF;
}

/* --------------------------------------------------------------------------
   5. Buttons

   OPAL's .btn verbatim: solid white face, dark label, a light sweep that
   crosses on hover by moving a 280%-wide gradient. Added here are the resets
   a <button> needs and never inherits, because the copy controls on this page
   are real buttons rather than links.
   -------------------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  margin: 0;
  border: 0;
  border-radius: 0;
  -webkit-appearance: none;
  appearance: none;
  cursor: pointer;
  font-family: var(--sans);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.22em;
  line-height: 1.25;
  text-transform: uppercase;
  text-align: center;
  padding: 20px 38px;
  background-image: linear-gradient(100deg,
    #FFFFFF 0%, #FFFFFF 26%, #FFFFFF 44%,
    #D8E2EF 56%,
    #FFFFFF 74%, #FFFFFF 100%);
  background-color: #FFFFFF;
  background-size: 280% 100%;
  background-position: 100% 0;
  transition: background-position 900ms cubic-bezier(0.2, 0.7, 0.2, 1);
}

/* #05080F on #FFFFFF is 20.04:1; on the #D8E2EF sweep stop, 15.31:1. */
.btn,
.btn:hover { color: var(--ink); }

.btn:hover { background-position: 0% 0; }

.btn:disabled { cursor: default; }

.btn--sm { padding: 14px 24px; font-size: 11px; }
.btn--wide { padding: 20px 44px; }

.btn--ghost {
  font-weight: 400;
  padding: 19px 34px;
  /* rgba(255,255,255,0.4) over ink is #696B6F -> 3.74:1, past the 3:1 that
     WCAG 1.4.11 asks of a control boundary. OPAL's 0.34 measured 2.99:1. */
  border: 1px solid var(--hairline-strong);
  background-image: none;
  background-color: transparent;
  transition: border-color 400ms linear, background-color 400ms linear;
}

.btn--ghost,
.btn--ghost:hover { color: #FFFFFF; }               /* 20.04:1 */

.btn--ghost:hover {
  border-color: #FFFFFF;
  background-color: rgba(255, 255, 255, 0.08);
}

.btn--ghost.btn--sm { padding: 13px 22px; }

.actions {
  display: flex;
  align-items: center;
  gap: 18px 22px;
  flex-wrap: wrap;
  margin: 0;
}

/* --------------------------------------------------------------------------
   6. Copy controls: pressed, copied, failed

   The shipped script says "Copied" by swapping the label and announcing it in
   the live region. CSS adds the press, holds the button's geometry steady
   across the swap, and keeps a state face ready: if a class is ever put on
   the button, every plausible name resolves to the same face rather than to
   nothing. --ink on --accent is 10.25:1, so that face stays readable.

   min-width is what the label swap actually needs. "Copy prompt 01" measures
   about 18em at this size and "Copied" about 11.5em, so without a floor the
   primary control of the page would shrink by a third and snap back two
   seconds later. The floor is min()-capped, so it can never widen a narrow
   viewport.
   -------------------------------------------------------------------------- */

[data-copy] { min-width: min(100%, 18.4em); }

[data-copy]:active { transform: translateY(1px); }

.btn.is-copied,
.btn.is-copied:hover,
.btn[data-state="copied"],
.btn[data-copied],
[data-copy][aria-pressed="true"] {
  color: var(--ink);                    /* 10.25:1 on --accent */
  background-image: none;
  background-color: var(--accent);
  border-color: var(--accent);
}

.btn.is-error,
.btn.is-error:hover,
.btn[data-state="error"] {
  color: var(--accent);                 /* 10.25:1 on --ink */
  background-image: none;
  background-color: transparent;
  border: 1px solid var(--accent);
}

[data-copy] {
  transition:
    background-position 900ms cubic-bezier(0.2, 0.7, 0.2, 1),
    background-color 220ms linear,
    border-color 220ms linear,
    color 220ms linear,
    transform 90ms linear;
}

/* --------------------------------------------------------------------------
   7. Skip link - first focusable thing in the document
   -------------------------------------------------------------------------- */

.skip {
  position: fixed;
  top: 0;
  left: -9999px;
  z-index: 100;                   /* over the scanlines (90) and the nav (70) */
  padding: 14px 20px;
  font-family: var(--sans);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  background: var(--accent);
}

.skip,
.skip:hover { color: var(--ink); }          /* 10.25:1 */

/* :focus is the fallback for engines without :focus-visible. A skip link is
   only ever reached from the keyboard, so both resolve to the same thing. */
.skip:focus { top: 8px; left: 8px; }

.skip:focus-visible {
  top: 8px;
  left: 8px;
  outline: 2px solid #FFFFFF;     /* the accent ring would vanish on accent */
  outline-offset: 2px;
}

/* --------------------------------------------------------------------------
   8. Nav - always visible

   OPAL hides its nav until the pinned hero is done. There is no pin here and
   the page is long, so the nav sticks from the first pixel. Solid ink plus a
   hairline, not a translucent panel: running text must not ghost through it,
   and blur panels are out of the house language.
   -------------------------------------------------------------------------- */

.nav {
  position: sticky;
  top: 0;
  z-index: 70;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 10px 24px;
  padding: 14px var(--gutter);
  background: var(--ink);
  border-bottom: 1px solid var(--line);
}

.nav__brand {
  min-width: 0;
  font-family: var(--serif);
  font-weight: 700;
  font-size: clamp(12px, 1.3vw, 17px);
  letter-spacing: 0.34em;
  line-height: 1.3;
  text-transform: uppercase;
  overflow-wrap: break-word;
}

.nav__brand,
.nav__brand:hover { color: #FFFFFF; }       /* 20.04:1 */

.nav__links {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 6px 32px;
  min-width: 0;
}

.nav__link {
  font-size: 11px;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--paper-mid);        /* 10.30:1 */
  transition: color 400ms linear;
}

.nav__link:hover { color: #FFFFFF; }        /* 20.04:1 */
.nav__link.is-active { color: var(--accent); }  /* 10.25:1 */

.nav__cta {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  justify-self: end;              /* also correct if .nav__cta is the button */
  min-width: 0;
}

/* --------------------------------------------------------------------------
   9. Hero - short on purpose. No stage, no video, no counter.
   -------------------------------------------------------------------------- */

.hero__eyebrow {
  display: flex;
  align-items: center;
  gap: 16px;
  margin: 0 0 clamp(18px, 2.2vw, 28px);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.32em;
  line-height: 1.7;
  text-transform: uppercase;
  color: var(--accent);           /* 10.25:1 */
}

/* OPAL draws the rule with a <span class="eyebrow__rule">. That class is not
   in the contract, so the rule is a pseudo-element and needs no markup. An
   empty child - which is all a hairline span can be - is folded away so the
   rule cannot be drawn twice. */
.hero__eyebrow::before {
  content: '';
  flex: none;
  width: 56px;
  height: 1px;
  background: currentColor;
}

.hero__eyebrow > :empty { display: none; }
.hero__eyebrow > * { min-width: 0; }

.hero__title {
  margin: 0 0 clamp(16px, 2vw, 26px);
  font-family: var(--serif);
  font-weight: 200;               /* clamps to the 400 axis floor */
  font-size: clamp(38px, 7.4vw, 96px);
  line-height: 0.94;
  letter-spacing: 0.005em;
  text-transform: uppercase;
  color: #FFFFFF;                 /* 20.04:1 */
}

.hero__stand {
  margin: 0 0 clamp(24px, 3vw, 38px);
  max-width: 38ch;
  font-family: var(--serif);
  font-style: italic;
  font-weight: 400;
  font-size: clamp(19px, 2.1vw, 30px);
  line-height: 1.34;
  color: rgba(255, 255, 255, 0.92);   /* #EBEBEC -> 16.84:1 */
}

.hero__meta {
  margin: clamp(24px, 3vw, 36px) 0 0;
  padding-top: 18px;
  max-width: 74ch;
  border-top: 1px solid var(--line);
  font-size: 11px;
  letter-spacing: 0.22em;
  line-height: 1.9;
  text-transform: uppercase;
  color: var(--paper-mid);        /* 10.30:1 */
}

/* --------------------------------------------------------------------------
   10. Watch - click-to-load facade

   The 16:9 box is reserved by aspect-ratio on .vid__frame, so the facade and
   the iframe the script swaps in occupy exactly the same space and the swap
   costs zero layout shift. Nothing here assumes which element carries
   .vid__thumb: <picture> and <img> are both covered.
   -------------------------------------------------------------------------- */

/* .vid may be the #watch section itself or a block inside it, so it carries no
   box of its own on purpose - the section rule owns the padding and the column
   in either shape, and the pieces below own their own geometry. */
.vid { min-width: 0; }

/* The attribute is matched alongside the class throughout this block: the
   reserved 16:9 box is what makes the facade-to-iframe swap cost zero layout
   shift, and that guarantee should not hang on a single class name surviving. */
.vid__frame,
[data-video-embed] {
  position: relative;
  width: 100%;
  max-width: var(--shell);
  margin-top: clamp(6px, 1.2vw, 18px);
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background: var(--ink-scrim);
  border: 1px solid var(--line);
}

.vid__facade,
[data-video-play] {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: block;
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  -webkit-appearance: none;
  appearance: none;
  background: var(--ink-scrim);
  color: #FFFFFF;                 /* 20.17:1 on the #04070D frame; the caption
                                     below states its ratio over the thumbnail */
  cursor: pointer;
  overflow: hidden;
  text-align: left;
}

/* The frame clips its children, so a ring drawn outside the facade would be
   cut off. It is drawn inside instead. --accent on ink is 10.25:1, far past
   the 3:1 a focus indicator needs. */
/* The ring cannot be an outline here: .vid__frame clips with overflow:hidden, so a
   positive offset is cut off and a negative one lands on the thumbnail. Measured
   against the shipped still it read a median 1.42:1, min 1.00:1, which fails
   SC 1.4.11. An inset box-shadow is not clipped and lets the accent band sit
   between two bands of page ink, so it is 10.25:1 whatever the frame contains. */
.vid__facade:focus-visible {
  outline: none;
  box-shadow:
    inset 0 0 0 3px var(--ink),
    inset 0 0 0 6px var(--accent),
    inset 0 0 0 9px var(--ink);
}

.vid__thumb,
.vid__thumb img,
.vid__facade img {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Vignette, the same idea as OPAL's hero: it holds the play glyph legible
   over the bright half of a thumbnail without a shadow or a blur panel. */
.vid__facade::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: radial-gradient(48% 48% at 50% 46%,
    rgba(4, 7, 13, 0.46) 0%,
    rgba(4, 7, 13, 0.12) 58%,
    rgba(4, 7, 13, 0) 78%);
}

/* Play glyph. A triangle, no ring and no stock icon. If the markup supplies
   an inline <svg> instead, the CSS triangle stands down. */
.vid__play {
  position: absolute;
  left: 50%;
  top: 50%;
  z-index: 2;
  display: grid;
  place-items: center;
  width: clamp(46px, 5.6vw, 74px);
  aspect-ratio: 1 / 1;
  color: #FFFFFF;                 /* the triangle is a glyph, not text; the
                                     vignette above keeps it off a bright frame */
  transform: translate(-50%, -50%);
  transition: transform 500ms var(--ease-out), opacity 400ms linear;
}

.vid__play::after {
  content: '';
  display: block;
  width: 62%;
  aspect-ratio: 1 / 1.14;
  background: currentColor;
  clip-path: polygon(0% 0%, 100% 50%, 0% 100%);
}

.vid__play:has(svg)::after,
.vid__play:has(img)::after { content: none; }

.vid__play svg {
  display: block;
  width: 100%;
  height: 100%;
  fill: currentColor;
}

.vid__facade:hover .vid__play { transform: translate(-50%, -50%) scale(1.07); }

/* Caption. Reads correctly whether it sits under the frame or inside the
   facade; the overlay variant carries its own scrim. */
.vid__caption {
  margin: 16px 0 0;
  max-width: 62ch;
  font-size: clamp(14px, 1.05vw, 16px);
  line-height: 1.5;
  color: #FFFFFF;                 /* 20.04:1 */
}

/* Over the thumbnail, the worst case is a pure white frame behind the 0.72
   stop where the text begins: white on that composite is 8.55:1, and it only
   improves further down the gradient. */
.vid__facade .vid__caption {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 2;
  margin: 0;
  max-width: none;
  padding: clamp(30px, 5vw, 52px) clamp(16px, 2.2vw, 26px) clamp(13px, 1.6vw, 18px);
  background: linear-gradient(180deg,
    rgba(4, 7, 13, 0) 0%,
    rgba(4, 7, 13, 0.72) 46%,
    rgba(4, 7, 13, 0.94) 100%);
}

.vid__note {
  margin: 16px 0 0;
  max-width: 62ch;
  font-size: 13px;
  line-height: 1.6;
  color: var(--paper-mid);        /* 10.30:1 */
}

.vid__link {
  display: inline-block;
  margin: 14px 0 0;
  font-size: 13px;
  line-height: 1.5;
}

/* The injected iframe fills the reserved box exactly. */
.vid__frame iframe,
[data-video-embed] iframe {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
}

/* --------------------------------------------------------------------------
   11. Pipeline - the map before the detail

   A vertical numbered list with a hairline running from each number down to
   the next. Explicitly not three equal columns: the steps are a chain, and a
   row of tiles would say they were alternatives.
   -------------------------------------------------------------------------- */

/* .pipe is whichever element the markup hangs the map on - the list itself, or
   a section wrapping it. The list reset is therefore element-qualified: a
   blanket `padding: 0` here would strip the gutter off a <section class="pipe">
   and push the steps against the edge of a phone screen. */
.pipe {
  margin-top: clamp(8px, 1.2vw, 16px);
  list-style: none;
}

ol.pipe,
ul.pipe {
  padding-inline-start: 0;
  padding-left: 0;
}

.pipe ol,
.pipe ul {
  margin: 0;
  padding: 0;
  list-style: none;
}

.pipe__item {
  position: relative;
  display: grid;
  grid-template-columns: var(--pipe-n-w) minmax(0, 1fr);
  column-gap: clamp(14px, 2vw, 26px);
  align-items: start;
  padding-bottom: clamp(26px, 3.4vw, 44px);
}

.pipe__item:last-child { padding-bottom: 0; }

/* The connector. It starts under the numeral and ends flush with the top of
   the next one, so the run reads as a single thread rather than as ticks. */
.pipe__item::before {
  content: '';
  position: absolute;
  left: calc(var(--pipe-n-w) / 2);
  top: clamp(30px, 3.8vw, 50px);
  bottom: 0;
  width: 1px;
  background: linear-gradient(180deg,
    var(--accent-deep) 0%,
    rgba(72, 86, 113, 0.35) 100%);
}

.pipe__item:last-child::before { content: none; }

.pipe__n {
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(22px, 3vw, 38px);
  line-height: 1.1;
  letter-spacing: 0.02em;
  text-align: center;
  color: var(--accent);           /* 10.25:1 */
}

.pipe__body {
  padding-top: clamp(2px, 0.6vw, 8px);
  max-width: 62ch;
  font-size: clamp(15px, 1.1vw, 17px);
  line-height: 1.7;
  color: var(--paper);            /* 14.67:1 */
}

.pipe__body p { margin: 0 0 8px; max-width: none; }
.pipe__body p:last-child { margin-bottom: 0; }

.pipe__body h3,
.pipe__body h4 {
  margin: 0 0 8px;
  font-family: var(--sans);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.24em;
  line-height: 1.6;
  text-transform: uppercase;
  color: var(--accent);           /* 10.25:1 */
}

.pipe__body strong,
.pipe__body b { color: #FFFFFF; font-weight: 500; }  /* 20.04:1 */

/* --------------------------------------------------------------------------
   12. Prompt cards - the substance of the page

   Four dense blocks. The separation between them is deliberately large and
   the top hairline is the only frame: boxed cards would turn the page into a
   grid of tiles and the prompts are meant to be read, not scanned.
   -------------------------------------------------------------------------- */

.card {
  margin: clamp(52px, 7vw, 104px) 0 0;
  padding-top: clamp(26px, 3vw, 44px);
  border-top: 1px solid var(--line);
}

.card:first-of-type { margin-top: clamp(30px, 4vw, 56px); }

.card__head {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  column-gap: clamp(14px, 1.8vw, 28px);
  align-items: baseline;
  margin: 0 0 clamp(20px, 2.4vw, 32px);
}

.card__n {
  margin: 0;                      /* it is a <p>, so the prose margin is off */
  font-family: var(--serif);
  font-weight: 200;               /* clamps to the 400 axis floor */
  font-size: clamp(38px, 6vw, 84px);
  line-height: 0.9;
  letter-spacing: 0.01em;
  color: var(--accent);           /* 10.25:1 */
}

.card__title {
  margin: 0;
  font-family: var(--serif);
  font-weight: 200;
  font-size: clamp(26px, 4vw, 56px);
  line-height: 1.0;
  letter-spacing: 0.01em;
  text-transform: uppercase;
  color: #FFFFFF;                 /* 20.04:1 */
}

/* Meta row: where the prompt is pasted, and what comes out. Hairline over
   each pair, the same figure/label rhythm OPAL uses for its facts. */
.card__meta {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
  gap: clamp(16px, 2vw, 30px);
  margin: 0 0 clamp(24px, 3vw, 38px);
  /* Squares the meta row off against the prompt block below it rather than
     against the prose measure: this is a data row, not a paragraph. */
  max-width: var(--pre-max);
}

.card__meta-item {
  margin: 0;
  padding-top: 14px;
  border-top: 1px solid var(--line);
  font-size: 14.5px;
  line-height: 1.6;
  color: var(--paper);            /* 14.67:1 */
}

/* The label half of the pair, whichever element carries it. A value-only item
   is left alone: only the shapes a label actually takes are matched. */
.card__meta-item > dt,
.card__meta-item > .label,
.card__meta-item > span:first-child,
.card__meta-item > strong:first-child,
.card__meta-item > b:first-child,
.card__meta-item > small:first-child {
  display: block;
  margin: 0 0 8px;
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.28em;
  line-height: 1.8;
  text-transform: uppercase;
  color: var(--accent-mid);       /* 6.66:1 */
}

.card__meta-item > dd,
.card__meta-item > p {
  margin: 0;
  max-width: none;
  color: var(--paper);            /* 14.67:1 */
}

.card__meta-item > strong:first-child { font-weight: 500; }

/* The editorial paragraph. This is the reason to publish the pack rather
   than a gist, so it gets a real measure and real leading: 62ch of Space
   Grotesk lands at roughly 68 characters a line. */
.card__why {
  margin: 0 0 clamp(24px, 3vw, 38px);
  max-width: 62ch;
  font-size: clamp(16px, 1.15vw, 18.5px);
  line-height: 1.75;
  color: var(--paper);            /* 14.67:1 */
}

.card__figure {
  position: relative;
  margin: 0;
  max-width: var(--pre-max);
}

.card__figure figcaption {
  margin: 0 0 10px;
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.28em;
  line-height: 1.8;
  text-transform: uppercase;
  color: var(--accent-mid);       /* 6.66:1 */
}

/* The prompt body. Verbatim text, so it wraps rather than scrolls sideways:
   a horizontal scrollbar on a prompt is a copy failure. overflow-wrap:anywhere
   also caps the block's min-content width, so a long unbroken token cannot
   widen the page. */
.card__pre,
[data-pre] {
  margin: 0;
  max-width: var(--pre-max);
  /* About 28rem, per the brief. Short viewports get a shorter box so there is
     still page to grab around it on a phone. */
  max-height: clamp(18rem, 52vh, 28rem);
  overflow-y: auto;
  overflow-x: hidden;
  padding: clamp(16px, 2vw, 26px);
  background: var(--ink-scrim);
  /* This is a scrollable region, not decoration: its edge has to be perceivable.
     --line is 1.37:1 against the panel, --hairline-strong is 3.77:1. */
  border: 1px solid var(--hairline-strong);
  font-family: var(--mono);
  font-size: clamp(12.5px, 0.9vw, 14px);
  line-height: 1.85;
  letter-spacing: 0;
  font-variant-numeric: tabular-nums;
  tab-size: 2;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  word-break: normal;
  -webkit-user-select: text;
  user-select: text;
  color: rgba(255, 255, 255, 0.92); /* #EBEBEC -> 16.94:1 on #04070D */
  scrollbar-width: thin;
  /* 0.34 measured 2.06:1 against the panel; SC 1.4.11 wants 3:1 for an author
     styled control. 0.48 is the exact crossing, 0.5 leaves margin at 3.21:1. */
  scrollbar-color: rgba(168, 187, 214, 0.5) transparent;
}

.card__pre::-webkit-scrollbar { width: 10px; }
.card__pre::-webkit-scrollbar-track { background: transparent; }

.card__pre::-webkit-scrollbar-thumb {
  background: rgba(168, 187, 214, 0.5); /* 3.21:1, was 0.3 = 1.84:1 */
  background-clip: padding-box;
  border: 3px solid transparent;
  border-radius: 4px;
}

.card__pre::-webkit-scrollbar-thumb:hover { background: var(--accent); }

/* An expand control, if one is wired, lifts the cap. The text is in the DOM
   either way - the cap is presentation only, never a truncation. */
.card__pre.is-open,
.card__pre.is-expanded,
.card__pre[data-expanded="true"],
.card.is-open .card__pre,
.card__figure.is-open .card__pre { max-height: none; }

/* Scroll fade. In flow directly after the pre and pulled back over it, so it
   is anchored to the bottom edge of the block without needing a positioned
   wrapper, and it can never end up covering .card__actions: its height and
   its negative margin cancel out to zero. The script owns the hidden state -
   at the end of the scroll, or when the block does not scroll at all. */
.card__fade {
  position: relative;
  z-index: 1;
  display: block;
  width: 100%;
  max-width: var(--pre-max);
  height: 5.5rem;
  margin-top: -5.5rem;
  pointer-events: none;
  /* Terminal alpha is capped at 0.44 on purpose. The prompt body is #EBEBEC on
     #04070D, 16.94:1 unfaded; at 0.44 the dimmest covered row still measures
     5.59:1, clear of AA. The previous 0.94 endpoint took the last rendered line
     of every capped prompt down to 1.10:1 - it dimmed the one thing on this page
     a visitor came to read. */
  background: linear-gradient(180deg,
    rgba(4, 7, 13, 0) 0%,
    rgba(4, 7, 13, 0.22) 40%,
    rgba(4, 7, 13, 0.34) 60%,
    rgba(4, 7, 13, 0.44) 100%);
  transition: opacity 320ms linear;
}

/* [hidden] has to be restated: the display:block above would otherwise beat
   the user-agent rule. aria-hidden is deliberately not in this list - it is
   the right attribute for a permanently decorative element and hiding on it
   would blank the fade for good. */
.card__fade[hidden] { display: none; }

/* If the fade is emitted inside the <pre> instead of after it, sticky keeps it
   on the bottom edge of the scrollport rather than letting it scroll away with
   the text. Same visual result, no markup argument. */
.card__pre > .card__fade {
  position: sticky;
  bottom: 0;
}

.card__fade.is-end,
.card__fade.is-hidden,
.card__fade.is-off,
.card__fade[data-end="true"],
.card.is-open .card__fade,
.card__figure.is-open .card__fade,
.card__pre.is-open + .card__fade,
.card__pre.is-expanded + .card__fade { opacity: 0; }

.card__actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 14px 20px;
  margin: clamp(20px, 2.4vw, 30px) 0 0;
}

.card__actions p {
  margin: 0;
  max-width: 46ch;
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--paper-low);        /* 7.76:1 */
}

/* --------------------------------------------------------------------------
   13. Result
   -------------------------------------------------------------------------- */

.result p {
  max-width: 62ch;
  font-size: clamp(16px, 1.15vw, 18.5px);
  line-height: 1.75;
  color: var(--paper);            /* 14.67:1 */
}

.result .actions { margin-top: clamp(20px, 2.4vw, 30px); }

/* The fictional-studio statement. It is an honesty line, so contrast is a
   floor and not a preference: it is set quieter through type - sentence case,
   near-zero tracking - rather than through luminance. */
.result__note {
  margin: clamp(22px, 2.6vw, 32px) 0 0;
  padding-top: 20px;
  max-width: 62ch;
  border-top: 1px solid var(--line);
  font-size: 13.5px;
  letter-spacing: 0.01em;
  line-height: 1.65;
  color: var(--paper-low);        /* #A0A1A4 -> 7.76:1 */
}

/* --------------------------------------------------------------------------
   14. Footer - OPAL's treatment: an enormous wordmark with a soft glow, a
   hairline, then the links and the legal row.

   The wordmark wraps rather than overflowing. OPAL's white-space:nowrap works
   for a four-letter name; this site's wordmark is longer, and a nowrap line
   at that size would push the page sideways on a phone.
   -------------------------------------------------------------------------- */

.footer {
  position: relative;
  overflow: hidden;
  display: grid;
  grid-template-columns: minmax(0, var(--shell));
  justify-content: center;
  padding: clamp(64px, 8vw, 108px) var(--gutter) clamp(32px, 4vw, 52px);
  border-top: 1px solid var(--line);
}

.footer__wordmark {
  margin: 0 0 clamp(28px, 4vw, 52px);
  font-family: var(--serif);
  font-weight: 100;               /* clamps to the 400 axis floor */
  font-size: clamp(40px, 13.5vw, 190px);
  line-height: 0.82;
  letter-spacing: -0.005em;
  text-transform: uppercase;
  overflow-wrap: break-word;
  color: var(--accent);           /* 10.25:1 */
  /* Plain rgba first so the glow survives without color-mix support. */
  text-shadow: 0 0 120px rgba(168, 187, 214, 0.45);
  text-shadow: 0 0 120px color-mix(in srgb, var(--accent) 45%, transparent);
}

.footer__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 18px 30px;
  flex-wrap: wrap;
  padding-top: 26px;
  border-top: 1px solid var(--line);
}

.footer__links {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 14px 30px;
}

.footer__link {
  font-size: 11px;
  letter-spacing: 0.24em;
  line-height: 1.7;
  text-transform: uppercase;
  color: var(--paper-mid);        /* 10.30:1 */
  transition: color 300ms linear;
}

.footer__link:hover { color: #FFFFFF; }     /* 20.04:1 */

.footer__legal {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 14px 30px;
  margin-top: 22px;
  font-size: 11px;
  letter-spacing: 0.24em;
  line-height: 1.7;
  text-transform: uppercase;
  color: var(--paper-mid);        /* 10.30:1 */
}

.footer__note {
  margin: 22px 0 0;
  max-width: 66ch;
  font-size: 12.5px;
  letter-spacing: 0.01em;
  line-height: 1.65;
  color: var(--paper-low);        /* #A0A1A4 -> 7.76:1 */
}

/* A real button that reads as a link, so consent can be withdrawn from the
   legal row as easily as it was given. A button inherits neither font nor
   colour, so the values are repeated rather than inherited. */
.linkish {
  -webkit-appearance: none;
  appearance: none;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  font-family: var(--sans);
  font-size: 11px;
  font-weight: 400;
  letter-spacing: 0.24em;
  line-height: inherit;
  text-transform: uppercase;
  color: var(--paper-mid);        /* 10.30:1 */
  cursor: pointer;
  transition: color 300ms linear;
}

.linkish:hover { color: #FFFFFF; }          /* 20.04:1 */

/* --------------------------------------------------------------------------
   15. Utilities
   -------------------------------------------------------------------------- */

/* The copy announcer. Visually hidden but still rendered - display:none or
   visibility:hidden would take it out of the accessibility tree and the
   polite announcement would never be made. */
.sr-live {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  white-space: nowrap;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
}

/* Reveals. The hidden state is applied by script and only to blocks still
   below the fold, so a dead script leaves the page readable rather than
   blank. The revealed class is matched under several plausible names: an
   extra selector costs nothing, a stuck-invisible section costs the page. */
[data-reveal].is-hidden {
  opacity: 0;
  transform: translate3d(0, 34px, 0);
}

[data-reveal].is-hidden.is-revealed,
[data-reveal].is-hidden.is-visible,
[data-reveal].is-hidden.is-in {
  opacity: 1;
  transform: translate3d(0, 0, 0);
  transition:
    opacity 900ms var(--ease-out),
    transform 1100ms var(--ease-out);
}

/* Focus ring. --accent on --ink is 10.25:1, well past the 3:1 WCAG 2.1
   SC 1.4.11 asks of a focus indicator, and the offset keeps it off the white
   face of .btn and on the dark page where it reads. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

/* A programmatic focus target - a container taking focus from the skip link -
   keeps the focus, it just does not paint a ring around half the page.
   tabindex="-1" is out of the tab order, so no reachable control loses its
   ring. A scrollable <pre> carries tabindex="0" and is unaffected. */
[tabindex="-1"]:focus-visible { outline: none; }

/* --------------------------------------------------------------------------
   16. Narrow viewports - down to 320px, no horizontal scroll, no clipped
   prompt text. Nothing is hidden at any width: every control in the nav is
   still reachable, it just rewraps.
   -------------------------------------------------------------------------- */

@media (max-width: 860px) {
  .card__meta { grid-template-columns: minmax(0, 1fr); }
}

@media (max-width: 620px) {
  :root { --nav-h: 108px; }       /* the nav is two rows below this width */

  .nav {
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas:
      'brand cta'
      'links links';
    gap: 10px 14px;
    padding: 12px var(--gutter);
  }

  .nav__brand { grid-area: brand; letter-spacing: 0.2em; }

  .nav__links {
    grid-area: links;
    justify-content: flex-start;
    gap: 6px 24px;
  }

  .nav__cta { grid-area: cta; }

  .nav__cta .btn {
    padding: 12px 16px;
    font-size: 10px;
    letter-spacing: 0.18em;
  }

  .btn { padding: 17px 26px; }
  .btn--wide { padding: 17px 28px; }
  .btn--ghost { padding: 16px 24px; }

  /* Tracks the smaller padding, so the label floor stays just wide enough. */
  [data-copy] { min-width: min(100%, 16.6em); }

  .hero__eyebrow { gap: 12px; }
  .hero__eyebrow::before { width: 32px; }

  .card__head {
    grid-template-columns: minmax(0, 1fr);
    row-gap: 4px;
  }

  .card__n { line-height: 1; }

  .pipe__item { column-gap: 12px; }

  .footer__row { justify-content: flex-start; }
}

@media (max-width: 380px) {
  .btn,
  .btn--wide { padding: 16px 20px; letter-spacing: 0.16em; }

  .btn--ghost { padding: 15px 18px; }

  [data-copy] { min-width: min(100%, 15.4em); }

  .actions { gap: 12px 14px; }

  .card__pre { padding: 14px 12px; }
}

/* --------------------------------------------------------------------------
   17. Reduced motion

   Every transition introduced in this file is listed. Nothing on this page is
   animation-driven, so nothing has to be replaced by a static fallback - the
   hidden reveal state is simply switched off, which is why it is neutralised
   rather than just un-transitioned: a visitor who asked for no motion should
   see the copy immediately, not have it appear on an observer callback.
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  [data-reveal].is-hidden,
  [data-reveal].is-hidden.is-revealed,
  [data-reveal].is-hidden.is-visible,
  [data-reveal].is-hidden.is-in {
    opacity: 1;
    transform: none;
    transition: none;
  }

  .btn,
  .btn--ghost,
  [data-copy],
  .nav__link,
  .footer__link,
  .linkish,
  .card__fade,
  .vid__play,
  .hero__stand a,
  .hero__meta a,
  .pipe__body a,
  .card__why a,
  .card__meta a,
  .result a:not(.btn),
  .result__note a,
  .vid__caption a,
  .vid__note a,
  .vid__link,
  .footer__note a { transition: none; }

  /* The copied face still changes instantly - only the crossfade is dropped -
     so the feedback survives for anyone who asked for no motion. */
  [data-copy]:active { transform: none; }

  .vid__facade:hover .vid__play { transform: translate(-50%, -50%); }
}
