/* =========================================================================
   HERO — one dark sheet, lit from the lower left.

   Three layers under the type, in paint order, none of them decoration:

     1. the bloom   a vermilion light bleeding in from the bottom-left
                    corner, so the sheet is lit from somewhere rather than
                    evenly grey. This is the background; everything else is
                    a modifier on it.
     2. the plot    the engineer's grid: 24px minor, 96px major, which is the
                    8px spacing scale at 3x and 12x. Faint, static, always,
                    and faded out well before the edges so it reads as a
                    surface rather than as a panel laid on one.
     3. the tooth   fine noise. Paper has a surface; a flat #0e1013 does not.

   The layer stack is FULL-BLEED and the content is not. .shell is on
   .hero__inner, never on the section: an inset-0 layer inside a max-width
   padded box paints a lighter rectangle with two hard vertical seams down
   the page, which is exactly what it looks like.

   Over them: the headline, and the collage — a single flattened cut-out, so
   the paste-up is artwork rather than CSS imitating artwork.

   Loads after sections.css: it overrides .hero, it does not restate it.
   ========================================================================= */

/* The hero paints its own paper, and the field does NOT get a z-index.
   Both are load-bearing for the tooth. `mix-blend-mode` blends with the
   backdrop inside the element's own stacking context, so a field at
   `z-index: -1` is an isolated context whose only backdrop is transparency
   — and `overlay` against nothing is just the source colour, which is how a
   grey veil ended up sitting over a #0e1013 hero. The field stays in flow
   order under .hero__inner instead, so the grain blends onto real paint.
   .hero keeps `isolation` so nothing in here can reach the page behind it. */
.hero {
  position: relative;
  background: var(--bg);
  isolation: isolate;
  overflow: clip;
  padding-inline: 0;     /* the field is the full width; .hero__inner insets */
}
.hero__inner { position: relative; z-index: 1; width: 100%; }

.hero__field {
  position: absolute;
  inset: 0;
  pointer-events: none;
  color: var(--ink-meta);
  /* 24 / 96 are the 8px rhythm at 3x and 12x — the grid is the spacing
     scale, not an arbitrary graph paper. */
  --plot-minor: 24px;
  --plot-major: 96px;
  --plot-ink: color-mix(in srgb, var(--ink-meta) 30%, transparent);
  --plot-ink-soft: color-mix(in srgb, var(--ink-meta) 10%, transparent);
}
/* Paper takes a stronger line than a dark screen does, but only up to a
   point: at 30% a major grid line behind --ink-meta measured 4.42:1. */
[data-theme="light"] .hero__field {
  --plot-ink: color-mix(in srgb, var(--ink-meta) 22%, transparent);
  --plot-ink-soft: color-mix(in srgb, var(--ink-meta) 9%, transparent);
}
[data-theme="light"] .hero__lens {
  --lens-ink:      color-mix(in srgb, var(--accent) 34%, transparent);
  --lens-ink-soft: color-mix(in srgb, var(--accent) 14%, transparent);
}

/* ---- 1 · The bloom -----------------------------------------------------
   The corner light is the whole background idea, so it is big: a 58%-wide
   light with its centre ON the bottom-left corner — 1%/97%, not -2%/104%.
   A radial-gradient whose centre sits outside its own box contributes only
   its tail, which is how this shipped as an invisible glow twice, with a much smaller second one
   behind the collage so the right column has a reason to be warmer than the
   middle of the page. It drifts with the pointer at a fraction of
   everything else's rate, which is what stops a static gradient reading as
   a static gradient.

   The alphas are a contrast decision. On paper the bloom *darkens* the
   background under dark text, so the light theme runs it at a third of the
   strength and further from the lead. */
.hero__bloom {
  position: absolute;
  inset: -8% 0 -14%;
  --bloom-a: color-mix(in srgb, var(--accent) 34%, transparent);
  --bloom-b: color-mix(in srgb, var(--accent) 10%, transparent);
  background-image:
    radial-gradient(58% 58% at 1% 97%,  var(--bloom-a) 0%, transparent 72%),
    radial-gradient(26% 32% at 78% 38%, var(--bloom-b) 0%, transparent 74%);
  translate: calc(var(--ink-x, 0) * 14px) calc(var(--ink-y, 0) * 10px);
}
[data-theme="light"] .hero__bloom {
  --bloom-a: color-mix(in srgb, var(--accent) 12%, transparent);
  --bloom-b: color-mix(in srgb, var(--accent) 4%, transparent);
}

