/*
Theme Name: BizNest Foundation
Theme URI: https://biznestfoundation.org
Author: BizNest Foundation
Description: Editorial "field record" theme for BizNest Foundation, with a scroll-scrubbed cinematic homepage. Built for Elementor on interior pages. Design system documented in design.md — read it before changing anything visual.
Version: 2.0.0
Requires at least: 6.4
Tested up to: 6.7
Requires PHP: 7.4
License: GPL-2.0-or-later
Text Domain: biznest
*/

/* Fonts are enqueued in functions.php, not imported here — an @import blocks the
   stylesheet's own parse and duplicated the request. */

/* ══ Tokens ══ See design.md. If a value is not here, it does not go on the page. ══ */
:root {
  --paper:       #FBF9F4;
  --paper-sunk:  #F2EEE4;
  --ink:         #1A1815;
  --ink-soft:    #5C564C;
  --rule:        #DDD6C8;
  --ember:       #A8622A;
  --verdant:     #2F4A3C;

  --font-display: 'Fraunces', Georgia, serif;
  --font-body:    'Public Sans', system-ui, sans-serif;
  --font-mono:    'IBM Plex Mono', ui-monospace, monospace;

  --step--1: 0.8125rem;
  --step-0:  1.0625rem;
  --step-1:  clamp(1.15rem, 0.5vw + 1rem, 1.3rem);
  --step-2:  clamp(1.4rem, 1vw + 1.1rem, 1.75rem);
  --step-3:  clamp(1.75rem, 2vw + 1.2rem, 2.5rem);
  --step-4:  clamp(2.25rem, 3.5vw + 1.2rem, 3.5rem);
  --step-5:  clamp(2.75rem, 5vw + 1.2rem, 4.5rem);

  --sp-1: 8px;   --sp-2: 16px;  --sp-3: 24px;  --sp-4: 32px;
  --sp-6: 48px;  --sp-8: 64px;  --sp-12: 96px; --sp-16: 128px;

  --r-none: 0;
  --r-sm: 3px;

  --ease-out: cubic-bezier(0.215, 0.61, 0.355, 1);
  --ease-in-out: cubic-bezier(0.645, 0.045, 0.355, 1);
  --dur: 200ms;

  --measure: 68ch;
  /* The cap, not the width. 1240 put 340px of dead film either side of a 1920 screen and
     the whole page read as a centred strip; the gutter below is what stops a wider cap
     from doing the same thing again at 2560. Measure is unchanged — the extra width is
     spent on the margin column, never on longer lines. */
  --shell: 1560px;
  --gutter: clamp(var(--sp-3), 4vw, var(--sp-12));
  /* Height of the sticky masthead. Pinned scenes pad past it so their first line is not
     rendered underneath it. */
  --masthead-h: 77px;
}

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

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

body {
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: var(--step-0);
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 500;           /* 500, not 700 — see design.md */
  line-height: 1.08;
  letter-spacing: -0.015em;
  text-wrap: balance;
}
h1 { font-size: var(--step-5); }
h2 { font-size: var(--step-4); }
h3 { font-size: var(--step-2); }
h4 { font-size: var(--step-1); }

p { text-wrap: pretty; }

a {
  color: var(--ember);
  text-decoration: none;
  background-image: linear-gradient(currentColor, currentColor);
  background-size: 0% 1px;
  background-repeat: no-repeat;
  background-position: 0 100%;
  transition: background-size var(--dur) var(--ease-out);
}
a:hover { background-size: 100% 1px; }

:focus-visible {
  outline: 2px solid var(--ember);
  outline-offset: 3px;
}

/* height:auto is load-bearing, not tidiness. Every <img> carries width/height attributes
   (WordPress emits them, and the plates set them by hand) and those map to a real `height`
   declaration. With both width and height specified, `aspect-ratio` is ignored — which is
   exactly what happened: the project cards rendered at each file's natural height, so a
   768x512 photo and a 1600x1066 plate sat in the same row at wildly different heights.
   Releasing height lets every aspect-ratio rule below actually govern the box. */
img { max-width: 100%; height: auto; display: block; }

::selection { background: var(--ember); color: var(--paper); }

/* ══ Shell & grid ══ */
.app-container { display: flex; flex-direction: column; min-height: 100vh; }
.main-content  { flex: 1; width: 100%; }

/* max-width, deliberately not width:min(). Two pinned scenes set `> .shell { width: 100% }`
   to defeat the auto-margin shrink-wrap (see the mandate and drift stages below); under
   max-width those declarations resolve to the cap, but under width:min() they would win and
   blow the pinned panels out to full stage width. */
.shell {
  max-width: var(--shell);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* 12-col editorial grid. The outer column is the document margin. */
.doc-grid {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: var(--sp-3);
}
.doc-margin { grid-column: 1 / span 2; }
.doc-body   { grid-column: 3 / span 7; }
.doc-wide   { grid-column: 3 / span 10; }
.doc-full   { grid-column: 1 / -1; }

/* Column spans live in CSS, not inline styles — an inline gridColumn cannot be
   overridden by the mobile media query below, which collapses every column. */
.col-3 { grid-column: span 3; }
.col-4 { grid-column: span 4; }
.col-5 { grid-column: span 5; }
.col-6 { grid-column: span 6; }
.col-7 { grid-column: span 7; }
.col-8 { grid-column: span 8; }

.measure { max-width: var(--measure); }

/* The entry lists are the one place a wider shell reaches the text directly: their second
   column is a bare fraction of the row, so at 1560 the working-area sector lists measured
   115ch and the partner roles 87ch. Capped at the same measure as every other run of prose
   — the row keeps the width, the reading line does not. */
.entry p { max-width: var(--measure); }

/* ══ Section rhythm — hairline rule + mono eyebrow ══ */
.section { padding-block: var(--sp-16); border-top: 1px solid var(--rule); }
.section:first-of-type { border-top: 0; }

.eyebrow {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-soft);
  display: block;
}

