/* Haisic Nigeria flagship visual system
   Thesis: engineered current. Product mass leads, type compresses from anxiety to control,
   and seven chapters change density without breaking one blue, orange, and variable-sans grammar.
   This file owns presentation only. Motion state and WebGL behavior remain progressive enhancements. */

@font-face {
  font-family: 'Inter Variable';
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  src: url('/assets/fonts/inter-latin-wght-normal.woff2') format('woff2-variations');
}

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

* {
  min-width: 0;
}

:root {
  /* Ink, tinted toward the established Haisic blue. */
  --ink-950: oklch(0.19 0.055 263);
  --ink-900: oklch(0.24 0.07 263);
  --ink-800: oklch(0.32 0.09 263);
  --ink-700: oklch(0.40 0.105 263);
  --ink-500: oklch(0.57 0.08 263);
  --ink-300: oklch(0.73 0.055 263);
  --ink-100: oklch(0.91 0.022 263);

  /* Paper is deliberately cool, never optical white or absolute black. */
  --paper-0: oklch(0.985 0.006 263);
  --paper-50: oklch(0.965 0.010 263);
  --paper-100: oklch(0.925 0.018 263);
  --paper-900: oklch(0.16 0.025 263);

  /* The ONE exception to the cool-paper rule: a product bay. The supplied packshots are cut out
     on white, so any tinted bay draws a visible rectangle around the product instead of behind
     it. The bay is white so the photograph has no edge. */
  --paper-bay: oklch(1 0 0);

  /* Haisic identity, calibrated to the canonical blue and orange. */
  --brand-blue: oklch(0.43 0.16 263);
  --brand-blue-deep: oklch(0.31 0.13 263);
  --brand-blue-soft: oklch(0.86 0.055 263);
  --brand-orange: oklch(0.66 0.18 35);
  --brand-orange-deep: oklch(0.54 0.17 34);
  --brand-orange-soft: oklch(0.91 0.06 39);
  --brand-warning: oklch(0.72 0.14 71);

  /* THE ACCENT RULE (operator, 2026-08-12: the accents are "not very deliberate").
     PRODUCT.md §Anti-references bans, in its own words, "sprayed orange accents" — and the site had
     70 orange declarations across 58 selectors, so orange had stopped being an accent and become a
     second body colour.
     Orange now marks exactly TWO things:
       1. the conversion path (CTAs, wa.me links, focus rings);
       2. the single ACTIVE item in a scroll-driven sequence (range / capacity / teardown / proof /
          current nav) — that is signalling, and signalling is what an accent is for.
     Every STATIC ornament — list pips, section indices, model codes, figcaption tails, carets —
     uses `--ornament` instead. It reclaims `--brand-blue`, which was used exactly ONCE in the whole
     stylesheet: the brand's own blue was absent from its own site while orange was everywhere.
     It is a TOKEN, not a fixed colour, because ornament sits on two different grounds — see the
     dark chapters, which flip it. This was not only a taste problem: measured against WCAG,
     orange ornament was 3.22:1 on paper and 4.02:1 on navy, BOTH failing the AA that rule.md
     requires. The replacements are 8.03:1 and 8.82:1. */
  --ornament: var(--brand-blue);

  /* Spacing follows one current: tight electrical groupings, large chapter intervals. */
  --space-2xs: 0.25rem;
  --space-xs: 0.5rem;
  --space-sm: 0.75rem;
  --space-md: 1rem;
  --space-lg: 1.5rem;
  --space-xl: 2rem;
  --space-2xl: clamp(2.75rem, 5vw, 5rem);
  --space-3xl: clamp(4.5rem, 9vw, 9rem);
  --space-chapter: clamp(5.5rem, 11vw, 11rem);
  --space-edge: clamp(1rem, 4.2vw, 4.5rem);

  /* SIX type steps, exactly three weight roles, and the six are a real modular scale: every step is
     at least 1.25x the one below it at EVERY viewport, and their order never changes.
     (The old comment said "Five" while declaring six — the scale was not being counted, let alone
     checked.)

     Why this was rewritten (2026-08-12, operator: the typography is "not very deliberate"):
     `--type-display` had been pushed to a 4.25rem ceiling to stop chapter titles wrapping badly.
     Nobody re-checked it against `--type-hero` (3.75rem), so DISPLAY OVERTOOK HERO. Measured at
     1440px: `.t-hero`, the home page's single biggest statement, rendered at 60px while `.t-head`,
     an ordinary route h1, rendered at 68px — the About page shouted louder than the home page, at
     every width from 768px up. Two steps were also flat on a phone (lead 1.19x, hero 1.11x), and a
     flat scale reads as uncommitted rather than restrained.

     Resolved and checked across 320/360/390/480/600/768/900/1024/1280/1440/1600/1920/2560:
     label 12 · body 16 · lead 20-24 · title 26-44 · display 36-56 · hero 46-72.
     If you retune ONE of these, re-check the whole ladder — that is precisely the mistake above. */
  --type-label: 0.75rem;
  --type-body: 1rem;
  --type-lead: clamp(1.25rem, 1.18rem + 0.3vw, 1.5rem);
  --type-title: clamp(1.625rem, 1.15rem + 1.9vw, 2.75rem);
  --type-display: clamp(2.25rem, 1.37rem + 2.68vw, 3.5rem);
  --type-hero: clamp(2.875rem, 1.66rem + 3.57vw, 4.5rem);
  --type-weight-air: 300;
  --type-weight-read: 550;
  --type-weight-say: 880;
  --type-track-label: 0.14em;
  --type-track-display: -0.052em;
  --type-measure: 67ch;

  /* Product depth is transform-only, so feedback never moves sibling boxes. */
  --depth-perspective: 76rem;
  --depth-rest-x: 1.5deg;
  --depth-rest-y: -2.25deg;
  --depth-lift: 0.75rem;
  --depth-shadow-sm: 0 0.25rem 1rem oklch(0.19 0.055 263 / 0.09);
  --depth-shadow-md: 0 1.25rem 3.5rem -1.25rem oklch(0.19 0.055 263 / 0.28);
  --depth-shadow-lg: 0 2.75rem 7rem -2.5rem oklch(0.14 0.04 263 / 0.52);

  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-soft: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-surge: cubic-bezier(0.12, 0.82, 0.16, 1);

  /* Semantic stacking, no magic 999 values. */
  --z-base: 0;
  --z-stage: 10;
  --z-content: 20;
  --z-sticky: 30;
  --z-nav: 40;
  --z-skip: 50;

  /* Compatibility aliases for the existing generator surface. */
  --sans: 'Inter Variable', 'Inter', system-ui, sans-serif;
  --numeric: 'tnum' 1, 'zero' 1, 'cv05' 1;
  --ink: var(--ink-950);
  --ink-2: var(--ink-700);
  --dim: var(--ink-500);
  --line: var(--ink-100);
  --line-2: var(--ink-300);
  --paper: var(--paper-0);
  --paper-2: var(--paper-50);
  --accent: var(--brand-orange);
  --accent-dk: var(--brand-orange-deep);
  --warn: var(--brand-warning);
  --t-mega: var(--type-hero);
  --t-display: var(--type-display);
  --t-title: var(--type-title);
  --t-lead: var(--type-lead);
  --t-body: var(--type-body);
  --t-label: var(--type-label);
  --tr-mega: var(--type-track-display);
  --tr-display: var(--type-track-display);
  --tr-title: -0.035em;
  --tr-label: var(--type-track-label);
  --r: var(--space-xs);
  --gap: var(--space-lg);
  --mast-h: 4.5rem;
  /* Band at the foot of the viewport the 3D product may not enter. The stage caption lives here,
     so a caption can never be written across the product it names. */
  --stage-foot: clamp(5rem, 10svh, 8rem);
  --band: var(--space-chapter);
  --measure: var(--type-measure);
  --edge: var(--space-edge);
}

html {
  max-width: 100%;
  scroll-behavior: smooth;
  -webkit-text-size-adjust: 100%;
  text-rendering: optimizeLegibility;
}

body {
  margin: 0;
  max-width: 100%;
  overflow-x: hidden;
  background: var(--paper-0);
  color: var(--ink-950);
  font-family: var(--sans);
  font-feature-settings: var(--numeric);
  font-size: var(--type-body);
  font-synthesis: none;
  font-variation-settings: 'wght' var(--type-weight-read);
  line-height: 1.62;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

@supports (overflow: clip) {
  body {
    overflow-x: clip;
  }
}

img, svg, video, canvas {
  display: block;
  max-width: 100%;
}

button, input, textarea, select {
  color: inherit;
  font: inherit;
}

a {
  color: inherit;
}

p, h1, h2, h3, h4, blockquote, figure {
  margin: 0;
}

ul, ol {
  margin: 0;
  padding: 0;
}

/* Long factual tokens must yield before the viewport does. Anywhere is an emergency break only:
   ordinary words and model names retain their natural shaping whenever space is available. */
:where(
  .t-head,
  .t-sub,
  .hero-line,
  .t-label,
  .t-note,
  .about-pillar h3,
  .card-model,
  .card-name,
  .figure,
  .tagline,
  .src,
  .known-facts dt,
  .known-facts dd,
  .specs th,
  .specs td
) {
  max-width: 100%;
  overflow-wrap: anywhere;
  word-break: normal;
}

/* Type roles */
.t-hero,
.t-head,
.t-sub,
.hero-line {
  font-variation-settings: 'wght' var(--type-weight-say);
  letter-spacing: var(--type-track-display);
  text-wrap: balance;
}

.t-hero {
  font-size: var(--type-hero);
  line-height: 0.91;
}

.hero-line {
  display: block;
  /* Each sentence is its own line box, so when one is too long for the measure it wraps INSIDE
     itself — and on a phone "Just steady power." does, leaving "power." alone on a line.
     `pretty` is the exact tool: it forbids a single-word last line and pulls a word down with it.
     It cannot be solved by shrinking the type — with five steps above a 16px body at 1.25x, the
     phone hero cannot go below ~39px, so the wrap is structural. Balance the break instead. */
  text-wrap: pretty;
  font-size: inherit;
  line-height: inherit;
}

.hero-line:nth-child(1) {
  font-variation-settings: 'wght' var(--type-weight-air);
}

.hero-line:nth-child(2) {
  font-variation-settings: 'wght' var(--type-weight-read);
}

.t-head {
  max-width: 14ch;
  font-size: var(--type-display);
  line-height: 0.96;
}

.t-sub {
  max-width: 20ch;
  font-size: var(--type-title);
  line-height: 1.04;
  letter-spacing: -0.035em;
}

/* THE LEAD READS, IT DOES NOT WHISPER.
   It wore `--type-weight-air` (300) at 20-24px — the LIGHTEST of the three roles on the paragraph
   that carries every chapter's argument, which is why it looked washed out next to its own heading.
   Contrast was never the problem (ink-700 on paper is 8.44:1); the WEIGHT was. PRODUCT.md calls the
   voice "precise, energetic"; a 300-weight lead is neither.
   The air weight keeps the jobs it earns: the hero's first line (part of a deliberate
   light -> medium -> heavy progression across three lines), small notes, source tags. */
.t-lead {
  max-width: var(--type-measure);
  color: var(--ink-700);
  font-size: var(--type-lead);
  font-variation-settings: 'wght' var(--type-weight-read);
  line-height: 1.55;
  text-wrap: pretty;
}

.t-body {
  max-width: var(--type-measure);
  color: var(--ink-700);
}

.t-label,
.card-model {
  display: block;
  color: var(--ink-500);
  font-size: var(--type-label);
  font-variation-settings: 'wght' var(--type-weight-say);
  letter-spacing: var(--type-track-label);
  line-height: 1.35;
  text-transform: uppercase;
}

/* The line under a call to action is REASSURANCE, not a second headline. Set in caps at label
   weight it shouted louder than the button it supports and hung off the hero as a stray block. */
.t-note {
  display: block;
  max-width: 42ch;
  color: var(--ink-500);
  font-size: 0.9375rem;
  font-variation-settings: 'wght' var(--type-weight-air);
  letter-spacing: normal;
  line-height: 1.5;
  text-transform: none;
}

.figure,
.specs td {
  font-variant-numeric: tabular-nums;
}

.figure {
  display: block;
  color: var(--ink-950);
  font-size: var(--type-title);
  font-variation-settings: 'wght' var(--type-weight-say);
  letter-spacing: -0.035em;
  line-height: 1;
  overflow-wrap: anywhere;
}

.figure-xl {
  font-size: var(--type-display);
}

/* A meter, not a stray rule. Without a track behind it a ratio-width bar reads as an underline
   someone mis-measured; with one it reads as this product's share of the range. */
.figure-bar {
  display: block;
  width: 100%;
  height: 0.1875rem;
  margin-top: var(--space-sm);
  background: var(--ink-100);
}

.figure-bar::after {
  content: '';
  display: block;
  width: calc(clamp(0, var(--ratio, 0), 1) * 100%);
  min-width: var(--space-2xs);
  height: 100%;
  background: var(--brand-orange);
  transform-origin: left center;
}

.ns,
.est {
  color: var(--ornament);
  font-variation-settings: 'wght' var(--type-weight-say);
  letter-spacing: 0.06em;
}

.ns {
  font-size: 0.875em;
}

.est {
  margin-inline-start: 0.4em;
  font-size: 0.8em;
}

.src {
  display: block;
  margin-top: var(--space-2xs);
  color: var(--ink-500);
  font-size: 0.75rem;
  font-variation-settings: 'wght' var(--type-weight-air);
  letter-spacing: 0.04em;
}

/* Layout grammar */
.wrap {
  width: min(100%, 88rem);
  margin-inline: auto;
  padding-inline: var(--space-edge);
}

.band {
  position: relative;
  padding-block: var(--space-chapter);
}

.stack,
.stack-lg {
  display: flex;
  flex-direction: column;
}

.stack {
  gap: var(--space-lg);
}

.stack-lg {
  gap: var(--space-2xl);
}

.split {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-2xl);
}

.tiles {
  counter-reset: proof;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-xl);
}

.tile {
  counter-increment: proof;
  position: relative;
  padding-block: var(--space-xl);
  border-top: 1px solid currentColor;
}

.tile::before {
  content: '0' counter(proof);
  display: block;
  margin-bottom: var(--space-2xl);
  color: var(--ornament);
  font-size: var(--type-label);
  font-variation-settings: 'wght' var(--type-weight-say);
  letter-spacing: var(--type-track-label);
}

.tile h3 {
  max-width: 18ch;
  font-size: var(--type-title);
  font-variation-settings: 'wght' var(--type-weight-say);
  letter-spacing: -0.035em;
  line-height: 1.04;
}

.tile p {
  max-width: 48ch;
  margin-top: var(--space-md);
  color: var(--ink-700);
}

.rule {
  height: 1px;
  margin: 0;
  border: 0;
  background: var(--ink-100);
}

.scroll-x {
  max-width: 100%;
  overflow-x: auto;
  overscroll-behavior-inline: contain;
  -webkit-overflow-scrolling: touch;
}

/* Multi-page composition. These pages share the grammar, not a repeated card template. */
/* BAND HEIGHT — measured, not guessed. Content filled only 43–72% of its own section: a 458 px
   band carrying 195 px of ink spent 258 px on padding, because --space-3xl tops out at 9rem and is
   applied top AND bottom. Sized so a band breathes at roughly its content plus a generous margin,
   not double it. */
main:not([data-home-journey]) > section {
  position: relative;
  padding-block: clamp(2.75rem, 4.5vw, 4.5rem);
}

main:not([data-home-journey]) > section:first-child {
  padding-block: clamp(3.25rem, 5.5vw, 5.5rem) clamp(2.75rem, 4.5vw, 4.5rem);
}

main:not([data-home-journey]) > section:first-child .t-head {
  margin-top: var(--space-sm);
}

main:not([data-home-journey]) > section:first-child .t-lead {
  margin-top: var(--space-lg);
}

.solution-paths {
  counter-reset: solution;
  margin-top: var(--space-3xl);
}

.solution-path {
  counter-increment: solution;
  display: grid;
  padding-block: var(--space-xl);
  grid-template-columns: minmax(4rem, 0.25fr) minmax(0, 0.75fr);
  gap: var(--space-lg);
  border-top: 1px solid var(--ink-300);
}

