@import url("fonts.css");

/* ==========================================================================
   Vektro Energy — design system

   Two pillars, two axes, two surfaces:
     · Engineering is measured in DEPTH   → vertical axis, blue,  light surface
     · Logistics   is measured in TRANSIT → horizontal axis, lime, ink surface

   Above grade is light. Below grade is ink. Crossing EL 0.00 inverts the page.
   That is not decoration: lime is unusable on light (1.7:1) and excellent on
   ink (10.4:1), so the colour lives exactly where the work happens.
   ========================================================================== */

/* --- tokens --------------------------------------------------------------- */

:root {
  /* surfaces.
     The page is plain white: the supplied photography fades to #ffffff at its
     own edges, so anything tinted would show a seam where an image meets the
     page. Cards carry the contrast instead — ink panels floating on white. */
  --base: #ffffff;          /* above grade */
  --base-rgb: 255, 255, 255; /* same value, for gradient scrims */
  --base-sunk: #f2f5f4;     /* recessed panel on light */
  --card: #fafbfa;          /* card fill: just off white, so the panel reads as
                               a panel without becoming a second surface */
  --ink: #0c1a26;           /* below grade + logistics */
  --ink-raised: #162b38;    /* raised panel on ink */
  --ink-lift: #1c3341;      /* a card sitting on an ink section */

  /* text */
  --fg: #0c1a26;
  --muted: #2f3a43;         /* 11.6:1 on --base. Secondary text is only a step
                               back from the body colour, not a grey wash: at
                               the old 6:1 the descriptors read as disabled. */
  --fg-ink: #eef2f1;
  --muted-ink: #b6c3cb;     /* 9.8:1 on --ink, 7.3:1 on a card over ink */

  /* rules */
  --line: #d6dcda;
  --line-ink: rgba(233, 240, 238, 0.16);

  /* brand.
     --blue is 3.86:1 on --base, so it is a FILL/RULE/OUTLINE token only: never
     `color:` on a light surface. --blue-deep carries every blue text role.
     This mirrors the lime rule exactly — each brand colour is confined to the
     surface where it actually passes. check.py enforces both. */
  --blue: #0082c2;          /* engineering. borders, fills, focus rings */
  --blue-deep: #005c8a;     /* engineering. all blue text on light (6.9:1) */
  --lime: #bdd136;          /* logistics. text on ink only (10.4:1) */
  --lime-line: #a8bd2a;     /* lime as a 1px rule on light. Never text: it is
                               2.1:1 there. A hairline is not text, so this is
                               the one place the brand lime works above grade. */
  --lime-line-ink: rgba(189, 209, 54, 0.45);
  --lime-wash: #f4f7e4;     /* lime at ~8% over white, for icon tiles */

  /* type */
  --f-display: "Archivo", system-ui, -apple-system, sans-serif;
  --f-ui: "Roboto", system-ui, -apple-system, sans-serif;
  --f-body: "Lato", system-ui, -apple-system, sans-serif;
  --f-mono: "Roboto Mono", ui-monospace, "Cascadia Mono", monospace;

  /* measure + rhythm */
  --gutter: clamp(1.25rem, 4vw, 4rem);
  --maxw: 1360px;
  --measure: 68ch;

  /* card geometry */
  --r-card: 28px;
  --r-tile: 16px;

  --s1: 0.5rem;
  --s2: 0.875rem;
  --s3: 1.25rem;
  --s4: 1.75rem;
  --s5: 2.5rem;
  --s6: 3.5rem;
  /* The top of the scale was tuned for a page with less in it. --s7 sets the gap
     before every sub-heading inside a section (fourteen of them) and --s8 the
     padding on the section itself, so between two sections it was counted twice:
     224px of nothing at a 1440px window. Both come down about a quarter. The
     lower steps are untouched — the problem was never the space inside a block,
     it was the space between them. */
  --s7: 4rem;
  --s8: 5.25rem;

  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
}

/* --- reset ---------------------------------------------------------------- */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* Anchored chapters must clear both sticky bars: the 72px header and the
     ~44px chapter rail below it. */
  scroll-padding-top: 8rem;
}

