/* ============================================================================
   Trengh Technologies — homepage (scroll-scrubbed film build)
   ----------------------------------------------------------------------------
   Consumes tokens.css only. A raw colour value in this file is a bug.

   THE FILM SECTION IS ALWAYS A DARK SURFACE
   -----------------------------------------
   Text in .filmstrip sits on top of video, so it cannot inherit the theme's
   ink colour — in light mode that would be black type on a dark frame. The
   film therefore pins its own contrast pair from the REFERENCE palette
   (--ref-smoke on a --ref-black scrim) rather than the semantic tokens. This
   is the one deliberate exception in the file and it is why those two
   --ref-* names appear below.

   PINNING is position:sticky throughout — never ScrollTrigger pin.
   Sticky dies silently under any ancestor with overflow hidden/clip/auto.

   REDUCED MOTION
   --------------
   The scrub and every scroll-scrubbed reveal RUN (ADR-0006 am.2) — they are
   driven by the reader's own scrolling. Only autonomous motion is gated:
   the loader's spin, and the CSS durations that tokens.css collapses.
   ========================================================================== */

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

body {
  background: var(--color-bg);
  color: var(--color-ink);
  font-family: var(--font-sans);
  font-size: var(--text-base);
  line-height: var(--leading-normal);
  -webkit-font-smoothing: antialiased;
}
/* while the loader is up, the page must not scroll behind it */
body.is-loading { overflow: hidden; }

/* MAIN IS THE CURTAIN. The footer is fixed to the viewport floor and sits at
   z-index 0; main rides above it at 1 and has to be OPAQUE, or the footer
   shows through the page from the first scroll instead of being uncovered at
   the end of it. The root background paints on the canvas, below everything,
   so it cannot do this job. See the footer section for the whole mechanism. */
main { position: relative; z-index: 1; background: var(--color-bg); }

::selection { background: var(--color-selection-bg); color: var(--color-selection-ink); }

a { color: var(--color-accent-text); text-decoration: none; }
a:hover { text-decoration: underline; text-underline-offset: 3px; }
a:focus-visible { outline: none; box-shadow: var(--focus-ring); border-radius: var(--radius-sm); }

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

.skip {
  position: absolute; left: var(--gutter); top: var(--space-2xs); z-index: 200;
  padding: var(--space-2xs) var(--space-sm);
  background: var(--color-surface); border-radius: var(--radius-sm);
  transform: translateY(-300%);
}
.skip:focus { transform: none; }

/* ══ THE WORDMARK ═════════════════════════════════════════════════════════
   Alfa Slab One, plus the single orange dot. Logo v2.1.

   ALFA SLAB ONE HAS ONE WEIGHT (400). The rules below previously asked for
   --weight-bold, which the browser answers by SYNTHESISING a bold — it smears
   the outlines, and on a slab serif that thickens the slabs unevenly and
   reads as a slightly out-of-focus logo. font-synthesis: none makes that
   impossible rather than merely discouraged.

   Alfa Slab One is also already very tight and very heavy, so it takes NO
   tracking. The old --tracking-tight is dropped here deliberately.

   THE DOT'S NUMBERS ARE MEASURED, NOT CHOSEN. Taken off the supplied
   trengh-logo-light.png (300x80) by classifying pixels, then solving for the
   implied font size two independent ways and requiring them to agree:

       ink width  278px / 3.596em (canvas actualBoundingBox)  ->  77.31px
       ascender    61px / 0.778em (full-word ink top to base) ->  78.40px
       agreement 98.6%  ->  the artwork is plain Alfa Slab One, no tracking

       dot diameter .............. 13px  ->  0.168em
       gap after the 'h' ......... 9px   ->  0.116em
       dot bottom above baseline .. 3px  ->  0.039em

   TWO MEASUREMENT TRAPS, both of which produced wrong numbers first time:
     - Range.getBoundingClientRect() returns LINE BOXES, not ink. It reports
       the same bottom for "trenh" and "trengh" because the g's descender is
       inside the line box either way. Use canvas measureText's
       actualBoundingBox* for real ink extents.
     - Do not take the ascender from the first few letters. 't' is shorter
       than 'h' in this face, so sampling only "tre" understates it and the
       two derivations then disagree by 12%.

   That last one is the detail worth protecting: the dot does NOT sit on the
   baseline like a full stop. It floats just clear of it. Setting it to
   vertical-align: baseline with no offset drops it 3px and the mark stops
   matching the artwork.
   ═══════════════════════════════════════════════════════════════════════ */

.hdr__logo, .loader__word, .ftr__word {
  font-family: var(--font-logo);
  font-weight: 400;              /* the only weight this face has */
  font-synthesis: none;          /* never let the browser fake one */
  letter-spacing: 0;
}

.hdr__stop, .ftr__stop, .loader__stop {
  display: inline-block;
  width: 0.168em;
  height: 0.168em;
  margin-left: 0.116em;
  margin-bottom: 0.039em;        /* lifts it clear of the baseline */
  vertical-align: baseline;
  border-radius: var(--radius-full);
  background: var(--color-accent);
}

/* ══ 1 · LOADER ═══════════════════════════════════════════════════════════ */

.loader {
  position: fixed; inset: 0; z-index: 150;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  gap: var(--space-sm);
  background: var(--color-bg);
  transition: opacity var(--dur-reveal) var(--ease-refract),
              visibility var(--dur-reveal) var(--ease-refract);
}
.loader.is-done { opacity: 0; visibility: hidden; pointer-events: none; }

.loader__mark {
  width: 84px; height: 84px;
  display: grid; place-items: center;
  border: 1px solid var(--color-line);
  border-radius: var(--radius-full);
  margin-bottom: var(--space-2xs);
}
.loader__dot {
  width: 18px; height: 18px;
  border-radius: var(--radius-full);
  background: var(--color-accent);
  /* autonomous — gated under reduced motion at the foot of this file */
  animation: pulse 1.4s var(--ease-inout) infinite;
}
@keyframes pulse {
  0%, 100% { transform: scale(1);   opacity: 1;   }
  50%      { transform: scale(.55); opacity: .55; }
}

.loader__word {
  font-size: var(--text-xl);     /* family/weight/tracking: see THE WORDMARK */
}

.loader__rail {
  width: min(240px, 60vw); height: 2px;
  background: var(--color-line); border-radius: var(--radius-full);
  overflow: hidden;
}
.loader__fill {
  display: block; height: 100%; width: 100%;
  background: var(--color-accent);
  transform: scaleX(0); transform-origin: 0 50%;
}
.loader__pct {
  font-size: var(--text-2xs); letter-spacing: var(--tracking-label);
  color: var(--color-ink-faint); font-variant-numeric: tabular-nums;
}
/* Shown only after SLOW_AFTER ms, so a slow link gets an explanation instead
   of a bar that looks stuck. [hidden] needs the explicit none — the flex
   display on .loader would otherwise override the UA default. */
.loader__note {
  font-size: var(--text-2xs);
  color: var(--color-ink-faint);
  max-width: 28ch; text-align: center;
}
.loader__note[hidden] { display: none; }

/* ══ HEADER ═══════════════════════════════════════════════════════════════ */

:root {
  /* ONE NUMBER, THREE CONSUMERS, the same arrangement --ftr-h uses further
     down. The bar is fixed, so anything that has to sit clear of it needs
     this figure: the anchor targets below, and the interior pages' masthead
     in pages.css.

     MEASURED, NOT DERIVED. 74px, and stable from 375px to 1440px wide,
     because what sets the height is the burger's 44px tap target plus two
     lots of --space-sm, not the logo's fluid font size. Deriving it from
     --text-lg gives 68px and tucks the top of the target under the bar. */
  --hdr-h: 4.625rem;
}

.hdr {
  position: fixed; inset: 0 0 auto 0; z-index: 60;
  display: flex; align-items: center; justify-content: space-between;
  padding: var(--space-sm) var(--gutter);
  transition: background-color var(--dur-base) var(--ease-out),
              box-shadow var(--dur-base) var(--ease-out),
              color var(--dur-base) var(--ease-out);
}
/* Over the film the header inverts, because the film is always dark.
   Scoped to the top bar only: once a panel is open it has its own solid
   ground, so its contents must NOT inherit the inverted ink. */
.hdr.is-onfilm { color: var(--ref-smoke); }
.hdr.is-onfilm .hdr__logo,
.hdr.is-onfilm > .nav > .nav__list > .nav__item > .nav__link { color: var(--ref-smoke); }
.hdr.is-onfilm .hdr__cta { color: var(--ref-peach); }
.hdr.is-onfilm .hdr__burger span { background: var(--ref-smoke); }

.hdr.is-scrolled:not(.is-onfilm) {
  background: color-mix(in srgb, var(--color-bg) 86%, transparent);
  backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
  box-shadow: 0 1px 0 var(--color-line);
}

.hdr__logo {
  font-size: var(--text-lg);     /* family/weight/tracking: see THE WORDMARK */
  color: var(--color-ink);
}
.hdr__logo:hover { text-decoration: none; }
/* ══ MEGA MENU ════════════════════════════════════════════════════════════
   Opened by :hover AND :focus-within. The second one is what makes it work
   from the keyboard with no script at all: tabbing into the trigger reveals
   the panel because the trigger is inside the item. JS only mirrors state
   into aria-expanded and wires Escape.

   The panel is display:block at all times and hidden with visibility, not
   with display:none. visibility:hidden removes its links from the tab order
   (so nobody tabs into an invisible menu) while still allowing a transition,
   which display:none does not.
   ═══════════════════════════════════════════════════════════════════════ */

.sprite { position: absolute; width: 0; height: 0; overflow: hidden; }
/* every icon is a stroked outline, never a filled shape */
.card__icon, .nav__ico { fill: none; stroke: currentColor; stroke-width: 1.5;
  stroke-linecap: round; stroke-linejoin: round; }

.nav__list {
  list-style: none; margin: 0; padding: 0;
  display: flex; align-items: center; gap: var(--space-xs);
  font-size: var(--text-sm);
}
/* NOT position:relative. The panel is absolutely positioned against .hdr
   (which is fixed, so it is the containing block) and spans the full header
   width. Anchoring to the item instead makes a wide panel hang off-screen
   under the right-hand triggers. */
.nav__item { position: static; }

.nav__link {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 10px 12px;
  font: inherit; font-size: var(--text-sm);
  color: var(--color-ink-muted);
  background: none; border: 0; border-radius: var(--radius-sm);
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease-out);
}
.nav__link:hover { color: var(--color-ink); text-decoration: none; }
.nav__item--mega > .nav__link::after {
  content: ""; width: 5px; height: 5px; margin-left: 2px;
  border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;
  transform: translateY(-2px) rotate(45deg);
  transition: transform var(--dur-base) var(--ease-out);
}
html:not(.nav-js) .nav__item--mega:hover > .nav__link::after,
html:not(.nav-js) .nav__item--mega:focus-within > .nav__link::after,
.nav__item--mega.is-open > .nav__link::after { transform: translateY(1px) rotate(225deg); }

.nav__ico { width: 15px; height: 15px; }
.nav__link--flag { color: var(--color-accent-text); }

.hdr__cta {
  display: inline-block; margin-left: var(--space-2xs);
  padding: 10px 18px;
  border-radius: var(--radius-full);
  background: var(--color-accent-text);
  color: var(--color-on-accent);
  font-weight: var(--weight-medium);
}
.hdr__cta:hover { background: var(--color-accent-hover); text-decoration: none; }
.hdr.is-onfilm .hdr__cta { background: var(--ref-red-orange); color: var(--ref-black); }