.solution-path::before {
  content: '0' counter(solution);
  grid-row: 1 / span 3;
  color: var(--ornament);
  font-size: var(--type-title);
  font-variation-settings: 'wght' var(--type-weight-air);
  letter-spacing: -0.035em;
}

.solution-path .t-label,
.solution-path .t-sub,
.solution-path p {
  grid-column: 2;
}

.solution-path p {
  max-width: var(--type-measure);
  color: var(--ink-700);
}

.load-scenarios {
  display: grid;
  margin-block: var(--space-2xl);
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-xl);
}

.load-scenario {
  padding-block: var(--space-xl);
  border-top: 1px solid var(--ink-300);
}

.load-scenario h3 {
  max-width: 28ch;
  font-size: var(--type-lead);
  font-variation-settings: 'wght' var(--type-weight-say);
  line-height: 1.2;
}

.load-scenario h3 a {
  display: inline-flex;
  width: fit-content;
  min-height: 2.75rem;
  align-items: center;
  vertical-align: middle;
  color: inherit;
  text-decoration-color: var(--brand-orange);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.25em;
  transition:
    color 180ms var(--ease-out),
    text-decoration-color 180ms var(--ease-out);
}

.load-scenario h3 a:hover,
.load-scenario h3 a:focus-visible {
  color: var(--brand-orange-deep);
  text-decoration-color: currentColor;
  text-decoration-thickness: 0.125rem;
}

.load-scenario .t-label {
  margin-top: var(--space-md);
  color: var(--ornament);
}

.load-scenario ul {
  display: flex;
  margin-top: var(--space-lg);
  flex-wrap: wrap;
  gap: var(--space-xs) var(--space-lg);
  list-style-position: inside;
  color: var(--ink-700);
}

.about-pillars {
  counter-reset: pillar;
  display: grid;
  margin-top: var(--space-2xl);
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-xl);
}

/* The four operating principles read as one authored argument: two premises, two closures. */
.about-pillars:has(.about-pillar:nth-child(4)) {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.about-pillar {
  counter-increment: pillar;
  position: relative;
  padding-top: var(--space-xl);
  border-top: 1px solid var(--ink-300);
}

.about-pillar::before {
  content: '0' counter(pillar);
  display: block;
  margin-bottom: var(--space-2xl);
  color: var(--ornament);
  font-size: var(--type-label);
  font-variation-settings: 'wght' var(--type-weight-say);
  letter-spacing: var(--type-track-label);
}

.about-pillar h3 {
  font-size: var(--type-title);
  font-variation-settings: 'wght' var(--type-weight-say);
  letter-spacing: -0.035em;
  line-height: 1.05;
}

.about-pillar p {
  max-width: 42ch;
  margin-top: var(--space-md);
  color: var(--ink-700);
}

.known-facts {
  margin-top: var(--space-xl);
}

.known-facts > div {
  display: grid;
  padding-block: var(--space-md);
  grid-template-columns: minmax(8rem, 0.4fr) minmax(0, 0.6fr);
  gap: var(--space-lg);
  border-bottom: 1px solid var(--ink-100);
}

.known-facts dt {
  color: var(--ink-500);
  font-size: var(--type-label);
  font-variation-settings: 'wght' var(--type-weight-read);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.known-facts dd {
  margin: 0;
  font-variation-settings: 'wght' var(--type-weight-say);
}

/* Secondary-route openings carry route-specific, content-led proof beside the proposition.
   These are solid editorial fields: no decorative glass, gradients, or empty mast acreage. */
.route-hero {
  padding-block: clamp(2.5rem, 4vw, 4rem);
  overflow: clip;
}

.route-hero-grid {
  display: grid;
  grid-template-columns: minmax(0, 7fr) minmax(18rem, 5fr);
  align-items: stretch;
  gap: clamp(var(--space-xl), 5vw, var(--space-3xl));
}

/* Top-aligned, NOT centred. The row is stretched to the panel beside it, so centring floated the
   heading ~176 px below the panel's top edge and opened dead space above both — the two halves
   read as separate objects that happened to land on the same page. They share a top edge now. */
.route-hero-copy {
  display: flex;
  max-width: 48rem;
  flex-direction: column;
  justify-content: start;
  gap: var(--space-lg);
}

.route-hero-copy .t-head,
.route-hero-copy .t-lead {
  margin-top: 0 !important;
}

.route-proof {
  position: relative;
  display: grid;
  min-height: 22rem;
  padding: clamp(var(--space-xl), 4vw, var(--space-2xl));
  align-content: space-between;
  gap: var(--space-xl);
  border: 1px solid currentColor;
}

.route-proof :where(p, ul, ol, dl) {
  max-width: 34ch;
}

.route-proof :where(ul, ol) {
  list-style: none;
}

.route-proof :where(li, dl > div) {
  display: grid;
  min-height: 2.75rem;
  padding-block: var(--space-sm);
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-md);
  border-top: 1px solid currentColor;
}

.route-proof .figure {
  color: inherit;
}

/* Each route derives its proof character from its real content, not decorative filler. */
:is(
  .route-proof--solutions,
  .route-proof-solutions,
  .route-proof[data-route='solutions'],
  .route-hero-solutions .route-proof
) {
  border-color: var(--brand-blue-deep);
  background: var(--brand-blue-deep);
  color: var(--paper-0);
  /* Dark ground: ornament inverts, or it is 1.61:1 and gone. */
  --ornament: var(--brand-blue-soft);
}

:is(
  .route-proof--about,
  .route-proof-about,
  .route-proof[data-route='about'],
  .route-hero-about .route-proof
) {
  border-color: var(--ink-950);
  background: var(--ink-950);
  color: var(--paper-0);
  /* Dark ground: ornament inverts, or it is 1.61:1 and gone. */
  --ornament: var(--brand-blue-soft);
}

:is(
  .route-proof--service,
  .route-proof-service,
  .route-proof[data-route='service'],
  .route-hero-service .route-proof
) {
  /* Navy, not orange. Orange is this brand's ACTION colour — it marks the WhatsApp button, the
     capacity meters and the active state. A full-bleed orange panel behind a list of city names
     spends it on wallpaper, and then the CTA sitting on the same page has nothing left to shout
     with. The darks across the site are one brand navy; this joins them. */
  border-color: var(--brand-blue-deep);
  background: var(--brand-blue-deep);
  color: var(--paper-0);
  /* Dark ground: ornament inverts, or it is 1.61:1 and gone. */
  --ornament: var(--brand-blue-soft);
}

:is(
  .route-proof--contact,
  .route-proof-contact,
  .route-proof[data-route='contact'],
  .route-hero-contact .route-proof
) {
  border-color: var(--ink-300);
  background: var(--paper-50);
  color: var(--ink-950);
}

.route-hero-solutions .route-proof.solution-paths {
  counter-reset: solution;
  margin-top: 0;
  align-content: start;
}

.route-hero-solutions .solution-path {
  padding-block: var(--space-md);
  grid-template-columns: 2.5rem minmax(0, 1fr);
  gap: var(--space-xs) var(--space-md);
  border-color: color-mix(in oklab, var(--paper-0) 32%, transparent);
}

.route-hero-solutions .solution-path::before {
  grid-row: 1 / span 3;
  color: var(--ornament);
  font-size: var(--type-lead);
}

.route-hero-solutions .solution-path .t-label,
.route-hero-solutions .solution-path .t-sub,
.route-hero-solutions .solution-path p {
  grid-column: 2;
  color: var(--paper-0);
}

.route-hero-solutions .solution-path .t-sub {
  font-size: var(--type-lead);
  letter-spacing: -0.025em;
}

.route-hero-solutions .solution-path p {
  color: var(--ink-100);
  font-size: var(--type-label);
  font-variation-settings: 'wght' var(--type-weight-read);
  letter-spacing: 0;
  text-transform: none;
}

.route-hero-about .route-proof {
  min-height: 18rem;
  align-content: center;
}

.route-hero-about .route-proof .t-lead {
  max-width: 18ch;
  color: var(--ink-100);
  font-size: var(--type-title);
  font-variation-settings: 'wght' var(--type-weight-say);
  letter-spacing: -0.035em;
  line-height: 1.05;
}

.route-proof-list {
  display: grid;
  list-style: none;
}

.route-hero-service .route-proof-list li {
  counter-increment: route-centre;
  grid-template-columns: auto minmax(0, 1fr);
  justify-content: start;
  /* Rules and numerals were inked for an orange ground; on navy they went near-invisible. */
  border-color: color-mix(in oklab, var(--paper-0) 26%, transparent);
  font-size: var(--type-lead);
  font-variation-settings: 'wght' var(--type-weight-say);
}

.route-hero-service .route-proof-list li::before {
  content: '0' counter(route-centre);
  color: var(--ornament);
  font-size: var(--type-label);
  letter-spacing: var(--type-track-label);
}

.route-hero-contact .route-proof > a {
  display: inline-flex;
  min-height: 2.75rem;
  align-items: center;
  color: var(--brand-blue-deep);
  font-size: var(--type-title);
  font-variation-settings: 'wght' var(--type-weight-say);
  letter-spacing: -0.035em;
  line-height: 1;
  overflow-wrap: anywhere;
  text-decoration-color: var(--brand-orange);
  text-decoration-thickness: 0.125rem;
  text-underline-offset: 0.2em;
}

.topic-links {
  display: grid;
  margin-top: var(--space-xl);
  list-style: none;
}

.topic-links li {
  border-top: 1px solid var(--ink-300);
}

.topic-links a {
  display: grid;
  min-height: 3.5rem;
  padding-block: var(--space-sm);
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-md);
  font-variation-settings: 'wght' var(--type-weight-say);
  text-decoration: none;
  transition: color 180ms var(--ease-out), padding-inline 180ms var(--ease-out);
}

.topic-links a:not(:has(.topic-link-cue))::after {
  content: '\2197';
  color: var(--brand-orange-deep);
  font-size: var(--type-lead);
  line-height: 1;
}

.topic-link-cue {
  color: var(--brand-orange-deep);
  font-size: var(--type-lead);
  line-height: 1;
}

.topic-links a:hover,
.topic-links a:focus-visible {
  padding-inline: var(--space-sm);
  color: var(--brand-orange-deep);
}

/* Frozen HomeDOM */
main[data-home-journey] {
  position: relative;
  isolation: isolate;
  overflow: clip;
  background: var(--paper-0);
}

/* P3 owns the normalized state; P2 turns it into continuous location feedback.
   Scale is compositor-only, does not reflow chapters, and a missing state remains at zero. */
main[data-home-journey]::before {
  content: '';
  position: fixed;
  z-index: var(--z-sticky);
  top: calc(var(--mast-h) - 0.1875rem);
  right: 0;
  left: 0;
  height: 0.1875rem;
  background: var(--brand-orange);
  pointer-events: none;
  transform: scaleX(clamp(0, var(--journey-progress, 0), 1));
  transform-origin: left center;
  will-change: transform;
}

main[data-home-journey] > section[data-journey-chapter] {
  position: relative;
  /* Auto is load-bearing: a section stacking context would trap its z-indexed copy below
     the fixed canvas. Chapter backgrounds stay low while copy joins the root content layer. */
  z-index: auto;
  display: grid;
  align-items: center;
  min-height: clamp(40rem, 88svh, 64rem);
  padding-block: var(--space-chapter);
}

main[data-home-journey] > section[data-journey-chapter] > *:not(#product-stage) {
  position: relative;
  z-index: var(--z-content);
}

[data-chapter-copy] {
  display: flex;
  width: min(100%, 44rem);
  flex-direction: column;
  gap: var(--space-lg);
}

[data-journey-chapter='hero'] {
  min-height: max(44rem, calc(100svh - var(--mast-h)));
  background: var(--paper-0);
}

[data-journey-chapter='hero'] [data-chapter-copy] {
  width: min(100%, 58rem);
}

[data-journey-chapter='hero'] .t-lead {
  max-width: 46ch;
}

[data-journey-chapter='reliability'] {
  color: var(--paper-0);
  background: var(--brand-blue-deep);
  /* Ornament inverts on navy: --brand-blue is 1.61:1 here (invisible), the soft tone is 8.82:1. */
  --ornament: var(--brand-blue-soft);
}

[data-journey-chapter='reliability'] .t-lead,
[data-journey-chapter='reliability'] .t-body,
[data-journey-chapter='reliability'] .tile p,
[data-journey-chapter='reliability'] .t-label {
  color: var(--ink-100);
}

[data-journey-chapter='intelligence'] {
  background: var(--paper-0);
}

[data-journey-chapter='intelligence'] [data-chapter-copy] {
  margin-inline-start: 0;
}

[data-journey-chapter='battery'] {
  color: var(--paper-0);
  background: var(--brand-blue-deep);
  /* Ornament inverts on navy: --brand-blue is 1.61:1 here (invisible), the soft tone is 8.82:1. */
  --ornament: var(--brand-blue-soft);
}

[data-journey-chapter='battery'] .t-lead,
[data-journey-chapter='battery'] .t-body,
[data-journey-chapter='battery'] .t-label,
[data-journey-chapter='battery'] .figure {
  color: var(--paper-100);
}

[data-journey-chapter='range'] {
  align-items: start;
  min-height: auto;
  background: var(--paper-0);
}

[data-journey-chapter='assurance'] {
  color: var(--paper-0);
  background: var(--brand-blue-deep);
  /* Ornament inverts on navy: --brand-blue is 1.61:1 here (invisible), the soft tone is 8.82:1. */
  --ornament: var(--brand-blue-soft);
}

[data-journey-chapter='assurance'] .t-lead,
[data-journey-chapter='assurance'] .t-body,
[data-journey-chapter='assurance'] .t-label,
[data-journey-chapter='assurance'] .centre-list .addr {
  color: var(--ink-100);
}

[data-journey-chapter='contact'] {
  min-height: min(54rem, 90svh);
  color: var(--ink-950);
  background: var(--brand-orange);
  /* This chapter IS the accent, drenched. Ornament on it is ink; a blue pip on orange would be
     both illegible and a second accent competing with the field. */
  --ornament: var(--ink-950);
}

[data-journey-chapter='contact'] .t-label,
[data-journey-chapter='contact'] .t-lead,
[data-journey-chapter='contact'] .t-body {
  color: var(--ink-900);
}

[data-journey-chapter='contact'] .cta {
  background: var(--ink-950);
  color: var(--paper-0);
}

.chapter-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  align-items: center;
  gap: var(--space-2xl);
}

.reliability-list {
  counter-reset: reliability;
  margin-top: var(--space-lg);
}

.reliability-point {
  counter-increment: reliability;
  display: grid;
  grid-template-columns: 2.75rem minmax(0, 1fr);
  gap: var(--space-md);
  padding-block: var(--space-lg);
  border-top: 1px solid var(--ink-700);
  transition:
    border-color 220ms var(--ease-out),
    box-shadow 220ms var(--ease-out);
}

/* ACTIVE STATE IS A RULE PLUS A WASH, NOT A SIDE STRIPE (operator, 2026-08-12: "the highlight
   orange line strip, i dont like, its also not even aligned and well done").
   It was `box-shadow: inset 0.1875rem 0 0 <orange>` — a 3px bar down the left edge. He is right on
   both counts. It could never align: an inset shadow paints the whole padding box, so it started
   above the first line of text and ran past the last, agreeing with neither the rule above it nor
   any baseline. And a coloured left-stripe on a list item is a stock template tell.
   The signal still has to read, so it moves to two things that CANNOT misalign: the item's own
   full-width top rule in the accent, and a low wash of the accent across the whole row. */
.reliability-point[data-proof-active='true'] {
  border-top-color: var(--brand-orange);
  background: color-mix(in oklab, var(--brand-orange) 12%, transparent);
}

.reliability-point::before {
  content: '0' counter(reliability);
  color: var(--ornament);
  font-size: var(--type-label);
  font-variation-settings: 'wght' var(--type-weight-say);
  letter-spacing: var(--type-track-label);
}

.reliability-point h3 {
  grid-column: 2;
  font-size: var(--type-lead);
  font-variation-settings: 'wght' var(--type-weight-say);
  line-height: 1.15;
}

.reliability-point p {
  grid-column: 2;
  color: var(--ink-100);
}

.pairing-list {
  list-style: none;
  margin-top: var(--space-lg);
}