.marginalia {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  line-height: 1.5;
  color: var(--ink-soft);
  /* balance, not the inherited `pretty`. These are all two- and three-line notes, and
     `pretty` only reworks the final line — it left "Section 8" alone on a line 17% the
     width of the one above it, and the footer's copyright line at 5%. `balance` equalises
     instead, which is the whole point of a note set beside the record it annotates.
     Chromium stops balancing past 6 lines, so this cannot misfire on a longer block. */
  text-wrap: balance;
}

.lede { font-size: var(--step-1); color: var(--ink-soft); }

/* ── Short blocks are balanced, long ones stay `pretty` ──
   Every run of copy on this site that is two or three lines long is a standfirst, an excerpt
   or a note, and `pretty` does not help those: it only reworks the final line, which left
   last lines at 5-30% of the width above them across the homepage. `balance` equalises them.
   The set is enumerated rather than applied to every p because there is no selector for
   "short" — but the failure mode is bounded either way, since Chromium declines to balance
   past 6 lines and those blocks keep the inherited `pretty`. */
p.measure,
.lede,
.drift__lede,
.drift__body,
.track__body,
.entry p { text-wrap: balance; }

/* ══ Section rule ══
   The textile motif that used to sit here was dropped. What replaced it is the plainest
   possible device: a 3px ember band, or a full-bleed hairline. The relief sequence carries
   the visual identity now, so the divider is deliberately mute. */
.rule {
  height: 3px;
  width: 100%;
  border: 0;
  margin: 0;
  background: var(--ember);
}
.rule--hair {
  height: 1px;
  background: var(--rule);
}

/* ══ Records table — credentials as identity ══ */
.record {
  width: 100%;
  border-collapse: collapse;
  font-family: var(--font-mono);
  font-size: var(--step--1);
}
.record th, .record td {
  text-align: left;
  padding: var(--sp-2) var(--sp-2) var(--sp-2) 0;
  border-bottom: 1px solid var(--rule);
  vertical-align: top;
}
/* The label column shrink-wraps to its longest label instead of taking a flat 34%. At 34%
   the column was 194px wide, so "CIN" sat three characters into a 194px cell and its value
   started 160px away, while "Sec. 8 licence" nearly touched its own — same anchor line,
   wildly different gap, and nothing for the eye to run along. Shrink-wrapped, the gutter is
   one constant sp-4 on every row. `width:1%` is the shrink-to-fit idiom for a table cell. */
.record th {
  font-weight: 500;
  color: var(--ink-soft);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  white-space: nowrap;
  width: 1%;
  padding-right: var(--sp-4);
}
/* Registration numbers are long unbreakable mono strings; without this they
   overflow their cell on narrow viewports. */
.record td { color: var(--ink); overflow-wrap: anywhere; }
.marginalia, .figure figcaption { overflow-wrap: anywhere; }
.record tr:last-child th, .record tr:last-child td { border-bottom: 0; }

/* ══ Buttons ══ */
.btn {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  font-family: var(--font-mono);
  font-size: var(--step--1);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 14px var(--sp-3);
  min-height: 44px;
  border: 1px solid var(--ink);
  border-radius: var(--r-none);
  background: transparent;
  color: var(--ink);
  cursor: pointer;
  background-image: none;
  transition: transform var(--dur) var(--ease-out), opacity var(--dur) var(--ease-out);
}
.btn:hover { transform: translateY(-2px); }
.btn:active { transform: translateY(0); }
.btn--primary { background: var(--ink); color: var(--paper); border-color: var(--ink); }
.btn--onDark  { border-color: var(--paper); color: var(--paper); }

/* ══ Figure — photo placeholder. The caption is the shot list. ══ */
.figure { margin: 0; }
.figure__slot {
  position: relative;
  width: 100%;
  background: var(--paper-sunk);
  border: 1px solid var(--rule);
  border-radius: var(--r-sm);
  overflow: hidden;
  display: grid;
  place-items: center;
}
.figure__slot::after {
  content: '';
  position: absolute;
  inset: 0;
  background-image: repeating-linear-gradient(
    45deg, transparent 0 11px, color-mix(in srgb, var(--ember) 7%, transparent) 11px 12px);
  pointer-events: none;
}
.figure__mark {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  color: var(--ember);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  z-index: 1;
  padding: var(--sp-2);
  text-align: center;
}
.figure__slot img { width: 100%; height: 100%; object-fit: cover; }

/* ── An empty slot is a commissioning brief, not a missing image ──
   A slot with a photograph in it earns its full 3:2. An empty one held the same area, so the
   project register rendered as three full-height hatched voids and one photograph, and the
   voids dominated the page. Capping the height keeps the shot list visible and legible while
   making the gap read as deliberate — which it is: the photograph has been specified and not
   yet taken. The aspect-ratio stays on the element so a real image still lands at 3:2. */
.figure__slot--empty {
  max-height: 200px;
  border-style: dashed;
}
.figure figcaption {
  margin-top: var(--sp-1);
  font-family: var(--font-mono);
  font-size: var(--step--1);
  color: var(--ink-soft);
  line-height: 1.5;
}

/* ══ Photographic record ══
   A set of frames from one activity, laid out as a 12-column band so the pairing can be 7/5
   and then 5/7. Zero radius, no border, no shadow: on paper the photographs are the only
   things with tone on the page and a frame around them would be a second device. */
.plates {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: var(--sp-3);
}
.plates__frame { margin: 0; min-width: 0; }
.plates__frame--wide { grid-column: span 7; }
.plates__frame--narrow { grid-column: span 5; }
.plates__frame img {
  display: block;
  width: 100%;
  aspect-ratio: 3 / 2;
  object-fit: cover;
}
.plates__note { margin-top: var(--sp-3); }

@media (max-width: 900px) {
  .plates { grid-template-columns: 1fr; }
  .plates__frame--wide,
  .plates__frame--narrow { grid-column: 1 / -1; }
}