/* --- the panel --------------------------------------------------------- */

.panel {
  position: absolute;
  top: 100%;
  left: 0; right: 0;
  /* The gap between trigger and panel is PADDING, not offset. As an offset it
     is dead space: the pointer leaves the item on its way down, :hover drops,
     and the panel closes before it can be reached. As padding it stays inside
     the item and bridges the gap. */
  padding-top: var(--space-xs);
  transform: translateY(-6px);
  z-index: 70;
  opacity: 0; visibility: hidden;
  transition: opacity var(--dur-base) var(--ease-out),
              transform var(--dur-base) var(--ease-refract),
              visibility var(--dur-base) var(--ease-out);
}
/* WITH JAVASCRIPT THIS MENU IS CLICK-DRIVEN, NOT HOVER-DRIVEN.
   Hover-open plus a click toggle fight each other: the pointer holds the panel
   open while the click flips .is-open off, so the panel blinks shut and
   immediately reopens. Rather than pick one and lose the other, the hover and
   focus-within rules are kept ONLY for the scripting-off case (docs/06 wants
   the menu usable without JS), and nav.js marks the document so they switch
   off the moment it runs. */
html:not(.nav-js) .nav__item--mega:hover > .panel,
html:not(.nav-js) .nav__item--mega:focus-within > .panel,
.nav__item--mega.is-open > .panel {
  opacity: 1; visibility: visible; transform: translateY(0);
}

/* --- panel body -------------------------------------------------------
   One grid for all three panels: a fixed rail, then two equal columns. Fixed
   rail plus minmax(0,1fr) twins is what makes the columns line up across
   panels regardless of how much copy each holds. Auto tracks would size to
   content and every panel would land in a slightly different place, which is
   the "offset" look. minmax(0,...) rather than plain 1fr because a bare 1fr
   has min-width:auto and refuses to shrink below its longest word.
   ---------------------------------------------------------------------- */
.panel__inner {
  max-width: min(1120px, calc(100vw - var(--space-lg)));
  margin-inline: auto;
  display: grid;
  grid-template-columns: 19rem minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--space-lg);
  padding: var(--space-lg);
  background: var(--color-surface);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-xl);
  box-shadow: var(--shadow-lg);
  color: var(--color-ink);
  text-align: left;
}

/* left rail */
.rail { padding-right: var(--space-lg); border-right: 1px solid var(--color-line); }
.rail__h {
  font-size: var(--text-xl); font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-tight);
}
.rail__p {
  margin-top: var(--space-2xs);
  font-size: var(--text-sm); line-height: var(--leading-normal);
  color: var(--color-ink-muted);
}
.rail__cta {
  display: inline-flex; align-items: center; gap: 6px;
  margin-top: var(--space-sm);
  padding: 9px 16px;
  border: 1px solid var(--color-line-strong);
  border-radius: var(--radius-full);
  font-size: var(--text-sm); color: var(--color-ink);
}
.rail__cta::after { content: "\2192"; }
.rail__cta:hover { border-color: var(--color-accent); text-decoration: none; }

.promo {
  display: block; margin-top: var(--space-md);
  padding: var(--space-sm);
  background: var(--color-surface-sunk);
  border-radius: var(--radius-lg);
  color: inherit;
}
.promo:hover { text-decoration: none; background: var(--color-accent-tint); }
.promo__tag {
  display: inline-block;
  font-size: var(--text-2xs); font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-label); text-transform: uppercase;
  color: var(--color-accent-text);
}
.promo__h {
  display: block; margin-top: 4px;
  font-size: var(--text-sm); font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-tight);
}
.promo__p {
  display: block; margin-top: 3px;
  font-size: var(--text-xs); color: var(--color-ink-muted);
}

/* columns */
.col__label {
  font-size: var(--text-2xs); font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-label); text-transform: uppercase;
  color: var(--color-ink-faint);
  padding-bottom: var(--space-2xs);
  margin-bottom: var(--space-2xs);
  border-bottom: 1px solid var(--color-line);
}

.mis { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; }

/* Same dashed warning the bench used for its placeholder names. Sample
   content that looks finished is exactly the kind that survives to launch, so
   it says so in the interface and not only in a comment. Delete this modifier
   the day something real is published. */
.col__label--warn {
  color: var(--color-accent-text);
  border-bottom: 1px dashed var(--color-accent);
  padding-bottom: 3px;
}

/* One row shape for every entry, so the icon column, the heading baseline and
   the description all align down the panel and across panels. */
.mi {
  display: grid;
  grid-template-columns: 2.25rem minmax(0, 1fr);
  gap: var(--space-2xs);
  align-items: start;
  padding: 10px;
  margin-inline: -10px;              /* optical: text lines up with the label */
  border-radius: var(--radius-lg);
  color: inherit;
  transition: background-color var(--dur-fast) var(--ease-out);
}
.mi:hover { background: var(--color-surface-sunk); text-decoration: none; }
.mi--plain { grid-template-columns: minmax(0, 1fr); }

.mi__ico {
  width: 2.25rem; height: 2.25rem;
  display: grid; place-items: center;
  border-radius: var(--radius-md);
  background: var(--color-surface-sunk);
  color: var(--color-ink);
}
.mi__ico svg { width: 1.15rem; height: 1.15rem; fill: none; stroke: currentColor;
  stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }
/* One tint per pillar, walking the brand spectrum in pillar order. Graphic
   use only: the icon stroke stays ink, never the tint. */
.mi__ico--a { background: color-mix(in srgb, var(--ref-peach) 26%, transparent); }
.mi__ico--b { background: color-mix(in srgb, var(--ref-coral) 22%, transparent); }
.mi__ico--c { background: color-mix(in srgb, var(--ref-red-orange) 18%, transparent); }
.mi__ico--d { background: color-mix(in srgb, var(--ref-firebrick) 15%, transparent); }

.mi__b { min-width: 0; }

/* Names the KIND of thing before its title, so a reader scanning the latest
   column knows whether they are about to open a twenty page argument or a
   two paragraph note. Sits above the title rather than after it, because by
   the time you have read the title you have already decided. */
.mi__kick {
  display: block; margin-bottom: 1px;
  font-size: var(--text-2xs); font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-label); text-transform: uppercase;
  color: var(--color-accent-text);
}
.mi__h {
  display: block;
  font-size: var(--text-sm); font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-tight); line-height: var(--leading-snug);
}
.mi__p {
  display: block; margin-top: 2px;
  font-size: var(--text-xs); line-height: var(--leading-normal);
  color: var(--color-ink-muted);
}

/* --- burger, hidden until the nav collapses ---------------------------- */
.hdr__burger {
  display: none;
  width: 42px; height: 42px; padding: 10px;
  background: none; border: 0; cursor: pointer;
  flex-direction: column; justify-content: space-between;
}
.hdr__burger span {
  display: block; height: 2px; width: 100%;
  background: var(--color-ink); border-radius: 2px;
  transition: transform var(--dur-base) var(--ease-refract),
              opacity var(--dur-fast) var(--ease-out);
}
.hdr__burger[aria-expanded="true"] span:nth-child(1) { transform: translateY(9px) rotate(45deg); }
.hdr__burger[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.hdr__burger[aria-expanded="true"] span:nth-child(3) { transform: translateY(-9px) rotate(-45deg); }

/* ── the nav collapses to a drawer ───────────────────────────────────────
   RESTORED 2026-08-22, after being deleted by accident. It sat between the
   two markers used to slice the platforms section out, and going with it left
   the site with no burger at all, a nav running 780px wide inside a 375px
   screen, and no way to navigate on a phone. Caught only when a seventh menu
   item went in. If a block like this is ever sliced by marker again, check
   what else lives between the markers first.

   BREAKPOINT RAISED TO 1120px, FROM 1040. It used to be set by the widest
   PANEL, and the panels still fit: all four measure 999px. What binds now is
   the NAV BAR. With Field notes there are seven items, and at 1041px the logo
   and the first link were eleven pixels apart. Measured, not guessed. */
@media (max-width: 1119px) {
  .hdr__burger { display: flex; }

  /* .hdr is position:fixed WITH a z-index, so it is its own stacking context
     and the top bar has to outrank the drawer inside it. The drawer is
     full-bleed at 375px, so there is no strip of scrim left to tap, which
     makes the burger the only way out, and a phone has no Escape key. */
  .hdr__logo, .hdr__burger { position: relative; z-index: 66; }

  .nav {
    position: fixed; inset: 0 0 0 auto;
    width: min(26rem, 100%);
    z-index: 65;
    /* Clears the top bar, which now sits above the drawer, so the first item
       starts below it rather than sliding underneath. */
    padding: 5.5rem var(--gutter) var(--space-2xl);
    background: var(--color-bg);
    border-left: 1px solid var(--color-line);
    overflow-y: auto; overscroll-behavior: contain;
    transform: translateX(100%);
    visibility: hidden;
    transition: transform var(--dur-reveal) var(--ease-refract),
                visibility var(--dur-reveal) var(--ease-refract);
  }
  .nav.is-open { transform: none; visibility: visible; }

  .nav__list { flex-direction: column; align-items: stretch; gap: 0; }
  .nav__item { border-bottom: 1px solid var(--color-line); }
  .nav__item--cta { border-bottom: 0; padding-top: var(--space-md); }

  /* NOT space-between. That split "Rescue" from its icon across a 334px row.
     flex-start, and margin-left:auto on the chevron by itself. */
  .nav__link {
    display: flex; width: 100%; justify-content: flex-start;
    padding: var(--space-xs) 0;
    font-size: var(--text-lg); color: var(--color-ink);
  }
  .nav__item--mega > .nav__link::after { margin-left: auto; }

  /* THE 0fr -> 1fr ACCORDION NEEDS THE COLLAPSING ELEMENT TO CARRY NO
     PADDING, BORDER OR MARGIN. min-height:0 zeroes content, not padding, and
     a padding-bottom held every closed panel open at 26px. All spacing goes
     on the last child, inside the clip. */
  .panel {
    position: static;
    display: grid; grid-template-rows: 0fr;
    padding: 0; transform: none;
    opacity: 1; visibility: visible;
    overflow: hidden;
    transition: grid-template-rows var(--dur-base) var(--ease-out);
  }
  .nav__item--mega.is-open > .panel { grid-template-rows: 1fr; }
  /* the no-JS path, same as the hover rules the desktop panel keeps */
  html:not(.nav-js) .nav__item--mega:focus-within > .panel { grid-template-rows: 1fr; }

  .panel__inner {
    min-height: 0;                       /* required, or the clip does nothing */
    max-width: none; margin: 0;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-sm);
    padding: 0;                          /* NOT a padding-bottom. See below. */
    background: none; border: 0; border-radius: 0; box-shadow: none;
  }
  /* THE COLLAPSING ITEM MUST CARRY NO PADDING, and this is the second time
     that has been proved the hard way. min-height:0 zeroes the CONTENT box,
     not the padding box, so a padding-bottom on .panel__inner leaves every
     CLOSED panel standing at exactly its own padding, 26px each, four of them
     stacked down the drawer. Put the breathing room on the last child, where
     it lives inside the clip and collapses with it. */
  .panel__inner > :last-child { padding-bottom: var(--space-md); }
  .rail { padding-right: 0; border-right: 0; }
  .rail__h { font-size: var(--text-lg); }
  .mi:hover { background: none; }

  .nav-scrim {
    position: fixed; inset: 0; z-index: 64;
    background: color-mix(in srgb, var(--ref-black) 45%, transparent);
    opacity: 0; visibility: hidden;
    transition: opacity var(--dur-base) var(--ease-out),
                visibility var(--dur-base) var(--ease-out);
  }
  .nav-scrim.is-open { opacity: 1; visibility: visible; }
  body.nav-locked { overflow: hidden; }
}