/* ONE ROW PER PAIR (operator, 2026-08-12: "the voltage family needs to be removed. and indicate
   more compactly"). The family token used to span `1 / -1`, so every pair cost TWO lines — the
   pair itself, then a lone "48V" underneath. It now rides the end of the same row as a compact
   tag, which is also where it belongs in the reading: it qualifies the pair it sits on. */
.pairing {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-sm);
  padding-block: var(--space-sm);
  border-top: 1px solid var(--ink-300);
  transition:
    border-color 220ms var(--ease-out),
    box-shadow 220ms var(--ease-out);
}

/* Same stripe, same removal — see .reliability-point[data-proof-active]. */
.pairing[data-pair-active='true'] {
  border-top-color: var(--brand-orange-deep);
  background: color-mix(in oklab, var(--brand-orange) 10%, transparent);
}

.pairing a {
  display: inline-flex;
  min-height: 2.75rem;
  align-items: center;
  font-variation-settings: 'wght' var(--type-weight-say);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.25em;
}

.pairing > span[aria-hidden='true'] {
  color: var(--ornament);
  font-size: var(--type-lead);
}

.pair-family {
  justify-self: end;
  color: var(--ink-700);
  font-size: var(--type-label);
  font-variation-settings: 'wght' var(--type-weight-say);
  /* NOT the 0.14em label tracking. On a three-character token that spacing reads as "4 8 V" —
     three separated glyphs rather than one value. Short alphanumeric tokens need almost none. */
  letter-spacing: 0.02em;
  text-transform: uppercase;
  white-space: nowrap;
}

.battery-scale {
  display: flex;
  margin-top: var(--space-lg);
  flex-direction: column;
  gap: var(--space-xs);
}

.battery-step {
  position: relative;
  display: grid;
  min-height: 4.75rem;
  padding: var(--space-md);
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: end;
  gap: var(--space-md);
  overflow: hidden;
  background: color-mix(in oklab, var(--paper-0) 10%, transparent);
  color: var(--paper-0);
  text-decoration: none;
  box-shadow: none;
  transition:
    background-color 220ms var(--ease-out),
    box-shadow 220ms var(--ease-out);
}

/* Stripe removed here too. This row already had a wash, so the active state keeps its lift and
   loses the bar; the capacity meter beneath it is the thing carrying the accent on this list. */
.battery-step[data-capacity-active='true'] {
  background: color-mix(in oklab, var(--paper-0) 22%, transparent);
}

.battery-step::before {
  content: '';
  position: absolute;
  inset: auto 0 0;
  width: calc(clamp(0.06, var(--ratio, 0.06), 1) * 100%);
  height: 0.25rem;
  background: var(--ornament);
  transform-origin: left center;
}

.battery-step span {
  font-size: var(--type-label);
  font-variation-settings: 'wght' var(--type-weight-say);
  letter-spacing: 0.05em;
  text-transform: uppercase;
}

.battery-step strong {
  font-size: var(--type-lead);
  font-variation-settings: 'wght' var(--type-weight-say);
  letter-spacing: -0.025em;
}

.battery-step:hover,
.battery-step:focus-visible {
  background: color-mix(in oklab, var(--paper-0) 17%, transparent);
}

.chapter-next {
  display: flex;
  min-height: 2.75rem;
  margin-top: var(--space-md);
  padding-block: var(--space-sm);
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  border-top: 1px solid currentColor;
  font-size: var(--type-label);
  font-variation-settings: 'wght' var(--type-weight-say);
  letter-spacing: var(--type-track-label);
  text-decoration: none;
  text-transform: uppercase;
  transition: color 180ms var(--ease-out), padding-inline 180ms var(--ease-out);
}

.chapter-next span {
  color: var(--ornament);
  font-size: var(--type-lead);
  line-height: 1;
}

.chapter-next:hover,
.chapter-next:focus-visible {
  padding-inline: var(--space-sm);
  color: var(--brand-orange);
}

/* The complete line-up, laid out — not a carousel that shows three of eleven and crops the third.
   A range the customer cannot see whole is a range he cannot choose from. */
.range-rail {
  position: relative;
  display: grid;
  margin-block: var(--space-2xl);
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 15.5rem), 1fr));
  gap: var(--space-lg);
}

.range-rail[data-range-active-index][data-range-active-model] {
  border-block-end-color: var(--brand-orange-deep);
}

.range-actions {
  display: flex;
  margin-top: var(--space-xl);
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm);
}

.range-actions .cta:first-child,
.range-actions a[href*='wa.me'] {
  border-color: var(--brand-orange);
  background: var(--brand-orange);
  color: var(--ink-950);
}

.range-actions .cta:first-child:hover,
.range-actions .cta:first-child:focus-visible,
.range-actions a[href*='wa.me']:hover,
.range-actions a[href*='wa.me']:focus-visible {
  border-color: var(--brand-orange-deep);
  background: var(--brand-orange-deep);
  color: var(--paper-0);
}

/* One progressive stage, visually fenced to the product half of wide screens. */
#product-stage,
#stage {
  position: fixed;
  z-index: var(--z-stage);
  inset: var(--mast-h) 0 0 auto;
  width: min(52vw, 58rem);
  height: calc(100svh - var(--mast-h));
  max-width: none;
  pointer-events: none;
}

/* The stage is a fixed pane and the chapters scroll THROUGH it, so a product framed to the full
   pane runs off the fold and gets sliced by the next chapter's background — a hard horizontal cut
   straight across the cabinet at every boundary, and a caption written over its feet.
   Two corrections: reserve a foot band the product may not enter (the caption lives there), and
   feather the pane's top and bottom edges so the model resolves into the page instead of ending
   at a butt joint against a colour change. */
/* The product SINKS INTO its chapter; it does not float across the page.
   The stage is one fixed pane and the chapters scroll through it, so a product framed to the whole
   pane spills into the next chapter and gets sliced by its background. Feathering the pane's edges
   only made the spill soft — the model still overlapped the section below.
   Instead the pane is clipped to the ACTIVE chapter's own top and bottom edges (reported each
   frame by the journey clock as viewport-relative px). The cabinet is cut cleanly at the boundary
   and appears to sink behind it, then the next chapter's model rises out of the same seam. */
#product-stage {
  bottom: var(--stage-foot);
  height: calc(100svh - var(--mast-h) - var(--stage-foot));
  opacity: 0;
  transition: opacity 320ms var(--ease-out);
  clip-path: inset(
    max(0px, calc(var(--stage-chapter-top, -100svh) - var(--mast-h)))
    0
    max(
      0px,
      calc(100svh - var(--stage-foot) - var(--stage-chapter-bottom, 200svh))
    )
    0
  );
}

#product-stage.is-live {
  opacity: 1;
}

.product-stage-label {
  position: fixed;
  z-index: calc(var(--z-stage) + 1);
  right: var(--space-edge);
  bottom: var(--space-xl);
  display: grid;
  width: min(calc(52vw - (2 * var(--space-edge))), 50rem);
  padding-top: var(--space-sm);
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: end;
  column-gap: var(--space-md);
  border-top: 1px solid currentColor;
  opacity: 0;
  pointer-events: none;
  color: var(--ink-950);
  font-size: var(--type-label);
  font-variation-settings: 'wght' var(--type-weight-say);
  letter-spacing: 0.08em;
  line-height: 1.35;
  text-transform: uppercase;
}

.product-stage-label__name {
  grid-column: 1;
}

.product-stage-label__scale {
  grid-column: 1;
  color: currentColor;
  font-variation-settings: 'wght' var(--type-weight-read);
  letter-spacing: 0.04em;
}

.product-stage-label__model {
  grid-row: 1 / span 2;
  grid-column: 2;
  align-self: center;
  color: var(--ornament);
  text-align: right;
}

/* Keyed to the chapter beneath the caption band (written by the journey clock), NOT to the chapter
   whose product is being named. The caption is pinned to the foot of the viewport, so at every
   boundary it stands on the arriving chapter while describing the departing one. */
:root:is(
  [data-stage-foot-chapter='reliability'],
  [data-stage-foot-chapter='battery'],
  [data-stage-foot-chapter='assurance']
) .product-stage-label {
  color: var(--paper-0);
}

:root:is(
  [data-stage-foot-chapter='reliability'],
  [data-stage-foot-chapter='battery'],
  [data-stage-foot-chapter='assurance']
) .product-stage-label__model {
  color: var(--brand-orange);
}

/* The closing chapter is a full-bleed orange field. The accent cannot mark anything on it —
   orange-on-orange put the model code at roughly 1.2:1 and it read as a smudge. */
:root[data-stage-foot-chapter='contact'] .product-stage-label,
:root[data-stage-foot-chapter='contact'] .product-stage-label__model {
  color: var(--ink-950);
}

.product-stage-label.is-live[data-stage-label-state='visible'] {
  opacity: 1;
}

/* The caption is fixed to the foot of the viewport; the arriving chapter's copy scrolls through
   that same band. Withhold it during the handover rather than printing it over that copy.
   `!important` is required and is not a shortcut here: stage.js writes `style.opacity` on this
   element every frame, and an inline declaration outranks any normal rule. */
:root[data-stage-foot-owned='false'] .product-stage-label {
  opacity: 0 !important;
}

#stage {
  opacity: 0;
  transition: opacity 600ms var(--ease-out);
}

#stage.is-live {
  opacity: 0.92;
}

[data-stage-model],
[data-stage-pose] {
  transform-style: preserve-3d;
}

.field {
  perspective: var(--depth-perspective);
  perspective-origin: 50% 42%;
}

/* A chapter's copy column is the depth CHAMBER. Without a perspective ancestor, translate3d's z
   term is mathematically applied and visually inert — the journey was writing --dz every frame
   and nothing on screen moved along z. This is what makes the depth real rather than declared. */
[data-chapter-copy] {
  /* Deliberately WEAK. Perspective scales an element by d/(d-z), and that scaling is what pushed
     a heading up and over its own eyebrow: at 60rem, +104px of z grew the line by 12%. At 120rem
     the same travel grows it by 5.7% and nothing collides. Depth is carried by the defocus below,
     which is the cue that actually reads, not by making type bigger as it approaches. */
  perspective: 120rem;
  perspective-origin: 22% 50%;
  transform-style: preserve-3d;
}

[data-depth],
.depth {
  transform: translate3d(0, var(--depth-y, var(--dy, 0)), var(--depth-z, var(--dz, 0)));
  transform-style: preserve-3d;
  backface-visibility: hidden;
  /* Capped in em so defocus scales with the type it is applied to. A flat 5.5px is a soft edge on
     a 68px headline and an illegible smear on a 12px label. */
  filter: blur(min(var(--depth-blur, 0px), 0.075em));
  will-change: transform, filter;
}

/* Defocus is a motion effect: it must vanish entirely when motion is not wanted. */
@media (prefers-reduced-motion: reduce) {
  [data-depth],
  .depth {
    filter: none;
  }
}

/* Product posters are always present. A live canvas may cover them, never replace their meaning. */
[data-static-product] {
  opacity: 1;
  visibility: visible;
}

.static-product[data-static-product],
.detail-shot[data-static-product] {
  position: relative;
  display: grid;
  width: min(100%, 34rem);
  place-items: center;
  align-content: center;
  justify-self: end;
  overflow: hidden;
  color: currentColor;
  font-size: var(--type-label);
  font-variation-settings: 'wght' var(--type-weight-say);
  letter-spacing: var(--type-track-label);
  text-align: center;
  text-transform: uppercase;
}

.static-product[data-static-product] {
  min-block-size: 0;
  block-size: clamp(20rem, 68svh, 34rem);
  max-block-size: 34rem;
  aspect-ratio: 4 / 5;
  contain: size layout paint;
}

.static-product[data-static-product] img,
.static-product[data-static-product] picture,
.detail-shot[data-static-product] img,
.detail-shot[data-static-product] picture {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.static-product[data-static-product]:empty::before,
.detail-shot[data-static-product]:empty::before {
  content: '';
  display: block;
  width: clamp(8rem, 30vw, 18rem);
  aspect-ratio: 0.72;
  border: 1px solid currentColor;
  border-radius: 0.75rem;
  background: color-mix(in oklab, currentColor 5%, transparent);
  box-shadow:
    inset 0 -3rem 0 color-mix(in oklab, currentColor 8%, transparent),
    var(--depth-shadow-lg);
}

.static-product[data-static-product]:empty::after,
.detail-shot[data-static-product]:empty::after {
  content: attr(data-static-product);
  position: absolute;
  inset-inline: var(--space-md);
  bottom: var(--space-xl);
}

.static-product figcaption {
  position: absolute;
  right: var(--space-md);
  bottom: var(--space-md);
  left: var(--space-md);
  display: flex;
  padding-top: var(--space-sm);
  justify-content: space-between;
  gap: var(--space-md);
  border-top: 1px solid currentColor;
  color: inherit;
  font-size: var(--type-label);
  font-variation-settings: 'wght' var(--type-weight-say);
  letter-spacing: 0.05em;
  text-align: left;
}

.static-product figcaption span:last-child {
  color: var(--ornament);
  overflow-wrap: anywhere;
  text-align: right;
}

.detail-shot[data-static-product] {
  min-height: 24rem;
  background: var(--paper-50);
}

.detail-shot.is-empty {
  padding: var(--space-xl);
}

/* Dimensional product selection */
.grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr));
  gap: clamp(var(--space-xl), 4vw, var(--space-3xl)) var(--space-xl);
  perspective: var(--depth-perspective);
}

.card,
[data-product-tilt] {
  position: relative;
  transform:
    perspective(var(--depth-perspective))
    rotateX(var(--product-tilt-x, var(--rx, var(--depth-rest-x))))
    rotateY(var(--product-tilt-y, var(--ry, var(--depth-rest-y))))
    translate3d(0, 0, var(--product-lift, 0));
  transform-style: preserve-3d;
  transform-origin: center;
  box-shadow: var(--depth-shadow-sm);
  transition:
    transform 320ms var(--ease-out),
    box-shadow 320ms var(--ease-out),
    background-color 240ms var(--ease-out),
    color 240ms var(--ease-out);
}

/* A catalogue card sits FLAT and square to the page. The resting tilt that reads as depth on a
   single hero product reads as a printing fault across a grid of twelve — every card a different
   parallelogram, no shared baseline. Depth on a card is earned on pointer, not spent at rest. */
.card {
  --depth-rest-x: 0deg;
  --depth-rest-y: 0deg;

  display: flex;
  min-height: 0;
  flex-direction: column;
  overflow: hidden;
  border: 1px solid var(--ink-100);
  background: var(--paper-0);
  color: var(--ink-950);
  text-decoration: none;
}

.card-shot {
  position: relative;
  display: grid;
  height: clamp(13rem, 15vw, 16rem);
  min-height: 0;
  padding: var(--space-lg);
  grid-template-rows: minmax(0, 1fr);
  flex: 0 0 auto;
  overflow: hidden;
  border-bottom: 1px solid var(--ink-100);
  background: var(--paper-bay);
}

/* The image STRETCHES to the bay and `object-fit` centres it. Do not centre it as a grid item
   instead: an unstretched item has no definite height to resolve `height: 100%` against, it keeps
   its intrinsic aspect, and overflow clips it — measured 250x418 inside a 297x241 bay. */
.card-shot img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  filter: drop-shadow(0 0.75rem 0.75rem oklch(0.19 0.055 263 / 0.1));
  transition: transform 420ms var(--ease-out);
}

.card:hover .card-shot img,
.card:focus-visible .card-shot img {
  transform: scale(1.03);
}

/* An honest empty bay, not a note dropped on top of a box. The 22 kWh has no photograph and the
   card must say so plainly — centred, legible, and clearly not a product. */
.card-shot.is-empty {
  display: grid;
  place-items: center;
  color: var(--ink-500);
  font-size: var(--type-label);
  font-variation-settings: 'wght' var(--type-weight-say);
  letter-spacing: var(--type-track-label);
  line-height: 1.45;
  text-align: center;
  text-transform: uppercase;
}

.card-shot.is-empty::before {
  content: '';
  position: absolute;
  inset: var(--space-lg);
  border: 1px dashed var(--ink-300);
}

.card-shot.is-empty > * {
  position: relative;
  max-width: 22ch;
}