/* ══ Project entry ══ */
.entry { display: grid; grid-template-columns: 1fr; align-items: start; gap: var(--sp-3); padding-block: var(--sp-4); border-top: 1px solid var(--rule); }
/* These three MUST stay below `.entry`. Declared above it they lost on source order at equal
   specificity, so every entry list silently collapsed to one column: the partner name and its
   role stacked with a 58px vertical step inside a 96px padded band, which is the trapped
   white space that made the list read as disconnected rows rather than one register. */
/* Baseline only where every column is text. On .entry--record one column is a figure, and a
   block box's baseline is its bottom margin edge — baseline alignment there dropped the
   status and the excerpt to the foot of the photograph and opened a ~200px void above them. */
.entry--split  { grid-template-columns: minmax(160px, 1fr) 3fr; align-items: baseline; }
.entry--states { grid-template-columns: minmax(140px, 1fr) 3fr; align-items: baseline; }
.entry--record { grid-template-columns: minmax(180px, 1fr) 2fr 1.4fr; }
.entry__status {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
.entry__status--ongoing        { color: var(--verdant); }
.entry__status--upcoming       { color: var(--ember); }
.entry__status--launching-soon { color: var(--ember); }
.entry__status--completed      { color: var(--ink-soft); }

/* ══ Forms ══ */
.field { display: block; margin-bottom: var(--sp-3); }
.field > span {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--step--1);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-soft);
  margin-bottom: var(--sp-1);
}
.input {
  width: 100%;
  font-family: var(--font-body);
  font-size: var(--step-0);
  padding: 12px var(--sp-2);
  min-height: 44px;
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: var(--r-none);
  transition: border-color var(--dur) var(--ease-out);
}
.input:focus { border-color: var(--ember); }

/* ══ Footer ══ */
.site-footer { background: var(--verdant); color: var(--paper); padding-block: var(--sp-8) var(--sp-4); }
.site-footer a { color: var(--paper); }
.site-footer .eyebrow, .site-footer .marginalia { color: rgba(251, 249, 244, 0.66); }
.site-footer .record th { color: rgba(251, 249, 244, 0.66); }
.site-footer .record td { color: var(--paper); }
.site-footer .record th, .site-footer .record td { border-bottom-color: rgba(251, 249, 244, 0.18); }

/* ══ Masthead ══ */
.masthead {
  position: sticky; top: 0; z-index: 50;
  background: var(--paper);
  border-bottom: 1px solid var(--rule);
}
.masthead__bar { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); padding-block: var(--sp-2); }
.masthead__mark { font-family: var(--font-display); font-size: var(--step-1); font-weight: 600; letter-spacing: -0.02em; color: var(--ink); background-image: none; }
.masthead__mark span { color: var(--ember); }
.nav { display: flex; gap: var(--sp-3); align-items: center; }
.nav a {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink);
  padding-block: var(--sp-1);
}
.nav a[aria-current='page'] { color: var(--ember); background-size: 100% 1px; }
/* .nav a would otherwise win on specificity and paint the button's label in --ink
   on an --ink ground, making it invisible. */
.nav a.btn { color: var(--ink); }
.nav a.btn--primary { color: var(--paper); }
.nav__toggle { display: none; }

.scene { position: relative; }
.scene__stage {
  min-height: 100vh;
  display: grid;
  align-content: center;
  gap: var(--sp-6);
  padding-block: var(--sp-12);
}

/* ══ The film ══════════════════════════════════════════════════════════════════════
   One clip is the background for the whole homepage. It is fixed to the viewport, sits
   behind every other layer, and its currentTime is driven by whole-document scroll
   progress in assets/js/scroll.js. There is no second background anywhere on this page:
   no per-section imagery, no terrain plate, no hero video of its own. Content floats.

   The <video> carries no src attribute in the markup — scroll.js assigns it from
   data-src, and only above 900px with motion allowed. Everyone else stays on the poster,
   which costs 53KB instead of 8.5MB.

   The source is re-encoded all-intra (see tools/make_film.py). The delivered clip had a
   single keyframe at frame 0, which makes currentTime seeking decode from the start every
   time — that is a still image with extra steps, not a scrub. */
.film {
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  /* Dark, not paper-sunk: this is what shows for the instant before the poster paints, and
     a cream flash behind reversed cream type reads as a broken page. */
  background: #17150F;
}
.film__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* The grade. This is what makes type possible on the film at all, and its strength is
   measured, not chosen: tools/film_luma.py reports the clip running at mean luminance
   91–157 with a local standard deviation up to 64 and fine detail peaking at 16.2 over the
   field mosaic. A mid-grey, high-detail ground is the worst case for legibility — dark ink
   vanishes into the hedgerows and light type vanishes into the sandbars, which is why the
   previous build ended up hiding the film behind near-opaque paper panels.

   Pulling the mean down to roughly 50 puts cream type at about 11:1 against it, and the
   footage is still entirely visible — darker, and reading as cinema rather than as
   wallpaper behind slabs. The gradient is slightly heavier top and bottom so the masthead
   and the footer have somewhere to sit without needing bars of their own.

   Raised 0.52/0.50 -> 0.64/0.62 on 2026-08-18. That middle band spans 18%-72% of the
   viewport, which is every body section on the page, and it was the shallowest part of the
   grade sitting under the smallest type. Measured against the film actually under them
   (page hidden, film alone captured, so the glyphs do not contaminate the sample) the low
   points were 4.61:1 on a sector chip and 6.24:1 on a record cell — passing on paper, but
   the footage moves, and text that survives only on its darkest frame reads as flickering
   in and out. The footage is still plainly visible at 0.62; this is a grade, not a slab,
   which is the line design.md drew when it rejected translucent paper panels. */