/* ══ 2 · THE FILM ═════════════════════════════════════════════════════════ */

.filmstrip {
  position: relative;
  /* The scroll length of the scrub. JS reads this element's height, so this
     one value retimes the whole scrub with no code change.

     Sizing it: the shots are NOT equal length — Cloud is the shortest at 3.2s
     of 21.2s, i.e. 15% of the travel. At 600svh the stage eats 100svh, leaving
     500svh of scroll, so Cloud still gets ~75svh to be read in. Drop much
     below this and the last beat flicks past before it can be finished. */
  height: 600svh;
  background: var(--ref-black);
}

.filmstrip__stage {
  position: sticky; top: 0;
  height: 100svh;
  overflow: hidden;                 /* clips the video, not the sticky itself */
  color: var(--ref-smoke);
}

.film, .film__video, .film__scrim { position: absolute; inset: 0; }

.film__video {
  width: 100%; height: 100%;
  object-fit: cover;
  background: var(--ref-black);
}

/* legibility bed for the beat copy — heavier at the left, where type sits */
.film__scrim {
  background:
    linear-gradient(90deg,
      color-mix(in srgb, var(--ref-black) 88%, transparent) 0%,
      color-mix(in srgb, var(--ref-black) 62%, transparent) 42%,
      color-mix(in srgb, var(--ref-black) 12%, transparent) 78%,
      transparent 100%),
    linear-gradient(180deg,
      color-mix(in srgb, var(--ref-black) 55%, transparent) 0%,
      transparent 28%,
      transparent 72%,
      color-mix(in srgb, var(--ref-black) 55%, transparent) 100%);
}

/* --- the beats: one visible at a time, chosen by [data-beat] on the section */
.beat {
  position: absolute;
  left: 0; top: 50%;
  width: 100%;
  transform: translateY(-50%);
  padding-inline: max(var(--gutter), calc((100vw - var(--container)) / 2 + var(--gutter)));
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--dur-slow) var(--ease-refract),
              visibility var(--dur-slow) var(--ease-refract);
  pointer-events: none;
}
.filmstrip[data-beat="0"] .beat[data-beat="0"],
.filmstrip[data-beat="1"] .beat[data-beat="1"],
.filmstrip[data-beat="2"] .beat[data-beat="2"],
.filmstrip[data-beat="3"] .beat[data-beat="3"],
.filmstrip[data-beat="4"] .beat[data-beat="4"] {
  opacity: 1; visibility: visible;
}

.beat__h1 {
  font-size: var(--display-2);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-display);
  line-height: var(--leading-tight);
  max-width: 14ch;
}
.beat__lede {
  margin-top: var(--space-md);
  font-size: var(--text-lg);
  max-width: 36ch;
  color: color-mix(in srgb, var(--ref-smoke) 78%, transparent);
}

/* REVEAL WIPE — the resting state the JS tweens toward.
   gsap.from({clipPath:"inset(0 100% 0 0)"}) animates FROM that value TO
   whatever the element already computes. An unstyled element computes
   `clip-path: none`, and there is no meaningful interpolation between an
   inset() and none — so without this declaration the heading snaps rather
   than wipes. Percent units on BOTH ends so the tween runs 100% -> 0% and
   never resolves the closing unit to px. */
[data-fx="wipe"] {
  clip-path: inset(0% 0% 0% 0%);

  /* The padding is NOT spacing — it is what stops the wipe shaving the type.
     These headings run --leading-tight (line-height 1.0), so the glyph ink
     overflows the border box: measured 8px above the ascenders and 7.3px
     below the descender of "grow" at a 70.72px font size. inset(0) clips to
     the border box, so it was slicing the tops and tails off every heading.
     Padding grows the box to cover the ink; the matching negative margin
     gives the space straight back, so layout is untouched. Fixing it here
     keeps the effect's own parameters exactly as supplied. */
  padding-block: 0.16em;
  margin-block: -0.16em;
}

.beat__eyebrow {
  font-size: var(--text-2xs); font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-label); text-transform: uppercase;
  color: var(--ref-peach);
  margin-bottom: var(--space-xs);
}
.beat__h2 {
  font-size: var(--display-3);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-display);
  line-height: var(--leading-tight);
  max-width: 16ch;
}
.beat__copy {
  margin-top: var(--space-md);
  font-size: var(--text-lg);
  max-width: 42ch;
  color: color-mix(in srgb, var(--ref-smoke) 80%, transparent);
}

/* --- film furniture */
.film__meta {
  position: absolute;
  left: 0; bottom: var(--space-xl);
  width: 100%;
  padding-inline: max(var(--gutter), calc((100vw - var(--container)) / 2 + var(--gutter)));
  display: flex; align-items: center; gap: var(--space-sm);
}
.film__rail {
  flex: 1; max-width: 320px; height: 2px;
  border-radius: var(--radius-full);
  background: color-mix(in srgb, var(--ref-smoke) 26%, transparent);
  overflow: hidden;
}
.film__fill {
  display: block; height: 100%; width: 100%;
  background: var(--ref-peach);
  transform: scaleX(0); transform-origin: 0 50%;
}
.film__count {
  font-size: var(--text-xs); font-variant-numeric: tabular-nums;
  letter-spacing: var(--tracking-label);
  display: flex; gap: var(--space-3xs);
}
.film__n { font-weight: var(--weight-bold); color: var(--ref-peach); }
.film__d { color: color-mix(in srgb, var(--ref-smoke) 55%, transparent); }

.film__cue {
  position: absolute; right: var(--gutter); top: 50%;
  transform: translateY(-50%) rotate(90deg);
  transform-origin: 100% 50%;
  font-size: var(--text-2xs); letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: color-mix(in srgb, var(--ref-smoke) 60%, transparent);
  transition: opacity var(--dur-base) var(--ease-out);
}
.filmstrip:not([data-beat="0"]) .film__cue { opacity: 0; }

/* ══ SHARED SECTION FURNITURE ════════════════════════════════════════════ */

.sec { padding-block: var(--section-y); position: relative; }
/* Clear the fixed bar, plus a little air. --space-xl alone was 68px against a
   74px header, so #ask landed 6px underneath it. That anchor is live now:
   the header CTA points at it. */
.sec[id] { scroll-margin-top: calc(var(--hdr-h) + var(--space-sm)); }

.sec__head { margin-bottom: var(--space-2xl); }
.sec__title {
  font-size: var(--display-3); font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-display); line-height: var(--leading-tight);
  max-width: 18ch;
}
.sec__sub {
  margin-top: var(--space-md);
  font-size: var(--text-lg); color: var(--color-ink-muted);
  max-width: 46ch;
}

.kicker {
  display: inline-block; margin-bottom: var(--space-md);
  padding: 6px 14px;
  border: 1px solid var(--color-line-strong); border-radius: var(--radius-full);
  font-size: var(--text-2xs); font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-label); text-transform: uppercase;
  color: var(--color-ink-muted);
}
/* flags the placeholder testimonial block in the UI itself */
.kicker--warn {
  color: var(--color-accent-text);
  border-color: var(--color-accent);
  border-style: dashed;
}

/* ══ 3 · SERVICES ═════════════════════════════════════════════════════════
   ONE tall track with ONE sticky stage, the same shape as .filmstrip above.
   Track height is the scrub length: change the svh figure and all four moves
   retime together with no code change. */

.sec--industries { padding-bottom: 0; }

/* --warm HAS TO BE DECLARED HERE, not left to the var() fallback on the
   layer that reads it. A fallback is what a property resolves to when it has
   no value, it is NOT a value: GSAP reads the property to find the start of
   its tween, finds nothing, and cuts straight to the end instead of easing.
   The symptom was the ground light snapping off at 0.40 rather than fading
   out across 0.40 to 0.54. */
.inds { position: relative; height: 380svh; --warm: 1; }

/* Phone, cards and badge all live in one grid cell so they overlap without a
   magic negative margin. The reference uses margin-top:-520px against a fixed
   760px mockup, which is only correct at its own breakpoint. */
.inds__stage {
  position: sticky; top: 0;
  height: 100svh;
  display: grid; grid-template-columns: minmax(0, 1fr);
  align-content: center;
  overflow: hidden;                 /* clips the group as it leaves frame */
}

/* ── THE GROUND IS LIT ────────────────────────────────────────────────────
   This section used to be the one stretch of the page with no colour in it,
   which is a strange thing for the section about ground to be, with a lit
   landscape sitting in the footer two sections later. Two lights: a low warm
   one rising from under the stage, which is the same idea as the footer's
   horizon, and a softer one behind the mockup so the cards are read against
   something rather than against nothing.

   IT LIVES ON THE STAGE, NOT ON THE SECTION, and it has to. The section is
   380svh tall and the stage is the 100svh that is sticky, so a gradient on
   the section would slide up the screen while the pinned content stayed
   still. On the stage it holds position for the whole scrub.

   Absolutely positioned so it does not become a grid item, z-index 0 against
   the group's 1, because the stage is sticky and therefore positioned: a
   positioned pseudo-element would otherwise paint over the cards.

   --warm is faded to 0 by homepage.js as the mockup leaves, so the aperture
   opens against flat page colour. The plus in the badge is painted a flat
   --color-bg, and a lit ground behind a flat shape scaling to 458x would show
   as a hard edge sweeping outward. */
.inds__stage::before {
  content: "";
  position: absolute; inset: 0; z-index: 0;
  opacity: var(--warm, 1);
  background:
    radial-gradient(62% 46% at 50% 52%,
      color-mix(in srgb, var(--ref-coral) 15%, transparent) 0%,
      transparent 70%),
    radial-gradient(115% 62% at 50% 104%,
      color-mix(in srgb, var(--ref-firebrick) 34%, transparent) 0%,
      color-mix(in srgb, var(--ref-coral) 10%, transparent) 42%,
      transparent 72%);
}
/* WIDER THAN THE PAGE CONTAINER, on purpose. Six cards in 75rem leaves each
   one 187px, and "Software and HiTech" does not survive that. --container-wide
   puts them back at the ~226px the five service cards used to have, so the
   card design did not have to change when the count did. */
/* ── THE VALLEY ───────────────────────────────────────────────────────────
   Layer fills are the SCENE GREENS mixed into the page's own ground at rising
   percentages toward the viewer, the same construction the footer's ridges
   use. That is what makes it survive the theme flip: the layers are defined
   by their distance from the ground colour, not by fixed darks. On black the
   valley reads as deep green under a low sun; on smoke it reads as a hazier,
   paler one, which is the same picture in different weather.

   Bottom aligned, width 100%, height from the viewBox. A wider window gets a
   bigger valley rather than a stretched one.

   --ref-valley and --ref-canopy are the only two colours on this page that
   are not brand colours, and they are scene paint only. See tokens.css. */