.card-body {
  position: relative;
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: var(--space-2xs);
  padding: var(--space-lg);
}

/* Model code and product family on ONE baseline — both are small labels answering "what kind of
   thing is this", so stacking them spent two of the card's six text rows saying one thing. */
.card-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2xs) var(--space-xs);
}

.card-family::before {
  content: '·';
  margin-right: var(--space-xs);
  color: var(--ink-300);
}

.card-name {
  max-width: 18ch;
  font-size: var(--type-lead);
  font-variation-settings: 'wght' var(--type-weight-say);
  letter-spacing: -0.025em;
  line-height: 1.12;
}

.card-decision {
  display: grid;
  margin-top: auto;
  padding-top: var(--space-md);
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-2xs);
  border-top: 1px solid var(--ink-100);
}

/* THE DATUM LABEL IS VISUALLY HIDDEN EVERYWHERE (operator, 2026-08-12: "remove the energy label
   text from there"). On a thumbnail the figure carries its own unit — "4,019 Wh" says energy — and
   its meter says magnitude, so the word ENERGY was a row of type restating what the number already
   states. Hidden, NOT removed: it is the accessible name for that number and stays in the tree for
   a screen reader. */
.card-decision .t-label {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  color: var(--ink-700);
}

/* LABEL AND VALUE SHARE A BASELINE inside the thumbnail. Stacked, "ENERGY" and "4,019 Wh" took two
   of the card's six text rows to express one fact. Scoped to .card-body so the range-card overlay
   (which is a different composition with its own dl) is untouched. */
.card-body .card-decision {
  grid-template-columns: minmax(0, auto) 1fr;
  align-items: baseline;
  justify-content: space-between;
  column-gap: var(--space-sm);
}

.card-datum {
  display: block;
  color: var(--ink-950);
  font-size: var(--type-lead);
  font-variation-settings: 'wght' var(--type-weight-say);
  line-height: 1.12;
}

/* One line, always. A capacity that wraps to "4,019 / Wh" turns a comparable figure into a
   two-storey headline and forces every card in the row to carry the extra height. */
.card-figure {
  margin-top: 0;
  font-size: clamp(1.375rem, 1.05rem + 0.9vw, 1.875rem);
  letter-spacing: -0.03em;
  white-space: nowrap;
}

/* The cue sits with the model code, not on top of the capacity meter. */
.card-link-cue {
  position: absolute;
  top: var(--space-lg);
  right: var(--space-lg);
  bottom: auto;
  color: var(--brand-orange-deep);
  font-size: var(--type-lead);
  line-height: 1;
  transition: transform 220ms var(--ease-out);
}

.card:hover .card-link-cue,
.card:focus-visible .card-link-cue {
  transform: translate3d(0.15rem, -0.15rem, 0);
}

.card:hover,
[data-product-tilt]:hover,
.card:focus-visible,
[data-product-tilt]:focus-visible {
  --product-lift: var(--depth-lift);
  --product-tilt-x: 0deg;
  --product-tilt-y: 0deg;
  box-shadow: var(--depth-shadow-md);
}

.card:hover .card-name,
.card:focus-visible .card-name {
  color: var(--brand-orange-deep);
}

/* Homepage range cards are decision surfaces, not image tiles. P1's figure caption owns the
   complete identity/spec/action layer; flex order puts that semantic header before the product bay
   while keeping DOM reading order, absent-media truth, and text zoom fully reflowable. */
.card[data-range-card] {
  min-height: 0;
  border: 1px solid var(--ink-100);
  background: var(--paper-0);
}

/* The whole-card arrow is a second, redundant call to action: the caption already ends in
   "View details ↗". Its empty body was the tall blank block under every range card. The markup
   stays (P1's cue contract is structural) — it just stops occupying the layout. */
.card[data-range-card] > .card-body {
  display: none;
}

.card[data-range-card]::before {
  content: '';
  position: absolute;
  z-index: 4;
  top: 0;
  right: 0;
  left: 0;
  height: 0.25rem;
  background: var(--ornament);
  pointer-events: none;
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 220ms var(--ease-out);
}

.card-shot[data-range-media] {
  display: flex;
  height: auto;
  min-height: 0;
  padding: 0;
  flex-direction: column;
  aspect-ratio: auto;
  border-bottom: 0;
  background: var(--paper-0);
}

/* Product first, then the facts that let you choose. The caption led the bay before, which put
   four lines of type above every photograph and made the grid read as a table of contents. */
.card-shot[data-range-media] > img {
  order: 1;
  width: 100%;
  height: clamp(10rem, 15vw, 13rem);
  padding: var(--space-lg);
  flex: 0 0 auto;
  border-bottom: 1px solid var(--ink-100);
  background: var(--paper-bay);
}

.card-shot.is-empty[data-range-media] {
  align-items: stretch;
  padding-bottom: 0;
  text-align: left;
}

.card-shot.is-empty[data-range-media]::before {
  display: none;
}

.absent-media-note {
  position: relative;
  z-index: 1;
}

.card-shot.is-empty[data-range-media] > .absent-media-note {
  display: grid;
  order: 1;
  min-height: clamp(10rem, 15vw, 13rem);
  padding: var(--space-lg);
  place-items: center;
  border-bottom: 1px solid var(--ink-100);
  background: var(--paper-bay);
  color: var(--ink-500);
  text-align: center;
}

/* The empty-bay note is uppercase; the decision layer that shares its figure is NOT. Without this
   reset the 22 kWh — the one product with no photograph — shouted "22 KWH BATTERY / 22,000 WH"
   while every other card said "22 kWh Battery", and the brand's W-versus-Wh law died with it. */
.range-card-decision {
  position: relative;
  z-index: 2;
  display: flex;
  order: 2;
  letter-spacing: normal;
  text-transform: none;
  min-height: 0;
  padding: var(--space-lg);
  flex: 1 1 auto;
  flex-direction: column;
  gap: var(--space-2xs);
  background: var(--paper-0);
  color: var(--ink-950);
  text-align: left;
  transition: background-color 220ms var(--ease-out);
}

.range-card-decision__model,
.range-card-decision__family {
  color: var(--ink-700);
}

.range-card-decision__name {
  max-width: 16ch;
}

.range-card-decision__facts {
  display: grid;
  margin: var(--space-sm) 0 0;
  grid-template-columns: minmax(0, 1fr);
  align-items: start;
  gap: var(--space-sm);
}

.range-card-decision__fact {
  display: grid;
  gap: var(--space-2xs);
}

.range-card-decision__facts dd {
  margin: 0;
}

.range-card-decision__datum {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
}

.range-card-decision .card-figure {
  margin-top: 0;
}

.range-card-decision__value {
  font-size: clamp(1.375rem, 1.05rem + 0.9vw, 1.875rem);
  letter-spacing: -0.03em;
  white-space: nowrap;
}

.range-card-decision > .figure-bar {
  margin-top: var(--space-sm);
}

.range-card-decision__action {
  display: flex;
  min-height: 2.25rem;
  margin-top: auto;
  padding-top: var(--space-md);
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  border-top: 1px solid var(--ink-100);
  color: var(--ornament);
  font-size: var(--type-label);
  font-variation-settings: 'wght' var(--type-weight-say);
  letter-spacing: var(--type-track-label);
  text-transform: uppercase;
}

.range-card-decision__cue {
  font-size: var(--type-lead);
  line-height: 1;
}

.card[data-range-card] .card-body {
  min-height: 4.75rem;
  padding: var(--space-md);
  background: var(--paper-0);
}

.range-rail .card {
  border: 1px solid var(--ink-100);
  transition:
    transform 320ms var(--ease-out),
    box-shadow 320ms var(--ease-out),
    background-color 240ms var(--ease-out),
    color 240ms var(--ease-out),
    border-color 220ms var(--ease-out),
    opacity 220ms var(--ease-out);
}

.range-rail .card[data-range-active='true'] {
  --product-lift: 0.25rem;
  z-index: 1;
  border-color: var(--brand-orange-deep);
  background: var(--paper-0);
  opacity: 1;
  box-shadow: var(--depth-shadow-md);
}

.range-rail .card[data-range-active='true']::before {
  transform: scaleX(1);
}

.range-rail .card[data-range-active='true'] .range-card-decision {
  background: color-mix(in oklab, var(--paper-0) 96%, var(--brand-orange) 4%);
}

.range-rail .card[data-range-active='true'] :is(
  .card-model,
  .range-card-decision__action
) {
  color: var(--brand-orange-deep);
}

.range-rail .card[data-range-active='true'] .card-link-cue {
  border-color: var(--brand-orange);
  background: var(--brand-orange);
  color: var(--ink-950);
}

@media (prefers-reduced-motion: no-preference) {
  .range-rail .card {
    opacity: clamp(0.9, var(--range-proximity, 1), 1);
  }
}

/* Shell */
.mast {
  position: sticky;
  z-index: var(--z-sticky);
  top: 0;
  min-height: var(--mast-h);
  border-bottom: 1px solid var(--ink-100);
  background: color-mix(in oklab, var(--paper-0) 96%, var(--brand-blue) 4%);
}

.mast-in {
  display: flex;
  min-height: var(--mast-h);
  align-items: center;
  gap: var(--space-lg);
}

.mast-logo {
  display: inline-flex;
  min-width: 2.75rem;
  min-height: 2.75rem;
  align-items: center;
  margin-inline-end: auto;
  text-decoration: none;
}

.mast-logo img {
  width: auto;
  height: 1.875rem;
}

.nav {
  display: flex;
  align-items: center;
  gap: clamp(var(--space-sm), 2vw, var(--space-xl));
}

.nav a {
  display: inline-flex;
  min-height: 2.75rem;
  align-items: center;
  border-bottom: 1px solid transparent;
  color: var(--ink-700);
  font-size: var(--type-label);
  font-variation-settings: 'wght' var(--type-weight-say);
  letter-spacing: 0.08em;
  text-decoration: none;
  text-transform: uppercase;
  transition: color 180ms var(--ease-out), border-color 180ms var(--ease-out);
}

.nav a:hover,
.nav a[aria-current='page'] {
  border-bottom-color: var(--brand-orange);
  color: var(--ink-950);
}

.nav-toggle {
  display: none;
  min-width: 2.75rem;
  min-height: 2.75rem;
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--ink-300);
  border-radius: 0.25rem;
  background: transparent;
  cursor: pointer;
  font-size: var(--type-label);
  font-variation-settings: 'wght' var(--type-weight-say);
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

/* Actions */
.cta {
  display: inline-flex;
  min-width: 2.75rem;
  min-height: 2.75rem;
  align-items: center;
  justify-content: center;
  padding: 0.8rem 1.3rem;
  border: 1px solid var(--brand-orange);
  border-radius: 0.25rem;
  background: var(--brand-orange);
  color: var(--ink-950);
  cursor: pointer;
  font-size: var(--type-label);
  font-variation-settings: 'wght' var(--type-weight-say);
  letter-spacing: 0.08em;
  line-height: 1.2;
  text-align: center;
  text-decoration: none;
  text-transform: uppercase;
  transition:
    background-color 180ms var(--ease-out),
    border-color 180ms var(--ease-out),
    color 180ms var(--ease-out),
    transform 180ms var(--ease-out);
}

.cta:hover {
  border-color: var(--brand-orange-deep);
  background: var(--brand-orange-deep);
  color: var(--paper-0);
  transform: translateY(-0.125rem);
}

.cta-ghost {
  border-color: var(--ink-300);
  background: transparent;
  color: var(--ink-950);
}

.cta-ghost:hover {
  border-color: var(--ink-950);
  background: var(--ink-950);
  color: var(--paper-0);
  /* Dark ground: ornament inverts, or it is 1.61:1 and gone. */
  --ornament: var(--brand-blue-soft);
}

[data-journey-chapter='assurance'] .cta-ghost {
  border-color: var(--paper-100);
  background: transparent;
  color: var(--paper-0);
}

[data-journey-chapter='assurance'] .cta-ghost:hover,
[data-journey-chapter='assurance'] .cta-ghost:focus-visible {
  border-color: var(--paper-0);
  background: var(--paper-0);
  color: var(--ink-950);
}

.cta-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
}

/* Filters, specifications, service and content surfaces */
.filters {
  display: flex;
  margin-block: var(--space-2xl);
  flex-wrap: wrap;
  gap: var(--space-sm);
}

.filters button {
  min-width: 2.75rem;
  min-height: 2.75rem;
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--ink-300);
  border-radius: 0.25rem;
  background: transparent;
  cursor: pointer;
  font-size: var(--type-label);
  font-variation-settings: 'wght' var(--type-weight-say);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  transition:
    background-color 180ms var(--ease-out),
    border-color 180ms var(--ease-out),
    color 180ms var(--ease-out);
}

.filters button:hover {
  border-color: var(--ink-950);
}

.filters button[aria-pressed='true'] {
  border-color: var(--ink-950);
  background: var(--ink-950);
  color: var(--paper-0);
  /* Dark ground: ornament inverts, or it is 1.61:1 and gone. */
  --ornament: var(--brand-blue-soft);
}

.specs {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--type-body);
  table-layout: fixed;
}

.specs th,
.specs td {
  padding-block: var(--space-md);
  border-bottom: 1px solid var(--ink-100);
  text-align: left;
  vertical-align: baseline;
}

.specs th {
  width: 42%;
  padding-inline-end: var(--space-lg);
  color: var(--ink-500);
  font-size: var(--type-label);
  font-variation-settings: 'wght' var(--type-weight-read);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.specs td {
  font-variation-settings: 'wght' var(--type-weight-say);
}

.note {
  max-width: var(--type-measure);
  padding: var(--space-lg);
  border: 1px solid var(--brand-warning);
  background: color-mix(in oklab, var(--brand-warning) 9%, var(--paper-0));
  color: var(--ink-700);
}

.centre-list {
  list-style: none;
}

.centre-list li {
  display: grid;
  grid-template-columns: minmax(6rem, 0.4fr) minmax(0, 1fr);
  gap: var(--space-lg);
  padding-block: var(--space-lg);
  border-bottom: 1px solid currentColor;
}

.centre-list .city {
  color: var(--ornament);
  font-size: var(--type-label);
  font-variation-settings: 'wght' var(--type-weight-say);
  letter-spacing: var(--type-track-label);
  text-transform: uppercase;
}

.centre-list .addr {
  color: var(--ink-700);
}

.faq {
  list-style: none;
}

.faq > li {
  border-bottom: 1px solid var(--ink-100);
}

.faq summary {
  min-height: 2.75rem;
  padding-block: var(--space-lg);
  cursor: pointer;
  list-style: none;
  font-size: var(--type-lead);
  font-variation-settings: 'wght' var(--type-weight-say);
}

.faq summary::-webkit-details-marker {
  display: none;
}

.faq summary::after {
  content: '+';
  float: right;
  margin-inline-start: var(--space-lg);
  color: var(--ornament);
}

.faq details[open] summary::after {
  content: '−';
}

.faq details > div {
  max-width: var(--type-measure);
  padding-bottom: var(--space-xl);
  color: var(--ink-700);
}

.viewer {
  position: relative;
  display: grid;
  width: 100%;
  min-height: 5rem;
  padding: var(--space-sm) var(--space-md);
  place-items: center;
  overflow: hidden;
  border: 1px solid var(--ink-100);
  background: var(--paper-50);
  cursor: pointer;
  transition:
    border-color 180ms var(--ease-out),
    background-color 180ms var(--ease-out);
}

.viewer::after {
  content: '\2192';
  position: absolute;
  right: var(--space-md);
  color: var(--ornament);
  font-size: var(--type-lead);
  line-height: 1;
}

.viewer:hover,
.viewer:focus-visible {
  border-color: var(--brand-orange-deep);
  background: var(--paper-100);
}

.viewer:is(
  [data-viewer-state='loading'],
  [data-viewer-state='running'],
  [data-viewer-state='still']
) {
  min-height: 0;
  padding: 0;
  aspect-ratio: 4 / 3;
  cursor: default;
}

.viewer:is(
  [data-viewer-state='loading'],
  [data-viewer-state='running'],
  [data-viewer-state='still']
)::after {
  content: none;
}

.viewer canvas {
  width: 100%;
  height: 100%;
}

.viewer-hint {
  max-width: calc(100% - 3rem);
  padding: var(--space-sm);
  color: var(--ink-500);
  font-size: var(--type-label);
  font-variation-settings: 'wght' var(--type-weight-say);
  letter-spacing: var(--type-track-label);
  text-align: center;
  text-transform: uppercase;
}

/* Footer.

   LIGHT ON PURPOSE (operator call, 2026-08-12). Every page ends on the navy `.route-close` band,
   and this footer used to be near-black (`--paper-900`) directly beneath it: two dark masses with
   little between them, so a page finished as one long dark slab and the closing CTA — the whole
   point of the band — stopped reading as the final statement.

   The band keeps its navy and stays the terminal note; the footer drops to paper and becomes what
   it actually is, quiet utility chrome. That is the separation, so no divider rule is needed.
   Every colour below flipped with it — see the h4, link, `.foot-base` and `.tagline` rules. */
footer.foot {
  position: relative;
  z-index: var(--z-content);
  padding-block: var(--space-3xl) var(--space-2xl);
  background: var(--paper-50);
  color: var(--ink-950);
}

.foot-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 13rem), 1fr));
  gap: var(--space-2xl);
}

