/* ==========================================================================
   korscope.com — "Graphite + Signal"
   Tokens mirror app/src/styles/theme.css dark theme ("ink inverted") per
   DESIGN.md. The page is one drawing board seen at dusk: achromatic graphite,
   one signal yellow, nothing else. Yellow stays under 5% of the surface.
   ========================================================================== */

:root {
  /* --- graphite ramp (chroma 0) --- */
  --bg: oklch(0.16 0 0);
  --bg-deep: oklch(0.115 0 0);
  --surface: oklch(0.21 0 0);
  --sl: oklch(0.25 0 0);
  --text: oklch(0.93 0 0);
  --muted: oklch(0.75 0 0);
  --light: oklch(0.65 0 0);
  --border: oklch(0.38 0 0);
  --bl: oklch(0.30 0 0);
  /* The hairline ramp above is tuned to be quiet, which puts it under the 3:1
     WCAG 1.4.11 asks of a control boundary. Anything a visitor has to find and
     click uses this instead — 3.23:1 on --bg. */
  --control-edge: oklch(0.50 0 0);

  /* --- signal --- */
  --accent: oklch(0.85 0.17 95);
  --accent-ink: oklch(0.20 0 0);
  /* RAG amber, lifted for dark per DESIGN.md ("re-derive until 4.5:1") */
  --warn: oklch(0.80 0.13 75);

  /* --- shape (squared scale) --- */
  --r4: 3px;
  --r8: 6px;
  --r12: 8px;

  /* --- space --- */
  --gutter: clamp(1.25rem, 5vw, 5.5rem);
  --stack: clamp(2.5rem, 6vw, 5rem);

  /* --- motion --- */
  --ease-out-quart: cubic-bezier(0.165, 0.84, 0.44, 1);
  --ease-out-expo: cubic-bezier(0.19, 1, 0.22, 1);
  --ease-in-out-quad: cubic-bezier(0.455, 0.03, 0.515, 0.955);

  /* --- z scale (semantic, never arbitrary) --- */
  --z-scene: 0;
  --z-veil: 1;
  --z-content: 2;
}

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

body {
  margin: 0;
  background: var(--bg-deep);
  color: var(--text);
  font-family: "IBM Plex Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
  font-size: 1rem;
  /* Light type on dark reads lighter — extra leading per the type notes. */
  line-height: 1.62;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

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

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: var(--r4);
}

/* A link in running text keeps the colour of the sentence it sits in and is
   marked by its underline instead — default browser blue is the one colour on
   this page that belongs to no one. Links with their own class (.skip, .mark,
   .hint, .btn, .titleblock a) set their own colour and win on specificity. */
a {
  color: inherit;
  text-decoration: underline;
  text-decoration-color: var(--border);
  text-underline-offset: 0.2em;
  transition: text-decoration-color 160ms var(--ease-out-quart);
}
@media (hover: hover) and (pointer: fine) {
  a:hover { text-decoration-color: var(--accent); }
}

.mono {
  font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
  font-variant-numeric: tabular-nums;
}

/* Visible to screen readers only. */
.sr {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.skip {
  position: absolute;
  top: 0.5rem; left: 0.5rem;
  z-index: 10;
  padding: 0.6rem 1rem;
  background: var(--accent);
  color: var(--accent-ink);
  font-weight: 600;
  text-decoration: none;
  border-radius: var(--r8);
  transform: translateY(-160%);
  transition: transform 160ms var(--ease-out-quart);
}
.skip:focus { transform: translateY(0); }

/* ==========================================================================
   Hero — the drawing board
   ========================================================================== */

.hero {
  position: relative;
  min-height: 100svh;
  display: grid;
  grid-template-rows: auto 1fr auto;
  padding: clamp(1.25rem, 3vw, 2rem) var(--gutter) clamp(1.5rem, 3vw, 2.5rem);
  overflow: clip;
  background: var(--bg);
}

/* WebGL drafting scene. Sits behind everything; decorative. */
#scene {
  position: absolute;
  inset: 0;
  z-index: var(--z-scene);
  display: block;
  width: 100%;
  height: 100%;
  /* Held at 0 until the renderer paints its first frame, so a WebGL failure
     shows the graphite ground rather than a flash of canvas. */
  opacity: 0;
  transition: opacity 900ms var(--ease-out-quart);
}
#scene.is-live { opacity: 1; }

/* Drafting ground. Always painted, so a WebGL failure still lands on paper
   rather than a flat rectangle. The canvas composites over it. */