.valley {
  position: absolute; inset: 0; z-index: 0;
  opacity: var(--warm, 1);
  pointer-events: none;
}
.valley__l {
  position: absolute; left: 0; bottom: 0;
  width: 100%; height: auto; display: block;
}
.valley__l--far  { fill: color-mix(in srgb, var(--ref-valley) 26%, var(--color-bg)); }
.valley__l--mid  { fill: color-mix(in srgb, var(--ref-valley) 52%, var(--color-bg)); }
.valley__l--near { fill: color-mix(in srgb, var(--ref-canopy) 78%, var(--color-bg)); }

/* Darker than the slope they stand on, so they read as silhouettes against
   it rather than as decoration laid over it. */
.valley__trees { fill: color-mix(in srgb, var(--ref-canopy) 88%, var(--color-bg)); }

/* Catching the same low sun that lights the sky above it. Coral, not peach:
   peach against this green goes yellow and reads as a sandbank. */
.valley__river { fill: color-mix(in srgb, var(--ref-coral) 62%, var(--color-bg)); }

.inds__grp { grid-area: 1 / 1; display: grid; grid-template-columns: minmax(0, 1fr);
             max-width: var(--container-wide);
             position: relative; z-index: 1; }   /* over the lit ground */
.phone { grid-area: 1 / 1; justify-self: center; align-self: center; }
.fan   { grid-area: 1 / 1; align-self: center; }

.phone {
  width: 23rem;
  height: min(44rem, 80svh);
  padding: var(--space-sm);
  border: 3px solid var(--color-ink);
  border-radius: var(--radius-2xl);
  flex-shrink: 0;
}
.phone__top {
  display: flex; align-items: center; justify-content: space-between;
  padding: var(--space-xs) var(--space-sm);
  border: 1px solid var(--color-line-strong);
  border-radius: var(--radius-lg);
}
.phone__word {
  font-family: var(--font-logo); font-weight: 400; font-synthesis: none;
  letter-spacing: 0; font-size: var(--text-lg);
}
/* same measured geometry as .hdr__stop, see THE WORDMARK */
.phone__stop {
  display: inline-block;
  width: 0.168em; height: 0.168em;
  margin-left: 0.116em; margin-bottom: 0.039em;
  vertical-align: baseline; border-radius: var(--radius-full);
  background: var(--color-accent);
}
.phone__glyph svg { display: block; width: 26px; height: 11px; fill: currentColor; }

/* Was a two item list, a label plus an "Explore" link. The link went where
   the cards already go, so it was a second door to one room; the label was
   the word "Sectors", which is the generic noun this whole section exists to
   avoid. One line now, and it makes a claim rather than naming a category. */
.phone__line {
  margin-top: var(--space-md);
  font-size: var(--text-2xl); font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-tight); line-height: var(--leading-snug);
}

/* --- the five cards ---------------------------------------------------
   The reference parks the start offsets in CSS as translate(calc(200% + 20px))
   and friends. They live in JS here instead, as xPercent plus a pixel gap,
   because a px transform baked at load is wrong after the first resize and
   xPercent is re-resolved against the element's current width. */
.fan {
  display: grid; grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: var(--space-sm);

  /* The drop off centre is `translate`, NOT margin-top. align-self:center
     centres the MARGIN box, so a margin only moves the row by half its value
     and every adjustment has to be doubled in your head. This is 1:1: raise it
     and the row moves down by exactly that much.

     Was 5rem, which pushed the row clear of the "Explore" link that used to
     sit in the mockup. That link is gone, so the row comes back up and the
     space inside the phone divides more evenly between the card above it and
     the badge below. */
  translate: 0 2rem;
}

/* The slot is what the fan moves. The card is what the pointer moves. They
   have to be two elements: both effects write `transform`, GSAP flattens
   transform into one matrix, and whichever ran last would win. Hovering a card
   while the page was still scrubbing would have thrown it out of the fan. */
.ind-slot { display: grid; }

.ind {
  position: relative;
  /* NO isolation, NO overflow, NO filter, NO clip-path HERE. Every one of
     those is a CSS grouping property, and a grouping property silently resets
     transform-style to flat. The 3D tilt below would still "work" in the
     sense that GSAP would write the rotation, and the card would simply
     render flat with no error anywhere to explain why. The glow is kept in
     place by DOM order instead, which is what sorts coplanar children under
     preserve-3d regardless. */
  transform-style: preserve-3d;
  display: flex; flex-direction: column;
  /* 19rem while these were service cards. Shorter now: six sectors need less
     card than five capabilities did, and the height it gives back is what
     lets the badge sit clear of the mockup's bottom edge. */
  min-height: 16rem;
  padding: var(--space-md);
  border-radius: var(--radius-lg);
  /* Not flat ink any more. Six per cent of the card's own accent warms the
     ground just enough that the six read as a family of related things rather
     than six identical black rectangles, and it is far too little to move the
     text contrast, which is 18:1 either way. */
  background: color-mix(in srgb, var(--ind-accent) 6%, var(--color-ink));
  border: 1px solid color-mix(in srgb, var(--ind-accent) 20%, transparent);
  color: var(--color-bg);
  /* They hang ABOVE the valley, so they have to cast onto it. Without this
     they read as holes cut in the scene rather than objects in front of it. */
  box-shadow: 0 18px 38px -16px color-mix(in srgb, var(--ref-black) 62%, transparent);
  transition: border-color var(--dur-base) var(--ease-out),
              background   var(--dur-base) var(--ease-out),
              box-shadow   var(--dur-base) var(--ease-out);
}
.ind:hover {
  text-decoration: none;
  background: color-mix(in srgb, var(--ind-accent) 11%, var(--color-ink));
  border-color: color-mix(in srgb, var(--ind-accent) 52%, transparent);
  box-shadow: 0 34px 62px -18px color-mix(in srgb, var(--ref-black) 78%, transparent);
}
.ind:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--ind-accent); }

/* THE LIGHT FOLLOWS THE POINTER. homepage.js writes --mx and --my as the
   pointer crosses the card and this paints a soft pool of the card's accent
   at that spot, so the cursor reads as the thing warming the ground. Opacity
   rather than display, so it fades rather than snapping on.

   Its own element rather than a pseudo-element because the pointer position
   is per card and a pseudo cannot be targeted by GSAP if the effect ever
   needs tweening. inset:0 with the card's radius so the pool is clipped to
   the card's corners. */
.ind__glow {
  position: absolute; inset: 0;
  border-radius: inherit;
  opacity: 0;
  transition: opacity var(--dur-slow) var(--ease-out);
  background: radial-gradient(16rem 16rem at var(--mx, 50%) var(--my, 50%),
    color-mix(in srgb, var(--ind-accent) 40%, transparent) 0%,
    color-mix(in srgb, var(--ind-accent) 12%, transparent) 38%,
    transparent 70%);
}
.ind:hover .ind__glow { opacity: 1; }

/* Four brand accents, six sectors, so the scale CYCLES a b c d a b rather
   than inventing two more colours. The mega menu runs the same cycle in the
   same order, so a sector carries the same tint in both places.

   ON THE CARD, NOT ON THE ICON, since 2026-08-22. The accent is no longer
   just the icon's background: the card's ground, its border, its hover state
   and the pool of light under the pointer are all mixed from it, so it has to
   be inherited by everything inside the card. --ind-on rides along because
   Firebrick is the one dark enough to need white on it. */
.ind--a { --ind-accent: var(--ref-peach);      --ind-on: var(--ref-black); }
.ind--b { --ind-accent: var(--ref-coral);      --ind-on: var(--ref-black); }
.ind--c { --ind-accent: var(--ref-red-orange); --ind-on: var(--ref-black); }
.ind--d { --ind-accent: var(--ref-firebrick);  --ind-on: var(--ref-white); }

.ind__ico {
  display: grid; place-items: center;
  width: 46px; height: 46px; border-radius: var(--radius-md);
  background: var(--ind-accent); color: var(--ind-on);
}
.ind__ico svg { width: 1.4rem; height: 1.4rem; fill: none; stroke: currentColor;
                stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }

/* THE CARD IS NOT A PICTURE OF A CARD, it has depth. Lifting the icon and the
   name off the card's own surface means that when the card tilts toward the
   pointer they travel further than the surface does, which is the difference
   between a rotating rectangle and something with objects sitting on it.
   Costs nothing: these are static transforms the browser resolves once, and
   GSAP never touches these elements. */
.ind__ico { transform: translateZ(34px); }
.ind__h   { transform: translateZ(20px); }
.ind__p   { transform: translateZ(12px); }

/* text-lg, not the text-xl a card this size would otherwise take. A sixth of
   the wide container is ~226px and "Manufacturing" is one unbreakable
   13-character word; at text-xl it runs past the card edge. */
.ind__h {
  margin-top: auto;
  font-size: var(--text-lg); font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-tight); line-height: var(--leading-snug);
}

/* Names the hard ground rather than the fix. Mixed from --color-bg and not
   from currentColor: currentColor mixes multiply if anything inside ever
   re-mixes them, and this card already sets its own inverse colour. */
.ind__p {
  margin-top: var(--space-3xs);
  font-size: var(--text-sm); line-height: var(--leading-snug);
  color: color-mix(in srgb, var(--color-bg) 62%, transparent);
}

/* --- the aperture -----------------------------------------------------
   Sits in the same cell as the phone and starts life parked inside it; JS
   measures the mockup and sets the offset, because it depends on the phone's
   resolved height and that is a clamp against svh.

   56px is the badge's resting size and JS divides by it to work out how far
   the shape has to scale, so the two must agree.

   NO will-change HERE, however much it looks like it belongs.
   will-change:transform promotes the shape to its own compositor layer, which
   is rasterised ONCE at 56px and then stretched as a bitmap. By the time the
   tween is scaling it past 400x the aperture is a blurred grey smear. Left
   alone, the browser re-rasterises the SVG at each step and the edges stay
   sharp. The translate-only tracks in the marquee do want it; anything that
   scales does not. */
.iris {
  grid-area: 1 / 1;
  justify-self: center; align-self: center;
  width: 56px; height: 56px;
  transform-origin: 50% 50%;
  z-index: 2;                       /* opens OVER the phone and the cards */
}
.iris circle { fill: var(--color-ink); }   /* the band's ground */
.iris path   { fill: var(--color-bg);  }   /* the page's ground */

/* ══ SECTION 3 ON A PHONE ═════════════════════════════════════════════════
   THIS BLOCK WAS MISSING. homepage.js has driven --rise from 100% to 0% below
   1025px since the initial import, and no rule in this file has ever consumed
   it: `git log -S "var(--rise)"` over every revision of this stylesheet
   returns nothing. So on a phone the fan stayed a SIX COLUMN grid, which at
   390px is six 45px slivers, and the sticky 380svh track held them there for
   three screens. That is what Suresh reported on 2026-08-29, and it was never
   right rather than having regressed.

   The desktop sequence cannot survive a phone and is not meant to. The fan
   needs six columns to fan into and the aperture needs 260svh of scroll to be
   worth taking. Both are released here; what replaces them is the cards
   planting one at a time, which is the move the footer wordmark makes and the
   thing the brand line claims.

   THE BREAKPOINT IS DUPLICATED IN homepage.js ON PURPOSE. CSS releases the
   track and collapses the grid; JS declines to build the fan and builds the
   rise instead. It is gsap.matchMedia rather than an innerWidth check, because
   matchMedia REVERTS its tweens on the way out: without it, rotating a tablet
   leaves six cards frozen mid-fan on top of each other.
   ═════════════════════════════════════════════════════════════════════════ */