.film__scrim {
  position: absolute;
  inset: 0;
  background:
    linear-gradient(
      to bottom,
      rgba(14, 13, 11, 0.78) 0%,
      rgba(14, 13, 11, 0.64) 18%,
      rgba(14, 13, 11, 0.62) 72%,
      rgba(14, 13, 11, 0.74) 100%
    ),
    rgba(20, 18, 15, 0.18);
}

/* z-index -1, not 0. The old value assumed .film was a sibling of .main-content competing
   with the propagated body background; it is not — it is .main-content's FIRST CHILD, and
   .main-content is position:relative + z-index:1, so it is a stacking context. Inside one,
   a positioned child at z-index 0 paints in step 8 while in-flow static content paints in
   step 4: the film's scrim was painted ON TOP of every static block on the page.

   It was invisible on the animated path because GSAP puts a transform on each .reveal,
   which promotes it above the film, and headings survive everywhere because .measure is
   position:relative. So the only content that vanished was static, un-animated blocks under
   prefers-reduced-motion: reduce — the working-area sectors, reported three times. Geometry
   and computed style are correct in that state (opacity 1, height 185, cream); no probe
   that reads them can see this. `document.elementFromPoint` can, and did.

   At -1 the film paints in step 3: above .main-content's own background, below all of its
   content. Nothing needs to be lifted above it any more. */
.masthead,
.main-content,
.site-footer { position: relative; z-index: 1; }
.film { z-index: -1; }
/* The masthead needs to outrank .main-content, not tie with it. At an equal z-index DOM
   order decides, and .main-content comes later — so it, and the film inside it, painted
   over the header. A sticky header has to sit above the page it is stuck to regardless. */
.masthead { z-index: 2; }

/* ══ Reversed on the film ══════════════════════════════════════════════════════════
   The homepage inverts. Everything below is scoped to `.js-film .home`, so the interior
   pages and the no-JS/small-screen homepage keep the paper palette untouched.

   This replaces a system of translucent paper panels. Those were legible but they covered
   the film — at the opacity the footage actually requires (0.93, forced by the detail
   measurement above) a "translucent plate" is an opaque slab, and the background may as
   well not have been there. Type reverses out of the graded film instead, and no content
   block owns a rectangle.

   Redefining the tokens rather than restating colours on each rule: every descendant that
   already reads --ink, --ink-soft or --rule inherits the reversal, including anything
   Elementor renders inside a homepage section. */
.js-film .home {
  --ink: #FBF9F4;
  --ink-soft: rgba(248, 244, 236, 0.76);
  --rule: rgba(248, 244, 236, 0.26);
  /* #A8622A is a mid-dark brown: on the graded film it reads as mud, and as a hairline it
     disappears. Lifted and warmed to hold at 1–2px against the footage. */
  --ember: #E2A063;
  color: var(--ink);
}

/* .btn--primary paints --ink as its ground and --paper as its label. With the tokens
   reversed both are cream, so the button rendered as a blank cream rectangle with an
   invisible word in it. Ember is the only token that reads as a filled control here. */
.js-film .home .btn--primary {
  background: var(--ember);
  border-color: var(--ember);
  color: #17150F;
}
/* :not(--primary) — this rule used to hit the primary button too, and because it is
   declared after .btn--primary it won on source order: an ember fill ringed in cream at
   0.76 alpha. The cream ring is exactly the weight of the body text around it, so the
   button's edge dissolved into the paragraph and it stopped reading as a control. Its own
   ember border at 2px anchors it instead. */
.js-film .home .btn:not(.btn--primary) { border-color: var(--ink-soft); }
.js-film .home .btn--primary { border-width: 2px; }
.js-film .home .btn:not(.btn--primary):hover { border-color: var(--ink); }

.js-film .home .masthead {
  background: transparent;
  backdrop-filter: none;
  border-bottom: 1px solid var(--rule);
}

/* No plate, no panel, no card. The blocks that carried a background now carry only
   rhythm, and the hairline comes back as the separator it is on paper — the reason it was
   hidden before was that the plates were already separating things. */
.js-film .home .section > .doc-grid,
.js-film .home .plate {
  background: none;
  backdrop-filter: none;
  border-top: 0;
  padding: 0;
  box-shadow: none;
}
.js-film .home .section {
  border-top: 1px solid var(--rule);
  padding-block: var(--sp-12);
}
.js-film .home .rule { display: block; border-color: var(--rule); }

/* Where real reading happens the film still has to be quietened, but with a pool rather
   than a panel: a radial wash that is strongest under the text and reaches zero before any
   edge, so there is nothing to read as a box. `filter: blur()` on top removes the last of
   the banding the gradient stops would otherwise show over flat sky. */
.js-film .home .main-content .measure,
.js-film .home .main-content .doc-grid p,
.js-film .home .main-content .track__body,
.js-film .home .main-content .drift__body,
.js-film .home .main-content .drift__focus,
.js-film .home .main-content .drift__partners,
.js-film .home .main-content .drift__lede,
.js-film .home .main-content .record { position: relative; }

.js-film .home .main-content .measure::before,
.js-film .home .main-content .doc-grid p::before,
.js-film .home .main-content .track__body::before,
.js-film .home .main-content .drift__body::before,
.js-film .home .main-content .drift__focus::before,
.js-film .home .main-content .drift__partners::before,
.js-film .home .main-content .drift__lede::before,
.js-film .home .main-content .record::before {
  content: '';
  position: absolute;
  /* inset: 0, not a negative inset. An absolutely positioned pseudo-element contributes to
     its ancestors' SCROLLABLE overflow, so bleeding the pool -2.4em past each side pushed
     the document 86px wider than the viewport and gave the page a horizontal scrollbar.
     The spread comes from blur() instead, which is ink overflow and creates no scroll area
     at all — same soft edge, no layout consequence. */
  inset: 0;
  z-index: -1;
  background: radial-gradient(
    ellipse at 38% 50%,
    rgba(10, 9, 8, 0.66) 0%,
    rgba(10, 9, 8, 0.5) 46%,
    rgba(10, 9, 8, 0.2) 74%,
    rgba(10, 9, 8, 0) 100%
  );
  filter: blur(24px);
  pointer-events: none;
}

