/* ==========================================================================
   BEHAVIORAL TWIN · HERO  ·  a photographic plate with drawn overlays
   ==========================================================================
   The scene is a composite, and the split is the argument the section makes:
   the physical world is a photograph, the model is drawn. Nothing here tries
   to render a car — there is a real one in the plate.

   FOUR LAYERS, one box.

     1  the plate      <picture>   the only photographic element
     2  the stream     <canvas>    hundreds of particles, cheap
     3  the twin       <svg>       52 nodes, crisp at any width
     4  the labels     HTML        sharp, selectable, translatable

   Layers 2–4 are positioned in PERCENTAGES OF THE PLATE, inside a box locked
   to the plate's aspect ratio. That is the whole registration strategy: there
   is no measurement code, no resize observer, nothing to drift. The SVG's
   viewBox is the plate's own pixel grid (1672×941), so a node authored at
   plate pixel 1450,330 lands on the monitor at 700px wide and at 836px wide
   alike. The node coordinates are generated by scripts/gen-twin-hero.py,
   which maps a car silhouette through the monitor's four measured corners —
   the monitor is photographed off-axis, so its lit area is a trapezoid and a
   plain rectangle would have put the twin visibly off the glass.

   THE 836px CEILING IS NOT A STYLE CHOICE. The plate is 1672px native and
   cannot be regenerated larger. Above 836px CSS it is being displayed under
   2× and it goes soft. On a wide viewport the hero gives the figure more air
   around it instead of scaling it up.

   THE FINISHED FRAME IS THE AUTHORED STATE. Everything in the markup — every
   node, every link, the traced path, all three labels with their counts — is
   drawn and visible by default. JavaScript adds .is-anim, which is what turns
   the finished frame back into an empty screen so the timeline can build it.
   No JavaScript, a thrown module, prefers-reduced-motion: all three land on
   the same complete picture, because that picture is the default and not a
   fallback anyone has to remember to maintain.

   COLOUR IS FUNCTIONAL. Teal is data. Blue is secondary structure. Orange is
   the one traced path and nothing else. There is no fourth accent.
   -------------------------------------------------------------------------- */

.bt-hero {
  --bth-teal: #4ecdc4;
  --bth-blue: #5b9fd4;
  --bth-trace: #ef8354;
  --bth-plate: 836px;

  position: relative;
  width: 100%;
  max-width: var(--bth-plate);
  margin-inline: auto;
  color: var(--bth-teal);

  /* The labels move out of the scene when THE PLATE is narrow, not when the
     window is. The hero sits in a 5/7 grid on a desktop, so a 1000px window
     can hand this figure 430px — a viewport media query would have left three
     reticles fighting over a postage stamp, which is exactly what it did. */
  container-type: inline-size;
  container-name: bthero;
}

/* The aspect box. Its height comes from the plate's own ratio, so the frame
   is the right size from the first layout pass and nothing shifts when the
   image decodes or when the canvas is sized. */
.bt-hero__frame {
  position: relative;
  aspect-ratio: 1672 / 941;
  border-radius: 12px;
  overflow: hidden;
  background: #05070a;
}

.bt-hero__plate,
.bt-hero__plate img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* ---- layer 2 · the stream ------------------------------------------------
   Only ever visible when the loop is running. With no JavaScript it stays at
   zero and the SVG carries a still, thinned stream instead. */
.bt-hero__stream {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  opacity: 0;
  pointer-events: none;
}

.bt-hero.is-anim .bt-hero__stream { opacity: 1; }

/* ---- layer 3 · the twin -------------------------------------------------- */
.bt-tw {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
}

/* The screen's own light. Faint at rest so the panel never reads as black
   glass, and lifted once there is something on it. */
.bt-tw__wash {
  fill: #4ecdc4;
  fill-opacity: 0.035;
  transition: fill-opacity 900ms ease;
}

.bt-tw__scan line {
  stroke: #4ecdc4;
  stroke-opacity: 0.05;
  stroke-width: 1;
}

/* The still stream: the thinned density, drawn once. Replaced by the canvas
   the moment the loop takes over. */
.bt-tw__mote { fill: var(--bth-teal); }
.bt-hero.is-anim .bt-tw__stream { display: none; }

/* ---- the twin's own marks ----------------------------------------------- */

/* The trunks. Five long runs down the length of the car, under everything.
   These are the reason the drawing reads as a nervous system rather than as
   a lattice: a nervous system has axons, not even spacing. */
.bt-tw__trunk {
  fill: none;
  stroke: var(--bth-blue);
  stroke-opacity: 0.42;
  stroke-width: 2.4;
  stroke-linecap: round;
}

/* The two wheel arches. Not data — they carry nothing and are never a hop —
   but two faint rings do more for "this is a car" than any node placement,
   which is why they are kept well below the graph in weight. */