footer.foot h4 {
  margin-bottom: var(--space-lg);
  color: var(--ink-700);
  font-size: var(--type-label);
  font-variation-settings: 'wght' var(--type-weight-say);
  letter-spacing: var(--type-track-label);
  text-transform: uppercase;
}

footer.foot ul {
  list-style: none;
}

footer.foot li + li {
  margin-top: var(--space-xs);
}

footer.foot a:not(.cta) {
  display: inline-flex;
  width: fit-content;
  min-width: 2.75rem;
  min-height: 2.75rem;
  margin-inline: -0.4rem;
  padding-inline: 0.4rem;
  align-items: center;
  justify-content: center;
  color: var(--ink-950);
  text-decoration: none;
  transition: color 180ms var(--ease-out);
}

footer.foot a:not(.cta):hover {
  color: var(--brand-orange-deep);
}

main ul.foot {
  padding: 0;
  background: transparent;
  color: var(--ink-950);
  list-style: none;
}

main ul.foot li {
  border-top: 1px solid var(--ink-300);
}

main ul.foot a {
  display: flex;
  min-height: 2.75rem;
  align-items: center;
  padding-block: var(--space-sm);
  color: var(--ink-950);
  font-variation-settings: 'wght' var(--type-weight-say);
  text-decoration-color: var(--brand-orange);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.25em;
}

.foot-base {
  display: flex;
  margin-top: var(--space-2xl);
  padding-top: var(--space-lg);
  flex-wrap: wrap;
  gap: var(--space-sm) var(--space-xl);
  border-top: 1px solid var(--ink-100);
  color: var(--ink-700);
  font-size: var(--type-label);
}

/* Used once, in `.foot-base`. It followed the footer from dark to light, so the plain orange
   (fine on near-black) became a weak tint on paper; the deep tone is the system's own answer. */
.tagline {
  color: var(--brand-orange-deep);
  font-variation-settings: 'wght' var(--type-weight-say);
  letter-spacing: var(--type-track-label);
}

/* ─────────────────────────────────────────────────────────────────────────────
   INSIDE THE PLANT + the certification record (About).

   An asymmetric grid, not a card wall: the lead frame spans two columns and two rows so the
   photograph that carries the story reads as the primary image and the rest fall in around it. Equal
   tiles would be the identical-card-grid failure rule.md Experience-law 6 bans, and would also be
   a lie about which of these frames matters.
   ───────────────────────────────────────────────────────────────────────────── */
/* THREE COLUMNS, AND THE SIX FRAMES FILL THEM EXACTLY.
   `auto-fit` was wrong here: it repacked the row by available width, so the lead frame left a hole
   beside it and the captions landed at four different heights — the ragged arrangement the operator
   called out. Six is not an accident, it is the number that closes this grid: the lead takes two
   columns AND two rows, two frames stack in the third column beside it, and the last three make the
   bottom row. Nothing is left dangling. */
.plant-grid {
  display: grid;
  margin-top: var(--space-2xl);
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-lg);
}

/* THE ASPECT RATIO SIZES THE IMAGE. NOTHING ELSE DOES.
   This was `grid-template-rows: 1fr auto` with `height: 100%` AND `aspect-ratio` on the image, to
   make captions align across a row. Chrome tolerated the contradiction; WEBKIT DID NOT — it
   resolved the image to a box taller than its 1fr track and PRINTED THE PHOTOGRAPH OVER ITS OWN
   CAPTION on every frame. Caught only by opening a real Safari screenshot: the numeric probe was
   perfectly happy (7/7 decoded, zero overflow, zero console errors), because overlap is not
   overflow and a decoded image says nothing about where it landed.
   A row of images that share one ratio already line up; asking the grid to stretch them as well was
   both redundant and the bug. */
.plant-shot {
  display: grid;
  margin: 0;
  gap: var(--space-sm);
  grid-template-rows: auto auto;
  align-content: start;
}

.plant-shot img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  background: var(--paper-100);
}

/* The lead frame earns its size: it is the Nigerian team inside the plant, and it is the only
   frame in the set with people in it. */
.plant-shot--lead {
  grid-row: span 2;
  grid-column: span 2;
}

/* The lead spans two rows, so it gets a ratio that fills them rather than `auto` — `auto` let the
   image size itself and left the second row short. */
.plant-shot--lead img {
  aspect-ratio: 41 / 33;
}

.plant-shot figcaption {
  max-width: 46ch;
  color: var(--ink-700);
}

/* The certificate names read as a fact row, not as chips: no borders, no pills. */
.cert-names {
  display: flex;
  margin-top: var(--space-md);
  flex-wrap: wrap;
  gap: var(--space-xs) var(--space-lg);
  color: var(--ink-700);
  font-size: var(--type-label);
  font-variation-settings: 'wght' var(--type-weight-say);
  letter-spacing: var(--type-track-label);
  list-style: none;
  text-transform: uppercase;
}

/* The sheet is a REFERENCE, not the page.
   At 46rem wide this portrait collage stood 1329px tall and swallowed the whole section — a wall of
   unreadable thumbnails that dominated everything around it. The twelve NAMES above already carry
   the fact; the sheet is there so a reader can see the documents exist. So it sits in the side
   column at a size that reads as evidence, and anyone who wants to inspect it opens it full size. */
.cert-sheet {
  margin: var(--space-xl) 0 0;
}

.cert-sheet img {
  display: block;
  width: 100%;
  max-width: 26rem;
  height: auto;
  border: 1px solid var(--ink-100);
}

.cert-sheet figcaption {
  margin-top: var(--space-sm);
  color: var(--ink-700);
}

/* A three-column grid is a WIDE-SCREEN composition. Held at three on a phone it produced 103px
   thumbnails — six stamps in a row, unreadable, and the captions longer than the pictures. Two
   columns in the tablet band, one on a phone, and the lead frame stops spanning what no longer
   exists. Measured at 390 / 768 / 1024 / 1440 after the change. */