body {
  margin: 0;
  background: var(--base);
  color: var(--fg);
  font-family: var(--f-body);
  font-size: 1.0625rem;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

/* `overflow-x: hidden` forces the used value of `overflow-y` to `auto`, which
   makes body a scroll container. `position: sticky` then resolves against body
   instead of the viewport, so the header, the chapter rail and the hero stage
   all stop sticking — worst on iOS, where it reads as the header tearing loose
   halfway down a scroll. `clip` clips the one axis without creating a scroll
   container; the declaration above stays as the fallback for anything without
   it. */
@supports (overflow: clip) {
  body { overflow-x: clip; }
}

/* No word may ever be broken: not hyphenated, not split across lines.
   `hyphens: none` overrides any locale-driven hyphenation the browser might
   apply from the lang attribute, and normal wrapping keeps long technical terms
   ("flexocompresión", "dimensionamiento") whole. Applied to everything so a
   future component cannot opt back in by accident. */
*,
*::before,
*::after {
  hyphens: none;
  -webkit-hyphens: none;
  overflow-wrap: normal;
  word-break: normal;
}

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

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

/* --- typography ----------------------------------------------------------- */

/* Headings are ranged left, and body copy stays justified.

   They used to be justified too, on the theory that one alignment across the
   page reads as one system. In practice the two do not behave alike: at 17px a
   line holds sixty-odd characters and the slack from justifying is spread thin
   enough to be invisible, but at 48px it holds eight or nine words and the same
   slack lands in a handful of spaces. "Servicios de ingeniería y suministro"
   came out with a hand's width between words.

   Once they are ragged, `text-wrap: balance` becomes the right call rather than
   the wrong one: it evens out how much each line carries, and with no
   justification there is no stretch for it to make worse. */
h1, h2, h3, h4 {
  font-family: var(--f-display);
  font-weight: 700;
  margin: 0;
  text-align: left;
  text-wrap: balance;
}

.h-display {
  font-size: clamp(2.5rem, 6.4vw, 5.25rem);
  line-height: 0.95;
  letter-spacing: -0.032em;
}

.h-section {
  font-size: clamp(1.85rem, 3.9vw, 3rem);
  line-height: 1.06;
  letter-spacing: -0.022em;
}

.h-sub {
  font-size: clamp(1.2rem, 1.9vw, 1.5rem);
  line-height: 1.2;
  letter-spacing: -0.014em;
  font-weight: 600;
}

/* Clamped from 1.2rem, not 1.09rem: below that the intro was setting smaller
   than the prose it introduces on a narrow window, which inverts the hierarchy
   at exactly the size where it is hardest to read anyway. */
.lead {
  font-size: clamp(1.2rem, 1.6vw, 1.4rem);
  line-height: 1.55;
  color: var(--muted);
  max-width: 56ch;
  /* Headings carry `margin: 0`, so the space above a lead was whatever the
     browser's default <p> margin happened to be — one fixed em of the lead's
     own size. That is why it measured the same 20px under a 48px section title
     and under an 82px display title, where it reads as the intro crowding the
     headline. One step for text following a section title -- the same --s4 that
     already separates an h2 from a `.prose` block, so the same heading no longer
     opens two different gaps depending on what comes next -- and one step up
     after the display size, which is nearly twice as tall. */
  margin: var(--s4, 1.75rem) 0 0;
}

.h-display + .lead { margin-top: var(--s5, 2.5rem); }

/* Prose fills its container. `--measure` still applies inside a `.split`, where
   the grid already halves the width — but a full-width section with a 68ch
   column left most of the page empty, which read as a mistake rather than as
   restraint. The trade is longer lines; if they get hard to track, put
   `max-width: var(--measure)` back on `.prose` and the columns return.

   19px, up from the 17px inherited from body. Beside a full-bleed photograph
   the smaller setting read as a caption rather than as the copy, which is the
   reverse of the weight these paragraphs carry. `--measure` is in ch, so the
   column grows with the type and the line length is unchanged. */
.prose {
  max-width: none;
  font-size: 1.1875rem;
  line-height: 1.6;
}
/* `:where()` holds this at (0,1,0) so it clears the browser margin without also
   outweighing the flow rule beneath it. As `.prose p` it scored (0,1,1), beat
   `.prose > * + *` at (0,1,0) and silently zeroed the gap between every pair of
   paragraphs -- the rule below was written, applied to nothing, and consecutive
   paragraphs touched. Reset first, flow second: both now score the same and
   source order decides, so the first child keeps no margin and each one after it
   gets the step. */
.prose :where(p) { margin: 0; }
.prose > * + * { margin-top: var(--s4, 1.75rem); }
.split .prose { max-width: var(--measure); }

/* --- justification -------------------------------------------------------- */

/* Every run of prose is justified. A single-line block is unaffected: the last
   line of a justified block is always set flush left, and a one-line block is
   entirely last line — so short descriptors keep their natural spacing and only
   genuinely multi-line copy is aligned to both edges.
   `inter-word` forces the browser to absorb the slack in the word spaces rather
   than tracking out the letters, which would wreck the type colour. */
/* `.lead` is not in this list. It sets at up to 22px on a 56ch measure, which is
   close enough to display scale that justifying it opens the same gaps the
   headings had — and it sits directly under one, where the mismatch shows. */
.prose,
.prose p,
.pillar p,
.card p,
.disc__desc,
.stat__d,
.stage p,
.track__note,
.norm__d,
.subblock p,
.subblock li,
.spec td,
.form__note,
.site-foot__desc,
.field__hint {
  text-align: justify;
  text-justify: inter-word;
}

/* Deliberately left ranged-left: labels, data and controls are short fragments
   or single lines where justification either does nothing or looks broken. */
.mono,
.eyebrow,
.btn,
.nav__link,
.lang,
.card__k,
.subblock__k,
.disc__i,
.acc__i,
.stage__i,
.track__i,
.stat__v,
.norm__k,
.source,
.spec thead th,
.spec caption,
.field label,
.chapters a,
.site-foot h2,
.site-foot__bot {
  text-align: left;
  text-justify: auto;
}

/* Mono is load-bearing, not decorative: eyebrows, units, norm references,
   and sequence numbers. It is what makes an engineering site read as one. */
.mono {
  font-family: var(--f-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  font-weight: 400;
  font-variant-numeric: tabular-nums;
}

.eyebrow {
  font-family: var(--f-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--blue-deep);
  display: block;
  margin-bottom: var(--s3, 1.25rem);
}

.num {
  font-family: var(--f-mono);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}

a { color: var(--blue-deep); text-decoration-thickness: 1px; text-underline-offset: 0.2em; }
a:hover { color: var(--ink); }

/* --- layout --------------------------------------------------------------- */

.wrap {
  width: 100%;
  max-width: var(--maxw);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.section { padding-block: clamp(2.75rem, 5.5vw, var(--s8, 5.25rem)); position: relative; }
.section--tight { padding-block: clamp(2rem, 4vw, var(--s5, 2.5rem)); }
.section--flush-top { padding-top: 0; }

/* Zones. The surface inversion IS the grade crossing. */
.zone-ink {
  background: var(--ink);
  color: var(--fg-ink);
}
.zone-ink .eyebrow { color: var(--lime); }
.zone-ink .lead { color: var(--muted-ink); }
.zone-ink a { color: var(--lime); }
.zone-ink a:hover { color: #fff; }

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

.grid { display: grid; gap: var(--s4, 1.75rem); }
.grid--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
/* Two, not four: these hold real paragraphs (values, PV advantages), and a
   ~300px column cannot be justified without opening rivers. */
.grid--4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }

.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 200;
  background: var(--ink);
  color: var(--fg-ink);
  padding: var(--s2, 0.875rem) var(--s3, 1.25rem);
}
.skip:focus { left: var(--s2, 0.875rem); top: var(--s2, 0.875rem); }

/* --- header --------------------------------------------------------------- */

.site-head {
  position: sticky;
  top: 0;
  z-index: 100;
  background: rgba(255, 255, 255, 0.88);
  backdrop-filter: blur(14px) saturate(140%);
  border-bottom: 1px solid var(--line);
}

.site-head__in {
  display: flex;
  align-items: center;
  gap: var(--s4, 1.75rem);
  min-height: 72px;
}

.brand { display: flex; align-items: center; flex: 0 0 auto; }
/* 48px of file for 42px of wordmark: make_assets.py surrounds the mark with 7%
   of clear space on each side, and that margin is part of the image box, so the
   file is always about 14% taller than the logo you see. Sizing to 42 here would
   render the mark at 37.

   48 in a 72px bar leaves 12px of air above and below, which is the most the
   mark can take before it starts crowding the nav rather than anchoring it.
   Same arithmetic in .site-foot img, which stays smaller: in the footer the
   lockup is a sign-off, not the masthead. */
.brand img { height: 48px; width: auto; }

.nav {
  display: flex;
  align-items: center;
  gap: var(--s4, 1.75rem);
  margin-left: auto;
}

.nav__link {
  font-family: var(--f-ui);
  font-size: 0.9375rem;
  font-weight: 500;
  color: var(--fg);
  text-decoration: none;
  padding-block: 0.4rem;
  border-bottom: 2px solid transparent;
}
.nav__link:hover { color: var(--blue-deep); }
.nav__link[aria-current="page"] { border-bottom-color: var(--blue); color: var(--blue-deep); }

.lang {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  font-family: var(--f-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}
.lang a { color: var(--muted); text-decoration: none; }
.lang a:hover { color: var(--fg); }
.lang [aria-current="true"] { color: var(--fg); font-weight: 500; }
.lang__sep { color: var(--line); }

.nav-toggle {
  display: none;
  margin-left: auto;
  background: none;
  border: 1px solid var(--line);
  padding: 0.5rem 0.7rem;
  font-family: var(--f-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  cursor: pointer;
}

/* --- buttons -------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.65rem;
  font-family: var(--f-ui);
  font-size: 0.875rem;
  font-weight: 500;
  letter-spacing: 0.01em;
  text-decoration: none;
  padding: 0.85rem 1.35rem;
  border: 1px solid transparent;
  background: none;
  cursor: pointer;
  transition: background-color 0.18s var(--ease), color 0.18s var(--ease),
    border-color 0.18s var(--ease);
}

/* The chevron is the mark. It doubles as the action glyph. */
.btn__mark {
  width: 0.72em;
  flex: 0 0 auto;
  transition: transform 0.22s var(--ease);
}
.btn:hover .btn__mark { transform: translateX(3px); }

/* --blue-deep, not --blue: white on brand blue is only 4.2:1 and this label is
   14px. White on --blue-deep is 7.2:1. */
.btn--primary { background: var(--blue-deep); color: #fff; }
.btn--primary:hover { background: var(--ink); color: #fff; }

.btn--ghost { border-color: var(--line); color: var(--fg); }
.btn--ghost:hover { border-color: var(--fg); }

/* On ink, logistics owns the accent. */
.zone-ink .btn--primary { background: var(--lime); color: var(--ink); }
.zone-ink .btn--primary:hover { background: #cfe25a; color: var(--ink); }
.zone-ink .btn--ghost { border-color: var(--line-ink); color: var(--fg-ink); }
.zone-ink .btn--ghost:hover { border-color: var(--fg-ink); }

.btn-row { display: flex; flex-wrap: wrap; gap: var(--s2, 0.875rem); align-items: center; }

/* --- hero ----------------------------------------------------------------- */

.hero {
  position: relative;
  padding-block: clamp(3rem, 6.5vw, 5.5rem) clamp(2.25rem, 4.5vw, 3.75rem);
  overflow: hidden;
}

.hero__in { position: relative; }

/* --- scroll progress ------------------------------------------------------ */

/* One registered property drives both scroll pieces. The CSS animation walks it
   from 0 to 1 on the compositor; every derived value is a calc() off it. Where
   `animation-timeline` is missing, site.js writes the same number by hand and
   nothing else changes. Registering it is what makes it interpolate — an
   unregistered custom property would jump between keyframes, not tween. */
@property --p {
  syntax: "<number>";
  initial-value: 0;
  inherits: true;
}

@keyframes scroll-progress {
  from { --p: 0; }
  to { --p: 1; }
}

/* --- the expanding hero (Home) -------------------------------------------- */

/* The reference component pins the scroll: preventDefault on wheel and touch,
   scrollTo(0,0) on every event until the media finishes growing. That breaks
   the keyboard, the space bar, find-in-page and every screen reader. A tall
   section with a sticky stage produces the same picture — the scene holds still
   while the page scrolls past it — and leaves scrolling alone. */
.expand {
  position: relative;
  height: 260svh;
  view-timeline-name: --expand;
}

/* The window the photograph opens from. Landscape, not the portrait the
   reference used: both hero frames are 3:2 aerials and a tall window would crop
   away the horizon, which is the whole subject. 38rem x 24rem is 1.58:1 — near
   enough the native ratio that the cover crop takes almost nothing — and 1.8x
   the area of the frame it replaces. */
.expand__stage {
  --card-w: min(38rem, 72vw);
  --card-h: min(24rem, 44svh);

  position: sticky;
  top: 0;
  height: 100svh;
  overflow: hidden;
  display: grid;
  grid-template-rows: auto 1fr;
  align-items: center;
  justify-items: center;
  padding-block: clamp(1.5rem, 4vh, 3rem);
  background: var(--base);
}

@supports (animation-timeline: view()) {
  .expand__stage {
    animation-name: scroll-progress;
    animation-duration: auto;
    animation-timing-function: linear;
    animation-fill-mode: both;
    animation-timeline: --expand;
    /* `contain` is the span where the section fully covers the viewport, which
       for a 260svh section is exactly the 160svh of runway below the fold. */
    animation-range: contain 0% contain 100%;
  }
}

.expand__bg,
.expand__media {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
}

/* The backdrop pushes in slowly while the window opens over it. Two rates of
   movement in one frame is what reads as depth; a still backdrop behind a
   growing photograph reads as a photograph on a poster. */
.expand__bg {
  z-index: 0;
  object-fit: cover;
  transform: scale(calc(1 + 0.06 * var(--p)));
}

/* One vertical wash doing three jobs: dark enough at the top to carry white
   type over a sky whose brightness is not ours to control, nearly clear across
   the middle so the window and the mark sit on the photograph itself, and
   resolving to the page colour at the foot so the call to action lands on white
   and the hero has no seam against the section below it.

   It holds 0.62 flat to 32% and only then releases, rather than tapering from
   the top. This photograph brightens towards its horizon — the sky runs from
   L 0.47 at the top edge to L 0.87 around a third of the way down — so a wash
   that thinned with depth would have been at its weakest exactly where the
   headline needs it most. 0.62 is the measured floor for 4.5:1; check.py
   recomputes it against the actual pixels.

   Under the media rather than over it: the window shows the photograph at full
   strength, and once the window is open the wash is completely covered. */
.expand__stage::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(
    180deg,
    rgba(12, 26, 38, 0.66) 0%,
    rgba(12, 26, 38, 0.62) 32%,
    rgba(12, 26, 38, 0.1) 52%,
    rgba(255, 255, 255, 0.62) 86%,
    var(--base) 100%
  );
}

/* The media never changes size: it is full-bleed from the first frame and gets
   revealed by an animated inset. Animating width/height would run layout on
   every frame; clip-path and transform stay on the compositor. */
.expand__media {
  z-index: 2;
  clip-path: inset(
    calc((50% - var(--card-h) / 2) * (1 - var(--p)))
    calc((50% - var(--card-w) / 2) * (1 - var(--p)))
    round calc(var(--r-card) * (1 - var(--p)))
  );
  will-change: clip-path;
  animation: hero-media-in 820ms cubic-bezier(0.22, 0.7, 0.2, 1) backwards;
}
/* 1.18 and not the 1.12 this started at: inside a 608px window the smaller
   push was arithmetic rather than an effect. */
.expand__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transform: scale(calc(1.18 - 0.18 * var(--p)));
}

/* The headline, in the top row where the mark used to be. No plate behind it
   any more — white type on the dark end of the wash, which is what the wash is
   for. The halves still travel apart as the window opens. */
.expand__type {
  grid-row: 1;
  position: relative;
  z-index: 3;
  width: 100%;
  text-align: center;
  animation: hero-rise-in 760ms cubic-bezier(0.22, 0.7, 0.2, 1) backwards;
}
.expand__type h1 {
  max-width: none;
  text-align: center;
  color: var(--fg-ink);
  text-shadow: 0 1px 20px rgba(12, 26, 38, 0.5);
}

/* Full-width blocks with centred text, so both halves share one centre by
   construction. They used to shrink-wrap with `display: table`, which was left
   over from the white plates: a plate has to hug its text, a bare line does not.
   Two boxes sized to their own content can only agree on a centre by arithmetic,
   and any difference in how each one rounds is a visible misalignment.

   The travel is quadratic, not linear. At the top of the runway a linear ramp
   already had the halves 60px apart in each direction, and where the two lines
   are close in length — as they are in English — that does not read as motion,
   it reads as a headline that failed to centre. Squaring the progress keeps them
   together through the first fifth of the scroll and still parts them fully by
   the end. */
.expand__a,
.expand__b {
  display: block;
  text-align: center;
  opacity: calc(1 - var(--p) * 1.7);
}
.expand__a { transform: translateX(calc(-54vw * var(--p) * var(--p))); }
.expand__b { transform: translateX(calc(54vw * var(--p) * var(--p))); margin-top: 0.1rem; }

/* The mark, large, across the window — the position the headline held before.
   Centred on the stage rather than placed in the middle grid row: the rows above
   and below it are not the same height, so the row's centre is not the stage's,
   and the mark has to agree with the window, which is centred on the stage.

   It keeps its official blue and lime and sits on a feathered white veil.
   `closest-side` ends the fade exactly at the padding edge, so there is no
   rectangle to see — a box is what you asked to get rid of, a veil is not one.
   Blue on that veil is 4.2:1, under the 4.5:1 a paragraph would need but well
   over the 3:1 that applies to a graphic this size. */
.expand__logo {
  position: absolute;
  left: 50%;
  top: 50%;
  z-index: 3;
  display: grid;
  place-items: center;
  /* A proportional box rather than padding around a clamped image. With padding,
     the mark's share of the veil changed with the viewport — at 400px it sat
     inside the opaque core and at 190px its edges reached out to where the veil
     had faded to half, which is where the blue stops being legible. Fixing the
     ratio fixes the alpha under the ink at every screen size, which is what lets
     check_contrast() state a number instead of an intention. */
  width: min(46rem, 88vw);
  aspect-ratio: 5 / 2;
  background: radial-gradient(
    ellipse closest-side at 50% 50%,
    rgba(255, 255, 255, 0.95) 0%,
    rgba(255, 255, 255, 0.93) 62%,
    rgba(255, 255, 255, 0.62) 82%,
    rgba(255, 255, 255, 0) 100%
  );
  transform: translate(-50%, -50%) scale(calc(1 + 0.06 * var(--p)));
  opacity: calc(1 - var(--p) * 2.4);
  animation: hero-mark-in 760ms cubic-bezier(0.22, 0.7, 0.2, 1) 160ms backwards;
}
/* 62% puts the outermost ink — the O on one side, the chevron on the other — at
   62% of the gradient's horizontal radius, where the veil is still 0.93. */
.expand__logo img { display: block; width: 62%; height: auto; }

/* The arrival. Every one of these ends on exactly the value its own declaration
   holds at --p: 0, and none of them uses `forwards`, so when the entrance
   finishes it hands the property back to the scroll and nothing jumps. */
@keyframes hero-media-in {
  from { opacity: 0; transform: scale(0.94); }
  to { opacity: 1; transform: scale(1); }
}
@keyframes hero-mark-in {
  from { opacity: 0; transform: translate(-50%, -50%) scale(0.9); }
  to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}
@keyframes hero-rise-in {
  from { opacity: 0; transform: translateY(0.85rem); }
  to { opacity: 1; transform: none; }
}

/* Motion turned down: collapse to an ordinary hero. The stage stops being
   sticky, the section stops being tall, and the media sits at a fixed height
   under the type. Nothing is lost but the movement. */
@media (prefers-reduced-motion: reduce), (max-width: 760px) {
  .expand { height: auto; }
  /* A column with explicit order, not the DOM order. The overlay stacks the mark
     on top of the photograph, so the markup puts the photograph first; laid out
     flat that would open the page with an image and bury the wordmark between
     the headline and the button. `order` restores the reading a static hero
     wants: mark, headline, photograph, call to action. */
  .expand__stage {
    position: static;
    height: auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    padding-block: var(--s7, 4rem) 0;
    animation-name: none;
  }
  .expand__logo { order: 1; }
  .expand__type { order: 2; }
  .expand__media { order: 3; }
  .expand__stage::after { display: none; }
  .expand__bg { display: none; }
  .expand__media {
    position: relative;
    width: 100%;
    height: clamp(240px, 42vw, 520px);
    margin-top: var(--s6, 3.5rem);
    clip-path: none;
    border-radius: var(--r-card);
    overflow: hidden;
    animation-name: none;
  }
  .expand__media img { transform: none; }
  .expand__type {
    transform: none;
    padding-inline: var(--gutter);
    animation-name: none;
  }
  .expand__type h1 { color: var(--fg); text-shadow: none; }
  .expand__a, .expand__b { transform: none; opacity: 1; }
  /* Out of the overlay and back into the flow: with no photograph behind it the
     veil has nothing to do, and a mark floating over the middle of a static
     block would have nothing to sit on. */
  .expand__logo {
    position: static;
    width: auto;
    aspect-ratio: auto;
    transform: none;
    opacity: 1;
    background: none;
    margin-bottom: var(--s4, 1.75rem);
    animation-name: none;
  }
  .expand__logo img { width: clamp(190px, 30vw, 320px); }
}

/* --- plain figures -------------------------------------------------------- */

/* Most of the supplied photography is square once its canvas margin is trimmed,
   so the frame sets the crop rather than letting a 1:1 block sit in a text
   column. Same radius as the cards, so images and panels read as one system. */
.figure { margin: var(--s5, 2.5rem) 0 0; }
.figure__img {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: var(--r-card);
}
.figure--wide .figure__img { aspect-ratio: 16 / 7; }
.figure__cap {
  margin-top: var(--s2, 0.875rem);
  font-family: var(--f-mono);
  font-size: 0.625rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
  text-align: left;
}
.zone-ink .figure__cap { color: var(--muted-ink); }

/* Two columns where a figure sits beside prose. */
.split {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s5, 2.5rem);
  align-items: center;
}
.split .figure { margin-top: 0; }

/* --- the laydown (Services, chapter 03) ----------------------------------- */

/* A framed photograph that starts tilted back in perspective and rights itself
   as it comes up the viewport. Pure rotateX and scale — no layout, no paint,
   one compositor layer. The frame borrows the ink card treatment rather than
   the reference component's monitor bezel, which would mean nothing here. */
.laydown {
  perspective: 1200px;
  perspective-origin: 50% 0%;
  margin-top: var(--s6, 3.5rem);
}

/* Same panel treatment as the cards: bone fill, lime hairline. The shadow does
   the lifting here rather than a dark fill, because the frame is tilted in
   perspective and needs to read as sitting above the page. */
.laydown__frame {
  position: relative;
  transform-origin: 50% 0%;
  transform: rotateX(22deg) scale(1.04);
  border: 1px solid var(--lime-line);
  border-radius: var(--r-card);
  padding: clamp(0.5rem, 1.4vw, 1rem);
  background: var(--card);
  box-shadow: 0 40px 80px -46px rgba(12, 26, 38, 0.55);
  will-change: transform;
}
.laydown__frame img {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 10;
  object-fit: cover;
  border-radius: calc(var(--r-card) - 0.5rem);
}

@supports (animation-timeline: view()) {
  .laydown__frame {
    animation-name: laydown;
    animation-duration: auto;
    animation-timing-function: linear;
    animation-fill-mode: both;
    animation-timeline: view();
    /* Finished by the time the frame is properly on screen, so the reader is
       never asked to look at a photograph that is still moving. */
    animation-range: entry 15% cover 45%;
  }
}

@keyframes laydown {
  from { transform: rotateX(22deg) scale(1.04); }
  to { transform: rotateX(0deg) scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  .laydown { perspective: none; }
  .laydown__frame { animation-name: none; transform: none; }
}

/* --- brand band ----------------------------------------------------------- */

/* Full-bleed brand graphic used as a rule between sections. Deliberately
   carries no text: the artwork's colour runs edge to edge, so anything set over
   it would be a contrast gamble.

   The band is a fixed-height window onto an oversized image, which drifts
   against the scroll. `--drift` is the overhang on each edge and therefore also
   the travel in each direction: raise it for a stronger effect, set it to 0 to
   pin the artwork still. The waves run diagonally, so vertical travel reads as
   movement along them. */
.brandband {
  --drift: clamp(36px, 7vw, 110px);
  position: relative;
  overflow: hidden;
  width: 100%;
  height: clamp(150px, 24vw, 320px);
  background: #fff;
}

.brandband__img {
  position: absolute;
  top: calc(var(--drift) * -1);
  left: 0;
  width: 100%;
  height: calc(100% + var(--drift) * 2);
  object-fit: cover;
  object-position: center;
}

/* Scroll-driven animation runs on the compositor: it cannot stutter, even while
   the main thread is busy. site.js carries a rAF fallback for browsers without
   it, and gates itself on this same query so the two never both run. Untouched
   browsers get the artwork centred and static — the state it had before. */
@supports (animation-timeline: view()) {
  .brandband__img {
    will-change: transform;
    animation-name: brandband-drift;
    animation-duration: auto;
    animation-timing-function: linear;
    animation-fill-mode: both;
    animation-timeline: view();
    animation-range: cover;
  }
}

/* Matches a fixed background's direction: scrolling down walks the window up
   through the artwork. Full travel is spent over `cover` — the band entering at
   the viewport's bottom edge through leaving at its top. */
@keyframes brandband-drift {
  from { transform: translate3d(0, calc(var(--drift) * -1), 0); }
  to   { transform: translate3d(0, var(--drift), 0); }
}


/* --- the turn: logistics runs horizontal ---------------------------------- */

/* The hinge between the two axes: a vertical stub arrives from the depth axis
   and meets the horizontal track at a right angle. Built from flush-stacked
   borders rather than an SVG so the two 2px strokes join exactly, at every
   viewport width, with no scaling distortion. */
.turn { position: relative; padding-top: 52px; }
.turn::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  width: 2px;
  height: 52px;
  background: var(--lime);
}

.track {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(150px, 1fr);
  gap: 0;
  border-top: 2px solid var(--lime);
  overflow-x: auto;
  scrollbar-width: thin;
}

.track__stop {
  position: relative;
  padding: var(--s4, 1.75rem) var(--s3, 1.25rem) var(--s3, 1.25rem) 0;
  min-width: 150px;
}
.track__stop::before {
  content: "";
  position: absolute;
  top: -2px;
  left: 0;
  width: 2px;
  height: 13px;
  background: var(--lime);
}
.track__i {
  font-family: var(--f-mono);
  font-size: 0.625rem;
  letter-spacing: 0.16em;
  color: var(--lime);
  font-variant-numeric: tabular-nums;
  display: block;
  margin-bottom: var(--s1, 0.5rem);
}
.track__name {
  font-family: var(--f-display);
  font-weight: 600;
  font-size: 1.0625rem;
  letter-spacing: -0.01em;
  display: block;
  margin-bottom: 0.4rem;
}
.track__note { font-size: 0.9375rem; color: var(--muted); line-height: 1.5; }
.zone-ink .track__note { color: var(--muted-ink); }

/* --- the card ------------------------------------------------------------- */

/* One treatment shared by every panel on the site: pillars, disciplines,
   value/service cards and method stages. A bone panel with a lime hairline on a
   white page, inverting to ink inside an ink section. */

.card,
.pillar,
.disc,
.stage {
  position: relative;
  isolation: isolate;
  border: 1px solid var(--lime-line);
  border-radius: var(--r-card);
  background: var(--card);
  color: var(--fg);
}

/* On an ink section the same card inverts, because a bone panel on ink would
   fight the surface it sits on. */
.zone-ink .card,
.zone-ink .stage,
.zone-ink .disc {
  background: var(--ink-lift);
  border-color: var(--lime-line-ink);
  color: var(--fg-ink);
}
.zone-ink .card p,
.zone-ink .stage p,
.zone-ink .disc__desc { color: var(--muted-ink); }
/* On ink the accent is lime, not blue: that is the surface rule. */
.zone-ink .card__k,
.zone-ink .disc__i { color: var(--lime); }

/* The icon tile. Lime wash with a lime rule, and the mark in the deep brand
   blue so it holds up at this size against a light panel. */
.tile {
  display: grid;
  place-items: center;
  width: 3.25rem;
  height: 3.25rem;
  flex: none;
  border-radius: var(--r-tile);
  background: var(--lime-wash);
  box-shadow: inset 0 0 0 1px var(--lime-line);
  color: var(--blue-deep);
  margin-bottom: var(--s4, 1.75rem);
}
/* Both axes pinned. Without an explicit height the reset's `height: auto` wins
   and the glyph scales to its viewBox — a 24px mark rendering 500px tall. */
.tile svg { display: block; width: 1.5rem; height: 1.5rem; flex: none; }
.zone-ink .tile {
  background: rgba(189, 209, 54, 0.14);
  box-shadow: inset 0 0 0 1px var(--lime-line-ink);
  color: var(--lime);
}

/* --- pillar cards --------------------------------------------------------- */

.pillars { display: grid; gap: var(--s3, 1.25rem); grid-template-columns: repeat(2, minmax(0, 1fr)); }

.pillar {
  display: flex;
  flex-direction: column;
  padding: var(--s5, 2.5rem);
  min-height: 100%;
}
.pillar h3 { margin-bottom: var(--s2, 0.875rem); }
.pillar p { margin: 0 0 var(--s4, 1.75rem); color: var(--muted); }
.pillar .btn { margin-top: auto; align-self: flex-start; }
.pillar .btn--ghost { border-color: transparent; padding-left: 0; }

/* --- discipline grid ------------------------------------------------------ */

.disciplines {
  display: grid;
  /* Three, not four, so each descriptor has room to justify. */
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--s3, 1.25rem);
}

.disc {
  padding: var(--s4, 1.75rem);
  text-decoration: none;
  color: inherit;
  display: block;
  transition: transform 0.22s var(--ease), box-shadow 0.22s var(--ease),
              border-color 0.22s var(--ease);
}
.disc:hover {
  color: inherit;
  transform: translateY(-3px);
  border-color: var(--lime);
  box-shadow: 0 18px 36px -26px rgba(12, 26, 38, 0.4);
}
.disc__i {
  font-family: var(--f-mono);
  font-size: 0.625rem;
  letter-spacing: 0.14em;
  color: var(--blue-deep);
  font-variant-numeric: tabular-nums;
  display: block;
  margin-bottom: var(--s3, 1.25rem);
}
.disc__name {
  font-family: var(--f-display);
  font-weight: 600;
  font-size: 1.0625rem;
  line-height: 1.2;
  letter-spacing: -0.012em;
  display: block;
  margin-bottom: 0.5rem;
}
.disc__desc { font-size: 0.9375rem; line-height: 1.5; color: var(--muted); }

/* --- stats ---------------------------------------------------------------- */

.stat { border-top: 2px solid var(--blue); padding-top: var(--s3, 1.25rem); }
.stat__v {
  font-family: var(--f-mono);
  font-size: clamp(2rem, 4.4vw, 3.25rem);
  font-weight: 500;
  line-height: 1;
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
  display: block;
  margin-bottom: var(--s2, 0.875rem);
}
.stat__d { font-size: 0.9375rem; line-height: 1.5; color: var(--muted); }
.zone-ink .stat { border-top-color: var(--lime); }
.zone-ink .stat__d { color: var(--muted-ink); }

/* Every sector figure carries its source. Non-negotiable. */
.source {
  font-family: var(--f-mono);
  font-size: 0.5625rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
  margin-top: var(--s4, 1.75rem);
  display: block;
}
.zone-ink .source { color: var(--muted-ink); }

/* --- spec tables ---------------------------------------------------------- */

.spec { width: 100%; border-collapse: collapse; text-align: left; }
.spec caption { text-align: left; margin-bottom: var(--s3, 1.25rem); color: var(--muted); font-size: 0.9375rem; }
.spec th,
.spec td { padding: var(--s2, 0.875rem) var(--s3, 1.25rem) var(--s2, 0.875rem) 0; border-bottom: 1px solid var(--line); vertical-align: top; }
.spec thead th {
  font-family: var(--f-mono);
  font-size: 0.625rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  font-weight: 400;
  color: var(--muted);
  border-bottom-color: var(--fg);
}
.spec td:first-child { font-family: var(--f-ui); font-weight: 500; width: 34%; }
.spec .num { white-space: nowrap; }
.zone-ink .spec th,
.zone-ink .spec td { border-bottom-color: var(--line-ink); }
.zone-ink .spec thead th { color: var(--muted-ink); border-bottom-color: var(--fg-ink); }

.table-scroll { overflow-x: auto; }

/* --- norm coverage -------------------------------------------------------- */

.norms { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0; }
.norm { border-left: 2px solid var(--blue); padding: 0 var(--s3, 1.25rem) 0 var(--s3, 1.25rem); }
.norm__k {
  font-family: var(--f-display);
  font-weight: 700;
  font-size: 1.35rem;
  letter-spacing: -0.02em;
  display: block;
  margin-bottom: 0.3rem;
}
.norm__d { font-size: 0.875rem; line-height: 1.45; color: var(--muted); }
.zone-ink .norm { border-left-color: var(--lime); }
.zone-ink .norm__d { color: var(--muted-ink); }

/* --- chapter rail (Services) ---------------------------------------------- */

.chapters {
  position: sticky;
  top: 72px;
  z-index: 50;
  background: rgba(243, 245, 244, 0.9);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--line);
}
.chapters__in { display: flex; gap: var(--s4, 1.75rem); overflow-x: auto; scrollbar-width: none; }
.chapters__in::-webkit-scrollbar { display: none; }
.chapters a {
  font-family: var(--f-mono);
  font-size: 0.625rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
  text-decoration: none;
  padding: var(--s3, 1.25rem) 0;
  border-bottom: 2px solid transparent;
  white-space: nowrap;
}
.chapters a:hover { color: var(--fg); }
.chapters a.is-active { color: var(--fg); border-bottom-color: var(--blue); }

/* --- accordion (disciplines detail) --------------------------------------- */

.acc { border-top: 1px solid var(--line); }
.acc__item { border-bottom: 1px solid var(--line); }
.acc__btn {
  width: 100%;
  display: flex;
  align-items: baseline;
  gap: var(--s3, 1.25rem);
  text-align: left;
  padding: var(--s4, 1.75rem) 0;
  background: none;
  border: 0;
  cursor: pointer;
}
.acc__i {
  font-family: var(--f-mono);
  font-size: 0.625rem;
  letter-spacing: 0.14em;
  color: var(--blue-deep);
  font-variant-numeric: tabular-nums;
  flex: 0 0 auto;
}
.acc__t {
  font-family: var(--f-display);
  font-weight: 600;
  font-size: clamp(1.05rem, 1.7vw, 1.3rem);
  letter-spacing: -0.014em;
  line-height: 1.25;
  flex: 1;
}
.acc__sign { flex: 0 0 auto; color: var(--blue-deep); font-family: var(--f-mono); font-size: 1rem; }
.acc__btn[aria-expanded="true"] .acc__sign { transform: rotate(45deg); }
.acc__sign { transition: transform 0.2s var(--ease); display: inline-block; }
.acc__panel { display: none; padding-bottom: var(--s5, 2.5rem); }
.acc__panel.is-open { display: block; }

/* Two columns: the discipline sub-blocks are the narrowest prose on the site. */
.subblocks { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s4, 1.75rem) var(--s5, 2.5rem); }
.subblock__k {
  font-family: var(--f-mono);
  font-size: 0.5625rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--muted);
  display: block;
  margin-bottom: var(--s2, 0.875rem);
  padding-bottom: var(--s1, 0.5rem);
  border-bottom: 1px solid var(--line);
}
.subblock ul { margin: 0; padding-left: 1.1rem; }
.subblock li { font-size: 0.9375rem; line-height: 1.5; margin-bottom: 0.35rem; }
.subblock p { margin: 0; font-size: 0.9375rem; line-height: 1.5; color: var(--muted); }
.zone-ink .subblock__k { color: var(--muted-ink); border-bottom-color: var(--line-ink); }
.zone-ink .subblock p { color: var(--muted-ink); }