@media (max-width: 1024px) {

  /* Release the pinned track. The 380svh only exists to buy scroll for a fan
     and an aperture that are both gone below this width. */
  .inds { height: auto; }

  .inds__stage {
    /* RELATIVE, NOT STATIC. .valley is position:absolute inset:0 against this
       box. Drop the stage to static and the nearest positioned ancestor
       becomes .inds, so the valley stretches over the whole released track
       instead of sitting behind the cards. */
    position: relative;
    height: auto;
    /* THE BASE RULE'S overflow: hidden IS KEPT, and releasing it was a bug.
       "Nothing leaves frame here" is true vertically and false horizontally:
       .valley__l is deliberately 170% wide and pulled to left:-35% just below,
       so with the clip released it pushed 272px of horizontal scroll onto the
       page at 834px wide. The stage is auto height and in flow here, so hidden
       costs nothing and the valley is clipped to the cards it sits behind. */
    padding-block: var(--space-xl);
  }

  /* Both are desktop devices with nothing to do here. The mockup exists for
     the cards to fan out of, and the aperture exists to open onto the band
     over 260svh of scroll. The section keeps its own .sec__title, so hiding
     the phone costs only its screen line. */
  .phone, .iris { display: none; }

  .fan {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-md);
    translate: none;                 /* the 2rem drop was an offset from the phone */
    max-width: 32rem; margin-inline: auto;
  }

  /* THE REVEAL, AND IT IS ON THE SLOT. clip-path resets transform-style to
     flat, and .ind must never carry it: that is what silently kills the 3D
     tilt on the desktop magnet. The slot has no 3D role at any width.
     `round` is not optional or the wipe squares off the corners.
     The resting value is 0%, so with no JavaScript the cards are simply
     visible rather than clipped to nothing. */
  .ind-slot {
    --rise: 0%;
    clip-path: inset(var(--rise) 0 0 0 round var(--radius-lg));
  }

  /* SVG height comes from width, so at 390px a 100% wide valley collapses to a
     strip about 140px tall and reads as a green stripe rather than a place.
     Overshooting the width and pulling it left keeps the crest and the dip. */
  .valley__l { width: 170%; left: -35%; }

  /* THE BAND HANDOVER, which the aperture normally performs. It opens onto
     --color-ink and the next section is that colour; with the aperture gone
     the section would end on a hard horizontal cut into the band. This fades
     the ground into it over the last stretch instead. Same colour on both
     sides of the seam, so there is nothing to line up. */
  .inds::after {
    content: ""; position: absolute; inset: auto 0 0 0;
    height: 28svh; z-index: 1; pointer-events: none;
    background: linear-gradient(to bottom, transparent, var(--color-ink));
  }
}

/* Two columns while there is room for two, so a row plants as a pair, which is
   the reading order anyway. Below this each card gets the width to itself. */
@media (max-width: 1024px) and (min-width: 721px) {
  .fan { grid-template-columns: repeat(2, minmax(0, 1fr)); max-width: 44rem; }
}

/* ══ THE INVERSE BAND ═════════════════════════════════════════════════════
   The pillars, the marquee and the assistant are ONE INVERSE BAND, and that
   is not a styling choice, it is what makes the aperture work. The hole in the plus is
   painted --color-ink; when it finishes opening, the thing behind it has to be
   the same colour or the handoff shows as a hard cut. So the band is
   --color-ink and its type is --color-bg, and both follow the theme, which
   means the band is black on a light page and smoke on a dark one.

   THE ACCENT HAS TO FLIP THE OTHER WAY HERE, for the same reason it does on
   the aperture: the page's --color-accent-text is chosen for the PAGE ground.
   Inheriting it puts Firebrick on black (3.09:1) or Red Orange on smoke
   (2.85:1). --band-accent is remapped in the same three places tokens.css
   remaps the real one, and moves in the opposite direction. */

/* --band-on-accent EXISTS BECAUSE --color-on-accent IS THE WRONG PARTNER HERE.
   That token is paired with --color-accent, and the band flips its accent in
   the OPPOSITE direction, so borrowing it inverts the pairing: it puts white
   on Red Orange (3.12:1) and black on Firebrick (3.09:1), and both fail. The
   correct partners are the other way round. Red Orange is a light colour and
   wants black on it at 6.69:1; Firebrick is a dark one and wants white at
   6.79:1. Anything that uses --band-accent as a BACKGROUND must take its
   foreground from here, never from the page-level token. */
.band { background: var(--color-ink); color: var(--color-bg);
        --band-accent:    var(--ref-red-orange);   /* 6.69:1 on black */
        --band-on-accent: var(--ref-black); }      /* 6.69:1 on red orange */
[data-theme="dark"] .band { --band-accent:    var(--ref-firebrick);  /* 6.18:1 on smoke */
                            --band-on-accent: var(--ref-white); }    /* 6.79:1 on firebrick */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .band { --band-accent:    var(--ref-firebrick);
                                          --band-on-accent: var(--ref-white); }
}

/* The band opens straight out of the aperture, so it must not start with a
   gap of its own or the reveal lands on empty ground. */
/* Not zero. The aperture hands off black to black, so top padding here costs
   nothing visually and keeps the kicker off the header once the reader
   scrolls past the reveal. */
/* ══ 4 · THE FOUR PILLARS ═════════════════════════════════════════════════
   Four panels that STACK. Each is position:sticky at the top of the viewport,
   so it stays put while the next scrolls up over it, and homepage.js scales
   and dims the covered one.

   POSITION:STICKY, NOT ScrollTrigger pin. The technique this came from uses
   `pin: true, pinSpacing: false`. ADR-0006 am.2 rules pin out for this site:
   it injects a pin-spacer into the DOM and fights the iOS address bar, and
   Lenis leaves document flow alone so sticky needs none of that. Four sticky
   siblings in one container ARE the stack, with no JS holding anything.

   The aperture in section 3 opens onto the band's ground, so this section
   must keep its ink and must not open with a gap. Top padding is not zero
   because ink meets ink, and the kicker needs to clear the header once the
   reader is past the reveal. Same reasoning the bench carried before it. */

.sec--method { padding-top: var(--space-2xl); padding-bottom: 0; }

.pillars { position: relative; }

/* Each panel gets exactly ONE SCREEN and no more. The reference solves tall
   panels with a fake-scroll tween and a computed bottom margin; designing the
   content to fit removes the need for it, and a homepage panel whose bottom
   nobody reaches is a panel nobody reads. */
.pil {
  position: sticky; top: 0;
  height: 100svh;
  display: grid;
  padding: var(--space-sm);
}

.pil--a { --pil-accent: var(--ref-peach);      }
.pil--b { --pil-accent: var(--ref-coral);      }
.pil--c { --pil-accent: var(--ref-red-orange); }
.pil--d { --pil-accent: var(--ref-firebrick);  }

/* THE PANEL IS WHAT MOVES, NOT THE STICKY BOX. Scaling .pil would scale its
   own padding, and a transform on a sticky element makes it a containing
   block for anything inside it. The panel is a plain child, so it can be
   scaled freely.

   Lifted off the band's ink by a little of its own accent, which is what
   makes the shrinking one visible against the ink behind it. Flat ink panels
   on an ink ground would stack invisibly. */
.pil__panel {
  height: 100%;
  display: flex; flex-direction: column;
  padding: clamp(var(--space-md), 3.4vw, var(--space-2xl));
  border-radius: var(--radius-2xl);
  /* Named, because the before/after drawing has to mix against the exact
     ground it is drawn on and recomputing that expression in four places is
     how two of them end up disagreeing. */
  --pil-ground: color-mix(in srgb, var(--pil-accent) 10%, var(--color-ink));
  background: var(--pil-ground);
  border: 1px solid color-mix(in srgb, var(--pil-accent) 30%, transparent);
  transform-origin: 50% 42%;        /* shrinks toward the reader's eyeline */
}

/* THE ACCENT IS NOT ALLOWED ON THIS TEXT, however much it wants to be. The
   panel's ground flips with the theme and --pil-accent does not: on the light
   theme's black band only Red Orange clears AA, on the dark theme's smoke
   band only Firebrick does, and these four are fixed one per pillar. Measured
   as text it came out at 2.98 and 1.51.

   So the pillar keeps its colour where colour is a graphic, in the number
   pill, the rule under each label, the border and the panel's own ground, and
   the words go back to the band's ink. The pillar is still unmistakably its
   own; you just do not have to squint at the name of it. */
.pil__eyebrow {
  display: flex; align-items: baseline; gap: var(--space-xs);
  font-size: var(--text-sm); font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-label); text-transform: uppercase;
  color: color-mix(in srgb, var(--color-bg) 82%, transparent);
}
.pil__no {
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  padding: 4px 8px; border-radius: var(--radius-full);
  background: color-mix(in srgb, var(--pil-accent) 34%, transparent);
  color: var(--color-bg);
  letter-spacing: 0;
}

/* The problem, in the words a client would use for it. text-3xl and not the
   section display size: this is one of four, and four display headings in a
   row stop being emphasis. */
.pil__h {
  margin-top: var(--space-md);
  max-width: 20ch;
  font-size: var(--text-3xl); font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-display); line-height: var(--leading-tight);
}

/* ── BEFORE AND AFTER ─────────────────────────────────────────────────────
   The panel had a dead band between the problem and the method, and the one
   thing worth putting in it is the claim the whole company is named for. So
   each pillar shows the same patch of ground twice: what it looks like now,
   and what it looks like when we are done. Labelled with the tagline's own
   two halves, so every pillar spells it out again without writing it.

   Drawn in the vocabulary the rest of the page already uses. Ground mass,
   marks on it, saplings, a light. The same shapes as the footer's ridges and
   the valley behind the industries, so this reads as another view of one
   world rather than a diagram borrowed from somewhere else.

   Each drawing says that pillar's specific thing:
     ERP        four levels and four answers become one level and one answer
     eCommerce  a gap becomes a crossing that carries both ways
     Data       every size at every height becomes one row on one line
     Cloud      left alone with one already down becomes standing, and lit

   margin-block:auto floats it in whatever room the panel has left after the
   heading has taken however many lines it needs. */
.pil__ba {
  margin: auto 0;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: end;
  gap: var(--space-md);
}
.pil__ba-side { min-width: 0; }

.pil__ba-label {
  margin-bottom: var(--space-2xs);
  font-size: var(--text-2xs); font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-label); text-transform: uppercase;
  color: color-mix(in srgb, var(--color-bg) 62%, transparent);
}

.pil__ba-art { display: block; width: 100%; height: auto; }

.pil__ba-turn { display: grid; place-items: center; padding-bottom: var(--space-lg); }
.pil__ba-turn svg {
  width: 22px; height: 22px;
  fill: none; stroke: color-mix(in srgb, var(--color-bg) 72%, transparent);
  stroke-width: 1.75; stroke-linecap: round; stroke-linejoin: round;
}

/* THE ACCENT DOES NOT DO THE CONTRAST WORK HERE, for the same reason it was
   taken off the eyebrow: it is a fixed colour per pillar and the ground it
   sits on inverts with the theme. Peach is light and Firebrick is dark, so no
   single tone can clear 3:1 against all four in both themes. Measured before
   this was fixed: the ground mass read 1.08 against the panel and what stood
   on it read 1.40. The drawing was there and invisible.

   So everything is built off ONE verified base. --ba-bare is the panel's own
   ground lifted toward its ink by a fixed 46%, which is the number that
   clears 3:1 at both ends (30% still sat at 2.06). Every other fill is that
   base pushed toward a hue, so the luminance stays in the neighbourhood that
   was measured, and anything standing ON the ground is near-ink, where the
   contrast is structural rather than lucky. */