/* ---- 2 · The plot grid ------------------------------------------------- */
.hero__plot,
.hero__lens {
  position: absolute;
  inset: 0;
  /* Aligned to the hero's own origin, so the lens grid lands exactly on the
     plot grid underneath it rather than beside it. */
  background-position: 0 0;
  background-image:
    linear-gradient(to right,  var(--plot-ink)      1px, transparent 1px),
    linear-gradient(to bottom, var(--plot-ink)      1px, transparent 1px),
    linear-gradient(to right,  var(--plot-ink-soft) 1px, transparent 1px),
    linear-gradient(to bottom, var(--plot-ink-soft) 1px, transparent 1px);
  background-size:
    var(--plot-major) var(--plot-major),
    var(--plot-major) var(--plot-major),
    var(--plot-minor) var(--plot-minor),
    var(--plot-minor) var(--plot-minor);
}

/* Centred on the headline and dead well inside every edge. A grid that runs
   to the viewport edge has an edge; a grid that fades to nothing at 70% of
   the radius is a surface the type happens to be sitting on. */
.hero__plot {
  opacity: 0.42;
  -webkit-mask-image: radial-gradient(86% 78% at 32% 46%, #000 0%, transparent 70%);
          mask-image: radial-gradient(86% 78% at 32% 46%, #000 0%, transparent 70%);
}

/* ---- 3 · The lens ------------------------------------------------------
   Same grid, warmed toward the accent, revealed in a soft disc around the
   pointer. --mx / --my are written by hero.js on a rAF; they default to the
   centre so the layer is never in a broken state before the first move.

   The band is a contrast decision, not a composition one. A vermilion grid
   line crossing --ink-meta drops it to 3.5:1, and turning the lens down far
   enough to survive that leaves nothing to see. So the lens is masked to the
   headline band instead: the only text inside it is display size, which is
   held to 3:1, and the eyebrow, lead, stats and scroll cue all sit outside.
   Two nested masks compose without mask-composite, so this works everywhere
   rather than only where `intersect` is supported. */
.hero__band {
  /* hero.js measures the headline and writes these; the fallbacks are a
     sane band for the moment before it runs. Deriving the band from the
     element rather than from percentages is what keeps the eyebrow clear
     of it at every viewport height instead of only at 900px. */
  --band-a: 21%;
  --band-b: 62%;
  position: absolute;
  inset: 0;
  -webkit-mask-image: linear-gradient(to bottom,
    transparent calc(var(--band-a) - 3%), #000 calc(var(--band-a) + 3%),
    #000 calc(var(--band-b) - 2%), transparent calc(var(--band-b) + 6%));
          mask-image: linear-gradient(to bottom,
    transparent calc(var(--band-a) - 3%), #000 calc(var(--band-a) + 3%),
    #000 calc(var(--band-b) - 2%), transparent calc(var(--band-b) + 6%));
}
.hero__lens {
  --mx: 50%;
  --my: 45%;
  /* The band mask hides everything outside the headline anyway, so the layer
     that repaints on every pointer frame is cut to the strip it can actually
     show — roughly half the hero instead of all of it. hero.js writes these
     in px, not percent, because the same number has to shift the background
     back by exactly as much as the box moved down: a percentage
     background-position means something else entirely. Default 0 keeps the
     full-height, correctly-aligned layer until that measurement lands. */
  top: var(--lens-top, 0px);
  bottom: var(--lens-bottom, 0px);
  background-position: 0 calc(-1 * var(--lens-top, 0px));
  --lens-ink:      color-mix(in srgb, var(--accent) 45%, transparent);
  --lens-ink-soft: color-mix(in srgb, var(--accent) 18%, transparent);
  opacity: 0;
  /* --mx / --my are hero-relative; the mask is lens-relative. */
  -webkit-mask-image: radial-gradient(circle 16rem at
                        var(--mx) calc(var(--my) - var(--lens-top, 0px)),
                        #000 0%, rgba(0,0,0,0.55) 42%, transparent 70%);
          mask-image: radial-gradient(circle 16rem at
                        var(--mx) calc(var(--my) - var(--lens-top, 0px)),
                        #000 0%, rgba(0,0,0,0.55) 42%, transparent 70%);
  transition: opacity var(--d-enter) var(--e-soft);
}
.hero.is-lens .hero__lens { opacity: 1; }

/* ---- 4 · The tooth -----------------------------------------------------
   The tile is a generated file, not an SVG filter, and that is the fix for
   a bug this shipped with twice. feTurbulence writes noise into the alpha
   channel as well as into RGB and hands the result back PREMULTIPLIED, so
   unpremultiplying it before any colour matrix drives every channel toward
   white. `overlay` then only ever lifts: measured, it took a #0e1013 hero
   to #272a2c — a lighter rectangle sitting on the page, which is exactly
   what it looked like. No amount of feColorMatrix fixes it, because the
   bias is upstream of the matrix.

   assets/tooth.webp is 192px of opaque grey noise, seeded, lossless, with a
   mean forced to exactly 128. overlay(base, 0.5) == base, so the flat field
   stays precisely the page colour at any opacity and only the grain departs
   from it. Regenerate it with the snippet in BUILD.md; if you change it,
   re-assert the mean.

   Static by design. An animated grain costs a full-viewport repaint every
   frame and buys nothing you can see at this opacity.

   The global .grain is a page-wide 3.8% film over the whole UI; this is the
   hero's own surface, and it sits under the type rather than over it. */
.hero__tooth {
  position: absolute;
  inset: 0;
  opacity: 0.5;
  mix-blend-mode: overlay;
  background-image: url("../assets/tooth.webp");
  background-size: 168px 168px;
}
/* Not multiply on paper. Multiply only darkens, so every noise pixel is a
   straight contrast tax on dark-on-light type — it cost --ink-meta 0.3 of a
   ratio and took the eyebrow, the stat labels and the scroll cue under 4.5.
   soft-light is symmetric too, and gentler at the extremes, which paper
   needs and a dark screen does not. */
[data-theme="light"] .hero__tooth { mix-blend-mode: soft-light; opacity: 0.85; }
@media (max-width: 819px) { .hero__tooth { opacity: 0.32; } }

/* =========================================================================
   LAYOUT — pitch left, proof right.
   ========================================================================= */
.hero__grid { display: grid; gap: var(--s-7); }

@media (min-width: 1000px) {
  .hero__grid {
    /* A percentage of the grid, not of the viewport. .shell caps at 82rem,
       so a vw-sized aside keeps growing past that cap while the row it sits
       in does not — at 1920 it ate enough of .hero__main that the container
       query sized the headline DOWN to 82px, smaller than it is at 1280. */
    grid-template-columns: minmax(0, 1fr) clamp(15rem, 30%, 24rem);
    align-items: center;
    column-gap: clamp(var(--s-5), 3.5vw, var(--s-8));
  }
  .hero__aside { align-self: center; }
}

/* The headline is sized against its own column, not the viewport, so it can
   never overflow the track however wide the aside gets. The longest line,
   "Engineer by training.", measures 7.92em, so 12% of the column per em
   sets it at 95% of the track — at every width, with no breakpoint. If the
   headline copy changes, re-measure the longest line and reset this. */
.hero__main { container-type: inline-size; }
@media (min-width: 1000px) {
  .hero__title { font-size: min(var(--t-hero), 12cqi); }
}

.hero__aside { display: grid; gap: var(--s-6); justify-items: start; }

/* ---- The collage -------------------------------------------------------
   Portrait, disc, concrete panel, hatch and scribbles are all one flattened
   cut-out with a real alpha channel. Every piece of that used to be a CSS
   approximation — a circular crop, a ring, a repeating-linear-gradient, a
   dot screen in multiply — and an approximation of a paste-up is the one
   thing a paste-up must not look like.

   It carries no radius, no filter and no blend mode: the artwork is already
   duotone and already grainy, so the page's own texture is in it rather
   than applied over it. */
.hero__art {
  position: relative;
  width: 100%;
  margin: 0;
  translate: calc(var(--ink-x, 0) * -9px) calc(var(--ink-y, 0) * -6px);
}
.hero__art-img {
  display: block;
  width: 100%;
  height: auto;
}

/* The handwriting is lifted out of the artwork into its own mask so it can
   take the page's ink. Baked in, it was black — which on #0e1013 is a note
   nobody can read, and on paper is the only pure black on the page. The
   arrow comes with it and takes the same colour: one hand, one weight. */
.hero__note {
  position: absolute;
  left: 81.25%;
  top: 9.42%;
  width: 18.75%;
  aspect-ratio: 440 / 372;
  background: var(--ink);
  -webkit-mask-image: url("../assets/hero-note.webp");
          mask-image: url("../assets/hero-note.webp");
  -webkit-mask-size: 100% 100%;
          mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  pointer-events: none;
  /* Forward of the artwork, so it moves with the pointer while the collage
     moves against it. Two directions is what reads as depth; one is drift. */
  translate: calc(var(--ink-x, 0) * 6px) calc(var(--ink-y, 0) * 4px);
}

/* ---- The scroll cue ----------------------------------------------------
   sections.css hangs it vertically off the right gutter, which is where the
   aside now is: at 1440 it landed on top of the stats column. It goes back
   under the call to action, reading left to right, which is also where the
   reference puts it. */
@media (min-width: 1000px) {
  .hero__scroll {
    /* The containing block is .hero__inner's padding box, so `left` starts
       at the shell edge and `top: 100%` at the end of the content, not at
       the end of the section. Both are deliberate: the cue lines up with
       the headline and sits in the air the flex centring leaves below it. */
    display: flex;
    right: auto;
    left: var(--gutter);
    bottom: auto;
    top: calc(100% + var(--s-5));
    writing-mode: horizontal-tb;
    align-items: center;
    gap: var(--s-4);
    /* One step up the ink ramp from the --ink-meta the other small type
       uses. The cue is the only label sitting inside the bloom's core, and
       against that backdrop --ink-meta measured 4.32:1 dark and 4.37:1
       light — both under 4.5. Moving the label is cheaper than moving the
       light, and --ink-secondary clears it in both themes. */
    color: var(--ink-secondary);
  }
  .hero__scroll::after {
    width: 56px;
    height: 1px;
    background: linear-gradient(to right, var(--accent), transparent);
  }
}

/* ---- The numbers -------------------------------------------------------
   In the aside the stats are a stacked list hung off a rule, not a strip:
   the rule does the separating that a border-top did when they ran across
   the page. The tick on the right is the reference's, and it is the only
   thing marking where the column ends. */
@media (min-width: 1000px) {
  .hero__stats {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--s-5);
    width: 100%;
    margin-top: 0;
    padding-top: 0;
    border-top: 0;
    padding-left: var(--s-5);
    border-left: 1px solid var(--border);
  }
  .hero__stats .metric { position: relative; padding-right: var(--s-5); }
  .hero__stats .metric::after {
    content: "";
    position: absolute;
    right: 0;
    top: 0.55em;
    width: 10px;
    height: 1px;
    background: var(--accent-line);
  }
  .hero__stats .metric__label { max-width: 24ch; }
}
/* Side by side only where the column is wide enough for the label to keep a
   readable measure beside a 52px numeral. */
@media (min-width: 1280px) {
  .hero__stats .metric {
    grid-template-columns: auto minmax(0, 1fr);
    align-items: baseline;
    column-gap: var(--s-4);
    gap: 0 var(--s-4);
  }
}

/* ---- Reduced motion ---------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  .hero__lens { display: none; }
}

/* ---- Chips on paper ----------------------------------------------------
   --accent-ink is the tightest colour in the palette (4.96:1 on paper), so
   an 11px pill label crossing a grid line fell to 4.24:1. Turning the grid
   down far enough to save it would have left nothing to see, so the pills
   get the page colour behind them instead and sit on the paper rather than
   in it. hero.css is only loaded by the home page, so the pills in the
   case-study heroes are untouched. */
.hero__eyebrow .pill { background: var(--bg); }

/* ---- Below the split ---------------------------------------------------
   Stacked, the aside follows the pitch. The collage comes down with it but
   not at full size — under 1000px it is a face beside the numbers, not the
   right half of a composition. */
@media (max-width: 999px) {
  .hero__aside {
    grid-template-columns: auto minmax(0, 1fr);
    align-items: center;
    gap: var(--s-6);
  }
  .hero__art { width: clamp(7rem, 20vw, 10rem); }
  .hero__stats { margin-top: 0; border-top: 0; padding-top: 0; }
}
/* On a phone the collage stays in the row rather than stacking above it:
   the stats are the taller element either way, so beside them the face is
   free, and above them it cost 112px off the bottom of the first screen. */
@media (max-width: 599px) {
  .hero__aside { gap: var(--s-5); }
  .hero__art { width: 6.5rem; }
  .hero__stats { grid-template-columns: minmax(0, 1fr); gap: var(--s-4); }
  .hero__stats .metric__label { max-width: none; }
  /* At 104px the handwriting is three grey smudges, not a note. */
  .hero__note { display: none; }
}

/* ---- Short viewports ---------------------------------------------------
   A 1366x768 laptop gives the hero about 700 usable pixels. At the standard
   rhythm the call to action landed 74px under the fold there, which is the
   one thing in the hero that must never be scrolled to. The type does not
   shrink; the air between it does. */
@media (min-width: 1000px) and (max-height: 860px) {
  .hero { padding-top: var(--s-9); padding-bottom: var(--s-5); }
  .hero__eyebrow { margin-bottom: var(--s-4); }
  .hero__title   { margin-bottom: var(--s-4); }
  .hero__actions { margin-top: var(--s-5); }
  .hero__stats   { gap: var(--s-4); }
  .hero__grid    { column-gap: var(--s-6); }
}