.hero::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: var(--z-scene);
  pointer-events: none;
  background-image:
    linear-gradient(to right, oklch(0.30 0 0 / 0.5) 0 1px, transparent 1px),
    linear-gradient(to bottom, oklch(0.30 0 0 / 0.5) 0 1px, transparent 1px);
  background-size: 3.5rem 3.5rem;
  mask-image: radial-gradient(120% 90% at 78% 60%, #000 0%, transparent 72%);
}

/* Keeps text off the busiest part of the scene without dimming the whole thing. */
.hero::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: var(--z-veil);
  pointer-events: none;
  background:
    linear-gradient(100deg, oklch(0.16 0 0 / 0.92) 0%, oklch(0.16 0 0 / 0.72) 42%, oklch(0.16 0 0 / 0.12) 72%, transparent 100%),
    linear-gradient(to top, oklch(0.115 0 0 / 0.85) 0%, transparent 45%);
}

/* Portrait: the copy spans the full width, so a horizontal veil can't protect
   it. Go vertical and hold the scene well back — legibility first. */
@media (orientation: portrait) {
  #scene.is-live { opacity: 0.45; }

  /* A full 100svh hero at 834x1112 left ~380px of empty ground above the
     headline and ~250 below the CTA, because the scene that was meant to fill
     it is held right back at portrait. Cap the height so the block sits on the
     fold instead of floating in the middle of a tall empty rectangle. On a
     390x844 phone this changes nothing — 52rem is 832px. */
  .hero { min-height: min(100svh, 52rem); }

  .hero::before {
    mask-image: radial-gradient(150% 55% at 60% 82%, #000 0%, transparent 74%);
  }

  .hero::after {
    background: linear-gradient(
      to bottom,
      oklch(0.16 0 0 / 0.93) 0%,
      oklch(0.16 0 0 / 0.88) 62%,
      oklch(0.16 0 0 / 0.62) 100%
    );
  }
}

.hero > * { position: relative; z-index: var(--z-content); }

/* --- masthead --- */

.masthead {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
}

/* The lock-up is 28px tall; the negative margin grows the hit box to 44 without
   moving a pixel of it. Same trick on .hint and the titleblock links. */
.mark {
  display: inline-flex;
  align-items: center;
  gap: 0.7rem;
  padding: 0.5rem 0;
  margin: -0.5rem 0;
  font-weight: 600;
  font-size: 1.0625rem;
  letter-spacing: -0.01em;
  text-decoration: none;
  color: var(--text);
}
.mark svg { display: block; overflow: visible; }

.masthead .status {
  font-size: 0.75rem;
  letter-spacing: 0.02em;
  color: var(--light);
}

/* --- headline block --- */

.lead {
  align-self: center;
  /* Capped in rem, not ch: a ch cap here resolves against the body font size
     and silently crushes the display type into extra lines. */
  max-width: min(40rem, 100%);
  padding: clamp(2rem, 6vh, 4rem) 0 clamp(1.5rem, 4vh, 2.5rem);
}

h1 {
  margin: 0;
  max-width: 19ch; /* in ch of h1's own size, which is the useful measure */
  font-size: clamp(2.125rem, 4.1vw, 3.375rem);
  line-height: 1.06;
  letter-spacing: -0.03em;
  font-weight: 600;
}

/* Each line is its own element so the entrance can stagger by line. */
.line { display: block; }
.line em {
  font-style: normal;
  /* Underline rather than yellow text: yellow on graphite is fine, but the
     rule is that yellow is never the sole signal — weight carries it too. */
  box-shadow: inset 0 -0.055em 0 var(--accent);
}

.lede {
  margin: 1.35rem 0 0;
  max-width: 44ch;
  font-size: clamp(1.0625rem, 1.6vw, 1.1875rem);
  color: var(--muted);
  text-wrap: pretty;
}

/* --- the warning light: a plotted burn meter, not a stat tile --- */

.meter {
  margin: clamp(1.5rem, 4vh, 2.25rem) 0 0;
  max-width: 28rem;
  border-top: 1px solid var(--border);
  padding-top: 0.85rem;
}

.meter-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem 1.25rem;
  flex-wrap: wrap;
  font-size: 0.75rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--light);
}

.meter-head h2 {
  margin: 0;
  font: inherit;
  letter-spacing: inherit;
}

.meter-head .flag {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  color: var(--warn);
  letter-spacing: 0.03em;
  white-space: nowrap;
}
.meter-head .flag::before {
  content: "";
  width: 0.5rem; height: 0.5rem;
  border-radius: 50%;
  background: currentColor;
}

/* A measured rule with a plotted fill. One texture only — a hatch on top of
   tick marks reads as a barcode. */