.pil__ba { --ba-bare: color-mix(in srgb, var(--color-bg) 46%, var(--pil-ground)); }

/* Hard ground and habitable ground. Same shape, and the reader is meant to
   compare them across the arrow, so they differ in HUE at a similar
   luminance rather than one being simply darker than the other. The green is
   the valley's own, the one behind the industries section. */
.ba__earth { fill: var(--ba-bare); }
.ba__green { fill: color-mix(in srgb, var(--ref-valley) 64%, var(--ba-bare)); }

/* Everything that stands on the ground is near-ink, so it reads against bare
   and green alike without a second set of values per surface. The tint is
   just enough to belong to the scene. */
.ba__tree { fill: color-mix(in srgb, var(--color-bg) 80%, var(--ref-canopy)); }
.ba__folk { fill: color-mix(in srgb, var(--color-bg) 92%, var(--pil-accent)); }
.ba__mark { fill: color-mix(in srgb, var(--color-bg) 92%, var(--pil-accent)); }
.ba__sun  { fill: color-mix(in srgb, var(--color-bg) 84%, var(--pil-accent)); }

/* The two places colour is allowed to be colour, because neither carries the
   meaning on its own: the water under the crossing, and the light around the
   sun. Coral for the water, which is the same warm the valley's river uses. */
.ba__water { fill: color-mix(in srgb, var(--ref-coral) 66%, var(--ba-bare)); }
.ba__glow  { fill: color-mix(in srgb, var(--pil-accent) 30%, transparent); }

/* The level all four terraces should have shared, and did not. Deliberately
   faint: it is a reference, and the four different heights are what carry the
   meaning. */
.ba__ghost {
  fill: none; stroke-width: 2; stroke-dasharray: 7 7;
  stroke: color-mix(in srgb, var(--color-bg) 38%, transparent);
}

.pil__sr {
  position: absolute; width: 1px; height: 1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}

/* Discovery, design, build, run, folded into three columns the reader can
   take in at a glance. margin-top:auto drops them to the foot of the panel so
   the problem and the method sit at opposite ends of the screen. */
/* NO margin-top:auto HERE ANY MORE. The before/after figure above carries
   `margin-block: auto` and absorbs all the slack in the panel, which centres
   it between the heading and this grid and leaves the grid sitting at the
   foot on its own. Two competing auto margins would have split the space
   unevenly, giving twice as much gap below the figure as above it. */
.pil__grid {
  display: grid;
  /* auto-fit RATHER THAN repeat(3), and no media query. With three items
     auto-fit collapses the empty tracks, so a wide panel still gets exactly
     three columns; a narrow one drops to two and then one on its own. The
     fixed repeat(3) held at EVERY width, which on a phone gave three columns
     of body copy about 110px wide.

     min(100%, 14rem) is the floor, not a bare 14rem: on a container narrower
     than 14rem a bare minimum overflows the panel rather than shrinking.
     minmax(0, ...) for the reason recorded against .fan, an auto floor is
     min-content and one long word would widen its whole column. */
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
  gap: var(--space-md) var(--space-lg);
  padding-top: var(--space-xl);
}
.pil__label {
  margin-bottom: var(--space-2xs);
  padding-bottom: var(--space-2xs);
  border-bottom: 1px solid color-mix(in srgb, var(--pil-accent) 34%, transparent);
  font-size: var(--text-2xs); font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-label); text-transform: uppercase;
  color: color-mix(in srgb, var(--color-bg) 62%, transparent);
}
.pil__t { font-size: var(--text-base); line-height: var(--leading-normal); }

.pil__plat {
  margin-top: var(--space-md);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  color: color-mix(in srgb, var(--color-bg) 62%, transparent);
}

/* MORE ON TOP THAN UNDERNEATH, and the asymmetry is the point. The pillar
   stack above ends with `padding-bottom: 0`, because the last sticky panel has
   to finish flush against its container or the stack unsticks early. That
   leaves nothing between the bottom edge of a full-screen panel and a line of
   75px type, so the marquee read as if it were sitting ON the panel rather
   than after it. All the clearance has to come from this side of the join. */
.marq {
  overflow: hidden;
  padding-top: var(--space-2xl);
  padding-bottom: var(--space-xl);
}
.marq__run { display: flex; width: max-content; will-change: transform; }

.marq__item {
  display: flex; align-items: center;
  padding-right: 0.28em;
  /* display-3, not display-2, and the reason is measured. The run pans by
     `runWidth - sectionWidth` while the strip crosses the screen, and that
     scroll window is capped by the viewport height, so the ONLY real lever on
     reading speed is how wide the run is. At display-2 this line ran about
     50px per character and panned at 3.4x scroll speed, which is a sentence
     nobody finishes. display-3 is about 35px per character and brings it to
     roughly 1.4x, which reads. Lengthen the copy and this gets worse in
     direct proportion; there is no scroll setting that rescues it. */
  font-size: var(--display-3); font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-display); line-height: var(--leading-tight);
  white-space: nowrap;
}
.marq__dot {
  display: inline-block; flex-shrink: 0;
  width: 0.14em; height: 0.14em; margin-left: 0.28em;
  border-radius: var(--radius-full);
  background: var(--band-accent);
}
/* The assistant moved to /start-a-conversation on 2026-08-29. Its wrapper
   there is full bleed so the band still reaches both edges. .wrap inside it
   re-measures the content to the same column as the rest of the page. */
.page .askband {
  margin-inline: calc(50% - 50vw);
  padding-block: var(--space-2xl);
}

.marq__sr {
  position: absolute; width: 1px; height: 1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}

/* NOTE ON PLACEMENT: these two blocks sit AFTER every .pil rule on
   purpose. A media query adds no specificity, so `.pil` inside one and
   `.pil` outside one are both (0,1,0) and the LATER rule wins. The first
   version of this was written above the pillars section and the base
   `position: sticky` simply overrode it, with no error and no warning.
   Anything overriding a bare class has to come after it in the file. */

/* ══ THE PILLAR STACK ON A PHONE ══════════════════════════════════════════
   THIS BLOCK WAS MISSING, and it is the second instance of exactly the fault
   found in section 3 on 2026-08-29. homepage.js has a mmPil branch for
   (max-width: 1024px) whose own comment reads "Below the stack's breakpoint
   the panels are ordinary blocks, so they get the same treatment every other
   block on this page gets". Nothing in this stylesheet ever made them ordinary
   blocks. `.pil` stayed `position: sticky; height: 100svh` at every width, so
   on a phone four panels were each pinned to one screen while holding a
   heading, a before/after figure, three steps and a platform line. The content
   did not fit and the bottom of every panel was cut off.

   The JS wrote a mobile branch for both sections. The CSS got neither. If a
   third `mmSomething.add("(max-width: ...)")` turns up in homepage.js, check
   this file for its counterpart before assuming it works.
   ═════════════════════════════════════════════════════════════════════════ */
@media (max-width: 1024px) {

  /* Ordinary blocks, which is what the JS branch already believes they are.
     RELATIVE, NOT STATIC: .pil__panel and the figure inside it are positioned
     against this box, so dropping to static hands them up to .pillars. */
  .pil {
    position: relative;
    height: auto;
  }

  /* The 100% was 100% OF A 100svh PARENT. With the parent released it would
     resolve against auto and collapse. */
  .pil__panel { height: auto; }
}

/* The before/after drawing is two scenes either side of a turn arrow. Three
   columns of that on a phone leaves each scene about 150px wide, which is the
   width the project notes already flag as the one where the figures inside it
   stop reading. Stacked, each scene gets the full panel and the arrow turns to
   point at the one below it. */
@media (max-width: 720px) {
  .pil__ba { grid-template-columns: minmax(0, 1fr); }
  .pil__ba-turn { padding-bottom: 0; transform: rotate(90deg); }
}

/* ══ 6 · PLATFORM MARQUEE ═════════════════════════════════════════════════
   Two rows of wordmarks travelling in opposite directions.

   WHY WORDMARKS AND NOT LOGO IMAGES: see the note on the section in
   index.html. Short version, vendor logos are trademarks whose guidelines
   generally restrict use to actual partners, and this site claims no partner
   status because the profile does not support one.

   THE LOOP IS SEAMLESS BECAUSE THE TRACK CARRIES ITS SET TWICE AND TRAVELS
   EXACTLY -50%. Not a pixel figure: a pixel figure is measured once and is
   then wrong after a resize, and wrong again when the webfont lands and every
   word changes width. -50% of a doubled set is the same frame by construction.
   ═══════════════════════════════════════════════════════════════════════ */

.pmarq { padding-block: var(--section-y) var(--space-2xl); overflow: hidden; }
.pmarq__h {
  margin-top: var(--space-2xs); margin-bottom: var(--space-xl);
  font-size: var(--text-2xl); font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-tight);
}
.pmarq__rail { overflow: hidden; }
.pmarq__rail + .pmarq__rail { margin-top: var(--space-sm); }

.pmarq__track {
  display: flex; width: max-content;
  /* will-change is deliberately absent. .ftr is position: fixed and anything
     between <body> and it that carries will-change becomes a containing block
     for fixed descendants, which silently un-fixes the footer. */
}

.pmarq__item {
  flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  margin-right: var(--space-sm);
  padding: var(--space-xs) var(--space-md);
  border: 1px solid color-mix(in srgb, var(--color-bg) 22%, transparent);
  border-radius: var(--radius-full);
  font-size: var(--text-lg); font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-tight);
  white-space: nowrap;
  color: var(--color-bg);          /* .band flips ink and ground */
}

@media (max-width: 720px) {
  .pmarq__item { font-size: var(--text-base); padding: var(--space-2xs) var(--space-sm); }
}

/* ══ 7 · TESTIMONIALS ═════════════════════════════════════════════════════
   SAMPLE CONTENT. .tst__flag says so on the page, and it is not decoration:
   see the note on the section in index.html for why fabricated praise on a
   consultancy homepage is the most dangerous thing that can sit there.
   ═══════════════════════════════════════════════════════════════════════ */

.tst { padding-block: var(--space-2xl) var(--section-y); }
.tst__h {
  margin-top: var(--space-2xs);
  font-size: var(--text-2xl); font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-tight);
}

/* The dashed accent treatment this site uses everywhere it is showing
   something unpublished, so a reader meets one visual language for it. */
.tst__flag {
  margin-top: var(--space-md);
  max-width: var(--container-narrow);
  padding: var(--space-sm) var(--space-md);
  border: 1px dashed var(--band-accent);
  border-radius: var(--radius-lg);
  font-size: var(--text-sm); line-height: var(--leading-normal);
  color: color-mix(in srgb, var(--color-bg) 82%, transparent);
}
.tst__flag-tag {
  display: block; margin-bottom: var(--space-3xs);
  font-size: var(--text-2xs); font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-label); text-transform: uppercase;
  color: var(--band-accent);
}

.tst__grid {
  margin-top: var(--space-xl);
  display: grid; gap: var(--space-md);
  /* minmax(0, ...) never a bare 1fr, whose auto floor is min-content. */
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
}

.tst__card {
  margin: 0;
  display: flex; flex-direction: column;
  padding: var(--space-md);
  border: 1px solid color-mix(in srgb, var(--color-bg) 18%, transparent);
  border-radius: var(--radius-lg);
  background: color-mix(in srgb, var(--color-bg) 5%, transparent);
}