/* ── The opening ──
   One viewport of copy over the film's cloud deck. It owns no media: the film is already
   behind it. Ink on the pale cloud tops rather than paper on a dark scrim — the first
   frames are bright, and reversing to light type there needs a scrim heavy enough to
   grey out the very thing the visitor is meant to be looking at. */
.open__stage {
  position: relative;
  min-height: 100vh;
  height: 100vh;
  display: grid;
  align-content: end;
  padding-block: 0 var(--sp-12);
}
.open__copy {
  position: relative;
  width: 100%;
  max-width: var(--shell);
  margin-inline: auto;
  /* Same inline gutter as .shell, so the wordmark starts on the exact left edge the section
     headings below it start on. A fixed sp-6 here put the hero 29px inside that edge once
     the gutter became proportional. */
  padding: var(--sp-8) var(--gutter);
}
/* The opening is the one place the film is at its brightest (mean 154 over the cloud deck)
   and its calmest (detail 2.1), so the title needs no pool of its own — only the grade. */
.js-film .home .open__copy { border-top: 2px solid var(--ember); }
/* The name sits above everything and is the largest thing on the page — step-5 against the
   headline's step-4. A <p>, not a heading: the page already has one h1 and the wordmark is
   an identity mark, not a level in the document outline. */
.open__wordmark {
  font-family: var(--font-display);
  font-size: var(--step-5);
  font-weight: 500;
  line-height: 1.02;
  letter-spacing: -0.02em;
  margin-bottom: var(--sp-4);
}
/* The headline is plain text and the browser owns the breaks. Hand-authored lines cannot
   survive here: h1 carries text-wrap:balance globally, which re-wraps inside each authored
   span at a narrower used width, so the three spans rendered as SIX lines ragged
   579/666/666/666/456/666. A width cap plus balance is the whole mechanism — an even rag at
   any viewport, and no break that a person has to maintain. */
.open__copy h1 { max-width: 22ch; margin-top: var(--sp-3); }
.open__copy h1 { font-size: var(--step-4); }
.open__cue { margin-top: var(--sp-4); }
.open__cue::after {
  content: '';
  display: block;
  width: 1px;
  height: var(--sp-6);
  margin-top: var(--sp-2);
  background: var(--ember);
}

/* ── The register: panels that arrive ──
   Four project cards flown in over the live film while the scene is pinned. They are not
   pinned to anything in the footage and make no claim about it: the film is moving under
   them continuously, so an anchored marker would drift off whatever it was pointing at.
   The leader rule is a compositional device, drawn from the card's own edge. */
.drift__stage {
  position: relative;
  display: grid;
  gap: var(--sp-6);
  padding-block: var(--sp-12);
}
.drift {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  /* Wide enough that the film reads between the cards. At sp-5 the four plates butted
     into one continuous slab with a single ember line running across it, so they read as
     one block rather than four projects. */
  gap: var(--sp-8);
}
/* Grid items already stretch to the tallest card in the row, but the CONTENT stopped
   wherever the excerpt happened to end — measured slack between the status badge and the
   card's own bottom edge was 139/50/50/109px, so four equal boxes still read as a ragged
   grid. A column flex with the status on `margin-top:auto` puts every status on one line
   and moves the variance into the gap above it, where nothing is drawn. */
.drift__card {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  height: 100%;
  padding: var(--sp-6);
  border-top: 1px solid var(--rule);
}
.drift__card > * { width: 100%; }
.drift__lead {
  position: absolute;
  top: -1px;
  left: 0;
  height: 2px;
  width: 100%;
  background: var(--ember);
  transform-origin: left center;
}
.drift__ord {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  color: var(--ink-soft);
}
/* The one photograph on this page. Zero radius and no frame, like everything else here — the
   film is already behind it, so a border would be a plate and content over the film owns no
   rectangle. Fixed 3:2 so a card with a photograph and a card without still line up. */
.drift__shot {
  display: block;
  position: relative;
  margin: var(--sp-2) 0 0;
}
.drift__shot img {
  display: block;
  width: 100%;
  aspect-ratio: 3 / 2;
  object-fit: cover;
}

/* Small, quiet, and never omitted. A generated plate is only acceptable in a slot that says
   so — without this the image reads as a record of something that happened. */
.drift__illus {
  position: absolute;
  left: 0;
  bottom: 0;
  padding: 6px var(--sp-2);
  background: rgba(14, 13, 11, 0.66);
  font-family: var(--font-mono);
  font-size: var(--step--1);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--paper);
}

.drift__title { font-size: var(--step-2); margin-top: var(--sp-1); }
/* The excerpt and partner line moved here when the duplicate Projects section was removed.
   They are the reason the card is worth arriving: without them a card is a label. */
.drift__body {
  margin-top: var(--sp-2);
  font-size: 0.95rem;
  line-height: 1.6;
  color: var(--ink);
}
.drift__partners {
  margin-top: var(--sp-2);
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.02em;
  color: var(--ink-soft);
}
.drift__lede {
  margin-top: var(--sp-2);
  /* The house measure, not a one-off. A 52ch cap here put this standfirst in a 541px box
     inside a 1406px column — 865px of unused room, and narrower than the h2 above it, which
     is what made the wrap read as arbitrary rather than as a measure. A single line would
     need ~100ch, far past anything readable, so this stays two lines by choice. */
  max-width: var(--measure);
  color: var(--ink-soft);
}
.drift__more { margin-top: var(--sp-4); font-family: var(--font-mono); font-size: 0.78rem; }
.drift__title a { text-decoration: none; }
.drift__focus { font-size: 0.9rem; color: var(--ink-soft); margin-top: var(--sp-1); }
.drift__status {
  display: inline-block;
  /* auto, not sp-3: this is the element that pins every card's content to the same bottom
     edge (see .drift__card). Declared here rather than beside .drift__card because that copy
     sat ABOVE this block and lost on source order at equal specificity — measured slack was
     unchanged at 137/48/48/107 until the declaration moved into the winning rule. */
  margin-top: auto;
  align-self: flex-start;
  width: auto;
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ember);
}