.track {
  position: relative;
  height: 1.5rem;
  margin: 0.75rem 0 0;
  border: 1px solid var(--bl);
  border-radius: var(--r4);
  background: oklch(0.21 0 0);
  /* No overflow clip: the plan marker deliberately breaks out above and below
     the rule, and its label sits underneath it. */
}

/* Tick marks sit above the fill so the scale stays readable through it. */
.track::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: repeating-linear-gradient(
    to right,
    transparent 0 calc(10% - 1px),
    oklch(0.38 0 0 / 0.55) calc(10% - 1px) 10%
  );
}

/* Hours burned, drawn as consumed budget. */
.fill {
  position: absolute;
  inset: 0 0 0 0;
  width: 94.1%; /* 1,318 of 1,400 h */
  transform-origin: left center;
  background: oklch(0.55 0 0 / 0.55);
}

/* The hours past plan, and the ink edge that ends the fill — one element, since
   the overrun's right edge *is* where the burn stops. Measured against the
   track, so the fill's scaleX entrance can't distort either of them. */
.overrun {
  position: absolute;
  top: 0; bottom: 0;
  left: 88.6%;   /* plan, 1,240 h */
  right: 5.9%;   /* burned, 1,318 h of 1,400 */
  background: var(--warn);
  opacity: 0.85;
  border-right: 2px solid var(--text);
}

/* The plan line: where the budget said we'd stop. */
.plan {
  position: absolute;
  top: -0.4rem; bottom: -0.4rem;
  left: 88.6%;
  width: 1px;
  background: var(--accent);
}
.plan span {
  position: absolute;
  top: 100%; left: 0;
  transform: translate(-50%, 0.35rem);
  font-size: 0.6875rem;
  color: var(--accent);
  white-space: nowrap;
}

.figures {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem clamp(1.25rem, 3vw, 2.5rem);
  margin: 1.5rem 0 0;
}
.figures div { min-width: 6.5rem; }
.figures dt {
  font-size: 0.75rem;
  color: var(--light);
  letter-spacing: 0.02em;
}
.figures dd {
  margin: 0.15rem 0 0;
  font-size: clamp(1.125rem, 2vw, 1.375rem);
  font-weight: 500;
  letter-spacing: -0.01em;
}
.figures dd.over { color: var(--warn); }

/* --- call to action --- */

.cta {
  display: flex;
  align-items: center;
  gap: clamp(1rem, 3vw, 1.75rem);
  flex-wrap: wrap;
  margin: clamp(2rem, 5vh, 2.75rem) 0 0;
}

.btn {
  --btn-lift: 0;
  display: inline-flex;
  align-items: center;
  gap: 0.7rem;
  min-height: 2.875rem;
  padding: 0.75rem 1.35rem;
  background: var(--text);
  color: var(--accent-ink);
  font: inherit;
  font-weight: 600;
  text-decoration: none;
  border: 0;
  border-radius: var(--r8);
  cursor: pointer;
  transform: translateY(var(--btn-lift));
  transition: background-color 150ms ease, transform 150ms var(--ease-out-quart);
}
.btn .arrow { transition: transform 200ms var(--ease-out-quart); }

@media (hover: hover) and (pointer: fine) {
  .btn:hover { background: #fff; --btn-lift: -1px; }
  .btn:hover .arrow { transform: translateX(3px); }
}
.btn:active { transform: translateY(0) scale(0.98); }

.cta p {
  margin: 0;
  max-width: 26ch;
  font-size: 0.875rem;
  color: var(--light);
}

/* --- scroll hint --- */

.hint {
  justify-self: start;
  align-self: end;
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  /* This is the affordance that pulls a visitor out of the hero, and it was a
     19px-tall target. Padded to 44 and pulled back into place. */
  padding: 0.8rem 0.6rem;
  margin: -0.8rem -0.6rem;
  font-size: 0.75rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--light);
  text-decoration: none;
}
.hint .rule {
  width: 2.5rem; height: 1px;
  background: var(--border);
  position: relative;
  overflow: hidden;
}
.hint .rule::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--accent);
  transform: translateX(-100%);
  animation: sweep 2.6s var(--ease-in-out-quad) infinite;
}
@keyframes sweep {
  0%, 15%  { transform: translateX(-100%); }
  55%      { transform: translateX(0); }
  90%, 100%{ transform: translateX(100%); }
}

/* ==========================================================================
   Titleblock — what it does, set out like a drawing sheet
   ========================================================================== */

.sheet {
  padding: var(--stack) var(--gutter) clamp(2rem, 5vw, 3rem);
  background: var(--bg-deep);
  border-top: 1px solid var(--bl);
}