.tst__logo { margin-bottom: var(--space-sm); }
.tst__logo svg { width: 40px; height: 40px; display: block; }
.tst__logo rect {
  fill: none;
  stroke: color-mix(in srgb, var(--color-bg) 34%, transparent);
  stroke-width: 1.5;
}
.tst__logo text {
  fill: var(--band-accent);
  font-family: var(--font-sans);
  font-size: 17px; font-weight: var(--weight-bold);
  letter-spacing: 0.02em;
}

.tst__quote { margin: 0; flex: 1; }
.tst__quote p {
  font-size: var(--text-base); line-height: var(--leading-normal);
  color: var(--color-bg);
}
.tst__quote p::before { content: "\201C"; }
.tst__quote p::after  { content: "\201D"; }

.tst__by {
  margin-top: var(--space-md); padding-top: var(--space-sm);
  border-top: 1px solid color-mix(in srgb, var(--color-bg) 16%, transparent);
  display: grid; gap: 2px;
}
.tst__name { font-size: var(--text-sm); font-weight: var(--weight-bold); color: var(--color-bg); }
/* 78% of the band ground, mixed from a FIXED token rather than currentColor.
   Re-mixing currentColor inside a descendant multiplies the alphas, which is
   how the footer once gave a link 0.6084 for no reason a reader could see. */
.tst__role,
.tst__co { font-size: var(--text-sm); color: color-mix(in srgb, var(--color-bg) 78%, transparent); }
.tst__co { font-weight: var(--weight-medium); }

/* ══ 8 · THE ASSISTANT ════════════════════════════════════════════════════
   Sits on the band, so it inherits the ink ground and --band-accent, and the
   seam into the footer reveal below it needs no work.

   The panel is deliberately the SAME OBJECT as a pillar panel: rounded, an
   accent-tinted ground, a hairline border. This is the last thing on the page
   before the footer and it is asking for someone's phone number, so it should
   look like it belongs to everything above it rather than like a widget
   someone bolted on. */

.sec--ask { padding-bottom: var(--space-3xl); }

/* FULL WIDTH OF THE CONTAINER, not a 52rem column. Capped, it floated in the
   middle of the band with the section heading left-aligned above it, which
   read as two unrelated things rather than one section. At container width it
   lines up with the heading and the panel finally looks like it belongs to
   the page rather than being dropped onto it.

   The line length problem that cap was solving moves to the bubbles instead,
   where it belongs: see .ask__bubble. This is how every chat interface is
   built, a wide surface with narrow messages on it. */
.ask {
  --ask-ground: color-mix(in srgb, var(--band-accent) 8%, var(--color-ink));
  margin-top: var(--space-xl);
  padding: var(--space-md);
  border-radius: var(--radius-xl);
  border: 1px solid color-mix(in srgb, var(--color-bg) 16%, transparent);
  background: var(--ask-ground);
}

/* A FIXED HEIGHT, NOT min-height. The log grows as the conversation does, and
   a panel that grows pushes the footer down under the reader's cursor while
   they are mid-sentence. Fixed and scrolling keeps the input where they left
   it. overscroll-behavior stops a flick inside the log from grabbing the page,
   and data-lenis-prevent is required on top of that because lenis.stop() only
   halts the scrolling, not the event capture. */
.ask__log {
  height: min(24rem, 52svh);
  overflow-y: auto;
  overscroll-behavior: contain;
  padding-right: var(--space-2xs);
  display: flex; flex-direction: column; gap: var(--space-2xs);
}

.ask__msg { display: flex; }
.ask__msg--bot { justify-content: flex-start; }
.ask__msg--you { justify-content: flex-end; }

.ask__bubble {
  /* THE CH UNIT IS THE POINT. Now the panel is container width, a percentage
     alone would let a bubble run to over 1000px, and a 1000px line of 14px
     text is unreadable no matter how much room there is for it. 46ch holds
     the measure wherever the panel ends up; the 88% only still matters on a
     phone, where 46ch is wider than the panel is. */
  max-width: min(88%, 46ch);
  padding: var(--space-xs) var(--space-sm);
  border-radius: var(--radius-lg);
  font-size: var(--text-sm); line-height: var(--leading-relaxed);
}
.ask__msg--bot .ask__bubble {
  background: color-mix(in srgb, var(--color-bg) 10%, transparent);
  border-bottom-left-radius: var(--radius-sm);
}
/* The visitor's own words in the band accent. --band-accent is the token that
   already flips per theme, so this stays legible on both grounds where the
   page's own accent would not. */
.ask__msg--you .ask__bubble {
  background: var(--band-accent);
  color: var(--band-on-accent);
  border-bottom-right-radius: var(--radius-sm);
}

.ask__cta {
  display: inline-block; margin-top: var(--space-2xs);
  color: var(--band-accent); font-weight: var(--weight-medium);
  border-bottom: 1px solid currentColor;
}

.ask__chips { margin-top: var(--space-sm); }
.ask__chips-h {
  margin-bottom: var(--space-3xs);
  font-size: var(--text-2xs); letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: color-mix(in srgb, var(--color-bg) 62%, transparent);
}
.ask__chip-row { display: flex; flex-wrap: wrap; gap: var(--space-3xs); }
.ask__chip {
  padding: var(--space-3xs) var(--space-xs);
  border: 1px solid color-mix(in srgb, var(--color-bg) 28%, transparent);
  border-radius: var(--radius-full);
  background: none; color: inherit;
  font: inherit; font-size: var(--text-xs);
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease-out),
              background   var(--dur-fast) var(--ease-out);
}
.ask__chip:hover {
  border-color: var(--band-accent);
  background: color-mix(in srgb, var(--band-accent) 12%, transparent);
}

/* 29px is a comfortable chip for a mouse and a mean one for a thumb, and on a
   phone these chips ARE the answer to the new-or-existing question rather than
   a shortcut past typing it. 44px on coarse pointers only, so the desktop
   layout keeps its lighter proportions. */
@media (pointer: coarse) {
  .ask__chip {
    min-height: 44px;
    padding-inline: var(--space-sm);
    font-size: var(--text-sm);
  }
}

.ask__form { margin-top: var(--space-sm); }
/* Visible label, not a placeholder doing the label's job: the capture changes
   what the field is asking for on every step, and a placeholder vanishes the
   moment someone starts typing, which is exactly when they need to remember. */
.ask__label {
  display: block; margin-bottom: var(--space-3xs);
  font-size: var(--text-2xs); letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: color-mix(in srgb, var(--color-bg) 62%, transparent);
}
.ask__row { display: flex; gap: var(--space-2xs); }
.ask__input {
  flex: 1 1 auto; min-width: 0;
  padding: var(--space-xs) var(--space-sm);
  border: 1px solid color-mix(in srgb, var(--color-bg) 28%, transparent);
  border-radius: var(--radius-full);
  background: color-mix(in srgb, var(--color-bg) 6%, transparent);
  color: var(--color-bg);
  font: inherit; font-size: var(--text-sm);
}
.ask__input::placeholder { color: color-mix(in srgb, var(--color-bg) 44%, transparent); }
.ask__input:focus-visible { outline: none; border-color: var(--band-accent); }

.ask__send {
  flex-shrink: 0;
  display: inline-flex; align-items: center; gap: var(--space-3xs);
  padding: var(--space-xs) var(--space-md);
  border: 0; border-radius: var(--radius-full);
  background: var(--band-accent); color: var(--band-on-accent);
  font: inherit; font-size: var(--text-sm); font-weight: var(--weight-medium);
  cursor: pointer;
}
.ask__send svg {
  width: 1em; height: 1em; fill: none; stroke: currentColor;
  stroke-width: 2; stroke-linecap: round; stroke-linejoin: round;
}
.ask__send:hover { background: var(--color-bg); color: var(--color-ink); }

.ask__hint {
  margin-top: var(--space-3xs); min-height: 1.2em;
  font-size: var(--text-xs); color: var(--band-accent);
}

/* Off-screen rather than display:none. A hidden field is not submitted by
   some browsers and is skipped by some bots, and the whole point of a
   honeypot is that a bot fills it in. */
.ask__pot {
  position: absolute; width: 1px; height: 1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}

.ask__legal {
  margin-top: var(--space-sm);
  font-size: var(--text-xs);
  color: color-mix(in srgb, var(--color-bg) 62%, transparent);
}
.ask__legal a { color: var(--band-accent); }

@media (max-width: 720px) {
  .ask__row { flex-wrap: wrap; }
  .ask__send { width: 100%; justify-content: center; }
  .ask__bubble { max-width: 100%; }
}

/* ══ 7 · THE FOOTER ═══════════════════════════════════════════════════════
   A REVEAL FOOTER. The footer is fixed to the viewport floor for the whole
   page; <main> is opaque and rides above it, so the footer is not scrolled
   to, it is UNCOVERED as main's bottom edge sweeps up the screen.

   THE MECHANISM IS THREE RULES AND ONE NUMBER
   -------------------------------------------
     .ftr-slot   an empty box in the flow. It buys the scroll distance.
     .ftr        position: fixed, bottom 0, same height as the slot.
     main        z-index 1 and an opaque ground. The curtain.

   Both the slot and the footer take their height from --ftr-h. Give them
   separate values and the reveal is out by exactly the difference: either the
   footer is still half covered at the bottom of the page, or it finishes
   early and the last screen is dead space.

   NOTHING BETWEEN <body> AND .ftr MAY CARRY transform, filter, perspective,
   backdrop-filter, will-change OR contain: paint. Any one of them makes the
   element a containing block for fixed descendants, at which point .ftr stops
   being fixed to the viewport and starts being absolute inside the slot. The
   symptom is not an error, it is a footer that scrolls normally and a reveal
   that silently does not happen.

   Lenis is fine with this. It moves the real document scroll rather than
   transforming a wrapper, which is exactly why fixed elements still behave.

   WHAT IS REVEALED FIRST IS THE BOTTOM. Main's edge sweeps upward, so the
   order is legal bar, near ridge, wordmark, ridges, columns. The scene is
   built to be read in that order: ground first, then the word growing out of
   it, then the sky.

   THE SCENE
   ---------
   The tagline drawn. Three ridges receding into a lit horizon, a sparse
   treeline on the middle distance, and the wordmark rising out of the nearest
   ridge: hard ground, planted, grown.

   There is no sun in it. THE LOGO'S DOT IS THE SUN, sitting just clear of the
   horizon with the glow radiating from it, which is why the wordmark is set
   at 22% and the stop stays at full Red Orange. That keeps the mark itself
   untouched and correct, and makes the whole sky lit by the brand's own dot
   rather than by a second orange circle competing with it.

   Ridge fills mix Firebrick into --color-ink at rising percentages toward the
   viewer, and the sky fades from pure --color-ink down to the glow, so the
   ridges darken as they approach because they are silhouetted against a
   bright horizon. That is why the scene survives the theme flip: it is
   defined by RELATIVE distance from the ground colour, not by fixed darks.

   The nearest ridge is pure --color-ink, the same value as the top of the
   sky, which is also the .band's ground. So the seam where the band hands
   over to the footer is invisible. Change --scene-near and you get a hard
   horizontal cut at the join.

   The footer carries .band as well as .ftr. That is deliberate reuse, not a
   shortcut: .band already owns the ink ground, the inverse text colour and
   the --band-accent remap that keeps the accent legible on it. The footer is
   a continuation of that band, so it should not have a second copy of those
   three decisions to keep in sync.
   ═══════════════════════════════════════════════════════════════════════ */