@media (max-width: 63.9375rem) {
  .plant-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (max-width: 47.9375rem) {
  .plant-grid {
    grid-template-columns: minmax(0, 1fr);
  }

  .plant-shot--lead {
    grid-row: auto;
    grid-column: auto;
  }

  .plant-shot--lead img {
    aspect-ratio: 4 / 3;
  }
}

/* Access and focus */
:where(a, button, summary, [tabindex]):focus-visible {
  outline: 0.1875rem solid var(--brand-orange);
  outline-offset: 0.25rem;
}

[data-journey-chapter='contact'] :where(a, button, summary, [tabindex]):focus-visible {
  outline-color: var(--ink-950);
}

.skip {
  position: fixed;
  z-index: var(--z-skip);
  top: var(--space-sm);
  left: var(--space-sm);
  min-height: 2.75rem;
  padding: var(--space-sm) var(--space-md);
  background: var(--ink-950);
  color: var(--paper-0);
  text-decoration: none;
  transform: translateY(calc(-100% - var(--space-xl)));
  transition: transform 180ms var(--ease-out);
}

.skip:focus,
.skip:focus-visible {
  transform: translateY(0);
  transition: none;
}

.vh {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  overflow: hidden;
  border: 0;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

/* ROUTE INTRO — adopted from the older Haisic app, which the operator reads as cleaner.
   Every one of its five inner pages opens the same way: ONE centred, narrow title band
   (`max-w-3xl mx-auto`, `text-center`), then full-width content bands. Not a two-column hero with
   a panel floating beside it — that shape put two unequal objects on one row, gave the page an
   asymmetric top edge, and is why these read rough while the old ones read composed.
   The proof panel keeps its content and becomes the first full-width band under the intro. */
@media (min-width: 56.25rem) {
  .route-hero-grid {
    grid-template-columns: minmax(0, 1fr);
    justify-items: center;
  }

  .route-hero-copy {
    max-width: 46rem;
    align-items: center;
    text-align: center;
  }

  .route-hero-copy .t-head,
  .route-hero-copy .t-lead {
    margin-inline: auto;
  }

  .route-hero-copy .cta-row {
    justify-content: center;
  }

  .route-proof {
    width: 100%;
  }
}

/* Wide composition */
@media (min-width: 56.25rem) {
  .split {
    grid-template-columns: minmax(0, 1.08fr) minmax(0, 0.92fr);
    align-items: start;
  }

  .tiles {
    grid-template-columns: 1.25fr 0.9fr 0.72fr;
    align-items: start;
  }

  .tile:nth-child(2) {
    margin-top: var(--space-2xl);
  }

  .tile:nth-child(3) {
    margin-top: var(--space-3xl);
  }

  .load-scenarios {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .about-pillars {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    align-items: start;
  }

  .about-pillar:nth-child(2) {
    margin-top: 0;
  }

  .about-pillar:nth-child(3) {
    margin-top: 0;
  }

  .about-pillar:nth-child(n + 3) {
    margin-top: var(--space-xl);
  }

  main[data-home-journey] > section[data-journey-chapter] {
    grid-template-columns: minmax(0, 1fr);
  }

  [data-journey-chapter] .wrap,
  [data-journey-chapter] > [data-chapter-copy] {
    display: grid;
    grid-template-columns: repeat(12, minmax(0, 1fr));
    column-gap: var(--space-lg);
  }

  [data-journey-chapter] .wrap > [data-chapter-copy],
  [data-journey-chapter] > [data-chapter-copy] {
    display: flex;
  }

  [data-journey-chapter='reliability'] [data-chapter-copy],
  [data-journey-chapter='battery'] [data-chapter-copy] {
    grid-column: 1 / span 6;
  }

  /* The hero line "Just steady power." must hold ONE line. Six of twelve columns left it 632px —
     short by a third — so text-wrap balance split it and orphaned "power." on a line of its own. */
  [data-journey-chapter='hero'] [data-chapter-copy] {
    grid-column: 1 / span 7;
    grid-row: 1;
  }

  [data-journey-chapter='intelligence'] [data-chapter-copy],
  [data-journey-chapter='assurance'] [data-chapter-copy] {
    grid-column: 7 / -1;
  }

  [data-journey-chapter='contact'] [data-chapter-copy] {
    grid-column: 2 / span 6;
  }

  [data-journey-chapter='reliability'] .static-product,
  [data-journey-chapter='battery'] .static-product {
    grid-column: 7 / -1;
    grid-row: 1;
  }

  [data-journey-chapter='hero'] .static-product {
    grid-column: 8 / -1;
    grid-row: 1;
  }

  [data-journey-chapter='intelligence'] .static-product,
  [data-journey-chapter='assurance'] .static-product {
    grid-column: 1 / span 6;
    grid-row: 1;
    justify-self: start;
  }

  [data-journey-chapter='contact'] .static-product {
    grid-column: 9 / -1;
    grid-row: 1;
  }

  [data-journey-chapter='range'] [data-chapter-copy],
  [data-journey-chapter='range'] .range-rail,
  [data-journey-chapter='range'] > .wrap > .cta,
  [data-journey-chapter='range'] .range-actions {
    grid-column: 1 / -1;
  }

  [data-journey-chapter='range'] [data-chapter-copy] {
    grid-row: 1;
  }

  [data-journey-chapter='range'] .range-actions {
    grid-row: 2;
  }

  [data-journey-chapter='range'] .range-rail {
    grid-row: 3;
  }

  [data-journey-chapter='range'] [data-chapter-copy] {
    width: min(100%, 54rem);
  }

  /* Alternate two chapters as a deliberate left-product/right-copy beat. The fallback and live
     canvas therefore read as one composition; P4 alone owns the canvas's inline side translation. */
  :is(
    [data-journey-chapter='intelligence'],
    [data-journey-chapter='assurance']
  ) [data-chapter-copy] {
    justify-self: stretch;
  }

  /* A restrained physical floor gives transparent product media something to occupy. It is a
     solid plane plus neutral shadow—no invented scene, gradient, or glow—and mirrors the chapter side. */
  :is(
    [data-journey-chapter='hero'],
    [data-journey-chapter='reliability'],
    [data-journey-chapter='intelligence'],
    [data-journey-chapter='battery'],
    [data-journey-chapter='assurance'],
    [data-journey-chapter='contact']
  )::after {
    content: '';
    position: absolute;
    z-index: var(--z-base);
    right: 0;
    bottom: clamp(5rem, 11svh, 8rem);
    width: min(47vw, 50rem);
    height: clamp(4.5rem, 8vw, 7.5rem);
    border-top: 1px solid color-mix(in oklab, currentColor 24%, transparent);
    background: color-mix(in oklab, currentColor 7%, transparent);
    box-shadow: 0 2.25rem 4rem -2.75rem oklch(0.14 0.04 263 / 0.48);
    pointer-events: none;
    transform: perspective(24rem) rotateX(66deg);
    transform-origin: top center;
  }

  :is(
    [data-journey-chapter='intelligence'],
    [data-journey-chapter='assurance']
  )::after {
    right: auto;
    left: 0;
  }

  /* …AND IT YIELDS THE MOMENT THE REAL PRODUCT ARRIVES.
     The floor above exists for the STATIC packshots, which are flat cut-outs with nothing under
     them. The live stage draws its own contact shadow at the product's real footprint — and this
     pseudo-element cannot know where that is, because the footprint moves with every chapter's
     pose. Left on, it sat at a fixed height and sliced a hard perspective slab straight through
     the rendered product's body: two floors at two different heights, one of them fake. Hidden
     the same way the packshots are, by observed stage state. */
  main[data-home-journey]:has(#product-stage.is-live)
    > section[data-journey-chapter]::after {
    opacity: 0;
  }

  .static-product[data-static-product] img {
    filter: drop-shadow(0 1.75rem 1.5rem oklch(0.14 0.04 263 / 0.28));
  }

  [data-journey-chapter='range'] {
    padding-block: clamp(4.5rem, 7vw, 7rem);
  }

  [data-journey-chapter='range'] .range-actions {
    margin-top: var(--space-lg);
  }

  [data-journey-chapter='range'] .range-rail {
    margin-block: var(--space-xl) 0;
    padding-block: var(--space-lg) var(--space-2xl);
  }
}

@media (min-width: 56.25rem) and (prefers-reduced-motion: no-preference) {
  :is(
    [data-journey-chapter='hero'],
    [data-journey-chapter='reliability'],
    [data-journey-chapter='intelligence'],
    [data-journey-chapter='battery'],
    [data-journey-chapter='assurance'],
    [data-journey-chapter='contact']
  )[data-journey-phase='arrive']::after,
  :is(
    [data-journey-chapter='hero'],
    [data-journey-chapter='reliability'],
    [data-journey-chapter='intelligence'],
    [data-journey-chapter='battery'],
    [data-journey-chapter='assurance'],
    [data-journey-chapter='contact']
  )[data-journey-phase='reveal']::after {
    opacity: clamp(0.55, var(--chapter-reveal, 1), 1);
  }

  :is(
    [data-journey-chapter='hero'],
    [data-journey-chapter='reliability'],
    [data-journey-chapter='intelligence'],
    [data-journey-chapter='battery'],
    [data-journey-chapter='assurance'],
    [data-journey-chapter='contact']
  )[data-journey-phase='depart']::after {
    opacity: clamp(0, calc(1 - var(--chapter-departure, 0)), 1);
  }
}

/* Wide live-stage closure. :has ties CSS to observed P3/P4 state without hiding the
   first-paint fallback.
   EVERY poster yields, not just the active chapter's. Hiding one left the other five on screen
   beside the live 3D: a photographed cabinet sitting in a hard white rectangle (the packshots are
   not cut out) next to the same brand's rendered model, two products claiming one viewport.
   Range keeps its real card imagery and makes the still-mounted shared stage fully visual-null. */
@media (prefers-reduced-motion: no-preference) {
  main[data-home-journey]:has(#product-stage.is-live)
    section[data-journey-chapter]
    .static-product {
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
  }

  main[data-home-journey]:has(
    section[data-journey-chapter='range'][data-journey-active]
  ) #product-stage.is-live {
    opacity: 0;
  }
}

/* Loaded portrait media never gets a vote on chapter height. The explicit figure box
   remains flagship-scale and useful as fallback, but its children are size-contained. */
@media (min-width: 67.5625rem) {
  /* SIZE COMES FROM THE LADDER, not from a literal. This rule used to hard-code
     `clamp(3rem, 4.2vw, 3.75rem)`, which capped the home hero at 60px on a 1440px screen and made
     `--type-hero` dead type on the widest breakpoint — the token said 72px and the page rendered
     60px. That is how the scale drifted out of order in the first place: the ladder is declared in
     one place and quietly overruled in another. Only `line-height` is tightened here now, which is
     a per-context decision the scale has no opinion about. */
  .chapter-hero .t-hero {
    font-size: var(--type-hero);
    line-height: 0.98;
  }

  .static-product[data-static-product] {
    block-size: clamp(28rem, 58svh, 37rem);
    max-block-size: calc(100svh - var(--mast-h) - var(--space-2xl));
  }

  .static-product-hero[data-static-product] {
    block-size: clamp(30rem, 60svh, 38rem);
  }

  .static-product[data-static-product] img,
  .static-product[data-static-product] picture {
    min-block-size: 0;
    max-block-size: 100%;
  }
}

/* Tablet and phone */
@media (max-width: 67.5rem) {
  /* The stage used to be display:none below 1080 px — the CSS half of the "no 3D on phones"
     decision, alongside the JS gate in stage.js. Both are lifted (operator ruling 2026-08-10):
     the phone gets the real product, not a fallback photograph. Geometry for the narrow stage
     band is set further down. */

  [data-static-product] {
    justify-self: center;
  }

  /* A long model token must never leave the partial "HS-" fragment seen at tablet width. */
  [data-journey-chapter='contact'] .static-product figcaption {
    align-items: flex-start;
    flex-direction: column;
    gap: var(--space-xs);
  }

  [data-journey-chapter='contact'] .static-product figcaption span:last-child {
    display: block;
    width: 100%;
    max-width: 100%;
    color: var(--ornament);
    line-height: 1.35;
    overflow-wrap: anywhere;
    text-align: left;
    white-space: normal;
  }
}

@media (min-width: 56.25rem) and (max-width: 67.5rem) {
  [data-journey-chapter='hero'],
  [data-journey-chapter='contact'] {
    min-height: calc(100svh - var(--mast-h));
    padding-block: var(--space-2xl);
  }

  :is(
    [data-journey-chapter='hero'],
    [data-journey-chapter='contact']
  ) [data-chapter-copy] {
    gap: var(--space-md);
  }

  /* Same rule as the wide breakpoint: the ladder sizes these, not a literal. Both of these
     clamps predated the scale and disagreed with it — the hero one topped out ABOVE the hero
     token and the contact one below it, so the same two headings changed rank as the window
     crossed 1081px. */
  .chapter-hero .t-hero {
    font-size: var(--type-hero);
  }

  [data-journey-chapter='contact'] .t-head {
    font-size: var(--type-display);
  }

  :is(
    [data-journey-chapter='hero'],
    [data-journey-chapter='contact']
  ) .static-product[data-static-product] {
    block-size: min(54svh, 31rem);
  }
}

@media (max-width: 55rem) {
  :root {
    --mast-h: 4rem;
    --space-chapter: clamp(4.5rem, 14vw, 7rem);
  }

  .nav-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }

  .nav {
    position: absolute;
    z-index: var(--z-nav);
    top: 100%;
    right: 0;
    left: 0;
    display: none;
    padding: var(--space-sm) var(--space-edge) var(--space-lg);
    border-bottom: 1px solid var(--ink-100);
    background: var(--paper-0);
  }

  .mast[data-open='true'] .nav {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
  }

  .nav a {
    width: 100%;
    padding-block: var(--space-sm);
    border-bottom: 1px solid var(--ink-100);
  }

  main[data-home-journey] > section[data-journey-chapter] {
    min-height: auto;
  }

  .route-hero-grid {
    grid-template-columns: minmax(0, 1fr);
  }

  .route-proof {
    min-height: 0;
    padding: var(--space-xl);
  }

  [data-journey-chapter='hero'] {
    min-height: calc(100svh - var(--mast-h));
  }

  [data-static-product] {
    min-height: 17rem;
  }

  /* P1's cue figure is deliberately pulled into the copy field on small screens. It remains
     first-viewport evidence while its absolute box contributes no poster-sized dead interval. */
  :is(
    [data-journey-chapter='reliability'],
    [data-journey-chapter='intelligence'],
    [data-journey-chapter='battery']
  ) .chapter-grid {
    position: relative;
    align-items: start;
  }

  :is(
    [data-journey-chapter='reliability'],
    [data-journey-chapter='intelligence'],
    [data-journey-chapter='battery']
  ) [data-chapter-copy] {
    width: 100%;
    padding-top: clamp(9rem, 35vw, 11rem);
  }

  .static-product.static-product-cue[data-static-product] {
    position: absolute;
    z-index: 1;
    top: 0;
    right: var(--space-edge);
    width: clamp(8rem, 32vw, 10.5rem);
    min-height: 0;
    block-size: clamp(8rem, 32vw, 10.5rem);
    max-block-size: none;
    aspect-ratio: 1;
    contain: size layout paint;
    opacity: 1 !important;
    visibility: visible !important;
    transform: none !important;
    box-shadow: none;
  }

  .static-product-cue img {
    padding: var(--space-xs) var(--space-xs) 2.25rem;
  }

  .static-product-cue figcaption {
    right: var(--space-xs);
    bottom: var(--space-xs);
    left: var(--space-xs);
    display: block;
    padding-top: var(--space-xs);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .static-product-cue figcaption span:first-child {
    display: none;
  }

  .static-product-cue figcaption span:last-child {
    display: block;
    overflow: hidden;
    color: var(--ornament);
    text-align: left;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .grid .card:nth-child(n) {
    margin-top: 0;
  }
}

@media (max-width: 30rem) {
  /* The hero/display token overrides that used to live here (`min(15vw, 4.5rem)` and
     `min(12vw, 3.75rem)`) are GONE. They produced a 58px hero on a 390px screen, which is exactly
     why every display role then had to override them back down with its own clamp — the tokens
     said one thing and the page rendered another. The phone ladder is now declared once, in the
     ≤47.9375rem block, and covers this range too. */
  :root {
    --space-edge: 1rem;
  }

  .t-head,
  .hero-line {
    overflow-wrap: normal;
    word-break: normal;
    hyphens: none;
  }

  .band,
  main[data-home-journey] > section[data-journey-chapter] {
    padding-block: var(--space-3xl);
  }

  .cta-row {
    align-items: stretch;
    flex-direction: column;
  }

  .cta-row .cta {
    width: 100%;
  }

  /* The P6 text-zoom oracle doubles the root size at 320px. Match the specificity of the authored
     four-pillar composition so it genuinely reflows to one readable column instead of two 96px tracks. */
  .about-pillars:has(.about-pillar:nth-child(4)) {
    grid-template-columns: minmax(0, 1fr);
  }

  .about-pillar p {
    overflow-wrap: anywhere;
  }

  /* Preserve the About proof as real enlarged text: reduce its inset, return the lead to the lead
     scale, and permit emergency character wrapping rather than clipping or shrinking the root zoom. */
  .route-hero-about .route-proof {
    padding: var(--space-md);
  }

  .route-hero-about .route-proof .t-lead {
    max-width: 100%;
    font-size: var(--type-lead);
    overflow-wrap: anywhere;
  }

  footer.foot a:not(.cta) {
    max-width: 100%;
    margin-inline: 0;
    overflow-wrap: anywhere;
  }

  .range-actions {
    margin-top: 0;
    align-items: stretch;
    flex-direction: column;
  }

  .range-actions .cta {
    width: 100%;
  }

  /* One full card plus a deliberate sliver of the next: the rail signals horizontal touch
     without widening the page itself. Padding belongs to the scroller, not the viewport. */
  .range-rail {
    width: calc(100% + (2 * var(--space-edge)));
    margin-inline: calc(-1 * var(--space-edge));
    padding-inline: var(--space-edge);
    grid-auto-columns: calc(100% - 1.5rem);
    gap: var(--space-xs);
  }

  /* At narrow width—and especially the 200% text-zoom oracle—the range decision facts must stack
     instead of squeezing two factual columns. The next-card sliver and arrow cue remain intact. */
  .range-card-decision {
    min-height: 0;
    padding: var(--space-md);
  }

  .range-card-decision__facts {
    grid-template-columns: minmax(0, 1fr);
    align-items: start;
    gap: var(--space-sm);
  }

  .range-card-decision__value {
    font-size: min(2.75rem, 14vw);
    overflow-wrap: normal;
    word-break: normal;
    white-space: nowrap;
  }

  .range-card-decision__model,
  .range-card-decision__family,
  .static-product figcaption {
    font-size: min(var(--type-label), 4vw);
  }

  .range-card-decision__model,
  .static-product figcaption span:last-child {
    overflow-wrap: break-word;
    word-break: normal;
  }

  [data-journey-chapter='range'] [data-chapter-copy] {
    gap: var(--space-md);
  }

  :is(
    [data-journey-chapter='assurance'],
    [data-journey-chapter='contact']
  ) .static-product[data-static-product] {
    grid-template-rows: minmax(0, 1fr) auto;
    align-content: stretch;
  }

  :is(
    [data-journey-chapter='assurance'],
    [data-journey-chapter='contact']
  ) .static-product[data-static-product] > img {
    min-height: 0;
    grid-row: 1;
  }

  :is(
    [data-journey-chapter='assurance'],
    [data-journey-chapter='contact']
  ) .static-product figcaption {
    position: static;
    width: 100%;
    grid-row: 2;
    align-self: end;
  }

  .card[data-range-card] .card-body {
    min-height: 4rem;
  }

  .centre-list li {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-xs);
  }

  .specs th,
  .specs td {
    display: block;
    width: 100%;
  }

  .specs th {
    padding-bottom: 0;
    border-bottom: 0;
  }

  .specs td {
    padding-top: var(--space-2xs);
    overflow-wrap: anywhere;
  }

  .known-facts > div {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-xs);
  }

  .known-facts dd,
  .known-facts .est,
  .known-facts .src {
    max-width: 100%;
    overflow-wrap: anywhere;
    white-space: normal;
  }

  .known-facts .est {
    margin-inline-start: 0.25em;
  }

  .solution-path {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-sm);
  }

  .solution-path::before {
    grid-row: auto;
    margin-bottom: var(--space-md);
  }

  .solution-path .t-label,
  .solution-path .t-sub,
  .solution-path p {
    grid-column: 1;
  }

  .card {
    min-height: 22rem;
  }

  .card-shot {
    height: 15rem;
  }

  [data-product-tilt],
  .card {
    --depth-rest-x: 0.75deg;
    --depth-rest-y: -1deg;
  }
}

/* Explicit no-WebGL closure. Static product identity remains the visual owner. */
html.no-webgl #product-stage,
html.no-webgl #stage,
html.no-webgl .product-stage-label,
.no-webgl #product-stage,
.no-webgl #stage,
.no-webgl .product-stage-label,
[data-webgl='false'] #product-stage,
[data-webgl='false'] #stage,
[data-webgl='false'] .product-stage-label,
[data-webgl='unavailable'] #product-stage,
[data-webgl='unavailable'] #stage,
[data-webgl='unavailable'] .product-stage-label {
  display: none;
}

html.no-webgl [data-static-product],
.no-webgl [data-static-product],
[data-webgl='false'] [data-static-product],
[data-webgl='unavailable'] [data-static-product] {
  opacity: 1;
  visibility: visible;
}

/* Motion never carries meaning. The still state keeps hierarchy, product names, and actions. */
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  *,
  *::before,
  *::after {
    animation-delay: 0s !important;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    scroll-behavior: auto !important;
    transition-delay: 0s !important;
    transition-duration: 0.01ms !important;
  }

  #product-stage,
  #stage,
  .product-stage-label {
    display: none !important;
  }

  main[data-home-journey]::before {
    display: none;
  }

  [data-depth],
  .depth,
  .hero-line,
  .card,
  [data-product-tilt],
  .card-shot,
  .card-body {
    transform: none !important;
  }

  [data-static-product] {
    opacity: 1 !important;
    visibility: visible !important;
  }
}

@media (forced-colors: active) {
  .cta,
  .filters button[aria-pressed='true'] {
    border: 1px solid ButtonText;
  }

  :where(a, button, summary, [tabindex]):focus-visible {
    outline-color: Highlight;
  }
}

/* ─────────────────────────────────────────────────────────────────────────────
   MACRO TEARDOWN — the product route's scroll sequence.

   A TALL track with a STICKY stage. The height is what the reader scrubs: too short and the six
   beats fire in a flick, too tall and the page feels padded. The stage is pinned inside it, so
   the unit stays put while the written steps travel past it.

   The steps are legible with no WebGL and no JavaScript — the active state only adds emphasis,
   it never supplies the meaning.
   ───────────────────────────────────────────────────────────────────────────── */

.teardown {
  position: relative;
  padding-block: var(--space-2xl) 0;
  background: var(--paper-0);
}

.teardown-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-2xl);
}

.teardown-rail > .t-sub {
  margin-top: var(--space-xs);
}

.teardown-rail > .t-lead {
  margin-top: var(--space-md);
}

.teardown-steps {
  list-style: none;
  margin: var(--space-2xl) 0 0;
  padding: 0;
}

.teardown-step {
  display: grid;
  padding-block: var(--space-lg);
  grid-template-columns: 2.75rem minmax(0, 1fr);
  gap: var(--space-xs) var(--space-md);
  border-top: 1px solid var(--ink-100);
  opacity: 0.45;
  transition: opacity 320ms var(--ease-out), border-color 320ms var(--ease-out);
}

.teardown-step[data-teardown-active='true'] {
  border-top-color: var(--brand-orange);
  opacity: 1;
}

.teardown-step__index {
  color: var(--ornament);
  font-size: var(--type-label);
  font-variation-settings: 'wght' var(--type-weight-say);
  letter-spacing: var(--type-track-label);
}

.teardown-step__title {
  grid-column: 2;
  margin: 0;
  font-size: var(--type-lead);
  font-variation-settings: 'wght' var(--type-weight-say);
  line-height: 1.15;
}

.teardown-step__body {
  grid-column: 2;
  margin: 0;
  max-width: 46ch;
  color: var(--ink-700);
}

/* SQUARE AT EVERY WIDTH. Camera framing depends on the canvas aspect, and the start-frame poster
   is captured at one aspect — a 4:5 stage on phones would have shown a still that does not line up
   with the 3D that replaces it. */
.teardown-stage {
  aspect-ratio: 1 / 1;
  min-height: 0;
  background: var(--paper-bay);
}

.teardown-readout {
  position: absolute;
  right: var(--space-md);
  bottom: var(--space-md);
  left: var(--space-md);
  color: var(--ink-500);
  font-size: var(--type-label);
  font-variation-settings: 'wght' var(--type-weight-say);
  letter-spacing: var(--type-track-label);
  text-align: left;
  text-transform: uppercase;
}