.sheet h2 {
  margin: 0 0 clamp(1.75rem, 4vw, 2.5rem);
  max-width: 32ch;
  font-size: clamp(1.375rem, 2.6vw, 1.875rem);
  line-height: 1.2;
  letter-spacing: -0.02em;
  font-weight: 600;
  text-wrap: balance;
}

/* Ruled rows, not a card grid. Reads as a specification schedule. */
.rows { border-top: 1px solid var(--bl); }

.row {
  display: grid;
  /* Wide enough for a question in two lines — the labels are questions, not
     one-word nouns. */
  grid-template-columns: minmax(0, 15rem) minmax(0, 1fr);
  gap: 0 clamp(1rem, 3vw, 2.5rem);
  align-items: baseline;
  padding: clamp(1.1rem, 2.4vw, 1.6rem) 0;
  border-bottom: 1px solid var(--bl);
}

.row h3 {
  margin: 0;
  font-size: 1.0625rem;
  font-weight: 600;
  letter-spacing: -0.01em;
}
.row p {
  margin: 0;
  max-width: 62ch; /* measure cap — the rows would otherwise run past 100ch */
  color: var(--muted);
  text-wrap: pretty;
}

@media (max-width: 40rem) {
  .row { grid-template-columns: minmax(0, 1fr); }
  .row p { margin-top: 0.35rem; }
}

/* ==========================================================================
   Footer — a real drawing titleblock
   ========================================================================== */

footer {
  padding: 0 var(--gutter) clamp(2.5rem, 6vw, 4rem);
  background: var(--bg-deep);
}

.titleblock {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  border: 1px solid var(--bl);
  border-radius: var(--r4);
}

.titleblock div {
  padding: 0.85rem 1rem;
  border-right: 1px solid var(--bl);
}
.titleblock div:last-child { border-right: 0; }

.titleblock dt {
  font-size: 0.6875rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--light);
}
.titleblock dd {
  margin: 0.3rem 0 0;
  font-size: 0.8125rem;
  color: var(--muted);
}
.titleblock a {
  display: inline-block;
  padding: 0.85rem 0;
  margin: -0.85rem 0;
  color: var(--muted);
}
/* Guarded like every other hover on this page — an unguarded :hover sticks to
   the last-tapped element on touch. */
@media (hover: hover) and (pointer: fine) {
  .titleblock a:hover { color: var(--text); }
}

@media (max-width: 30rem) {
  .titleblock div { border-right: 0; border-bottom: 1px solid var(--bl); }
  .titleblock div:last-child { border-bottom: 0; }
}

/* ==========================================================================
   Entrance choreography

   Content is visible by default. `.js-motion` is added by motion.js only when
   it is definitely going to run, so a JS failure, a headless render, or a
   hidden tab can never leave the page blank.
   ========================================================================== */

.js-motion [data-reveal] {
  opacity: 0;
  transform: translateY(0.7rem);
}

.js-motion [data-reveal].is-in {
  opacity: 1;
  transform: none;
  transition:
    opacity 620ms var(--ease-out-quart),
    transform 720ms var(--ease-out-expo);
}

.js-motion .track .fill { transform: scaleX(0); }
.js-motion .track .fill.is-in {
  transform: scaleX(1);
  transition: transform 1150ms var(--ease-out-expo);
}

/* The overrun sits still and fades in behind the sweep. ease-out-expo puts the
   fill past the plan mark at roughly 450ms, so that is when the amber lands —
   it reads as the fill arriving there rather than as a separate event. */
.js-motion .track .overrun { opacity: 0; }
.js-motion .track .fill.is-in ~ .overrun {
  opacity: 0.85;
  transition: opacity 320ms var(--ease-out-quart) 450ms;
}

.js-motion .plan { opacity: 0; }
.js-motion .plan.is-in {
  opacity: 1;
  transition: opacity 300ms var(--ease-out-quart);
}

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

  /* No movement anywhere: reveals become a plain crossfade, ambient loops stop. */
  .js-motion [data-reveal] { transform: none; }
  .js-motion [data-reveal].is-in { transition: opacity 200ms ease; }
  .js-motion .track .fill { transform: scaleX(1); }
  .js-motion .track .fill.is-in { transition: none; }
  .js-motion .track .overrun,
  .js-motion .track .fill.is-in ~ .overrun { opacity: 0.85; transition: none; }
  .hint .rule::after { animation: none; transform: translateX(0); }
  #scene { transition: none; }
  .btn, .btn .arrow { transition: none; }
}