/* --- stages (numbered, because the content genuinely is a sequence) ------- */

.stages { display: grid; gap: var(--s3, 1.25rem); }
.stage {
  display: grid;
  grid-template-columns: 3.25rem 1fr;
  gap: var(--s4, 1.75rem);
  align-items: start;
  padding: var(--s4, 1.75rem);
}
/* The sequence number is the tile here: the content genuinely is ordered, so a
   number carries more than an icon would. */
.stage__i {
  display: grid;
  place-items: center;
  width: 3.25rem;
  height: 3.25rem;
  border-radius: var(--r-tile);
  background: var(--lime-wash);
  box-shadow: inset 0 0 0 1px var(--lime-line);
  font-family: var(--f-mono);
  font-size: 1rem;
  color: var(--blue-deep);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.05em;
}
.zone-ink .stage__i {
  background: rgba(189, 209, 54, 0.14);
  box-shadow: inset 0 0 0 1px var(--lime-line-ink);
  color: var(--lime);
}
.stage h3 { margin-bottom: 0.4rem; }
/* Same pairing as `.card h3` above, so it takes the same step: without this the
   title sat 6px off its own paragraph while the identical pattern inside a card
   sat at 14px. */
.stage h3 { margin-bottom: var(--s2, 0.875rem); }
.stage p { margin: 0; color: var(--muted); }