/* Wide: the stage pins beside the steps and the track becomes the scrub length. */
@media (min-width: 56.25rem) {
  .teardown {
    padding-block: var(--space-chapter) 0;
  }

  .teardown-grid {
    grid-template-columns: minmax(0, 5fr) minmax(0, 6fr);
    align-items: start;
    gap: var(--space-3xl);
  }

  .teardown-head {
    grid-column: 1;
    grid-row: 1;
  }

  .teardown-steps {
    grid-column: 1;
    grid-row: 2;
  }

  .teardown-stage-col {
    position: sticky;
    top: calc(var(--mast-h) + var(--space-xl));
    grid-column: 2;
    grid-row: 1 / span 2;
  }

  .teardown-stage {
    aspect-ratio: 1 / 1;
    max-height: calc(100svh - var(--mast-h) - var(--space-3xl));
  }

  /* The scrub length. Every step gets roughly a viewport of travel. */
  .teardown-steps {
    margin-block: var(--space-2xl) 60svh;
  }

  .teardown-step {
    min-height: 42svh;
    align-content: start;
  }
}

/* Motion off: the unit is shown assembled and still, and every step reads at full strength. */
@media (prefers-reduced-motion: reduce) {
  .teardown-step {
    opacity: 1;
  }
}

/* ─────────────────────────────────────────────────────────────────────────────
   THE CLOSING BAND. Mined from the 2026-05 Haisic app, whose every page ends on one — which is
   why its pages feel finished and ours felt like they ran out. Full-bleed, centred, in the brand
   navy so it reads as the page's floor rather than one more white section.
   ───────────────────────────────────────────────────────────────────────────── */

/* No margin-top: the band changes colour, which IS the separation. The margin was a second,
   redundant gap in front of it. */
.route-close {
  margin-top: 0;
  padding-block: clamp(3rem, 5vw, 5rem);
  background: var(--brand-blue-deep);
  color: var(--paper-0);
  --ornament: var(--brand-blue-soft);
}

.route-close-inner {
  display: flex;
  max-width: 46rem;
  flex-direction: column;
  align-items: center;
  gap: var(--space-md);
  text-align: center;
}

.route-close__title {
  max-width: 22ch;
  margin: 0;
}

.route-close__lead {
  margin: 0;
  color: var(--ink-100);
}

.route-close__actions {
  justify-content: center;
  margin-top: var(--space-sm);
}

.route-close .t-note {
  color: color-mix(in oklab, var(--paper-0) 62%, transparent);
}

.route-close .cta-ghost {
  border-color: color-mix(in oklab, var(--paper-0) 45%, transparent);
  color: var(--paper-0);
}

.route-close .cta-ghost:hover,
.route-close .cta-ghost:focus-visible {
  border-color: var(--paper-0);
  background: color-mix(in oklab, var(--paper-0) 12%, transparent);
}

/* The primary action keeps the brand's action colour on the dark ground. */
.route-close .cta:not(.cta-ghost) {
  border-color: var(--brand-orange);
  background: var(--brand-orange);
  color: var(--ink-950);
}

/* ─────────────────────────────────────────────────────────────────────────────
   BAND INTERIORS — one grid rhythm, mined from the 2026-05 app.

   Every band on every one of its pages is the same shape: a heading, then an EVEN grid of
   same-shaped blocks — `md:grid-cols-2`, `-3`, `-4`. Never a bespoke layout per section.
   Ours were five different one-off shapes (pillars, solution paths, load scenarios, FAQ), which
   is precisely the "rough around the edges" read: each band re-invented its own geometry, so the
   page had no rhythm to settle into.

   Declared last so it wins over the earlier single-column defaults without raising specificity.
   ───────────────────────────────────────────────────────────────────────────── */

@media (min-width: 56.25rem) {
  .about-pillars,
  .solution-paths {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    align-items: start;
    gap: var(--space-2xl) var(--space-xl);
  }

  .load-scenarios,
  .faq {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    align-items: start;
    /* Row gap tightened: these blocks are short, and a 5rem row gap left a band that was mostly
       the space between two four-item lists. */
    gap: var(--space-xl) var(--space-2xl);
  }

  /* The stagger that made the tiles look scattered belongs to the home page, not to a band grid. */
  .solution-path,
  .about-pillar {
    margin-top: 0;
  }
}

/* A block inside a band grid is a CARD TITLE, not a headline. In the mined app these are h3-scale;
   ours inherited `.t-sub` display type, so "Customer-Centric Approach" nearly filled its column
   and four equal blocks read as four competing headlines. */
@media (min-width: 56.25rem) {
  .about-pillars .t-sub,
  .solution-paths .t-sub,
  .load-scenarios h3 {
    font-size: clamp(1.25rem, 1.05rem + 0.5vw, 1.5rem);
    letter-spacing: -0.02em;
    line-height: 1.2;
  }
}

/* ── Solution detail routes ────────────────────────────────────────────────── */

.solution-path__more {
  display: inline-flex;
  margin-top: var(--space-md);
  align-items: center;
  gap: var(--space-xs);
  color: var(--ornament);
  font-size: var(--type-label);
  font-variation-settings: 'wght' var(--type-weight-say);
  letter-spacing: var(--type-track-label);
  text-transform: uppercase;
}

/* A quieter alternate band, so a page reads white -> tinted -> white like the mined app. */
.band-alt {
  background: var(--paper-50);
}

.solution-features,
.solution-pair {
  display: grid;
  margin-top: var(--space-2xl);
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-xl);
}

.solution-feature {
  display: grid;
  padding-top: var(--space-md);
  gap: var(--space-xs);
  border-top: 1px solid var(--ink-300);
}

.solution-feature__index {
  color: var(--ornament);
  font-size: var(--type-label);
  font-variation-settings: 'wght' var(--type-weight-say);
  letter-spacing: var(--type-track-label);
}

.solution-feature__title {
  margin: 0;
  font-size: var(--type-lead);
  font-variation-settings: 'wght' var(--type-weight-say);
  line-height: 1.2;
}

.solution-feature p {
  margin: 0;
  color: var(--ink-700);
}

.solution-pair-card {
  display: grid;
  padding: var(--space-lg);
  gap: var(--space-2xs);
  border: 1px solid var(--ink-100);
  background: var(--paper-0);
  color: var(--ink-950);
  text-decoration: none;
  transition: border-color 200ms var(--ease-out), box-shadow 200ms var(--ease-out);
}

.solution-pair-card:hover,
.solution-pair-card:focus-visible {
  border-color: var(--brand-orange);
  box-shadow: var(--depth-shadow-sm);
}

.solution-pair-card__shot {
  display: grid;
  height: clamp(11rem, 14vw, 14rem);
  min-height: 0;
  margin: 0 0 var(--space-md);
  padding: var(--space-md);
  grid-template-rows: minmax(0, 1fr);
  place-items: center;
  overflow: hidden;
  border: 1px solid var(--ink-100);
  background: var(--paper-bay);
}

.solution-pair-card__shot img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

@media (min-width: 56.25rem) {
  .solution-features {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    align-items: start;
    gap: var(--space-2xl) var(--space-xl);
  }

  .solution-pair {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-2xl);
  }
}

/* The caveat sits UNDER its pairing, not against it. Without the clear margin the note tucked up
   beside the left card and read as belonging to that product rather than to the pair. */
.solution-pair + .note {
  margin-top: var(--space-2xl);
  max-width: 52ch;
}

/* ── The teardown's start-frame still ──────────────────────────────────────────
   Shown until the live stage draws. It is the SAME frame the 3D opens on — captured from the
   viewer itself — so the swap is invisible rather than a pop. No fade: a cross-fade between two
   identical images is just a flicker. */

.teardown-poster {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  pointer-events: none;
}

/* The moment the canvas has drawn its first frame, the still steps aside. */
.teardown-stage[data-stage-live='true'] .teardown-poster {
  visibility: hidden;
}

/* With a poster present the invitation is a quiet caption, not a placeholder sitting on nothing. */
.teardown-stage:has(.teardown-poster) .viewer-hint {
  position: absolute;
  right: var(--space-md);
  bottom: var(--space-md);
  left: var(--space-md);
  color: var(--ink-500);
  text-align: center;
}

/* ═════════════════════════════════════════════════════════════════════════════
   MOBILE. Measured at 390x844 before writing any of this: the home page was
   13,383 px tall (sixteen screens), a single product card ran ~850 px, and 38 text elements sat
   under 13 px. Nothing was broken — no overflow, no clipping — it was simply the DESKTOP layout
   poured into one column, keeping desktop type sizes, desktop band heights and desktop card
   proportions. On a phone that reads as endless scrolling past oversized furniture.

   This block sizes the phone on its own terms rather than inheriting the wide layout's.
   ═════════════════════════════════════════════════════════════════════════════ */

@media (max-width: 47.9375rem) {
  /* THE PHONE GETS ITS OWN LADDER — as a LADDER, not as four separate opinions.
     A phone headline competes with nothing, so it does not need to shout at 56px; that part was
     always right. What was wrong is HOW it was done: each display role carried its own hand-tuned
     clamp on the COMPONENT (.t-hero, .t-head, .t-sub, .t-lead), so nothing held them in proportion
     to each other. Measured, the rendered phone ladder was
        320px: body 16 · lead 17 · sub 22 · head 28 · hero 34
     which is 1.06x from body to lead — the lead was indistinguishable from body text — and 1.21x
     from head to hero. Two flat steps on every phone width, which is the looseness the operator
     kept seeing on mobile and never had a name for.

     `assertTypeScale` could not catch it: it reads the :root TOKENS, and these were component
     overrides that beat the tokens. The gate has since been extended to check every declaration
     set, and a rendered-ladder probe now measures the real page.

     So the roles stop redefining size and the TOKENS carry the phone too. Sizes barely move; the
     PROPORTIONS become real: 320px -> 20.5 / 25.6 / 32 / 40.3, every step >= 1.25x. */
  :root {
    --type-lead: clamp(1.25rem, 1.1rem + 0.9vw, 1.375rem);
    --type-title: clamp(1.5625rem, 1.3rem + 1.5vw, 1.875rem);
    --type-display: clamp(1.96rem, 1.6rem + 2vw, 2.375rem);
    --type-hero: clamp(2.5rem, 2rem + 2.6vw, 3rem);
  }

  .chapter-hero .t-hero {
    line-height: 1.02;
  }

  .t-head {
    max-width: 18ch;
  }

  .t-lead {
    line-height: 1.5;
  }

  /* 12 px labels are a desktop luxury read at arm's length; on a phone they are just small. */
  .t-label,
  .card-model,
  .t-note {
    font-size: 0.8125rem;
  }

  /* Chapters are CONTENT-sized on a phone. A 40–64 rem floor per chapter is what turned the home
     page into sixteen screens; there is no wide-screen composition here for that height to serve. */
  main[data-home-journey] > section[data-journey-chapter] {
    min-height: 0;
    padding-block: var(--space-2xl);
  }

  [data-journey-chapter='hero'] {
    min-height: 0;
  }

  main:not([data-home-journey]) > section,
  main:not([data-home-journey]) > section:first-child,
  .route-hero,
  .route-close {
    padding-block: var(--space-2xl);
  }

  /* Product cards: the bay was a 4:3 slab at full column width, so one card cost most of a screen.
     A wider, shorter bay shows the same product and lets two cards share the fold. */
  .card-shot {
    height: clamp(9rem, 42vw, 12rem);
    padding: var(--space-md);
  }

  .card-body {
    gap: 0;
    padding: var(--space-md);
  }

  .card-figure,
  .range-card-decision__value {
    font-size: 1.375rem;
  }

  .card-name,
  .range-card-decision__name {
    font-size: 1.0625rem;
  }

  .card-link-cue {
    top: var(--space-md);
    right: var(--space-md);
  }

  .grid,
  .range-rail {
    gap: var(--space-md);
  }

  /* Filters wrapped to two rows of tall pills and ate 320 px before a single product appeared. */
  .filters button {
    padding: 0.5rem 0.875rem;
    font-size: 0.75rem;
  }

  /* The teardown: the stage keeps its square, the steps stop reserving wide-screen scrub length. */
  .teardown-steps {
    margin-block: var(--space-xl) 0;
  }

  .teardown-step {
    min-height: 0;
    padding-block: var(--space-md);
  }

  .solution-features,
  .solution-pair,
  .load-scenarios,
  .about-pillars {
    gap: var(--space-lg);
  }
}

/* Two-up on the phone. Measured: the range chapter alone was 4,722 px of a 12,248 px home page —
   39% of it — because eleven cards each took a full column width. A product card does not need
   390 px to show a cabinet, a name and one figure. The footer's link columns are the same story. */
@media (max-width: 47.9375rem) {
  .grid,
  .range-rail {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-sm);
  }

  /* TIGHTER TWO-UP CARDS (operator, 2026-08-12: "the grids of thumbnails has too much space, and
     too big fonts and it could have been more concise and organized"). The card was already
     size-reduced here, but it still read loose: the decision block carried a full --space-md rule
     of air above it, the bay took a quarter of the viewport width, and the figure sat only one
     step under the product name so the two competed instead of ranking. Air comes out of the
     seams, not out of the type: the name stays the card's voice and only the supporting parts
     step down. */
  .card-shot,
  .card-shot[data-range-media] > img {
    height: clamp(5.75rem, 22vw, 8rem);
    padding: var(--space-xs);
  }

  .card-body,
  .range-card-decision {
    padding: var(--space-sm);
  }

  .card-decision {
    padding-top: var(--space-xs);
  }

  /* On a ~170px two-up card the label and the figure cannot share a baseline: measured, the pair
     overflowed and clipped "30,720 Wh" to "30,720 WI". The figure and its meter carry the fact at
     this size, so the word ENERGY goes VISUALLY hidden — not `display: none`, because it is the
     accessible name of the number and must stay in the tree for a screen reader. */
  .card-body .card-decision .t-label {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  .card-body .card-decision {
    grid-template-columns: minmax(0, 1fr);
  }

  /* The model code and family fit one line at this size once the gap stops fighting them. */
  .card-meta {
    gap: 0 var(--space-2xs);
  }

  /* The longest model codes still wrap the pair onto two lines at this width, and a separator
     that begins a line reads as a bullet, not as a join. It only earns its place inline. */
  .card-family::before {
    content: none;
  }

  .card-figure,
  .range-card-decision__value {
    font-size: 1.0625rem;
  }

  .card-name,
  .range-card-decision__name {
    font-size: 0.9375rem;
    line-height: 1.2;
  }

  .card-model,
  .range-card-decision__action {
    font-size: 0.6875rem;
  }


  /* The whole-card arrow overlaps a two-up card's own title; the caption already says "View details". */
  .card-link-cue {
    display: none;
  }

  /* TWO COLUMNS THAT PACK, rather than a grid that aligns rows or a single tall stack.

     History, because both earlier attempts were wrong in opposite directions. It began as
     `repeat(2, minmax(0, 1fr))`: the four groups are wildly unequal — Products carries 8 links,
     Company 2, Support 3, WhatsApp 1 plus a CTA — and a GRID ROW IS AS TALL AS ITS TALLEST CELL,
     so Company sat in a two-item box beside an eight-item list and opened a void six links deep.
     I then collapsed it to ONE column, which closed the void but made the footer a very long
     stack (operator: "the products list ... should not be listed like that, could have been
     grided side by side"). Both fixes treated the symptom.

     The actual cause is row alignment, so the cure is to stop laying out in rows: multi-column
     FLOWS the groups and fills both columns to their natural heights, so unequal lists pack with
     no void. `break-inside: avoid` keeps a group whole.

     The WhatsApp block spans both columns, because it is the one part that genuinely needs the
     full measure — in a ~165px half-column the number broke mid-number ("+234 916 625 / 5381")
     and the CTA wrapped to three lines. */
  .foot-grid,
  .footer-grid {
    display: block;
    columns: 2;
    column-gap: var(--space-xl);
  }

  .foot-grid > *,
  .footer-grid > * {
    margin-bottom: var(--space-2xl);
    break-inside: avoid;
  }

  .foot-grid > *:last-child,
  .footer-grid > *:last-child {
    margin-bottom: 0;
    column-span: all;
  }

  /* A phone number is one token; a line break inside it reads as two broken numbers. */
  footer.foot a:not(.cta) {
    text-wrap: nowrap;
  }
}

/* Cards size to their content on a phone. `margin-top: auto` on the figure aligns it to the card
   floor, which is right when a row of cards must share a baseline — but on a two-up phone grid it
   opened ~200 px of white between the product name and its capacity. */