.bt-tw__wheel {
  fill: none;
  stroke: var(--bth-blue);
  stroke-opacity: 0.4;
  stroke-width: 2.2;
  stroke-dasharray: 6 6;
}

.bt-tw__link {
  fill: none;
  stroke: var(--bth-teal);
  stroke-opacity: 0.34;
  stroke-linecap: round;
}

/* The few links that cross a domain boundary are blue and thinner, so the
   three clusters separate visually before any label names them. */
.bt-tw__link--seam {
  stroke: var(--bth-blue);
  stroke-opacity: 0.5;
}

.bt-tw__node { fill: var(--bth-teal); }
.bt-tw__node--hub { fill: #9df3ec; }
.bt-tw__node--mid { fill: var(--bth-teal); fill-opacity: 0.85; }
.bt-tw__node--sml { fill: var(--bth-teal); fill-opacity: 0.6; }

.bt-tw__halo { opacity: 0.85; }

/* The single traced path. The only orange in the block. */
.bt-tw__trace {
  fill: none;
  stroke: var(--bth-trace);
  stroke-width: 1.9;
  stroke-linecap: round;
  stroke-opacity: 0.9;
  filter: drop-shadow(0 0 4px rgba(239, 131, 84, 0.45));
}

.bt-tw__pulse { fill: #d7fffa; }

/* ---- layer 3b · the leaders ---------------------------------------------
   Drawn in plate coordinates because they have to land on a real node. The
   label they come from is HTML, positioned at the same percentage. */
.bt-tw__leader {
  fill: none;
  stroke: var(--bth-teal);
  stroke-opacity: 0.45;
  stroke-width: 1.2;
}

.bt-tw__leader-tip {
  fill: none;
  stroke: var(--bth-teal);
  stroke-opacity: 0.9;
  stroke-width: 1.4;
}

/* ==========================================================================
   LAYER 4 · THE LABELS
   ==========================================================================
   Outside the panel, in the dark studio air, pointing in. Not filled boxes:
   corner ticks, like a targeting reticle — four L-shaped marks made from the
   two pseudo-elements on the label and the two on its frame. Mono, uppercase,
   wide tracking, teal at 85% and never pure white, with a soft outer glow so
   they read as projected into the room rather than printed on top of it.
   -------------------------------------------------------------------------- */

.bt-hero__labels {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.bt-lab {
  position: absolute;
  padding: 7px 12px 6px;
  white-space: nowrap;
  font-family: var(--mono);
  line-height: 1.25;
}

/* The four corner ticks. */
.bt-lab::before,
.bt-lab::after,
.bt-lab .bt-lab__frame::before,
.bt-lab .bt-lab__frame::after {
  content: "";
  position: absolute;
  width: 9px;
  height: 9px;
  border: 1px solid rgba(78, 205, 196, 0.72);
}

.bt-lab .bt-lab__frame::before { top: 0; left: 0; border-right: 0; border-bottom: 0; }
.bt-lab::before { top: 0; right: 0; border-left: 0; border-bottom: 0; }
.bt-lab .bt-lab__frame::after { bottom: 0; right: 0; border-left: 0; border-top: 0; }
.bt-lab::after { bottom: 0; left: 0; border-right: 0; border-top: 0; }

.bt-lab__text {
  display: block;
  font-size: 0.6rem;
  font-weight: 500;
  letter-spacing: 0.17em;
  text-transform: uppercase;
  color: rgba(120, 232, 222, 0.85);
  text-shadow: 0 0 10px rgba(78, 205, 196, 0.5);
}

/* The count belongs to the demo's own dataset — how many nodes this drawing
   contains — and to nothing else. It is never an outcome and never a percent. */
.bt-lab__count {
  display: block;
  margin-top: 3px;
  font-size: 0.52rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: rgba(91, 159, 212, 0.7);
}

/* Positions: the same percentages the leaders were generated against, so the
   line always meets the box. See LABELS in scripts/gen-twin-hero.py. */
@container bthero (min-width: 640px) {
  .bt-lab--info { left: 74%;   top: 11%; transform: translate(-50%, calc(-100% - 7px)); }
  .bt-lab--body { left: 56.5%; top: 20%; transform: translate(calc(-100% - 7px), -50%); }
  .bt-lab--chas { left: 64.5%; top: 78%; transform: translate(calc(-100% - 7px), -50%); }
}

/* ==========================================================================
   THE LOOP
   ==========================================================================
   Only reachable through .is-anim, which JavaScript adds and which nothing
   else in the stylesheet depends on. Every rule below is therefore a rule
   about an animation that is definitely running: there is no state here that
   a visitor without JavaScript, or with reduced motion, can ever land in.

   The staggering is CSS, not JavaScript. Each node and link is given a --d
   once at boot and the phase is a single attribute write per phase change —
   so the timeline costs three attribute writes a loop, not 130 style writes
   a frame. The rAF budget goes to the particles and the pulses, which
   genuinely need it.
   -------------------------------------------------------------------------- */

.bt-hero.is-anim .bt-tw__trunk,
.bt-hero.is-anim .bt-tw__wheel,
.bt-hero.is-anim .bt-tw__link,
.bt-hero.is-anim .bt-tw__node,
.bt-hero.is-anim .bt-tw__halo,
.bt-hero.is-anim .bt-tw__leader,
.bt-hero.is-anim .bt-tw__leader-tip,
.bt-hero.is-anim .bt-lab,
.bt-hero.is-anim .bt-tw__trace {
  opacity: 0;
}

/* -- assemble: hubs first, then the rest, then the links between them -----
   "Assembled" is every phase except the two before it, written as a negation
   so adding a phase later cannot silently drop the twin off the screen. */
.bt-hero.is-anim .bt-tw__node,
.bt-hero.is-anim .bt-tw__halo {
  transition: opacity 620ms ease var(--d, 0ms), fill-opacity 400ms ease;
}

.bt-hero.is-anim .bt-tw__link,
.bt-hero.is-anim .bt-tw__trunk,
.bt-hero.is-anim .bt-tw__wheel {
  transition: opacity 700ms ease var(--d, 0ms), stroke-opacity 400ms ease;
}

.bt-hero.is-anim[data-phase]:not([data-phase="rest"]):not([data-phase="emit"]) :is(
  .bt-tw__node, .bt-tw__halo, .bt-tw__link, .bt-tw__trunk, .bt-tw__wheel
) {
  opacity: 1;
}

.bt-hero.is-anim[data-phase]:not([data-phase="rest"]):not([data-phase="emit"]) .bt-tw__wash {
  fill-opacity: 0.075;
}

/* -- recognise: one cluster at a time, then its label arrives ------------- */
.bt-hero.is-anim .bt-tw__node[data-c],
.bt-hero.is-anim .bt-tw__link[data-c] { fill-opacity: 0.45; stroke-opacity: 0.22; }

.bt-hero.is-anim .bt-tw__node.is-lit { fill-opacity: 1; }
.bt-hero.is-anim .bt-tw__link.is-lit { stroke-opacity: 0.55; }
.bt-hero.is-anim .bt-tw__trunk.is-lit { stroke-opacity: 0.6; }

/* Once every cluster has been named, the whole twin comes back up together —
   the point of the sequence is that the domains are regions of ONE model. */
.bt-hero.is-anim:is([data-phase="trace"], [data-phase="hold"]) .bt-tw__node[data-c] { fill-opacity: 0.92; }
.bt-hero.is-anim:is([data-phase="trace"], [data-phase="hold"]) .bt-tw__link[data-c] { stroke-opacity: 0.4; }

/* Labels arrive one at a time, each with a small horizontal settle.
   The whole block is behind a min-width query so that the narrow layout,
   which takes the labels out of the scene entirely, never has to fight a
   more specific .is-in rule for control of `transform`. */
@container bthero (min-width: 640px) {
  .bt-hero.is-anim .bt-lab {
    transition: opacity 520ms ease, transform 620ms cubic-bezier(0.2, 0.9, 0.25, 1);
  }

  .bt-hero.is-anim .bt-lab--info { transform: translate(-50%, calc(-100% - 7px)) translateX(-10px); }
  .bt-hero.is-anim .bt-lab--body { transform: translate(calc(-100% - 7px), -50%) translateX(-14px); }
  .bt-hero.is-anim .bt-lab--chas { transform: translate(calc(-100% - 7px), -50%) translateX(-14px); }

  .bt-hero.is-anim .bt-lab.is-in { opacity: 1; }
  .bt-hero.is-anim .bt-lab--info.is-in { transform: translate(-50%, calc(-100% - 7px)); }
  .bt-hero.is-anim .bt-lab--body.is-in,
  .bt-hero.is-anim .bt-lab--chas.is-in { transform: translate(calc(-100% - 7px), -50%); }
}

/* The leader draws itself, then a pulse runs down it. --len is measured once
   at boot from the path's own geometry. */
.bt-hero.is-anim .bt-tw__leader {
  stroke-dasharray: var(--len) var(--len);
  stroke-dashoffset: var(--len);
  transition: opacity 200ms ease, stroke-dashoffset 620ms cubic-bezier(0.3, 0.8, 0.3, 1);
}

.bt-hero.is-anim .bt-tw__leader.is-in {
  opacity: 1;
  stroke-dashoffset: 0;
}

.bt-hero.is-anim .bt-tw__leader-tip { transition: opacity 400ms ease 450ms; }
.bt-hero.is-anim .bt-tw__leader-tip.is-in { opacity: 1; }

/* -- trace: the orange run, drawn once across all three clusters ---------- */
/* The transition is declared on the resting rule, not on the phase rule.
   A transition only runs if transition-property was already set in the
   before-change style — declaring it alongside the new value is the classic
   way to get a property that snaps, or starts late. */
.bt-hero.is-anim .bt-tw__trace {
  stroke-dasharray: var(--len) var(--len);
  stroke-dashoffset: var(--len);
  transition: opacity 260ms ease, stroke-dashoffset 2000ms cubic-bezier(0.5, 0, 0.4, 1);
}

.bt-hero.is-anim:is([data-phase="trace"], [data-phase="hold"]) .bt-tw__trace {
  opacity: 1;
  stroke-dashoffset: 0;
}

/* ==========================================================================
   NARROW
   ==========================================================================
   Under a 640px PLATE the photograph stays — it is the point of the section —
   but the labels come out of the scene and stack beneath it. Three reticles
   floating over a 400px-wide photograph is unreadable, and shrinking the type
   to fit is worse than moving it. The leaders go with them, since a leader
   with nothing at the far end is just a stray line.

   The threshold is a container query on the figure, so it fires on a phone
   AND in the 900–1100px desktop range where the two-column hero hands this
   figure less room than a phone in landscape does.
   -------------------------------------------------------------------------- */

@container bthero (max-width: 639.98px) {
  /* THE DOMAIN LABELS GO. Over the picture they annotate parts of it; dropped
     into a strip underneath they annotate nothing, and they arrived as three
     reticles of near-identical teal type reading "0 NODES" more often than
     not — the counts animate up from zero and the strip sits below the fold
     the observer watches. The hero's job here is the car and the mirror
     beside it, which is the whole idea: this is what you built, and this is
     the model of it. The node counts are not carrying that. */
  .bt-hero__labels { display: none; }

  .bt-hero__frame { border-radius: 12px; }

  /* A leader with nothing at the far end is just a stray line. */
  .bt-tw__leaders { display: none; }
}

@container bthero (max-width: 400px) {
  .bt-lab__text { font-size: 0.68rem; letter-spacing: 0.08em; }
  .bt-lab { padding: 6px 9px 5px; }
}

/* ==========================================================================
   REDUCED MOTION
   ==========================================================================
   One frame of the finished state, and no loop scheduled at all — the
   controller checks the same query and returns before it requests a frame,
   so this block only has to make sure nothing here transitions. Because the
   finished state is the authored state, there is nothing to restore.
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  .bt-hero__stream { display: none; }

  .bt-tw__wash,
  .bt-tw__wheel,
  .bt-lab,
  .bt-tw__leader,
  .bt-tw__leader-tip,
  .bt-tw__node,
  .bt-tw__link,
  .bt-tw__trunk,
  .bt-tw__trace {
    transition: none !important;
  }
}

/* ==========================================================================
   § THE COLOUR GRADE  ·  added 2026-08-12
   ==========================================================================
   The plate is a photograph and the rest of the page is not. Review found it
   reading as a foreign asset dropped into the layout — right subject, wrong
   world. The fix is a grade rather than a new plate: the photograph is pulled
   toward the site palette in CSS, over the existing image files.

   DONE IN CSS ON PURPOSE. /assets/hero/twin-mirror-plate-v2.* are untouched, so
   the grade stays tunable in review — three numbers to argue about instead of a
   round trip through an image pipeline. If it is ever settled, baking it into
   the asset is a straight swap and this section comes out.

   THREE MOVES, IN ORDER.
     1  saturation down, so the plate stops competing with the drawn overlays
     2  a teal wash in the shadows via a screened gradient, so the dark end of
        the photograph and the dark end of the page are the same dark
     3  contrast up a touch, because desaturating flattens a picture and the
        plate's job is to look like a real object

   THE GRADE MUST NOT REACH THE OVERLAYS. It is a filter on the <img> plus a
   pseudo-element on the <picture>, both of which sit below the canvas, the SVG
   and the labels in the frame's stacking order. Grading the frame instead would
   desaturate the twin, which is the one thing on screen that has to stay teal.
   ========================================================================== */

.bt-hero__plate {
  position: relative;
}

.bt-hero__plate img {
  filter: saturate(0.58) contrast(1.08) brightness(0.94);
}

.bt-hero__plate::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  /* Teal into the shadows, a colder blue into the upper corner where the
     studio light is, and nothing in the midtones. `screen` lifts only what is
     already dark, which is what keeps the car's body from going green. */
  background:
    radial-gradient(120% 100% at 12% 8%, rgba(91, 159, 212, 0.22) 0%, transparent 58%),
    linear-gradient(190deg, rgba(78, 205, 196, 0.10) 0%, rgba(16, 27, 46, 0.42) 100%);
  mix-blend-mode: screen;
}