.track-viewport { overflow: hidden; }
.track { display: grid; gap: var(--sp-8); }
.track__panel { border-top: 1px solid var(--rule); padding-top: var(--sp-4); }
.track__ord {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--step-1);
  letter-spacing: 0.08em;
  color: var(--ember);
}
/* sp-4 left a 32px void under a 24px ordinal, so the number read as floating rather than
   as a label belonging to the heading below it. */
.track__text { margin-top: var(--sp-2); }
.track__title { font-size: var(--step-3); }
.track__body { margin-top: var(--sp-4); font-size: var(--step-1); color: var(--ink-soft); max-width: 40ch; }

/* The pinned mandate stage has to be exactly one viewport tall. Left to grow, the heading
   pushes the track below centre and pins something taller than the screen, which reads as
   a gap. Grid rather than flex: a `flex: none` header shrink-wraps to its content on the
   cross axis, which pulled the centred .shell in by 260px the moment the pin engaged. */
.js-scroll .scene--mandate .scene__stage {
  height: 100vh;
  min-height: 0;
  display: grid;
  /* minmax(0, 1fr), not the implicit `auto`: the track is ~4560px of max-content, and an
     auto column sizes to it, which collapsed the centred .shell to 716px and shifted the
     heading 260px right the moment the pin engaged. */
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: auto 1fr;
  align-content: stretch;
  gap: 0;
  /* The masthead is sticky, so a pinned stage starting at viewport top slides its first
     line underneath it. */
  padding-block: calc(var(--masthead-h) + var(--sp-6)) 0;
  overflow: hidden;
}
/* .shell carries margin-inline: auto, and an auto margin makes a grid item shrink-wrap
   instead of stretch — which is what actually collapsed it to 716px. An explicit width
   leaves the auto margins nothing to absorb.

   Deliberately NOT scoped to .js-scroll. It was, and the shrink-wrap it prevents has
   nothing to do with the choreography — it is the grid stage, which exists on every path.
   Under reduced motion the two scene heads were left shrink-wrapping to their own text and
   centring: measured left edges 400 and 503 against 249 for every other block on the page,
   and the two did not even agree with each other. */
.scene--mandate .scene__stage > .shell { width: 100%; }
.js-scroll .track-viewport { min-height: 0; display: flex; align-items: stretch; }
/* Full-height panels put the hairline rule at one consistent height right across the
   track, so the band reads as ruled ledger columns instead of as empty paper. */
.js-scroll .track { height: 100%; align-items: stretch; }
/* Ordinal pinned under the rule, text on the baseline. Centring the whole block instead
   left a dead band above and below it and made the panels read as floating. */
/* flex-end, not space-between. The panel is as tall as the pinned 100vh stage, so
   space-between parked the ordinal against the panel's top edge and its own heading ~500px
   below it — the number read as belonging to nothing. Both children now sit at the bottom of
   the stage, which keeps the composition, with the ordinal one sp-2 above the title it
   labels. The reduced-motion path never showed this: unpinned, the panel is only as tall as
   its content, so space-between had no free space to distribute and measured a correct 16px. */
.js-scroll .track__panel { display: flex; flex-direction: column; justify-content: flex-end; padding-bottom: var(--sp-12); }
.js-scroll .track__text { margin-top: var(--sp-2); }
.js-scroll .track {
  display: flex;
  grid-auto-flow: unset;
  gap: var(--sp-12);
  padding-inline: var(--sp-8);
  will-change: transform;
}
.js-scroll .track__panel { flex: 0 0 min(78vw, 760px); }

/* ── Masked line reveal ──
   Each span is one line and is clipped by its own mask rather than by a mask around the
   whole heading: a single outer overflow:hidden crops the descenders of the last line,
   and the padding/margin pair below is what gives the glyphs room without opening a gap
   the rise would be visible through. Spans are sized so they do not wrap — a wrapped span
   is two lines sliding as one block, which reads wrong. */
.line-mask { display: block; }
.line-mask > span { display: block; }
.js-scroll .line-mask > span {
  overflow: hidden;
  padding-bottom: 0.14em;
  margin-bottom: -0.14em;
}
/* The displacement itself is owned by scroll.js, not declared here — with the offset in
   both places GSAP had no un-transformed state to tween back to. CSS only opens the clip;
   if the script never runs, the lines simply sit where they belong. */
.line-mask > span > span { display: block; }

/* ── Static tally. Deliberately not a count-up: the honest numbers are small, and an
   animated "4" draws attention to exactly the wrong thing. ── */
.tally {
  font-family: var(--font-mono);
  font-size: var(--step-1);
  letter-spacing: 0.02em;
  color: var(--ink);
}
.tally span { color: var(--ink-soft); }

/* ══ Responsive ══ */

/* ── Fixes measured off the live film, not guessed ──────────────────────────────── */

/* The mandate panels reverse like everything else. They keep a hairline and their padding
   so the six objectives still read as ruled ledger columns rather than as loose text. */
.js-film .track__panel {
  background: none;
  backdrop-filter: none;
  border-top: 1px solid var(--rule);
  padding: var(--sp-6) var(--sp-6) var(--sp-6) 0;
  box-shadow: none;
}
/* Reduced motion keeps the film but drops the horizontal track, so the six panels stack.
   Without this they inherit the paper fallback, which was written for a page that had no
   background: the plates ran the full width of the viewport with the copy against the left
   edge. Over the film they have to sit in the same column as every other plate. */