@media (max-width: 47.9375rem) {
  .grid,
  .range-rail {
    align-items: start;
  }

  .card-decision {
    margin-top: var(--space-sm);
    padding-top: var(--space-sm);
  }

  .card-figure {
    margin-top: 0;
  }

  .range-card-decision__action {
    margin-top: var(--space-sm);
  }
}

/* The legacy phone block pins `.card { min-height: 22rem }` — 352 px, which on a two-up grid is
   taller than the content and left every card trailing white below its capacity meter. */
@media (max-width: 47.9375rem) {
  .card {
    min-height: 0;
  }
}

/* THE TEARDOWN ON A PHONE.
   Measured at 390x844 before this: the stage was NOT sticky below 900 px and sat after the steps
   in the document, so at track progress 0 it was 1,156 px off screen and 0% visible. The 3D did
   not start until progress 0.86 — by which point the sequence is at "back together". A reader on
   a phone scrolled the entire teardown and saw text, then a model, and never the teardown.
   The stage pins under the mast and the steps scroll beneath it, which is the whole point. */
@media (max-width: 47.9375rem) {
  .teardown-grid {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-md);
  }

  .teardown-stage-col {
    position: sticky;
    /* Pinned flush to the mast, and its own padding carries the background across the gap —
       otherwise step text shows through the strip between the mast and the stage. */
    top: var(--mast-h);
    z-index: 1;
    padding-block: var(--space-xs);
    background: var(--paper-0);
  }

  /* Step text scrolls UNDER the pinned stage, which is how sticky works — but a sentence sheared
     off mid-line at a hard edge reads as a rendering fault. This dissolves it instead. */
  .teardown-stage-col::after {
    content: '';
    position: absolute;
    z-index: 1;
    top: 100%;
    right: 0;
    left: 0;
    height: var(--space-2xl);
    background: linear-gradient(to bottom, var(--paper-0) 12%, transparent);
    pointer-events: none;
  }

  .teardown-stage {
    max-height: 46svh;
  }

  /* Each step gets real scrub length, so the six beats are not crossed in one flick. */
  .teardown-step {
    min-height: 22svh;
    align-content: start;
  }

  .teardown-steps {
    margin-block: var(--space-md) 12svh;
  }
}

/* ── The home journey on a phone: no scattered fallback photographs ────────────
   The home 3D is gated off below 960 px, so every chapter fell back to its static product PNG.
   With no wide two-column composition to sit in, those became what they looked like: arbitrary
   photographs at arbitrary sizes, cropped by their own figure box, with the caption rule printed
   straight across the product and the model code colliding with it.

   The home page does not need six decorative product shots on a phone — it has the complete
   eleven-product grid further down, which is the real product presence. So the chapters carry
   their argument in copy, and exactly ONE product leads the page: the hero's, properly contained
   with its caption UNDER it rather than over it. */
@media (max-width: 59.9375rem) {
  /* Scoped to a LIVE stage (html[data-stage-live], set by stage.js from a painted frame). These
     stills are the whole fallback: hiding them unconditionally meant any phone that could not run
     the 3D — old iOS, reduced motion, no WebGL — got voids where the product should be. */
  html[data-stage-live] main[data-home-journey] > section[data-journey-chapter]:not([data-journey-chapter='hero'])
    .static-product {
    display: none;
  }

  /* EVERY chapter still, not just the hero's. When the stage cannot run, these six ARE the phone's
     product presence, so they all get the treatment the hero already proved: full width, contained
     (never cropped), caption as a line of text underneath. Measured on WebKit — while only the
     hero was styled, the other five kept the desktop overlay caption and printed their rule and
     model code straight across the product, which is the defect in the operator's screenshots. */
  main[data-home-journey] .static-product[data-static-product] {
    /* Back into NORMAL FLOW. The cue variant is pinned `position: absolute; top: 0; right: edge`
       as a small decorative badge beside a live stage — which, with no stage running, is exactly
       the clipped top-right pane with its caption struck through the product that the operator
       photographed. Un-pinning it is the fix; the reset must be explicit or `position` is
       inherited from that rule even though this selector outranks it. */
    position: relative;
    z-index: auto;
    top: auto;
    right: auto;
    width: 100%;
    max-width: none;
    /* Contain, do not crop: the figure box was shorter than the cabinet and cut it top and bottom. */
    block-size: auto;
    min-block-size: 0;
    max-block-size: 40svh;
    aspect-ratio: auto;
    contain: none;
    justify-self: stretch;
    margin-block: var(--space-md) 0;
  }

  main[data-home-journey] .static-product[data-static-product] img {
    height: 34svh;
    object-fit: contain;
  }

  [data-journey-chapter='hero'] .static-product[data-static-product] img {
    height: 40svh;
  }

  /* The caption sits under the product, as a line of text — not a rule drawn over its feet. */
  main[data-home-journey] .static-product figcaption {
    position: static;
    display: flex;
    margin-top: var(--space-sm);
    padding-top: var(--space-sm);
    flex-wrap: wrap;
    gap: var(--space-2xs) var(--space-sm);
    border-top: 1px solid var(--ink-100);
    font-size: 0.6875rem;
  }

  main[data-home-journey] .static-product figcaption span:last-child {
    text-align: left;
  }

  /* The cue variant hides its name on desktop because the pane is decorative there. In fallback it
     is the only label the product has, so it comes back. */
  main[data-home-journey] .static-product-cue figcaption span:first-child {
    display: inline;
  }
}

/* ── THE HOME STAGE ON A PHONE ─────────────────────────────────────────────────
   The 3D runs here now (operator ruling), so there are no fallback photographs to place badly.
   A phone has no room for a side pane, so the stage becomes a BAND pinned under the mast and each
   chapter's copy scrolls beneath it — the same shape the teardown already uses, which works.
   The chapter-clip still applies, so the product still sinks at each boundary. */
@media (max-width: 59.9375rem) {
  #product-stage {
    /* WHERE THE BAND SITS. motion.js fits it into the gap the non-journey blocks (the expo
       banner above the journey, the stand showcase inside it) leave free, and writes both edges.
       With neither on the page the vars are absent and this collapses to the original
       "pinned under the mast, 42svh tall", unchanged. */
    inset: var(--stage-band-y, var(--mast-h)) 0 auto 0;
    width: 100%;
    height: var(--stage-band-h, 42svh);

    /* The chapter clip measures from the BAND's own edges, not the mast's. Left against the mast
       it clipped a band that no longer starts there — a product sliding out of a window that is
       not where the window is. */
    clip-path: inset(
      max(0px, calc(var(--stage-chapter-top, -100svh) - var(--stage-band-y, var(--mast-h))))
      0
      max(0px, calc(
        var(--stage-band-y, var(--mast-h)) + var(--stage-band-h, 42svh)
        - var(--stage-chapter-bottom, 200svh)
      ))
      0
    );
    /* OPAQUE, and it has to be. The band is pinned under the mast while the chapter's copy scrolls
       up through it, and the chapter's padding-top only clears it for the first screenful — so
       past that, headings and body slid under a TRANSPARENT canvas and showed through the product.
       Painted in the active chapter's own colour (written by motion.js from the chapter element,
       never restated here) the copy passes cleanly behind it, like any sticky media panel. The
       colour matches the chapter, so the "sink in" handover at each boundary is unchanged.

       THE LAST 14% FADES (operator, 2026-08-12: "the cut off from 3d to the next sections cuts off
       not well"). Solid to the edge, the band guillotined whatever line happened to be crossing it
       — at ~800px it sliced a chapter heading through the middle of the glyphs, which reads as a
       broken page rather than as copy passing under a panel. The fade is on the BACKDROP only:
       the canvas above it still draws the full product, and the clip-path that produces the
       sink-in/rise-out handover is untouched. */
    background: linear-gradient(
      to bottom,
      var(--stage-chapter-bg, transparent) 0 86%,
      transparent 100%
    );
  }

  /* A band squeezed to a letterbox by the blocks around it shows nothing worth seeing, so it
     yields entirely rather than printing a sliver of product. */
  :root[data-stage-band-fits='false'] #product-stage {
    opacity: 0;
  }

  /* Copy clears the pinned band instead of running under it — but ONLY when a band is actually
     pinned there. Unscoped, this reserved 42svh of emptiness at the top of every chapter on any
     phone whose stage never started: the dead gaps in the operator's screenshots. */
  html[data-stage-live] main[data-home-journey] > section[data-journey-chapter] {
    padding-top: calc(42svh + var(--space-md));
  }

  /* NO stage caption on a phone (operator ruling 2026-08-10). The rule, the product name, the
     dimension line and the model code are desktop furniture: at this width they stack into four
     lines of chrome directly under the product, and the page already names the product in the
     copy beneath. The caption stays on PC, where it has the room to read as a caption. */
  .product-stage-label {
    display: none;
  }

  .product-stage-label__model {
    grid-row: auto;
    grid-column: 1;
    text-align: left;
  }

  /* With the stage live the hero's own photograph is redundant; the closure above hides the rest.
     Without a live stage it is the ONLY product on the page above the range grid, so it stays —
     contained, with its caption under it, exactly as the block above already styles it. */
  html[data-stage-live] [data-journey-chapter='hero'] .static-product {
    display: none;
  }

  /* No depth defocus on a phone. The blur reads as focus pull when copy travels through a focal
     plane beside the product; in a single column under a pinned band the hero simply sits far from
     focus and stays blurred, so the first thing a reader meets is unreadable. */
  [data-depth],
  .depth {
    filter: none;
  }
}


/* ── TEMPORARY EXPO BANNER (Nigeria Energy Exhibition, Oct 2026) — see EXPO_BANNER in build.mjs.
   Delete this block when the flag is retired. ─────────────────────────────────────────────── */
.expo-banner {
  background: #0b1220;
  border-bottom: 1px solid var(--ink-100);
}
.expo-banner picture { display: block; }
.expo-banner-art {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 2400 / 1028;
  object-fit: cover;
}
/* The phone crop is a different shape, so the reserved box has to be too — otherwise the aspect
   above letterboxes the narrower artwork and hands back the thin strip it was cut to avoid. */
@media (max-width: 47.9375rem) {
  .expo-banner-art { aspect-ratio: 1560 / 1028; }
}
.expo-banner-line {
  box-sizing: border-box;
  padding-inline: var(--space-edge);   /* the same edge gutter .wrap uses */
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem 1.25rem;
  align-items: baseline;
  margin: 0 auto;
  padding-block: 0.7rem 0.75rem;
  font-size: 0.82rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: color-mix(in oklab, var(--paper-0) 82%, transparent);
}
.expo-banner-line strong {
  color: var(--paper-0);
  font-weight: 700;
}
@media (max-width: 640px) {
  .expo-banner-line { font-size: 0.72rem; gap: 0.25rem 0.9rem; padding-block: 0.6rem; }
}

/* ═══════════════════════════════════════════════════════════════════════════════════════════
   STAND SHOWCASE — the exhibition stand, directly under the expo banner.
   TEMPORARY, tied to EXPO_BANNER in build.mjs. After the show both go together.

   It takes the banner's near-black ground (#0b1220) on purpose: the two read as one announcement
   block at the top of the page, and the renders themselves are lit against a dark hall, so a
   paper ground would have put a bright frame around a dark picture.
   ═══════════════════════════════════════════════════════════════════════════════════════════ */
.stand-showcase {
  position: relative;
  z-index: var(--z-content);
  padding-block: clamp(3rem, 6vw, 5.5rem);
  background: #0b1220;
  color: var(--paper-0);
}

.stand-showcase .t-label {
  color: var(--brand-orange);
}

.stand-showcase .t-sub {
  margin-top: var(--space-sm);
  color: var(--paper-0);
}

.stand-showcase .t-lead {
  margin-top: var(--space-md);
  max-width: 44rem;
  color: color-mix(in oklab, var(--paper-0) 76%, transparent);
}

.stand-head {
  display: grid;
  gap: var(--space-lg);
  align-items: end;
}

.stand-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.25rem;
  font-size: var(--type-label);
  font-variation-settings: 'wght' var(--type-weight-say);
  letter-spacing: var(--type-track-label);
  text-transform: uppercase;
  color: color-mix(in oklab, var(--paper-0) 62%, transparent);
}

.stand-meta span + span {
  position: relative;
  padding-left: 1.25rem;
}

.stand-meta span + span::before {
  content: '';
  position: absolute;
  left: 0;
  top: 50%;
  width: 0.375rem;
  height: 0.375rem;
  border-radius: 50%;
  background: var(--brand-orange);
  transform: translateY(-50%);
}

.stand-head-side .cta {
  margin-top: var(--space-md);
}

/* THE VIEWER.
   The poster and the canvas occupy the SAME box, stacked. The poster is a real 5K render of this
   stand, so the section is complete before a line of WebGL runs; the canvas fades in over it when
   a visitor asks for the live one, and fades back out if WebGL ever fails. Nothing reflows on the
   handover, because nothing moves. */
.stand-viewer {
  position: relative;
  margin-top: var(--space-xl);
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border: 1px solid color-mix(in oklab, var(--paper-0) 12%, transparent);
  background: #05060a;
}

.stand-poster,
.stand-canvas {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
}

.stand-poster {
  object-fit: cover;
}

.stand-canvas {
  opacity: 0;
  transition: opacity 420ms var(--ease-out);
}

[data-stand-state='live'] .stand-canvas {
  opacity: 1;
  cursor: grab;
}

[data-stand-state='live'] .stand-canvas:active {
  cursor: grabbing;
}

.stand-launch {
  position: absolute;
  left: 50%;
  bottom: clamp(1rem, 4%, 2rem);
  transform: translateX(-50%);
  padding: 0.85rem 1.5rem;
  border: 0;
  border-radius: 2px;
  background: var(--brand-orange);
  color: var(--paper-0);
  font: inherit;
  font-size: var(--type-label);
  font-variation-settings: 'wght' var(--type-weight-shout);
  letter-spacing: var(--type-track-label);
  text-transform: uppercase;
  cursor: pointer;
  transition: opacity 240ms var(--ease-out), background 160ms var(--ease-out);
}

.stand-launch:hover { background: var(--brand-orange-deep); }
.stand-launch:disabled { opacity: 0.55; cursor: progress; }
[data-stand-state='unsupported'] .stand-launch { display: none; }

/* The controls only exist once the live stand does. */
.stand-controls {
  position: absolute;
  left: clamp(0.75rem, 2%, 1.25rem);
  bottom: clamp(0.75rem, 2%, 1.25rem);
  right: clamp(0.75rem, 2%, 1.25rem);
  display: none;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
}

[data-stand-state='live'] .stand-controls { display: flex; }

.stand-chip {
  padding: 0.5rem 0.85rem;
  border: 1px solid color-mix(in oklab, var(--paper-0) 42%, transparent);
  border-radius: 2px;
  background: color-mix(in oklab, #05060a 55%, transparent);
  color: var(--paper-0);
  font: inherit;
  font-size: var(--type-label);
  font-variation-settings: 'wght' var(--type-weight-say);
  letter-spacing: var(--type-track-label);
  text-transform: uppercase;
  cursor: pointer;
  backdrop-filter: blur(8px);
  transition: background 160ms var(--ease-out), border-color 160ms var(--ease-out);
}

.stand-chip:hover { border-color: var(--paper-0); }

.stand-chip[aria-pressed='true'] {
  background: var(--brand-blue);
  border-color: var(--brand-blue);
}

.stand-controls-sep {
  width: 1px;
  height: 1.5rem;
  background: color-mix(in oklab, var(--paper-0) 26%, transparent);
}

.stand-note {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem 1.25rem;
  margin-top: var(--space-md);
  font-size: var(--type-label);
  color: color-mix(in oklab, var(--paper-0) 66%, transparent);
}

.stand-note-fine {
  color: color-mix(in oklab, var(--paper-0) 44%, transparent);
}

@media (max-width: 47.9375rem) {
  .stand-viewer { aspect-ratio: 4 / 3; }
  /* On a phone the preset row would cover the stand it is meant to steer. Modes stay; the camera
     presets give way to the drag, which is the better instrument on a touch screen anyway. */
  .stand-chip:not(.stand-chip-mode),
  .stand-controls-sep { display: none; }
}

@media (min-width: 60rem) {
  .stand-head {
    grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
  }

  .stand-head-side {
    justify-self: end;
    text-align: right;
  }

  .stand-meta {
    justify-content: flex-end;
  }
}