/* --- value / service cards ------------------------------------------------ */

.cards { display: grid; gap: var(--s3, 1.25rem); }
.card { padding: var(--s5, 2.5rem) var(--s4, 1.75rem) var(--s4, 1.75rem); }
.card__k {
  font-family: var(--f-mono);
  font-size: 0.5625rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  /* Lime is 1.6:1 on a light card. It is the accent only on ink, where the
     .zone-ink rule below puts it back. */
  color: var(--blue-deep);
  display: block;
  margin-bottom: var(--s3, 1.25rem);
}
.card h3 { margin-bottom: var(--s2, 0.875rem); }
.card p { margin: 0; color: var(--muted); font-size: 0.9375rem; line-height: 1.55; }

/* --- form ----------------------------------------------------------------- */

.form { display: grid; gap: var(--s4, 1.75rem); max-width: 46rem; }
.field { display: grid; gap: 0.45rem; }
.field--half { grid-column: span 1; }
.form__row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s3, 1.25rem); }

.field label {
  font-family: var(--f-mono);
  font-size: 0.625rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
}
.field .req { color: var(--blue-deep); }

.field input,
.field select,
.field textarea {
  font-family: var(--f-body);
  font-size: 1rem;
  padding: 0.8rem 0.9rem;
  border: 1px solid var(--line);
  background: #fff;
  border-radius: 0;
  width: 100%;
}
.field textarea { min-height: 8rem; resize: vertical; line-height: 1.55; }
.field input:focus-visible,
.field select:focus-visible,
.field textarea:focus-visible { outline: 2px solid var(--blue); outline-offset: 1px; border-color: var(--blue); }
.field__hint { font-size: 0.875rem; color: var(--muted); }
.field__err { font-size: 0.875rem; color: #a3261c; font-weight: 700; }

.field input[aria-invalid="true"],
.field select[aria-invalid="true"],
.field textarea[aria-invalid="true"] { border-color: #a3261c; }

/* Honeypot: must be reachable by bots, invisible and skipped for people. */
.hp {
  position: absolute !important;
  left: -9999px !important;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.form__note { font-size: 0.875rem; color: var(--muted); max-width: 46rem; }

.form-status { padding: var(--s3, 1.25rem); border-left: 2px solid var(--blue); background: #fff; display: none; }
.form-status.is-shown { display: block; }
.form-status--ok { border-left-color: var(--blue); }
.form-status--err { border-left-color: #a3261c; }

/* --- footer --------------------------------------------------------------- */

.site-foot { background: var(--ink); color: var(--fg-ink); padding-block: var(--s6, 3.5rem) var(--s4, 1.75rem); }
.site-foot__top { display: grid; grid-template-columns: 1.5fr repeat(3, 1fr); gap: var(--s5, 2.5rem); }
.site-foot img { height: 39px; width: auto; margin-bottom: var(--s3, 1.25rem); }
.site-foot__desc { color: var(--muted-ink); font-size: 0.9375rem; max-width: 34ch; }
.site-foot h2 {
  font-family: var(--f-mono);
  font-size: 0.625rem;
  font-weight: 400;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--lime);
  margin-bottom: var(--s3, 1.25rem);
}
.site-foot ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.55rem; }
.site-foot a { color: var(--fg-ink); text-decoration: none; font-size: 0.9375rem; }
.site-foot a:hover { color: var(--lime); }

/* The mark inherits the link's colour, so it picks up the lime on hover with no
   second rule. 1.05em rather than a fixed size keeps it locked to the label. */
.social { display: inline-flex; align-items: center; gap: 0.5rem; }
.social__i { width: 1.05em; height: 1.05em; flex: none; }
.site-foot__bot {
  margin-top: var(--s6, 3.5rem);
  padding-top: var(--s3, 1.25rem);
  border-top: 1px solid var(--line-ink);
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3, 1.25rem);
  justify-content: space-between;
  font-size: 0.8125rem;
  color: var(--muted-ink);
}

/* --- motion --------------------------------------------------------------- */

/* One orchestrated load sequence, then quiet reveals. Nothing loops. */
[data-reveal] { opacity: 0; transform: translateY(10px); }
[data-reveal].is-in {
  opacity: 1;
  transform: none;
  transition: opacity 0.55s var(--ease), transform 0.55s var(--ease);
}

.draw-in {
  transform: scaleX(0);
  transform-origin: left;
}
.draw-in.is-in { transform: scaleX(1); transition: transform 0.7s var(--ease); }

/* --- contact channels ----------------------------------------------------- */

/* The contact page carries no form, so these are the page. Set at display size
   and ruled like the rest of the system: a mono label, the value large enough
   to read as the thing you came for, one lime hairline between each. */

/* Announced, never drawn: the WhatsApp row shows a mark where the other two
   show a word, and a row whose label is only a glyph has no name without this. */
.u-sr {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.channels { margin: 0; display: grid; }
.channel {
  display: grid;
  grid-template-columns: minmax(9.5rem, 14rem) 1fr;
  gap: var(--s3, 1.25rem) var(--s5, 2.5rem);
  align-items: baseline;
  padding-block: var(--s4, 1.75rem);
  border-top: 1px solid var(--lime-line);
}
.channel:last-child { border-bottom: 1px solid var(--lime-line); }

.channel__k {
  font-family: var(--f-mono);
  font-size: 0.9375rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
  line-height: 1.4;
  display: flex;
  align-items: center;
}

/* Sized against the label beside it, not against the value: these three cells
   are one column and should read as one weight. Inherits `currentColor`, the
   same way the footer sets its marks -- a brand green would be 2.3:1 here. */
.channel__i { width: 1.75rem; height: 1.75rem; flex: none; }
.channel__v {
  margin: 0;
  font-family: var(--f-display);
  font-weight: 700;
  /* The floor is set by the longest value, the email address: it has to fit a
     360px screen whole, because nothing on this site may break a word. */
  font-size: clamp(1.125rem, 2.3vw, 1.75rem);
  line-height: 1.15;
  letter-spacing: -0.02em;
}
.channel__v a { text-decoration-thickness: 2px; text-underline-offset: 0.14em; }

@media (max-width: 640px) {
  /* The label stops earning its column once the value has to wrap. */
  .channel { grid-template-columns: 1fr; gap: var(--s1, 0.5rem); }
}

/* --- spacing utilities ---------------------------------------------------- */

/* These exist because the site is served under a Content-Security-Policy whose
   `style-src 'self'` has no `'unsafe-inline'`, and that directive blocks every
   `style="..."` attribute in the markup. Locally the dev server sends no CSP, so
   inline spacing worked there and silently collapsed to zero the moment the site
   was behind Apache -- headings sat flush against the blocks under them on the
   real domain and nowhere else. Declared here, they are subject to no such rule.
   check.py fails the build if an inline style attribute comes back. */

.mt-3 { margin-top: var(--s3, 1.25rem); }
.mt-4 { margin-top: var(--s4, 1.75rem); }
.mt-5 { margin-top: var(--s5, 2.5rem); }
.mt-6 { margin-top: var(--s6, 3.5rem); }
.mt-7 { margin-top: var(--s7, 4rem); }
.mb-4 { margin-bottom: var(--s4, 1.75rem); }
.mb-5 { margin-bottom: var(--s5, 2.5rem); }

.hero--short { min-height: 56vh; }
.split--airy { gap: var(--s7, 4rem); align-items: start; }
.h-display--sm { font-size: clamp(2rem, 4.5vw, 3.5rem); }

.note--flag {
  margin-top: var(--s4, 1.75rem);
  border-left: 2px solid var(--blue);
  padding-left: var(--s3, 1.25rem);
}

/* --- responsive ----------------------------------------------------------- */

/* Below 1500px the left margin is narrower than the rail, so it would sit on top
   of the text. The elevation labels it annotates are surfaced in the below-grade
   content anyway, so nothing is lost by retiring it. */
@media (max-width: 1240px) {
  .disciplines { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 760px) {
  .nav-toggle { display: block; }
  .nav {
    display: none;
    position: absolute;
    left: 0;
    right: 0;
    top: 100%;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    background: var(--base);
    border-bottom: 1px solid var(--line);
    padding: var(--s2, 0.875rem) var(--gutter) var(--s4, 1.75rem);
    margin: 0;
  }
  .nav.is-open { display: flex; }
  .nav__link { padding-block: 0.8rem; border-bottom: 1px solid var(--line); }
  .nav__link[aria-current="page"] { border-bottom-color: var(--blue); }
  .nav .btn { margin-top: var(--s3, 1.25rem); justify-content: center; }
  .lang { padding-top: var(--s3, 1.25rem); }

  .disciplines { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .grid--2, .grid--3, .grid--4, .norms, .subblocks { grid-template-columns: 1fr; }
  .form__row { grid-template-columns: 1fr; }
  .stage { grid-template-columns: 1fr; gap: var(--s3, 1.25rem); }
  .site-foot__top { grid-template-columns: 1fr; }
  .spec td:first-child { width: auto; }

  /* The expanding hero is laid out flat at this width — the static-hero block
     above now carries `(max-width: 760px)` alongside `prefers-reduced-motion`.
     Two screens of thumb-dragging to open one image is a poor trade on a
     handset, and the scene needs a sticky stage, which the browser chrome
     resizing itself mid-scroll keeps knocking out of step. Desktop is
     untouched. */
  .laydown { perspective: 700px; }

  /* backdrop-filter re-blurs the strip behind these two bars on every scrolled
     frame, which is the most expensive thing a phone GPU can be handed while a
     thumb is dragging. At this width an opaque background reads the same. */
  .site-head { backdrop-filter: none; background: rgba(255, 255, 255, 0.97); }
  .chapters { backdrop-filter: none; background: rgba(243, 245, 244, 0.97); }
}

@media (max-width: 420px) {
  .disciplines { grid-template-columns: 1fr; }
}

/* --- a11y + print --------------------------------------------------------- */

:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }
.zone-ink :focus-visible, .site-foot :focus-visible { outline-color: var(--lime); }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    transition-duration: 0.001ms !important;
  }
  [data-reveal] { opacity: 1; transform: none; }
  .draw-in { transform: none; }
  /* Not just a shorter animation — no drift at all. The band sits centred. */
  .brandband__img { animation-name: none; transform: none; }
}

@media print {
  .site-head, .chapters, .nav-toggle { display: none !important; }
  body { background: #fff; color: #000; }
  .zone-ink { background: #fff !important; color: #000 !important; }
}