.js-film:not(.js-scroll) .track-viewport {
  /* width:100% is not redundant. .scene__stage is a grid, and margin-inline:auto makes a
     grid item shrink-wrap rather than stretch — the same trap already documented for
     .shell twice above. Without it the viewport came out 737px inside a 1425px stage and
     the panels sat 500px narrower than the plates below them. */
  width: 100%;
  max-width: var(--shell);
  margin-inline: auto;
  /* --gutter, not sp-3: this is the layout the reduce path actually renders, and a fixed
     24px here left the stacked panels 53px inside the section headings beside them once
     the shell gutter became proportional. */
  padding-inline: var(--gutter);
}
.js-film:not(.js-scroll) .track {
  gap: var(--sp-6);
  /* One full-width column. Left implicit the panels sized to their own content and came out
     ~500px narrower than the section plates below them, which reads as an accident. */
  grid-template-columns: minmax(0, 1fr);
}
.js-film:not(.js-scroll) .track__panel { width: 100%; }
/* 40ch exists to keep a 760px horizontal panel from running long. Stacked, the panel is the
   full shell and that cap leaves ~900px of empty film beside three lines of text. */
.js-film:not(.js-scroll) .track__body { max-width: var(--measure); }


.js-film .track__panel .track__ord { color: var(--ink-soft); }

/* .shell carries margin-inline:auto, and an auto margin makes a grid item shrink-wrap
   rather than stretch — the same trap already documented for the mandate stage. Without
   this the four cards collapsed to ~265px each and the longest title ran outside its own
   card and into the next one. */
.drift__stage > .shell { width: 100%; }
/* Grid items default to min-width:auto, which refuses to shrink below the longest
   unbreakable run of content. That is the other half of the same overflow. */
.drift__card { min-width: 0; }
.drift__title { overflow-wrap: break-word; }


/* Scene headings carry an ember hairline above them instead of a plate — the same mark
   that separates every other block, so the two pinned scenes are not special-cased. */
.js-film .home .scene__head {
  display: block;
  padding: var(--sp-4) 0;
  background: none;
  backdrop-filter: none;
  border-top: 2px solid var(--ember);
  box-shadow: none;
}

/* The margin-top the two scene headings used to carry inline. Inline styles outrank the
   wide-viewport grid below, which needs to zero this when the eyebrow moves out of the
   normal flow and into the margin column. */
.scene__head h2 { margin-top: var(--sp-2); }

/* ── Wide viewports: spend the width on the margin column, not on longer lines ──
   Above 1440 the 12-column document grid has a real outer margin to give away, so the mono
   eyebrow leaves the body column and becomes a margin note beside the heading — which is
   what the outer column was drawn for. Every other section on this page already does this
   with .doc-margin; the two pinned scenes were the only ones stacking their label on top.
   Measure is untouched: the heading keeps .measure and the lede keeps its 52ch. */
@media (min-width: 1440px) {
  .js-film .home .scene__head,
  .scene__head {
    display: grid;
    grid-template-columns: repeat(12, 1fr);
    gap: 0 var(--sp-3);
    align-items: start;
  }
  .scene__head > .eyebrow {
    grid-column: 1 / span 2;
    /* Optically aligned to the heading's first baseline rather than its box top — the
       display face has far more leading above the cap than the mono does. */
    padding-top: 0.5em;
  }
  .scene__head > h2,
  .scene__head > .drift__lede { grid-column: 3 / span 9; }
  .scene__head > h2 { margin-top: 0; }
}

@media (max-width: 900px) {
  /* Below this width scroll.js never assigns the film's src at all, so the horizontal
     track collapses to a stacked column here and no phone downloads 8.5MB it cannot see. */
  .js-scroll .scene--mandate .scene__stage { height: auto; display: block; overflow: visible; }
  .js-scroll .track-viewport { min-height: 0; display: block; }
  .js-scroll .track { height: auto; }
  .js-scroll .track__panel { display: block; }
  /* sp-3, not 0: the horizontal track sits outside .shell, so zeroing its inline padding
     put the six objectives hard against the screen edge while every other block on the
     page kept the shell's 24px margin. */
  .js-scroll .track { display: grid; padding-inline: var(--sp-3); gap: var(--sp-8); }
  .js-scroll .track__panel { flex: none; }
  .scene__stage { min-height: 0; padding-block: var(--sp-12); }

  /* Below 900px the film is never given a src, so the fixed layer holds the poster still.
     It stays as the page ground and reverses exactly like the desktop film — one treatment,
     not two. The grade goes heavier here because a still frame gives the eye more time to
     read the detail underneath the type than a moving one does, and because the copy is a
     single narrow column over a full-bleed image. */
  .js-film .film__scrim {
    background:
      linear-gradient(to bottom, rgba(14, 13, 11, 0.78) 0%, rgba(14, 13, 11, 0.66) 50%, rgba(14, 13, 11, 0.8) 100%),
      rgba(20, 18, 15, 0.24);
  }
  .js-film .home .section > .doc-grid,
  .js-film .home .plate,
  .js-film .home .open__copy,
  .js-film .home .drift__card { padding: var(--sp-6) 0; }

  .open__stage { height: auto; min-height: 0; padding-block: var(--sp-12); }
  .open__copy h1 { font-size: var(--step-3); }

  /* The cards stack; scroll.js checks the same 900px breakpoint and skips the arrival
     timeline entirely rather than animating against this layout. */
  .drift { grid-template-columns: minmax(0, 1fr); }
  .drift__lead { display: none; }
}

@media (max-width: 900px) {
  :root { --sp-16: 72px; --sp-12: 56px; }
  .doc-margin { grid-column: 1 / -1; }
  .doc-body, .doc-wide { grid-column: 1 / -1; }
  .col-3, .col-4, .col-5, .col-6, .col-7, .col-8 { grid-column: 1 / -1; }
  .entry, .entry--split, .entry--states, .entry--record { grid-template-columns: 1fr; }
  .masthead__bar { flex-wrap: wrap; }
  .nav { flex-wrap: wrap; gap: var(--sp-2) var(--sp-3); width: 100%; padding-bottom: var(--sp-1); }
}