:root {
  /* One number, two consumers. Capped under the viewport because a fixed box
     taller than the screen has a bottom nobody can ever scroll to. */
  --ftr-h: min(92svh, 47rem);
}

.ftr-slot { height: var(--ftr-h); }

.ftr {
  position: fixed;
  inset: auto 0 0 0;
  height: var(--ftr-h);
  /* Below main (1), above the canvas. Also makes .ftr a stacking context, so
     every z-index below is local to the footer and cannot reach the header. */
  z-index: 0;
  overflow: hidden;
  display: grid;
  grid-template-rows: 1fr auto;   /* columns take the slack, legal bar hugs */

  /* What .band sets `color` to. Every muted value in this footer mixes from
     THIS token rather than from currentColor, because currentColor mixes do
     not compose: re-mixing one inside a descendant MULTIPLIES the alphas.
     .ftr__col p at 78% with a link inside it also asking for 78% gave the
     link 0.6084, measurably dimmer than the address text it sits in, for no
     reason a reader could see. Mixing from a fixed token means 78% means 78%
     wherever it is written. (Inheriting a currentColor mix is fine; it is
     re-mixing that compounds.) */
  --ftr-ink: var(--color-bg);

  --scene-glow: color-mix(in srgb, var(--ref-firebrick) 34%, var(--color-ink));
  --scene-far:  color-mix(in srgb, var(--ref-firebrick) 20%, var(--color-ink));
  --scene-mid:  color-mix(in srgb, var(--ref-firebrick)  9%, var(--color-ink));
  --scene-near: var(--color-ink);
}

/* ── the scene ──────────────────────────────────────────────────────────
   Two decorative containers, not one, because the wordmark has to sit
   BETWEEN them: behind the near ridge so the ground cuts it, in front of the
   middle distance. The word is real content and belongs in the document, so
   it cannot live inside an aria-hidden container to get that z-order. */

.scene { position: absolute; inset: 0; pointer-events: none; }
.scene--back { z-index: 0; }
.scene--fore { z-index: 2; }

.scene__sky {
  position: absolute; inset: 0;
  background:
    /* the dot's own light, centred where the stop lands */
    radial-gradient(78% 60% at 88% 78%,
      color-mix(in srgb, var(--ref-coral) 30%, transparent) 0%,
      transparent 70%),
    linear-gradient(180deg,
      var(--color-ink) 0%,
      var(--color-ink) 34%,
      var(--scene-glow) 100%);
}

/* Ridges bottom-align and differ only in aspect ratio, so a taller viewport
   or a wider one keeps them stacked in the same order. height:auto off a
   viewBox means the range grows with the page rather than stretching. */
.scene__ridge {
  position: absolute;
  left: 0; bottom: 0;
  width: 100%; height: auto;
  display: block;
}

.ridge--far  { fill: var(--scene-far); }
.ridge--mid  { fill: var(--scene-mid); }
.ridge--near { fill: var(--scene-near); }
.treeline    { fill: var(--scene-near); }

/* ── the wordmark ───────────────────────────────────────────────────────
   Full bleed and cropped at the edges. Sits at 22% so it reads as ground the
   word is made of rather than as a headline; the stop stays at full strength
   and becomes the brightest point in the frame. */
.ftr__word {
  position: absolute;
  left: 0; right: 0;
  bottom: 0.30em;                /* em of its own size, so it tracks the clamp */
  z-index: 1;
  margin: 0;
  font-size: clamp(3.25rem, 22vw, 19rem);
  line-height: 1;
  text-align: center;
  white-space: nowrap;
  letter-spacing: var(--tracking-tight);
  color: color-mix(in srgb, var(--ftr-ink) 22%, transparent);
  user-select: none;
}

/* ── the columns ────────────────────────────────────────────────────────── */

.ftr__body {
  position: relative; z-index: 3;
  padding-top: var(--space-xl);
  align-self: start;
}

.ftr__cols {
  display: grid;
  /* minmax(0,1fr), never bare 1fr: the address column is the longest string
     in the footer and an auto floor would let it widen its own track and
     squeeze the other three. */
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--space-md) var(--space-sm);
}

.ftr__col h3 {
  margin-bottom: var(--space-xs);
  font-size: var(--text-2xs);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: color-mix(in srgb, var(--ftr-ink) 62%, transparent);
}

.ftr__col ul { list-style: none; padding: 0; }
.ftr__col li,
.ftr__col p {
  font-size: var(--text-sm);
  line-height: var(--leading-relaxed);
  color: color-mix(in srgb, var(--ftr-ink) 78%, transparent);
}
.ftr__col p + p { margin-top: var(--space-xs); }

/* Mixed from --ftr-ink, NOT from currentColor. currentColor here would be the
   already-muted colour of the .ftr__col p this link sits inside, so the two
   78% mixes would multiply and land the link at 0.6084: dimmer than the plain
   address text beside it, which is backwards for the only two things in this
   footer anyone can act on.

   Underlined rather than tinted, so they do not rely on colour alone to read
   as links. The hover colour is the only place --band-accent is used down
   here, and it is the token that flips per theme, so it stays legible on both
   grounds where the page's own accent would not. */
.ftr__col a {
  color: color-mix(in srgb, var(--ftr-ink) 78%, transparent);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  transition: color var(--dur-fast) var(--ease-out);
}
.ftr__col a:hover { color: var(--band-accent); }

/* ── the legal bar ──────────────────────────────────────────────────────
   Sits on the near ridge, which is pure ink, so it needs no ground of its
   own. The rule above it is the horizon line the ridge already implies. */
.ftr__base {
  position: relative; z-index: 3;
  padding-block: var(--space-sm);
}
.ftr__base .wrap {
  display: flex; flex-wrap: wrap; gap: var(--space-xs) var(--space-md);
  align-items: baseline; justify-content: space-between;
  border-top: 1px solid color-mix(in srgb, var(--ftr-ink) 16%, transparent);
  padding-top: var(--space-sm);
}
.ftr__legal,
.ftr__note {
  font-size: var(--text-xs);
  color: color-mix(in srgb, var(--ftr-ink) 62%, transparent);
}

/* ── responsive ─────────────────────────────────────────────────────────
   THE REVEAL IS RELEASED ON HEIGHT AS WELL AS WIDTH. A short laptop window is
   the same problem as a phone: the fixed box stops fitting, and its bottom
   rows become unreachable because there is no scroll left to give them.
   homepage.js reads the exact inverse of this query through gsap.matchMedia
   and does not build the parallax below it. */
@media (max-width: 900px), (max-height: 700px) {
  .ftr-slot { height: auto; }

  /* RELATIVE, NOT STATIC. The scene, the wordmark and the legal bar are all
     absolutely positioned against the footer. Drop it to static and the
     nearest positioned ancestor becomes the initial containing block, so the
     whole landscape detaches and anchors itself to the viewport instead.
     position: relative puts the footer back in the flow while keeping it the
     containing block, so the layout below is the desktop one, unpinned. */
  .ftr {
    position: relative;
    height: auto;
    display: block;
    padding-top: var(--space-xl);
    /* Reserves the room the absolute layers need, since none of them
       contribute height. The tallest is the wordmark's box top at 1.30em of a
       22vw face, just over the far ridge's 27.5vw. */
    padding-bottom: max(34vw, 14rem);
  }

  /* Was a grid row, and the grid is gone. It has to be pinned to the floor by
     hand or it lands above the reserved area instead of on the near ridge. */
  .ftr__base { position: absolute; inset: auto 0 0 0; }
}

@media (max-width: 1024px) {
  .ftr__cols { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 520px) {
  .ftr__cols { grid-template-columns: minmax(0, 1fr); }
  .ftr__base .wrap { justify-content: flex-start; }
}

/* ===========================================================================
   THE CONSENT BAR
   Added 2026-09-04 with ADR-0007's Google Tag Manager. Lives in this file and
   not in pages.css because the homepage loads only homepage.css, while the
   generated pages load both. Anything the whole site needs belongs here.

   Z-INDEX 120, chosen against the ladder already in use rather than picked:
   above .panel (70) so the mega menu cannot cover the choice, below .loader
   (150) so it never appears on top of the homepage loading screen, and below
   .skip (200) which must stay the first thing a keyboard reaches.
   =========================================================================== */

.cbar {
  position: fixed; z-index: 120;
  left: 0; right: 0; bottom: 0;
  padding: var(--space-sm);
  background: var(--color-surface);
  border-top: 1px solid var(--color-line-strong);
  /* The footer is fixed to the viewport floor and uncovered as main scrolls
     off it, so a bar sitting on the same floor needs its own ground rather
     than inheriting whatever is behind it at that moment. */
  box-shadow: 0 -18px 40px -24px rgb(0 0 0 / 45%);
}
.cbar[hidden] { display: none; }

.cbar__inner {
  max-width: 74rem; margin: 0 auto;
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--space-sm) var(--space-md);
}
.cbar__text { flex: 1 1 22rem; min-width: 0; }

.cbar__h {
  font-size: var(--text-2xs); font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-label); text-transform: uppercase;
  color: var(--color-accent-text);
}
.cbar__p {
  margin-top: var(--space-3xs); max-width: 68ch;
  font-size: var(--text-sm); line-height: var(--leading-normal);
  color: var(--color-ink);
}
.cbar__p a { color: var(--color-accent-text); }

.cbar__acts {
  display: flex; flex-wrap: wrap; gap: var(--space-2xs);
  flex: 0 0 auto;
}

/* Reject and Accept are the same size and the same weight of decision.
   A ghost "reject" beside a solid "accept" is a dark pattern with a
   stylesheet, and it is also the thing regulators look for first. */
.cbar__btn {
  padding: 10px 20px; border-radius: var(--radius-full);
  font: inherit; font-size: var(--text-sm); font-weight: var(--weight-medium);
  cursor: pointer; border: 1px solid transparent;
}
.cbar__btn--yes {
  background: var(--color-accent-text); color: var(--color-on-accent);
}
.cbar__btn--yes:hover { background: var(--color-accent-hover); }
.cbar__btn--no {
  /* --color-ink-disabled, not --color-line-strong: this is a control outline,
     and 1.4.11 wants 3:1 for the thing that says where the control is. The
     same trap the contact form's field borders fell into. */
  background: var(--color-surface); color: var(--color-ink);
  border-color: var(--color-ink-disabled);
}
.cbar__btn--no:hover { border-color: var(--color-accent-text); }
.cbar__btn:focus-visible { outline: none; box-shadow: var(--focus-ring); }

@media (max-width: 40rem) {
  .cbar__inner { flex-direction: column; align-items: stretch; }
  /* flex-basis follows the MAIN axis. Once this stack turns vertical, the
     22rem basis on .cbar__text stops meaning "at least 22rem wide" and starts
     meaning "352px tall", which grew the bar to 445px and pushed the buttons
     off the bottom of a phone. Reset it to content height. */
  .cbar__text { flex: 0 0 auto; }
  .cbar__acts { justify-content: stretch; }
  .cbar__btn { flex: 1 1 auto; }
}

/* The footer's "Cookie settings" opener. A button, not a link: it changes
   state on this page rather than going anywhere. */
.ftr__consent {
  background: none; border: 0; padding: 0;
  font: inherit; color: inherit; cursor: pointer;
  text-decoration: underline; text-underline-offset: 3px;
}
.ftr__consent:hover { color: var(--color-accent-text); }
.ftr__consent:focus-visible { outline: none; box-shadow: var(--focus-ring); }