@media (max-width: 520px) {
  :root { --step-0: 1rem; }
  .shell { padding-inline: var(--sp-2); }
  .record th { width: auto; white-space: normal; }
  .record, .record tbody, .record tr, .record th, .record td { display: block; }
  .record th { border-bottom: 0; padding-bottom: 0; }
  .record td { padding-top: 2px; }
  .btn { width: 100%; justify-content: center; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  .reveal { opacity: 1; transform: none; }
}

/* ══ Compatibility shims — admin screens still use the v1 class vocabulary ══ */
.glass-card {
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: var(--r-none);
  padding: var(--sp-3);
}
.gradient-text { color: var(--ember); }
.cyber-btn, .cyber-btn.primary {
  display: inline-flex; align-items: center; gap: var(--sp-1);
  font-family: var(--font-mono); font-size: var(--step--1);
  letter-spacing: 0.06em; text-transform: uppercase;
  padding: 14px var(--sp-3); min-height: 44px;
  border: 1px solid var(--ink); border-radius: var(--r-none);
  background: transparent; color: var(--ink); cursor: pointer; background-image: none;
  transition: transform var(--dur) var(--ease-out);
}
.cyber-btn:hover { transform: translateY(-2px); }
.cyber-btn.primary { background: var(--ink); color: var(--paper); }
.cyber-label {
  display: block; font-family: var(--font-mono); font-size: var(--step--1);
  text-transform: uppercase; letter-spacing: 0.06em; color: var(--ink-soft); margin-bottom: var(--sp-1);
}
.cyber-input {
  width: 100%; font-family: var(--font-body); font-size: var(--step-0);
  padding: 12px var(--sp-2); min-height: 44px; color: var(--ink);
  background: var(--paper); border: 1px solid var(--rule); border-radius: var(--r-none);
}
.cyber-input:focus { border-color: var(--ember); }
.grid-2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: var(--sp-3); }
.grid-3 { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: var(--sp-3); }

/* ══ WordPress + Elementor integration ══ */

/* Elementor wraps page content in its own containers; give them the shell width and
   let the design system's own .shell handle the gutters instead of doubling them. */
.elementor-section.elementor-section-boxed > .elementor-container,
.e-con { max-width: var(--shell); }
.elementor-widget-container > :last-child { margin-bottom: 0; }

/* Elementor's default typography reset can fight the design system; these are the
   selectors it emits for headings and body copy inside widgets. */
.elementor-widget-heading .elementor-heading-title { font-family: var(--font-display); font-weight: 500; }
.elementor-widget-text-editor { font-family: var(--font-body); }

.alignwide { width: min(100%, var(--shell)); margin-inline: auto; }
.alignfull { width: 100%; }
.screen-reader-text {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}
.skip-link:focus {
  position: fixed; top: var(--sp-2); left: var(--sp-2); z-index: 999;
  background: var(--paper); padding: var(--sp-2); border: 1px solid var(--ink);
}
.admin-bar .masthead { top: 32px; }
@media (max-width: 782px) { .admin-bar .masthead { top: 46px; } }

/* WP editor content inside .doc-body keeps the measure without extra markup. */
.entry-content > * { max-width: var(--measure); }
.entry-content > .alignwide,
.entry-content > .alignfull,
.entry-content > .record,
.entry-content > figure { max-width: none; }
.entry-content > * + * { margin-top: var(--sp-3); }

/* ══ Working area — one column per state ══
   Three revisions, and the first two both failed the same way. As a middle-dot paragraph it
   was a run-on sentence; as wrapping flex chips the chip widths followed the words, so
   Assam's five sectors made two ragged rows (79px) while Nagaland's five fit one (35px) and
   no two states aligned. Both treated a state as a ROW. The section's actual claim is a
   comparison — same sector, three states — and that only reads as three columns standing
   side by side with the sectors listed down each one. */
.sectors {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--sp-6);
  /* Sits between the heading above and the section's bottom rule, clear of both. */
  margin-block: var(--sp-6) var(--sp-4);
}
.sectors__state {
  font-family: var(--font-display);
  font-size: var(--step-1);
  font-weight: 500;
  padding-bottom: var(--sp-2);
  border-bottom: 1px solid var(--rule);
}
.sectors__list {
  list-style: none;
  margin: var(--sp-2) 0 0;
  padding: 0;
}
.sectors__list li {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  line-height: 1.5;
  letter-spacing: 0.04em;
  /* --ink, not --ink-soft. These are the smallest characters on the page and they sit over
     moving footage; at 0.76 alpha a sector measured 4.61:1 against the brightest frame under
     it, which is the low point of the whole page. The shadow is the second half of that:
     it does not lift the average contrast much, but it holds the stroke edges when a bright
     sandbar drifts under a glyph, which is when this list appeared to vanish. */
  color: var(--ink);
  text-shadow: 0 1px 3px rgba(14, 13, 11, 0.7);
}
.sectors__list li + li { margin-top: var(--sp-1); }

@media (max-width: 900px) {
  .sectors { grid-template-columns: 1fr; gap: var(--sp-4); }
}

/* ══ Last section before the footer ══
   The partner list ended exactly where the green footer began — section bottom and footer
   top were the same 11089px, so the colour changed with no pause in it. The section's own
   sp-16 is the page's standard rhythm and correct between sections; the join into a
   full-bleed colour block needs more than the rhythm, not a different rhythm. */
/* Both selectors, because .js-film .home .section (0,3,0) resets padding-block to sp-12
   and would otherwise beat a bare .section--outro (0,1,0) — the homepage is the only place
   this class is used, so scoping it only to the paper rule would have fixed nothing. */
.section--outro,
.js-film .home .section--outro { padding-bottom: calc(var(--sp-12) + var(--sp-8)); }
