/* ═══════════════════════════════════════════════════════════════════════
   Inyosi Technologies — bespoke stylesheet
   Layers ON TOP of primitives.css. Load order: primitives.css, then this.

   Every token below is from Brand\01-brand-foundation.md §2 and §3. The gold
   ramp was sampled from the logo's own gradient pixels, and the contrast
   figures in the comments are MEASURED, not estimated.

   Class prefix: iy-  (02-art-direction.md §4)
   ═══════════════════════════════════════════════════════════════════════ */

:root {
  /* ── ground ─────────────────────────────────────────────────────────── */
  --iy-black:      #050506;
  --iy-ink:        #0E0E10;
  --iy-ink-2:      #16161A;

  /* ── gold ramp — sampled from the logo gradient ─────────────────────── */
  --iy-gold-100:   #F7D68A;   /* on black 14.49 · on paper  1.26 */
  --iy-gold-200:   #F3B431;   /* on black 11.04 · on paper  1.65 */
  --iy-gold-400:   #D18B07;   /* on black  7.19 · on paper  2.54  ← FAILS on light */
  --iy-gold-600:   #A66C05;   /* on black  4.63 · on paper  3.94  ← AA-large on light */
  --iy-gold-800:   #5A3A05;   /* on black  1.98 · on paper  9.20  ← gold body text on light */
  --iy-metal:      linear-gradient(160deg,#F3B431 0%,#D18B07 46%,#A66C05 100%);

  --iy-teal:       #0E4A56;
  --iy-teal-deep:  #0A363F;

  --iy-paper:      #F4F2EE;
  --iy-paper-2:    #E9E5DE;

  --iy-on-dark:      #FFFFFF;
  --iy-on-dark-soft: #B9B6B0;   /* on black 10.07 */
  --iy-on-light:     #141414;   /* on paper 16.48 */
  --iy-on-light-soft:#4A4A48;   /* on paper  7.85 - body copy on the light chapters */

  --iy-line-dark:  rgba(243,180,49,.16);
  --iy-line-light: rgba(20,20,20,.12);

  /* ── geometry — the motif as a reusable token ───────────────────────── */
  --hex: polygon(50% 0%,100% 25%,100% 75%,50% 100%,0% 75%,0% 25%);
  /* The same hexagon, quarter-turned. Used for the mobile philosophy band:
     rotating the ELEMENT would rotate the photograph inside it too. */
  --hex-90: polygon(0% 50%,25% 0%,75% 0%,100% 50%,75% 100%,25% 100%);

  /* ── motion ─────────────────────────────────────────────────────────── */
  --ease:     cubic-bezier(.22,.61,.36,1);   /* overrides the primitives default */
  --ease-out: cubic-bezier(.16,1,.3,1);

  /* ── type ───────────────────────────────────────────────────────────── */
  --display: 'Archivo', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --body:    'Inter', system-ui, -apple-system, 'Segoe UI', sans-serif;

  /* ── the type lattice ───────────────────────────────────────────────────
     ONE geometric scale, ratio sqrt(1.25) ~= 1.118, anchored at 12.8px.
     The TEXT tier takes consecutive rungs; the DISPLAY tier jumps 3, then 4,
     then 5 rungs, so the ladder OPENS OUT as it rises. That gap is the whole
     point: the previous scale ran giant/tiny/tiny with a 3.4x cliff between
     the chapter heading and the next level down, and four pairs of levels
     resolved to within 0.4px of each other.

     Measured against the reference the client supplied. thoroughtec.com's own
     display type is 57px and its second level is 25px (resolved inline styles
     in Context\_source-captures\, six occurrences of font-size:57px, three at
     weight 800). The old --fs-chapter resolved to 67.2px at 1440 — LARGER
     than the reference's largest heading, in a heavier face inside a narrower
     measure. "The text is too large" was literally correct.

     The comment after each token is its resolved value at 1440px / 390px.
     Nine of the ten pin between 1436 and 1446px, so a 1920 monitor renders
     type identical to a 1440 laptop and the extra width goes to the layout.
     Those figures are asserted in _build\verify_phase5.mjs — if you change a
     clamp here, that gate fails until you update it too.

     INFRASTRUCTURE is 14 characters and overflowed at 390px in Phase 1; the
     measured floor is 1.9rem with 7.4vw. Solved — do not re-derive. */
  --fs-hero-heavy: clamp(1.9rem,    7.4vw,  5.96rem);   /* 95.36 · 30.40 */
  --fs-chapter:    clamp(1.625rem,  3.8vw,  3.41rem);   /* 54.56 · 26.00 */
  --fs-h3:         clamp(1.35rem,   2.42vw, 2.18rem);   /* 34.85 · 21.60  ← the missing mid-tier */
  --fs-hero-light: var(--fs-h3);
  --fs-sub:        clamp(1.25rem,   1.74vw, 1.5625rem); /* 25.00 · 20.00 */
  --fs-card:       clamp(1.2rem,    1.55vw, 1.4rem);    /* 22.32 · 19.20 */
  --fs-lead:       clamp(1.0625rem, 1.39vw, 1.25rem);   /* 20.00 · 17.00 */
  --fs-small:      clamp(.9rem,     1.11vw, 1rem);      /* 15.98 · 14.40 */
  --fs-micro:      clamp(.8125rem,  .99vw,  .895rem);   /* 14.26 · 13.00 */
  --fs-eyebrow:    clamp(.7rem,     .89vw,  .8rem);     /* 12.80 · 11.20 */

  /* ── measure ────────────────────────────────────────────────────────── */
  --maxw:    1280px;
  --gutter:  clamp(1.25rem, 5vw, 3.5rem);
  --nav-h:   92px;
  --nav-h-c: 68px;   /* compact, once scrolled */

  /* ── primitives.css hook overrides ──────────────────────────────────── */
  --focus-ring:        rgba(243,180,49,.75);
  --btn-bg:            #D18B07;
  --btn-color:         #1A1200;
  --btn-border:        #D18B07;
  --btn-radius:        2px;
  --input-bg:          #16161A;
  --input-color:       #FFFFFF;
  /* Was rgba(243,180,49,.22), which blends to ~#3B3122 over --input-bg and
     measures ~1.5:1 against the fill — below the 3:1 WCAG 1.4.11 floor for a
     UI component boundary. Nothing caught it because no form existed until
     chapter 10.
     ⚠ .42 was then ESTIMATED, and the gate's first measurement of it returned
     2.71:1 — still under the floor. Raised to .50, which measures 3.33:1 over
     --input-bg. Do not lower it by eye; the figure is asserted in
     _build\verify_phase5.mjs section D and that check fails below 3.0. */
  --input-border:      rgba(243,180,49,.50);
  --input-border-focus:#D18B07;
  --input-radius:      2px;
  --error-color:       #E4855F;
  --error-bg:          rgba(228,133,95,.08);

  /* ══ THE SEMANTIC LAYER ══════════════════════════════════════════════════
     Devices read THESE, never the raw ramp. Re-pointing the layer is what
     lets one component render correctly on black, on ink and on paper without
     being hand-coloured three times.

     Why this exists: .iy-light used to re-point three devices, which was only
     ever enough because the one light chapter happened to contain nothing
     harder than an eyebrow, a heading and a lead. Chapters 03, 05 and 07 now
     go light too, and they carry hex frames, hex faces, gold cell names,
     gold-100 text, a gold rail, gold node fills, hover states, a black veil,
     filled buttons and a focus ring — none of which followed.

     The split that matters is TEXT vs MARK. Gold as text needs 4.5:1; gold as
     a rule, tick or hairline is a graphical object and needs 3:1. On paper
     those land on two different rungs of the ramp, so they cannot share a
     token. Every figure in the comments is measured (Brand\01-brand-foundation.md §2).
     ═════════════════════════════════════════════════════════════════════ */

  /* ground */
  --iy-bg:          var(--iy-black);
  --iy-bg-raised:   var(--iy-ink);
  --iy-media-bg:    var(--iy-ink-2);

  /* text */
  --iy-fg:          var(--iy-on-dark);        /* 20.37 on black */
  --iy-fg-soft:     var(--iy-on-dark-soft);   /* 10.07 */
  --iy-fg-accent:   var(--iy-gold-200);       /* 11.04 — gold TEXT */
  --iy-fg-lead:     var(--iy-gold-100);       /* 14.49 — the cell-name gold */

  /* marks — rules, ticks, node fills, hex hairlines. NON-text, 3:1 floor. */
  --iy-mark:        var(--iy-gold-400);       /*  7.19 */
  --iy-rule:        rgba(243,180,49,.16);
  --iy-rule-2:      rgba(243,180,49,.58);     /* the engage rail */
  --iy-frame:       rgba(243,180,49,.34);     /* the hex hairline */
}

/* ── the light scope ──────────────────────────────────────────────────────
   Keeps --iy-paper as the ground: every measured figure in the fact ledger is
   against THIS paper, so changing it would invalidate the whole contrast table.
   Note --iy-fg-accent and --iy-fg-lead BOTH collapse to gold-800 here. On paper
   gold-100 is 1.26 and gold-200 is 1.65 — both are invisible, not merely weak.
   ─────────────────────────────────────────────────────────────────────── */
.iy-light {
  --iy-bg:          var(--iy-paper);
  --iy-bg-raised:   #FFFFFF;
  --iy-media-bg:    var(--iy-paper-2);
  --iy-fg:          var(--iy-on-light);       /* 16.48 */
  --iy-fg-soft:     var(--iy-on-light-soft);  /*  7.85 */
  --iy-fg-accent:   var(--iy-gold-800);       /*  9.20 — NOT gold-200 (1.65) */
  --iy-fg-lead:     var(--iy-gold-800);       /*  9.20 — NOT gold-100 (1.26) */
  --iy-mark:        var(--iy-gold-600);       /*  3.94 — at the non-text floor */
  --iy-rule:        rgba(20,20,20,.13);
  /* ⚠ MEASURED, not chosen. rgba(90,58,5,.42) blends to ~#B0A38E on paper,
     ~1.6:1 — below the 3:1 floor for a graphical object. Raised until the
     gate's measured figure cleared 3.0. Do not lower it by eye. */
  --iy-rule-2:      rgba(90,58,5,.78);
  --iy-frame:       var(--iy-gold-600);
  --focus-ring:     rgba(90,58,5,.85);
  background: var(--iy-bg);
  color: var(--iy-fg);
}

/* ── the dark island ──────────────────────────────────────────────────────
   A dark object standing ON a light chapter. One class and every token goes
   back. This is what makes chapter 05 work: the eight capability tiles carry
   dark abstract textures authored to read bright-on-dark, so at 18% over
   white they become grey smudges. The fix is not to lighten the tile — it is
   to move the GROUND out from under it and leave the tile dark.
   ─────────────────────────────────────────────────────────────────────── */
.iy-invert {
  --iy-bg-raised:   var(--iy-ink);
  --iy-fg:          var(--iy-on-dark);
  --iy-fg-soft:     var(--iy-on-dark-soft);
  --iy-fg-accent:   var(--iy-gold-200);
  --iy-fg-lead:     var(--iy-gold-100);
  --iy-mark:        var(--iy-gold-400);
  --iy-rule:        rgba(243,180,49,.16);
  --iy-frame:       rgba(243,180,49,.34);
}

/* ═══ base ═══════════════════════════════════════════════════════════════ */

html { scroll-padding-top: calc(var(--nav-h-c) + 16px); }   /* overrides primitives' 80px */

/* ⚠ LOAD-BEARING — do not delete, and do not "tidy" it back to hidden.
   primitives.css:36 sets `html, body { overflow-x: hidden }`, which makes the
   root a CLIP CONTAINER. `position: sticky` inside a clipped ancestor does not
   stick, which would silently kill the two sticky chapter covers — they would
   simply scroll past looking like ordinary sections, with no error anywhere.
   `clip` does the same job primitives wanted (no horizontal scroll ever) but
   does NOT create a scroll container, so sticky survives.
   index.html loads primitives.css THEN this file, and both selectors are
   (0,0,1)+(0,0,1), so this wins on source order without !important.
   Proven by measurement in verify_phase5.mjs against a position:static control.

   Note this does NOT fix the other consequence: scrollWidth still reports no
   overflow even when content is clipped away, so overflow must be caught by
   measuring element rects, never by trusting scrollWidth. */
html, body { overflow-x: clip; }

body {
  background: var(--iy-bg);
  color: var(--iy-fg);
  font-family: var(--body);
  font-size: clamp(1rem, .95rem + .25vw, 1.118rem);   /* 17.89 · 16.18 */
  line-height: 1.65;
}

::selection { background: var(--iy-gold-400); color: #1A1200; }

/* ══ grounds ═══════════════════════════════════════════════════════════════
   These replace the per-chapter `background` declarations that used to sit on
   .iy-creed, .iy-philo, .iy-pillars, .iy-caps and .iy-engage.

   ⚠ WHY THAT MATTERED, because it is not obvious and it would have cost a day:
   `.iy-light` is specificity (0,1,0). So were all five of those chapter rules
   — and every one of them was declared LATER in this file. Adding .iy-light to
   a chapter therefore produced NO VISIBLE CHANGE AT ALL; the chapter's own
   background simply won on source order. Same family as the Phase 4 bug that
   put both nav lockups in the bar at once. The offender is never the rule you
   wrote, it is the one you did not.

   The fix is NOT to move these later or to raise their specificity — that
   would just be the same race with a different winner. The fix is that no
   chapter block sets `background` at all any more. Every one of those five
   declarations is deleted. A chapter's ground is now decided in exactly one
   place: the class on the <section>. If you find yourself adding `background`
   to a chapter rule, you are re-introducing the bug.

   verify_phase5.mjs asserts the rendered ground of all eleven sections against
   the frozen rhythm, so a regression here fails the gate rather than shipping.
   ═══════════════════════════════════════════════════════════════════════ */
.iy-ground--dark { background: var(--iy-bg); color: var(--iy-fg); }
.iy-ground--ink  { --iy-bg: var(--iy-ink); background: var(--iy-bg); color: var(--iy-fg); }

/* ═══ shared devices ═════════════════════════════════════════════════════ */

/* width:100% is load-bearing, not belt-and-braces. As a FLEX ITEM this box
   would otherwise get width:auto and shrink to fit its content, so max-width
   never applies and the chapter's content silently centres instead of filling
   the measure. Seen for real on the Phase 4 stub sections. Phase 5 composes
   every chapter inside .iy-wrap — leave this in. */
.iy-wrap { width: 100%; max-width: var(--maxw); margin-inline: auto; padding-inline: var(--gutter); }

/* The eyebrow / label style — the wordmark's own typographic behaviour,
   lifted from the logo: light weight, very wide tracking, uppercase. */
.iy-eyebrow {
  font-family: var(--display); font-weight: 300;
  font-size: var(--fs-eyebrow);
  letter-spacing: .34em; text-transform: uppercase;
  color: var(--iy-fg-accent);
  display: flex; align-items: center; gap: .85rem;
  margin: 0 0 1.1rem;
}
.iy-eyebrow::after { content: ""; flex: 1; height: 1px; background: var(--iy-rule); max-width: 220px; }

/* A hex this small cannot hold an outline — render it solid. Session 1 finding. */
.iy-hexnode {
  width: 10px; height: 11px; flex: none;
  clip-path: var(--hex); background: var(--iy-mark);
}

/* The hex node rule — a gold hairline interrupted by a small open hexagon.
   Lifted directly from Inyosi's own posters, where every eyebrow sits on it. */
.iy-rule-node { display: flex; align-items: center; gap: .75rem; margin: 1.4rem 0; }
.iy-rule-node::before, .iy-rule-node::after { content: ""; height: 1px; background: var(--iy-rule); }
.iy-rule-node::before { width: 54px; flex: none; }
.iy-rule-node::after  { flex: 1; max-width: 420px; }

.iy-sr {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ═══ buttons ════════════════════════════════════════════════════════════ */

/* ⚠ font-size is ON THE LATTICE, and that is not cosmetic. At the ad-hoc
   .875rem (14px) the button sat 1.8% from --fs-micro (14.26px), which put the
   "Send enquiry" button and the explanatory note UNDER it at visually the same
   size — a call to action smaller than its own footnote. That is precisely the
   tie the lattice exists to remove, and it was invisible until the generalised
   tie detector asked the question chapter by chapter.
   The lattice comment exempts form CONTROLS (inputs need 16px to stop iOS
   zooming). A button has no such constraint and belongs on the scale. */
.iy-btn {
  font-family: var(--body); font-weight: 500; font-size: var(--fs-small);
  letter-spacing: .04em;
  display: inline-flex; align-items: center; justify-content: center; gap: .65rem;
  padding: .95rem 1.75rem; min-height: 48px;
  border-radius: 2px; text-decoration: none; border: 1px solid transparent;
  transition: transform .3s var(--ease), background .3s var(--ease),
              border-color .3s var(--ease), color .3s var(--ease);
}
.iy-btn--solid { background: var(--iy-metal); color: #1A1200; font-weight: 600; }
.iy-btn--solid:hover { transform: translateY(-2px); }
.iy-btn--ghost { border-color: var(--iy-mark); color: var(--iy-fg-accent); }
.iy-btn--ghost:hover { border-color: var(--iy-fg-accent); background: rgba(243,180,49,.08); transform: translateY(-2px); }

/* On paper the metal gradient's own lightest stop is 2.54 against the ground,
   so a filled button has no discernible EDGE and fails 1.4.11 as a component
   boundary — the fill is legible, the shape is not. Give it a real border.
   The ghost hover fill has to flip too: an 8% gold wash is invisible on paper. */
.iy-light .iy-btn--solid { border-color: var(--iy-gold-800); }
.iy-light .iy-btn--ghost:hover { background: rgba(90,58,5,.06); }

.iy-btn__hex { width: 9px; height: 10px; flex: none; clip-path: var(--hex); background: currentColor; }

/* ═══════════════════════════════════════════════════════════════════════
   00 · site-nav
   Transparent over the hero, frosting to black glass once scrolled. The
   frosting is driven by the frozen engine's [data-header] hook, not by
   bespoke scroll code.
   ═══════════════════════════════════════════════════════════════════════ */

.iy-nav {
  position: fixed; inset: 0 0 auto 0; z-index: 60;
  height: var(--nav-h);
  display: flex; align-items: center;
  background: transparent;
  border-bottom: 1px solid transparent;
  transition: height .4s var(--ease), background .4s var(--ease),
              border-color .4s var(--ease), backdrop-filter .4s var(--ease);
}
/* HEIGHT ONLY. The bar's ground used to live here, but the page now alternates
   paper and black under a fixed bar, so the ground has to depend on WHICH
   chapter is underneath rather than merely on whether we have scrolled. It
   moved to .is-dark / .is-light in the MOTION section. Two orthogonal class
   sets, no collision, and engine.js stays frozen. */
.iy-nav.is-scrolled { height: var(--nav-h-c); }
.iy-nav__inner {
  width: 100%; max-width: var(--maxw); margin-inline: auto;
  padding-inline: var(--gutter);
  display: flex; align-items: center; gap: clamp(1rem, 3vw, 2.5rem);
}

/* ── the responsive lockup ──────────────────────────────────────────────
   Two assets, not one. The full lockup's legibility floor is a MEASURED
   48px — below that TECHNOLOGIES is a smear, not a word (see
   _build\image-manifest.md §6). So the full lockup shows only where it has
   the room: desktop, before the bar compacts. Everywhere else carries mark
   + INYOSI. Same alt on both; exactly one is ever in the a11y tree, because
   display:none removes the other from it entirely.
   ───────────────────────────────────────────────────────────────────── */
/* align-self: stretch makes the logo LINK fill the bar's height, so its tap
   box is the bar rather than the 36px image inside it. Measured 145x36 at 390
   before this line — the "back to top" control, and the easiest thing on the
   page to miss with a thumb. It changes no pixel of the rendering. */
.iy-nav__logo { display: flex; align-items: center; align-self: stretch; flex: none; margin-right: auto; }
/* NOTE — no `display` on this rule. `.iy-nav__logo img` is specificity (0,1,1)
   and would BEAT `.iy-nav__logo--compact { display:none }` at (0,1,0), which
   put both lockups in the bar at once: the desktop nav read
   "INYOSI TECHNOLOGIES  [mark] INYOSI". Found by looking at the render; lint
   was green. primitives.css already sets img{display:block}, so the variant
   rules below are the only thing that decides which one shows. */
.iy-nav__logo img { width: auto; transition: height .4s var(--ease); }
.iy-nav__logo--full    { height: 52px; }
.iy-nav__logo--compact { height: 40px; display: none; }

.iy-nav.is-scrolled .iy-nav__logo--full    { display: none; }
.iy-nav.is-scrolled .iy-nav__logo--compact { display: block; }

/* ── desktop links ──────────────────────────────────────────────────── */
.iy-nav__links { display: flex; align-items: center; gap: clamp(.9rem, 2.2vw, 2rem); }
.iy-nav__link {
  position: relative;
  font-size: .875rem; letter-spacing: .02em; text-decoration: none;
  color: var(--iy-on-dark-soft);
  padding: .5rem 0;
  transition: color .3s var(--ease);
  white-space: nowrap;
}
.iy-nav__link:hover { color: var(--iy-on-dark); }

/* Active-section indicator — a small gold hexagon beside the current
   chapter's link. Driven by an IntersectionObserver in script.js: the frozen
   engine has no section-spy hook (checked at source), so this is bespoke and
   does NOT touch engine.js. It becomes aria-current="page" on inner pages. */
.iy-nav__link::before {
  content: ""; position: absolute; left: -14px; top: 50%;
  width: 8px; height: 9px; margin-top: -4.5px;
  clip-path: var(--hex); background: var(--iy-gold-400);
  opacity: 0; transform: scale(.4);
  transition: opacity .35s var(--ease), transform .35s var(--ease);
}
.iy-nav__link.is-active { color: var(--iy-on-dark); }
.iy-nav__link.is-active::before { opacity: 1; transform: scale(1); }

.iy-nav__cta { flex: none; }
.iy-nav__cta.iy-btn { padding: .7rem 1.35rem; min-height: 44px; font-size: .82rem; }

/* ── burger ─────────────────────────────────────────────────────────── */
.iy-burger {
  display: none; flex: none;
  width: 46px; height: 46px; padding: 0;
  background: none; border: 1px solid var(--iy-line-dark); border-radius: 2px;
  align-items: center; justify-content: center; gap: 5px; flex-direction: column;
}
.iy-burger span { display: block; width: 20px; height: 1.5px; background: var(--iy-gold-200); transition: transform .3s var(--ease), opacity .3s var(--ease); }

/* ── mobile overlay ─────────────────────────────────────────────────── */
.iy-mobile {
  position: fixed; inset: 0; z-index: 90;
  background: var(--iy-black);
  display: flex; flex-direction: column;
  padding: var(--gutter);
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity .35s var(--ease), visibility .35s var(--ease);
}
body.menu-is-open .iy-mobile { opacity: 1; visibility: visible; pointer-events: auto; }
.iy-mobile__top { display: flex; align-items: center; justify-content: space-between; min-height: 52px; }
.iy-mobile__close {
  width: 46px; height: 46px; background: none; color: var(--iy-gold-200);
  border: 1px solid var(--iy-line-dark); border-radius: 2px; font-size: 1.4rem; line-height: 1;
}
.iy-mobile__links { display: flex; flex-direction: column; gap: .35rem; margin-top: clamp(1.5rem, 6vh, 3rem); }
.iy-mobile__links a {
  font-family: var(--display); font-weight: 300; letter-spacing: .18em; text-transform: uppercase;
  font-size: clamp(1.05rem, 5vw, 1.5rem); text-decoration: none; color: var(--iy-on-dark);
  padding: .7rem 0; border-bottom: 1px solid rgba(243,180,49,.10);
  display: flex; align-items: center; gap: .8rem;
}
.iy-mobile__cta { margin-top: 1.6rem; }
/* A one-tap route out for the phone user — the IA requires the phone and
   email inside the overlay, not just the anchors. */
.iy-mobile__foot { margin-top: auto; padding-top: 1.5rem; border-top: 1px solid var(--iy-line-dark); display: grid; gap: .55rem; }
/* The five menu links and the CTA are 56px tall. These two were 25px — and
   they are the phone number and the email address, the one-tap route out that
   this block exists to provide. Measured at 390 before this line. */
.iy-mobile__foot a {
  color: var(--iy-gold-200); text-decoration: none; font-size: .95rem;
  display: flex; align-items: center; gap: .6rem;
  min-height: 46px;
}

@media (max-width: 900px) {
  .iy-nav__links, .iy-nav__cta { display: none; }
  .iy-burger { display: flex; }
  /* Mobile always carries the compact lockup — the full one is a smear at
     this size, and the bar has no room for it beside the burger. */
  .iy-nav__logo--full    { display: none; }
  .iy-nav__logo--compact { display: block; height: 36px; }
  :root { --nav-h: 74px; --nav-h-c: 62px; }
}

/* ═══════════════════════════════════════════════════════════════════════
   01 · hero — the living lattice
   No image. The canvas IS the hero, which is why the heaviest chapter on
   most sites costs nothing here.
   ═══════════════════════════════════════════════════════════════════════ */

.iy-hero {
  position: relative;
  min-height: 100vh;
  min-height: 100svh;
  display: flex; align-items: flex-end;
  overflow: hidden;
}

.iy-hero__canvas {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  display: block;
}

/* Three-part scrim:
   1. a top band, because the lattice is densest toward the upper right and
      that is exactly where the nav links sit. Over the hero the bar is
      transparent by design (it only frosts once scrolled past), so this band
      is the only thing keeping the links off lit cells. Found by looking:
      "The Hive" and "Capabilities" were sitting on top of lit hexagons.
   2. a radial that holds the lattice bright where it should be dense
   3. a linear that pulls the bottom to black, so the headline sits on clean
      ground rather than on top of the grid. */
.iy-hero__scrim {
  position: absolute; inset: 0; pointer-events: none;
  background:
    linear-gradient(to bottom, rgba(5,5,6,.80) 0%, rgba(5,5,6,.42) 7%, transparent 17%),
    radial-gradient(120% 90% at 78% 22%, transparent 0%, rgba(5,5,6,.55) 55%, var(--iy-black) 100%),
    linear-gradient(to top, var(--iy-black) 2%, rgba(5,5,6,.45) 40%, transparent 78%);
}

.iy-hero__inner {
  position: relative; z-index: 2; width: 100%;
  padding-block: clamp(2.5rem, 8vh, 5.5rem);
  padding-top: calc(var(--nav-h) + clamp(2rem, 6vh, 4rem));
}

/* ── the display pairing ────────────────────────────────────────────────
   Archivo 300 tracked over Archivo 800 tight. One family at two extremes,
   which is how the reference resolves Inyosi's two existing typographic
   voices — the wide-tracked wordmark and the heavy poster caps.
   ───────────────────────────────────────────────────────────────────── */
.iy-hero__h1 { margin: 0; }

.iy-hero__light,
.iy-hero__heavy { display: block; text-transform: uppercase; font-family: var(--display); }

.iy-hero__light {
  font-weight: 300; letter-spacing: .34em;
  font-size: var(--fs-hero-light);
  color: var(--iy-gold-200);         /* 11.04 on black */
  margin-bottom: .4rem;
  /* A tracked word carries a trailing letter-space on its last character.
     Left-aligned that is invisible; centred (mobile) it shifts the word left
     of true centre. The negative margin cancels exactly that one space. */
  margin-right: -.34em;
}
.iy-hero__heavy {
  font-weight: 800; letter-spacing: -.025em;
  font-size: var(--fs-hero-heavy);
  line-height: .9; color: var(--iy-on-dark);
  overflow-wrap: break-word;
}

.iy-hero__sub {
  margin: 0;
  color: var(--iy-on-dark-soft);
  font-size: var(--fs-lead);
}

.iy-hero__ctas { display: flex; flex-wrap: wrap; gap: .9rem; margin-top: clamp(1.75rem, 4vh, 2.5rem); }

/* ── scroll cue ─────────────────────────────────────────────────────── */
.iy-hero__cue {
  position: absolute; left: 50%; bottom: 1.5rem; z-index: 3;
  transform: translateX(-50%);
  display: flex; flex-direction: column; align-items: center; gap: .5rem;
  color: var(--iy-gold-400);
}
.iy-hero__cue span { width: 11px; height: 12px; clip-path: var(--hex); background: currentColor; }
.iy-hero__cue span:nth-child(2) { opacity: .5; }
.iy-hero__cue span:nth-child(3) { opacity: .22; }
@media (prefers-reduced-motion: no-preference) {
  .iy-hero__cue span { animation: iy-cue 2.4s var(--ease) infinite; }
  .iy-hero__cue span:nth-child(2) { animation-delay: .18s; }
  .iy-hero__cue span:nth-child(3) { animation-delay: .36s; }
}
@keyframes iy-cue {
  0%, 62%, 100% { transform: translateY(0); }
  30%           { transform: translateY(5px); }
}

/* ── load reveal ────────────────────────────────────────────────────────
   Each line rises out of an overflow:hidden mask, heavy first then light,
   90ms apart. This is a LOAD animation, not a scroll reveal, so it does not
   use the engine's [data-reveal] hook.

   The hidden state is gated on html.js-anim, which a tiny inline script in
   <head> adds BEFORE first paint — and only when motion is allowed and this
   is not a ?cap=1 QA run. So:
     • no JS at all      → no .js-anim → the headline is simply present
     • reduced motion    → no .js-anim → the headline is simply present
     • ?cap=1            → no .js-anim → deterministic screenshots
   The headline must never depend on a script in order to be readable, and
   gating on <html> rather than on the hero avoids a flash of the finished
   headline before the mask closes.
   ───────────────────────────────────────────────────────────────────── */
/* The heavy word runs at line-height:.9, so its line box is SHORTER than its
   glyphs. overflow:hidden on a box that tight crops the cap-heights. The
   padding opens the mask enough to clear them and the negative margin takes
   the layout space back, so nothing below shifts. The start transform is
   118% rather than 105% because the element must clear the padded box too. */
.iy-mask {
  display: block; overflow: hidden;
  padding-block: .14em; margin-block: -.14em;
}
.iy-mask > * { display: block; }

html.js-anim .iy-mask > * { transform: translateY(118%); opacity: 0; }
html.js-anim .iy-hero.is-ready .iy-mask > * {
  transform: translateY(0); opacity: 1;
  transition: transform 1s var(--ease-out), opacity .8s var(--ease-out);
}
html.js-anim .iy-hero.is-ready .iy-mask--heavy > * { transition-delay: .09s; }
html.js-anim .iy-hero.is-ready .iy-mask--sub   > * { transition-delay: .30s; }

html.js-anim .iy-hero__ctas,
html.js-anim .iy-hero__rule,
html.js-anim .iy-hero__cue { opacity: 0; }
html.js-anim .iy-hero.is-ready .iy-hero__ctas,
html.js-anim .iy-hero.is-ready .iy-hero__rule,
html.js-anim .iy-hero.is-ready .iy-hero__cue {
  opacity: 1; transition: opacity .9s var(--ease-out) .42s;
}

/* Under reduced motion the hero is a composed static layout, not a blank
   frame: the lattice paints one settled field (hive.js) and the headline is
   simply present. */

/* ── mobile ─────────────────────────────────────────────────────────── */
@media (max-width: 640px) {
  .iy-hero { align-items: center; }
  .iy-hero__inner {
    text-align: center;
    /* The desktop rule adds nav height PLUS a clamp on top. Against
       align-items:center that padding fights the centring and pushes the block
       low. On a phone the nav clearance alone is what is needed. */
    padding-top: var(--nav-h);
    /* Nudge the block up off true centre. The nav visually occupies the top of
       the frame, so a mathematically centred block reads bottom-heavy — the
       first render left the lower third looking left over rather than composed. */
    padding-bottom: clamp(3rem, 11vh, 6rem);
  }
  .iy-hero__light { letter-spacing: .22em; margin-right: -.22em; }  /* .34em overflows at 390px */
  .iy-hero__rule { justify-content: center; }
  .iy-hero__rule::before { width: 34px; }
  .iy-hero__rule::after  { max-width: 34px; flex: none; width: 34px; }
  /* The cue stays on mobile. It was cut on the first pass on the reasoning that
     the CTAs are already at the fold, and that left the bottom third empty. It
     is the composed answer to that space, and on a phone a scroll cue earns its
     place more, not less. */
  .iy-hero__cue { bottom: 1.1rem; }
  .iy-hero__cue span { width: 9px; height: 10px; }

  /* Two auto-width buttons stacked and centred read as ragged, because the
     primary is nearly twice the ghost's width. One constrained column, both
     filling it, reads as composed. */
  .iy-hero__ctas { flex-direction: column; align-items: center; gap: .7rem; }
  .iy-hero__ctas .iy-btn { width: 100%; max-width: 320px; }

  /* The desktop scrim is built for type in the bottom-left gutter. Centred type
     needs the dark where the type is, so the mobile scrim is a vignette: it
     holds the lattice at the edges, darkens the middle enough for the headline
     to sit on clean ground, and puts a little weight under the nav. */
  .iy-hero__scrim {
    background:
      radial-gradient(92% 52% at 50% 50%, rgba(5,5,6,.88) 0%, rgba(5,5,6,.62) 46%, transparent 80%),
      linear-gradient(to bottom, rgba(5,5,6,.72) 0%, transparent 22%),
      linear-gradient(to top, var(--iy-black) 0%, rgba(5,5,6,.30) 26%, transparent 58%);
  }
}

/* ═══════════════════════════════════════════════════════════════════════
   PHASE 5 — chapters 02 creed · 03 philosophy · 04 pillars ·
             05 capabilities · 06 sectors · 07 engage

   Composed against the devices that already existed: .iy-wrap, .iy-eyebrow,
   .iy-rule-node, .iy-hexnode, .iy-btn, .iy-mask and the --hex token. Nothing
   here re-implements one of them.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── the static path, in ONE place ──────────────────────────────────────
   html.js-anim is set by the inline <head> script BEFORE first paint, and
   only when animation should actually run. Its absence means one of three
   things, and all three want the same answer — the finished state, present:

     • prefers-reduced-motion: reduce
     • ?cap=1                     (deterministic QA screenshots)
     • no JavaScript at all

   That third case matters more than it looks. primitives.css sets
   [data-reveal] { opacity: 0 } UNCONDITIONALLY, so without this block a page
   whose JS failed to load would render every chapter below the hero
   invisible — content that is present in the DOM, indexed by a crawler, and
   unreadable by a human. The hero was already guarded this way in Phase 4;
   this extends the same guard over everything Phase 5 adds.

   02-art-direction.md §4 requires "?cap=1 disables the lattice animation and
   forces static reveals". This is where the second half of that is honoured,
   without touching the frozen engine.
   ───────────────────────────────────────────────────────────────────── */
html:not(.js-anim) [data-reveal],
html:not(.js-anim) [data-reveal-stagger] > *,
html:not(.js-anim) .iy-cell,
html:not(.js-anim) .iy-flow__node,
html:not(.js-anim) .iy-flow__rail,
html:not(.js-anim) .iy-philo__hex { opacity: 1 !important; transform: none !important; }
html:not(.js-anim) .iy-creed__w   { color: var(--iy-gold-200) !important; }

/* ═══ shared chapter devices ═════════════════════════════════════════════ */

.iy-chapter { position: relative; padding-block: clamp(4.5rem, 12vh, 9rem); }

/* ── chapter heads ──────────────────────────────────────────────────────
   The old single `max-width: 48rem` block is what produced the dead right
   half. 48rem is 768px inside a 1168px measure at 1440, so a 67px headline
   wrapped to two or three short lines and left ~400px of empty ground beside
   it in five chapters. That whitespace read as left over, not composed — which
   is the actual complaint behind "the design doesn't look good".

   Two changes together, and BOTH are needed. The type came down (see the
   lattice at the top of this file); this is the layout half.
   ─────────────────────────────────────────────────────────────────────── */
.iy-chapter__head { margin-bottom: clamp(2.6rem, 6vh, 4.6rem); }

/* --centre : no lead, headline is the whole block. Wider than the left-aligned
   head on purpose: centred, 48rem broke the four pillars headline onto three
   lines, which stacked a tall type block above a tall geometry block and left
   the chapter reading top-heavy. */
.iy-chapter__head--centre { max-width: 56rem; margin-inline: auto; text-align: center; }
.iy-chapter__head--centre .iy-chapter-h { max-width: 20em; margin-inline: auto; }

/* --split : asymmetric two columns, lead on the right. `align-items: end` puts
   the headline's last line and the lead's last line on ONE baseline, so the
   two columns close together instead of raggedly. The gap is not a hole — it
   carries a hairline, which is the page's own divider logic turned vertical. */
.iy-chapter__head--split {
  display: grid;
  grid-template-columns: minmax(0,1.08fr) minmax(0,.92fr);
  column-gap: clamp(2rem, 5.5vw, 5rem);
  max-width: none; position: relative;
}
/* ⚠ EXPLICIT PLACEMENT, and it is not optional.
   This block has THREE children (eyebrow, h2, lead) in a TWO-column grid. Left
   to auto-flow they land eyebrow=1/1, headline=1/2, lead=2/1 - which puts the
   HEADLINE IN THE RIGHT COLUMN and leaves a large hole at the top left. That
   is the exact dead-space defect this treatment exists to fix, just moved to a
   different corner, and it renders perfectly happily with no error anywhere.
   Found by opening the render; nothing in the toolchain could see it.

   align-items is deliberately NOT set on the container. It was "end", which
   also pushed the eyebrow down to sit on the headline. The baseline pairing we
   actually want is between the headline and the lead only, so it is expressed
   as align-self on the lead alone. */
.iy-chapter__head--split .iy-eyebrow   { grid-column: 1; grid-row: 1; }
.iy-chapter__head--split .iy-chapter-h {
  grid-column: 1; grid-row: 2; margin-bottom: 0;
  /* ⚠ `align-self: end` on the lead plus a hand-tuned padding-bottom was NOT
     the same thing as a shared baseline, and the difference was invisible at
     the width it was tuned at. `end` aligns the two BOXES; the last lines then
     sit apart by the difference in half-leading and descent between a 54px
     display face and a 20px body face. Measured: 5.72px out at 1440 and 1920,
     and 31px (capabilities) / 47px (engage) out at 960 — because the two
     columns wrap to different line counts as the measure narrows, and a
     padding tuned at one width cannot know that.
     `last baseline` asks the browser for the thing actually wanted. Measured
     0.00px at 960, 1100, 1280, 1440 and 1920. Both sides need it: baseline
     alignment is a group operation, not a property of one item. */
  align-self: last baseline;
}
.iy-chapter__head--split .iy-lead {
  grid-column: 2; grid-row: 2;
  align-self: last baseline;           /* shares a last baseline with the h2 */
  max-width: none;
  /* The rule that fills the gap. On the LEAD rather than on a ::before of the
     container, because the container pseudo-element needed the column boundary
     computed by hand from the fr ratio and the gap - a number that silently
     drifts the moment either changes. A border on the element that defines the
     boundary cannot drift. */
  padding-left: clamp(1rem, 2.75vw, 2.5rem);
  border-left: 1px solid var(--iy-rule);
}

/* --wide : headline across the full measure, lead indented right. Breaks the
   run of splits so three consecutive chapters do not share one silhouette. */
.iy-chapter__head--wide { max-width: none; }
.iy-chapter__head--wide .iy-chapter-h { max-width: 22em; }
.iy-chapter__head--wide .iy-lead { margin-left: 40%; max-width: 34em; }

/* ⚠ Trap 2 and Trap 3 both live in this one block.
   Trap 2 — `display` changes from grid to block here, and `align-items: end`
   written for the grid would keep applying under block, just meaning something
   else. It is restated explicitly rather than left to inherit a new meaning.
   Trap 3 — media queries add NO specificity. Both reset selectors below are
   (0,2,0), matching the rules they override, and win only on source order. Do
   not "simplify" either of them to a single class or a pseudo-class: a
   :first-child reset at (0,3,0) is exactly what a later media query could not
   override in Phase 5, and it left a whole cluster hanging outside the measure. */
@media (max-width: 900px) {
  .iy-chapter__head--split,
  .iy-chapter__head--wide { display: block; align-items: start; }
  /* display is now block, so grid-column/grid-row are inert - but the border
     and padding are NOT, and a stray vertical rule down the left of a phone
     layout is exactly the kind of leftover that reads as a bug. */
  /* Trap 2 again, for the baseline pairing this time: `align-self: last
     baseline` is inert under block layout rather than reinterpreted, but it is
     restated anyway so the reset reads as complete and nobody has to work out
     which alignment properties survive a display switch and which do not. */
  .iy-chapter__head--split .iy-chapter-h { margin-bottom: 1.15rem; align-self: auto; }
  .iy-chapter__head--split .iy-lead,
  .iy-chapter__head--wide  .iy-lead {
    margin-left: 0; margin-top: 1.1rem; max-width: 60ch;
    padding-left: 0; border-left: 0; padding-bottom: 0; align-self: auto;
  }
}

/* The centred eyebrow needs a rule on BOTH sides — the base device only has
   the trailing one, which reads as unfinished when the block is centred. */
.iy-eyebrow--centre { justify-content: center; }
.iy-eyebrow--centre::before {
  content: ""; flex: 1; height: 1px; background: var(--iy-rule); max-width: 220px;
}

/* ── the rag ────────────────────────────────────────────────────────────
   Round 3. Measured by counting the WORDS on each block's last line rather
   than its width — the first attempt asked "is the last line under 16% of the
   measure" and sailed straight past "did." ending a 150px headline at 33%.
   An orphan is one word alone, whatever width it happens to be.

   Found: eleven blocks ending on a single word — the capabilities headline
   breaking "We build intelligent / business / ecosystems.", the philosophy
   headline ending "did." on line six at 390, three of its paragraphs, three
   engage descriptors, and the footer's own copyright line.

   `balance` evens the line lengths of a short block and is right for
   headings; `pretty` leaves the rag alone but refuses to leave a single word
   on the last line, which is what body copy wants. Neither costs a byte of
   markup and both degrade to normal wrapping where unsupported. */
/* ⚠ `text-wrap: pretty` IS NOT USED HERE, and that is a measured decision, not
   an oversight. It is the obvious tool for this — its whole purpose is to
   refuse a single-word last line — and in this engine it does nothing at all.
   Controlled directly (_build\_probe\probe_textwrap.mjs) on a block built with
   a deliberate orphan:

     Edg/151          CSS.supports('text-wrap','pretty') -> true
                      computed value                     -> "pretty"
                      last line, normal                  -> 107px
                      last line, pretty                  -> 107px   (no change)
                      last line, balance                 -> 197px   (works)

   So the property parses, claims support, computes correctly and changes
   nothing. Shipping it would have left eleven orphans in place behind a rule
   that looked like it had fixed them — the worst kind of dead code, because
   the next person reads the declaration and stops looking.

   `balance` is real, so it is used wherever the block is short enough for it
   (Chromium ignores it past a handful of lines). Listed explicitly rather than
   applied to `p`, because .iy-creed__band and .iy-foot__word are paragraphs
   that are architecture, not prose. */
.iy-cell__desc,
.iy-tile__desc,
.iy-flow__desc,
.iy-post__p,
.iy-detail dd,
.iy-form__note,
.iy-foot__legal,
.iy-philo__pull,
.iy-foot__creed { text-wrap: balance; }

.iy-chapter-h {
  text-wrap: balance;
  font-family: var(--display); font-weight: 800;
  font-size: var(--fs-chapter);
  letter-spacing: -.018em; line-height: 1.05;
  /* Type-RELATIVE measure, not a pixel one: 14em holds every headline to the
     same ~22-26 display characters per line at any size. Resolves to 764px at
     1440 and 364px at 390 — wider than the phone measure, so it is inert on
     mobile and cannot fight the gutter. */
  max-width: 14em;
  text-wrap: balance;
  margin: 0 0 1.15rem;
}
.iy-lead {
  margin: 0; max-width: 62ch;
  font-size: var(--fs-lead); line-height: 1.6;
  color: var(--iy-fg-soft);
}

/* NOTE — the old `.iy-light` block that used to sit here has been deleted, not
   moved. It re-pointed three devices and set its own background, and it lost
   the background race to every chapter rule below it (see the grounds comment
   in the base section). The semantic token layer at the top of this file
   replaces it and covers 24 of 30 devices; the six that genuinely need a
   hand-written rule are marked `on paper` / `on black` where they occur. */

/* ═══════════════════════════════════════════════════════════════════════
   02 · creed — the four-beat band
   ═══════════════════════════════════════════════════════════════════════ */

.iy-creed {
  border-top: 1px solid var(--iy-rule);
  border-bottom: 1px solid var(--iy-rule);
  padding-block: clamp(1.7rem, 4.5vh, 2.9rem);
}
.iy-creed__band {
  margin: 0;
  display: flex; flex-wrap: wrap; align-items: center; justify-content: center;
  gap: clamp(.7rem, 2.6vw, 2.1rem);
}
.iy-creed__w {
  font-family: var(--display); font-weight: 300;
  text-transform: uppercase; letter-spacing: .34em;
  font-size: var(--fs-card);
  color: var(--iy-fg-soft);
  /* A tracked word carries a trailing letter-space on its last character.
     Centred, that shifts the word left of true centre. Same fix as the hero's
     light half — cancel exactly that one space. */
  margin-right: -.34em;
  transition: color .55s var(--ease);
  transition-delay: var(--d, 0ms);
}
.iy-creed__band.is-revealed .iy-creed__w { color: var(--iy-fg-accent); }

/* Solid, not an outline. A hex below ~14px cannot hold one — clip-path cuts
   a border and an inset box-shadow both (session-1 finding 1).

   Sized 12x14 rather than 9x10 after looking at the render: at 9px, and dimmed
   to .5, the shape read as a BULLET. The copy deck picks a hexagon here
   precisely so it is not the bullet their own collateral uses, so a hexagon
   that reads as a dot defeats the only reason it exists. */
.iy-creed__sep {
  flex: none; width: 12px; height: 14px;
  clip-path: var(--hex); background: var(--iy-mark); opacity: .8;
}

@media (max-width: 640px) {
  .iy-creed__w { letter-spacing: .22em; margin-right: -.22em; }
}

/* On a phone the four tracked words do not fit on one line, and left to wrap
   naturally they broke as "CONNECT ⬡ PROTECT ⬡ SUPPORT ⬡" / "GROW" — a
   separator dangling at the end of the first line with nothing after it, which
   is precisely the loose-bullet reading the copy deck chose a hexagon to
   avoid. Breaking deliberately after PROTECT gives two balanced pairs.

   The middle separator is the 4th SPAN in the band (word, sep, word, sep, …),
   which is what :nth-of-type counts — it counts elements of a type, not of a
   class. Given a full line to itself and no height, it becomes the break. */
@media (max-width: 560px) {
  .iy-creed__band { row-gap: .25rem; }
  .iy-creed__sep:nth-of-type(4) { width: 100%; height: 0; background: none; }
}

/* ═══════════════════════════════════════════════════════════════════════
   03 · philosophy — the hive
   ═══════════════════════════════════════════════════════════════════════ */

.iy-philo {
  min-height: 100vh;
  min-height: 100svh;
  display: flex; align-items: center;
  overflow: hidden;
}
.iy-philo__inner { position: relative; z-index: 2; }
.iy-philo__copy  { max-width: 34rem; }

/* The macro bleeds past .iy-wrap deliberately: the art direction asks for it
   to fill the right two-thirds, which a boxed image inside the measure cannot
   do. Absolutely positioned, then veiled to black on its left. */
.iy-philo__art {
  position: absolute; top: 0; right: 0; bottom: 0;
  width: 62%;
  /* Both axes stated. A flex container's align-items defaults to `stretch`,
     which is what distorted the OG-card logo on 2026-08-20 and is logged as
     session-3 finding 7. Never inherit the cross axis around an <img>. */
  display: flex; align-items: center; justify-content: flex-end;
  padding-right: clamp(0px, 2.5vw, 3.5rem);
}
/* The wrapper holds a FIXED clip-path and the WRAPPER is what scales.
   02-art-direction.md §4 — never tween the polygon. */
.iy-philo__hex {
  flex: 0 0 auto;
  width: min(86%, 620px);
  aspect-ratio: 1 / 1.1547;
  clip-path: var(--hex);
  opacity: 0; transform: scale(.9);
  transition: transform 1.4s var(--ease-out), opacity 1.1s var(--ease);
}
.iy-philo__art.is-revealed .iy-philo__hex { opacity: 1; transform: scale(1); }
/* object-fit:cover crops on ONE axis only. Source is 1024x1024 into a 0.866
   box, so the crop is HORIZONTAL and object-position's X is the value that
   does anything here. Centre is right for a macro. */
.iy-philo__hex img {
  display: block; width: 100%; height: 100%;
  object-fit: cover; object-position: center;
}
.iy-philo__veil {
  position: absolute; inset: 0; pointer-events: none;
  background:
    linear-gradient(to right, var(--iy-black) 0%, rgba(5,5,6,.93) 16%, rgba(5,5,6,.44) 50%, rgba(5,5,6,.10) 84%, transparent 100%),
    linear-gradient(to bottom, var(--iy-black) 0%, transparent 16%),
    linear-gradient(to top,    var(--iy-black) 0%, transparent 18%);
}

/* ── 03 on paper ────────────────────────────────────────────────────────
   The veil exists to fade a bleeding photograph into a black page. On paper
   there is nothing to fade INTO — a black-to-transparent gradient over a paper
   ground is just a dirty smear across the image. It goes.

   Which changes what the art has to be. Without a veil the image cannot bleed,
   because a hard photo edge against paper reads as a mistake. So it becomes a
   CONTAINED hexagon with paper visible on all four sides, and it gets a real
   edge — and that edge has to be two stacked clipped layers, because
   `clip-path` cuts a border and an inset box-shadow both (session-1 finding 1,
   the same reason .iy-cell__frame/__face exist).

   The dark gold-lit macro on paper is now the strongest object in the chapter,
   which is the whole reason this chapter was worth flipping. */
.iy-light .iy-philo__veil { display: none; }
.iy-light .iy-philo__art {
  position: static; width: auto;
  padding-right: 0; justify-content: center;
}
.iy-light .iy-philo__hex { background: var(--iy-frame); }
.iy-philo__hex-in {
  position: absolute; inset: 1.5px;
  clip-path: var(--hex);
  overflow: hidden;
}
/* The chapter becomes a real two-column grid rather than copy + an absolutely
   positioned bleed. .iy-philo keeps its flex centring for the dark path; this
   overrides the inner. */
.iy-light .iy-philo__inner {
  display: grid;
  grid-template-columns: minmax(0,1fr) minmax(0,1.05fr);
  column-gap: clamp(2rem, 5vw, 4.5rem);
  align-items: center;
}
.iy-light .iy-philo__copy { max-width: 38rem; }

.iy-philo__p {
  margin: 0 0 1.05rem;
  font-size: inherit; line-height: 1.72;
  color: var(--iy-fg-soft);
}
.iy-philo__p:last-of-type { margin-bottom: 0; }
.iy-philo__pull {
  margin: 0;
  /* Without this the line broke as "...what comes / next." — an orphan on a
     pulled quote, which is the most visible place to have one. */
  text-wrap: balance;
  font-family: var(--display); font-weight: 300;
  font-size: var(--fs-h3);
  letter-spacing: .01em; line-height: 1.4;
  color: var(--iy-fg-accent);           /* 11.04 on black */
}

@media (max-width: 900px) {
  .iy-philo { display: block; min-height: 0; }
  .iy-philo__art {
    position: static; width: auto; padding-right: 0;
    justify-content: center;
    margin-bottom: clamp(2rem, 5vh, 3rem);
  }
  /* The hex is retained but rotated to sit as a horizontal band — a genuinely
     better shape for a full-bleed strip above the copy, not a degradation. */
  .iy-philo__hex { width: 100%; aspect-ratio: 16 / 7; clip-path: var(--hex-90); }
  .iy-philo__veil { display: none; }
  .iy-philo__copy { max-width: none; }

  /* ⚠⚠ TRAP 3, IN THE CHAPTER THIS PAGE IS BUILT AROUND, AND IT SHIPPED.
     The two-column grid is declared on `.iy-light .iy-philo__inner` at
     specificity (0,2,0). Everything above resets `.iy-philo` and
     `.iy-philo__art` at (0,1,0) — and a media query adds NO specificity, so
     none of it could reach the grid. At 390 the chapter therefore stayed TWO
     COLUMNS: the copy ran in a 155px measure with the honeycomb band beside
     it, and the headline set to six lines of two or three words each.
     Read the old _qa\r1\mobile-philosophy.png.

     Nothing could see it. There is no overflow — a 155px column is perfectly
     legal, the text wraps inside it, the contrast is unchanged and the ink
     never leaves its box. 240 checks were green. It is visible in one glance
     at the render and in no other way.

     Reset at MATCHING (0,2,0) specificity, and the placement stated rather
     than left to source order (Trap 8) so the band lands above the copy the
     way the comment above says it does — the copy is first in the DOM, so
     without grid-row the "band above the copy" was never going to happen
     either. */
  .iy-light .iy-philo__inner {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    column-gap: 0;
    align-items: start;
  }
  .iy-light .iy-philo__art  { grid-row: 1; grid-column: 1; }
  .iy-light .iy-philo__copy { grid-row: 2; grid-column: 1; max-width: none; }
}

/* ═══════════════════════════════════════════════════════════════════════
   04 · pillars   +   05 · capabilities — the honeycomb
   One geometry, two weights. The pillars are hex OUTLINES on black; the
   capability doors are FILLED tiles on ink with a texture behind. Same
   lattice, different mass, so the two chapters read as one system rather
   than as the same idea twice.
   ═══════════════════════════════════════════════════════════════════════ */

.iy-comb {
  --comb-gap: 10px;
  --ch: calc(var(--cw) * 1.1547);      /* a regular pointy-top hexagon */
  display: flex; justify-content: center;
}
.iy-comb__row { display: flex; gap: var(--comb-gap); }

.iy-cell {
  position: relative;
  /* flex:0 0 auto is load-bearing. Without it a flex item's default
     `flex-shrink:1` lets the row squeeze a cell narrower than --cw while
     `height` stays put, and the hexagon silently stops being regular. */
  flex: 0 0 auto;
  width: var(--cw); height: var(--ch);
  display: flex; align-items: center; justify-content: center;
  text-decoration: none; color: inherit;
  opacity: 0; transform: scale(.94);
  transition: opacity .7s var(--ease), transform .7s var(--ease-out);
  transition-delay: var(--d, 0ms);
}
/* The cells light in sequence off ONE [data-reveal] on the cluster — the same
   propagation language as the hero lattice. The per-cell offsets are inline
   --d values in the markup, so the propagation order is readable there. */
.iy-comb.is-revealed .iy-cell { opacity: 1; transform: scale(1); }

/* Two stacked clipped layers, because clip-path cuts a border and an inset
   box-shadow both. The frame IS the hairline; the face is inset by its
   width. Session-1 finding 1 / reference_clip_path_kills_borders. */
.iy-cell__frame {
  position: absolute; inset: 0;
  clip-path: var(--hex);
  background: rgba(243,180,49,.34);
  transition: background .4s var(--ease);
}
.iy-cell__face {
  position: absolute; inset: 1.5px;
  clip-path: var(--hex);
  background: var(--iy-bg-raised);
  overflow: hidden;
  transition: background .4s var(--ease);
}
.iy-cell__body {
  position: relative; z-index: 2;
  width: 100%;
  padding-inline: clamp(.9rem, 2.4vw, 2rem);
  text-align: center;
  display: flex; flex-direction: column; align-items: center; gap: .45rem;
}
.iy-cell__icon { display: none; }      /* only appears in the <480px list form */

/* ── 04 pillars ─────────────────────────────────────────────────────── */

/* D7 · the field lattice.
   `position: relative` + `overflow: hidden` on the section, exactly as
   .iy-contact does it — the canvas is sized to the element and a lattice cell
   half-outside the chapter would otherwise paint over the seam.
   ⚠ .iy-wrap inside this chapter needs its own stacking context or the canvas
   paints over the type; the wrap rule below states z-index explicitly rather
   than relying on source order, which is the same discipline as Trap 8. */
.iy-pillars { position: relative; overflow: hidden; }
.iy-pillars__canvas {
  position: absolute; inset: 0;
  width: 100%; height: 100%; display: block;
  z-index: 0;
}
/* The headline is the one thing in this chapter that must have nothing moving
   behind it, so the scrim is heaviest across the top third and clears toward
   the hexagons, which can carry texture. The bottom band keeps the field off
   the seam so the cut stays clean. */
.iy-pillars__scrim {
  position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background:
    linear-gradient(to bottom, var(--iy-black) 0%, rgba(5,5,6,.90) 20%, rgba(5,5,6,.34) 44%, rgba(5,5,6,.62) 100%),
    radial-gradient(120% 60% at 50% 60%, transparent 0%, rgba(5,5,6,.34) 74%, rgba(5,5,6,.92) 100%);
}
.iy-pillars > .iy-wrap { position: relative; z-index: 2; }
/* 272px cells holding type this size read as four big empty outlines with a
   caption floating in the middle — "unfinished", not "composed". Measured on
   the first render: the type filled 23% of the hexagon's usable band. The fix
   is the standing one — BIGGER TYPE, not more words — plus a smaller cell. */
/* Second pass, same problem: at --cw 244 the cell is 282px tall with a ~110px
   usable band, and even after the type went up the block filled only 53% of
   it. Four hairline outlines that big, in a mostly empty black field, still
   read as unfinished. Cell down to 190, type up to --fs-sub: band ~110px
   carrying an ~82px block, about 75%. The gate's threshold goes 42% -> 55%. */
/* ⚠ THIRD pass, and this one is the first with a MEASURED advance width.
   The 190px cell above was derived from an assumed 0.53-0.63em per character.
   Archivo 300 uppercase at .16em tracking actually measures 0.7406 em/char, so
   INTELLIGENT at --fs-sub needed 204px of ink inside 142px of available text
   width — 62px over. It did not clip, because .iy-cell__body is not the clipped
   layer: it simply ran out of the hexagon and into its neighbour, and
   CONNECTED / INTELLIGENT rendered as the single word CONNECTEDINTELLIGENT
   with a 0px gap between them. Read _qa\r1\desktop-cut-philosophy-pillars.png.
   228 checks were green: the gate measured the BODY's box, which is width:100%
   of the cell and therefore can never report an overflow. It now measures the
   ink (section C, the Range-based check).

   Three levers, measured against each other across seven widths:
     · type down to --fs-card      -> fits, but fill drops to 0.53, which is
       the exact figure the second pass rejected as "reads as unfinished";
     · cell up, type unchanged     -> fits at 245px, fill 0.52. Same problem.
     · tracking .16em -> .08em     -> fits with 17-30px to spare at every width
       from 640 to 1920, ink gap 55px, and fill 0.56-0.62 — still clear of the
       0.55 floor. The type stays at the size the second pass raised it to.
   Tracking was where the room was: .16em was adding 44px to an 11-character
   word, 22% of its whole width. */
.iy-comb--pillars { --cw: clamp(190px, 16vw, 226px); gap: var(--comb-gap); }
.iy-comb--pillars .iy-cell__body { padding-inline: clamp(.45rem, 1vw, .85rem); gap: .55rem; }
.iy-comb--pillars .iy-cell__name { letter-spacing: .08em; margin-right: -.08em; }

.iy-cell__name {
  font-family: var(--display); font-weight: 300;
  text-transform: uppercase; letter-spacing: .16em;
  font-size: var(--fs-sub);
  color: var(--iy-fg-lead);           /* 14.49 on black — the cell's lead */
  margin: 0 -.16em 0 0;                /* cancel the trailing letter-space */
}
.iy-cell__desc {
  margin: 0;
  font-size: var(--fs-small); line-height: 1.5;
  color: var(--iy-fg-soft);
  /* Three of the four descriptors were breaking with a single word alone on
     line two. Across a row of four that reads as ragged rather than composed. */
  text-wrap: balance;
  /* Two lines of space, always. The body is vertically CENTRED in the cell, so
     a one-line descriptor beside a two-line one pushes their names to
     different heights — four pillars on four baselines. Measured spread before
     this line: 9.6px. Reserving the second line costs nothing and removes the
     whole class of defect. */
  min-height: 3em;
}

/* ── 05 capabilities ────────────────────────────────────────────────── */
.iy-comb--caps {
  /* ⚠ 24vw -> 28vw, and the reason is the descriptions, not the hexagons.
     At 24vw the cell ran 216px at 901 and 246px at 1024, which pushed the
     longest description ("Modernise, secure and optimise your technology
     environment.") to THREE lines while the other seven stayed at two. The
     body is vertically centred in the cell, so one three-line description
     lifted that whole tile's name 9.4px above the two beside it — visible in
     _qa\probe\caps-960.png, and present across the entire 861-1099 band.
     A three-line min-height reserve fixes the symptom and costs every other
     tile a dead line. Widening the cell removes the CAUSE: measured at 28vw,
     every description is two lines from 861px up and the worst row spread is
     0.0px at 861, 901, 960, 1024, 1099 and 1160.
     The 300px ceiling is unchanged, so nothing moves at 1071px and above. */
  --cw: clamp(150px, 28vw, 300px);
  flex-direction: column; align-items: center;
}
/* 3-2-3 packing. The two-cell row centres under the three-cell row, which
   lands it at exactly the half-pitch offset a pointy-top honeycomb wants —
   no magic number needed. Row pitch is .75*ch plus the gap. */
.iy-comb--caps .iy-comb__row + .iy-comb__row {
  margin-top: calc(var(--comb-gap) - var(--ch) * .25);
}
.iy-comb--caps .iy-cell__face { background: var(--iy-ink-2); }
.iy-comb--caps .iy-cell__frame { background: rgba(243,180,49,.20); }

/* ⚠ THE DARK ISLAND — read this before touching chapter 05's ground.
   This chapter is now LIGHT, but the eight tiles stay DARK. That is deliberate
   and it is the single biggest visual win in this pass.

   The four textures (iy-tex-fibre/mesh/circuit/server) are dark tech
   photographs whose subject only reads bright-on-dark. At 18% over white they
   become grey smudges. Lowering the opacity makes them worse, and swapping
   them for a CSS halftone throws away 250KB of paid-for brand imagery.

   So the tile does not move — the GROUND moves out from under it. Each tile
   carries .iy-invert in the markup, which puts --iy-bg-raised back to ink, the
   name back to white and the frame back to gold-400, while the chapter around
   it is paper. Eight near-black hexagons on paper is the highest-contrast
   object available on this page, and it fixes the old dark-on-dark defect
   (tiles at 18% on --iy-ink barely separated from their own ground) in the
   same move. */

/* The one thing the token layer CANNOT do for the island, because the ring is
   drawn OUTSIDE the tile. outline-offset:4px puts it on the paper, where the
   island's own gold-200 is 1.65:1 and effectively invisible — a keyboard user
   would see nothing. The ring has to read the PARENT ground, not the island's.
   This is exactly the class of defect that ships silently; it is measured. */
.iy-light .iy-tile:focus-visible { outline-color: var(--iy-gold-800); }   /* 9.20 on paper */

/* .iy-tile__tex is (0,1,0) and primitives' `img` rule is (0,0,1), so these
   win — but that is exactly the specificity trap that put both logo lockups
   in the nav in Phase 4, so the height/width are stated rather than assumed. */
.iy-tile__tex {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover; object-position: center;
  opacity: .18;
  transition: opacity .45s var(--ease), transform .7s var(--ease);
}
.iy-tile__name {
  font-family: var(--display); font-weight: 800;
  font-size: var(--fs-card);
  letter-spacing: -.012em; line-height: 1.15;
  color: var(--iy-fg);
  /* Two lines of space, always — the SAME defect .iy-cell__desc solves below,
     one level up and missed because only the descriptors were checked. "Network
     Infrastructure" sets to two lines and "Connectivity" to one, and with the
     body vertically centred in the hexagon that put their names 12.8px apart
     on one row. Visible in desktop-capabilities.png before this line; nothing
     else in the toolchain can see it. 2 lines x 1.15 line-height = 2.3em. */
  min-height: 2.3em;
}
.iy-tile__desc {
  font-size: var(--fs-micro); line-height: 1.45;
  color: var(--iy-fg-soft);
  min-height: 2.9em;                   /* same reason as .iy-cell__desc */
}
.iy-tile__go {
  width: 10px; height: 11px; margin-top: .3rem;
  clip-path: var(--hex); background: var(--iy-mark); opacity: .5;
  transition: opacity .35s var(--ease), transform .35s var(--ease);
}

.iy-comb.is-revealed .iy-tile:hover { transform: scale(1) translateY(-6px); }
.iy-tile:hover .iy-cell__frame { background: var(--iy-mark); }
.iy-tile:hover .iy-tile__tex   { opacity: .30; transform: scale(1.06); }
.iy-tile:hover .iy-tile__go    { opacity: 1; transform: translateY(2px) scale(1.15); }

/* The focus ring lives on the UNCLIPPED <a>, not on the frame. An outline on
   a clip-path'd element is cut away with everything else outside the polygon,
   which would leave a keyboard user with no visible focus at all. */
.iy-tile:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 4px; }
.iy-tile:focus-visible .iy-cell__frame { background: var(--iy-mark); }

/* ── honeycomb, narrow ──────────────────────────────────────────────── */
@media (max-width: 900px) {
  .iy-comb--pillars {
    --cw: clamp(150px, 34vw, 250px);
    flex-direction: column; align-items: center;
  }
  /* Row 2 sits half a pitch right of row 1, which is the honeycomb position.
     The two rows are offset a QUARTER pitch each, in opposite directions,
     rather than shifting row 2 by a half and the container back by a quarter.

     ⚠ That first version did not work, and the reason is worth keeping: the
     container carries [data-reveal], so primitives' own
     `[data-reveal].is-revealed { transform: translateY(0) }` at (0,2,0) BEAT
     `.iy-comb--pillars { transform: ... }` at (0,1,0) and the counter-shift
     silently never applied. Measured at 640px: the cluster ran to x=649
     inside a wrap ending at x=625, clipped and off-centre, with lint and the
     scrollWidth check both green. Same family as the Phase 4 double-logo bug.
     Never put a transform on an element whose transform something else owns. */
  .iy-comb--pillars .iy-comb__row:first-child {
    transform: translateX(calc(-1 * (var(--cw) + var(--comb-gap)) / 4));
  }
  .iy-comb--pillars .iy-comb__row + .iy-comb__row {
    margin-top: calc(var(--comb-gap) - var(--ch) * .25);
    transform: translateX(calc((var(--cw) + var(--comb-gap)) / 4));
  }
}

/* Hexagons at phone width collapse into unreadable slivers, so chapter 05
   drops honeycomb packing entirely for a plain 2-col grid of squared cards.
   display:contents dissolves the three row wrappers so the eight cells become
   grid items directly, which keeps ONE markup for both packings.

   ⚠ 760 -> 860, and it is a MEASURED boundary now rather than a guess about
   phones. At 780 the three-across honeycomb puts each cell at 187px, which
   made "Modernise, secure and optimise your technology environment." set to
   FOUR lines and drove the body to 139% of the hexagon's usable band — content
   running out into the taper, with the row's three names 18.8px apart. Read
   _qa\probe\caps-780.png: it is the cramped, unfinished reading the card form
   exists to avoid, and 187px is a sliver by exactly the argument above. From
   861 up, cells at 28vw hold every description in two lines with the body at
   65-79% of the band. Below 861 the cards do it properly. */
@media (max-width: 860px) {
  .iy-comb--caps {
    display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: .7rem;
    /* stretch, stated explicitly. The base rule sets align-items:center for
       the desktop COLUMN flex layout, and that declaration does not stop
       applying when display switches to grid — it just changes meaning, to
       align-self:center on every card. Measured at 390px: the two cards in a
       row differed in height (158 vs 174.5) and neither their tops nor their
       bottoms lined up, because the shorter one was being centred in the row.
       A cross-axis default doing something invisible to lint, again. */
    align-items: stretch;
  }
  .iy-comb--caps .iy-comb__row { display: contents; }
  .iy-comb--caps .iy-comb__row + .iy-comb__row { margin-top: 0; }
  /* flex-start, not the inherited centre. As squared cards the cells are
     equal height, but a centred body puts a one-line name and a two-line name
     on different baselines across a row. Top-aligned, every card's name starts
     at the same y. */
  .iy-comb--caps .iy-cell { width: auto; height: auto; min-height: 158px; align-items: flex-start; }
  .iy-comb--caps .iy-cell__frame,
  .iy-comb--caps .iy-cell__face { clip-path: none; border-radius: 2px; }
  .iy-comb--caps .iy-cell__body {
    padding: 1.15rem 1rem; text-align: left; align-items: flex-start; gap: .4rem;
  }
}

/* Under 480 the pillars lose the frame entirely and the hexagon becomes a
   small leading icon beside each name — 02-art-direction.md §3. */
@media (max-width: 480px) {
  .iy-comb--pillars { display: block; }
  /* :first-child is listed EXPLICITLY. The 900px rule that offsets row 1 is
     `.iy-comb--pillars .iy-comb__row:first-child` at specificity (0,3,0), and
     a plain `.iy-comb--pillars .iy-comb__row` reset at (0,2,0) LOSES to it
     even from a later media query — so row 1 kept its quarter-pitch shift in
     the list form and hung 20px outside the measure. Caught by the centring
     check, not by the eye. Media queries do not add specificity. */
  .iy-comb--pillars .iy-comb__row,
  .iy-comb--pillars .iy-comb__row:first-child,
  .iy-comb--pillars .iy-comb__row + .iy-comb__row { display: block; margin: 0; transform: none; }
  .iy-comb--pillars .iy-cell {
    display: block; width: 100%; height: auto;
    padding-block: 1.05rem;
    border-bottom: 1px solid var(--iy-rule);
  }
  .iy-comb--pillars .iy-comb__row:last-child .iy-cell:last-child { border-bottom: 0; }
  .iy-comb--pillars .iy-cell__frame { display: none; }
  .iy-comb--pillars .iy-cell__body {
    padding: 0; text-align: left;
    flex-direction: row; align-items: flex-start; gap: .85rem;
  }
  .iy-comb--pillars .iy-cell__icon { display: block; margin-top: .42rem; }
  .iy-comb--pillars .iy-cell__name { margin-right: 0; margin-bottom: .25rem; }
  /* The reserved second line exists to keep names on one baseline when the
     cells are hexagons side by side. In the list form the names are already
     aligned by the layout, so the reservation is just dead space under every
     row and it pushes each divider low. */
  .iy-comb--pillars .iy-cell__desc { min-height: 0; }
}

/* ═══════════════════════════════════════════════════════════════════════
   06 · sectors — public and private.  The first light chapter.
   ═══════════════════════════════════════════════════════════════════════ */

.iy-sectors__grid {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(1.6rem, 4vw, 3.5rem);
}
/* This chapter is now DARK, and that changes what a panel has to be.
   Both photographs are dark and gold-lit. Dropped straight onto black they
   reproduce exactly the defect chapter 05 used to have: a dark image on a dark
   ground, with no discernible boundary, so the picture reads as a smudge
   rather than as a framed object.

   The fix is an EDGE, not more brightness. Do NOT put a filter on these images
   to lift them - they were generated at the right key, and raising their black
   point turns them muddy. Instead the panel becomes a raised surface with a
   hairline, and a 1px gold rule under the media does the work the veil used to
   do in chapter 03. */
.iy-panel {
  background: var(--iy-bg-raised);
  border: 1px solid var(--iy-rule);
  padding-bottom: clamp(1.4rem, 2.4vw, 2rem);
}
.iy-panel__media {
  position: relative; aspect-ratio: 4 / 3; overflow: hidden;
  background: var(--iy-media-bg);
  border-bottom: 1px solid var(--iy-mark);
}
/* The card has an edge now, so the type inside it needs a gutter or it sits
   hard against the hairline. */
.iy-panel .iy-rule-node,
.iy-panel__h,
.iy-panel__p { margin-inline: clamp(1.2rem, 2.2vw, 1.8rem); }
.iy-panel__h { margin-top: clamp(1.2rem, 2.2vw, 1.6rem); }
.iy-panel__media img {
  position: absolute; inset: 0;
  display: block; width: 100%; height: 100%;
  object-fit: cover; object-position: center;
}
.iy-panel__h {
  font-family: var(--display); font-weight: 800;
  font-size: var(--fs-h3);
  letter-spacing: -.015em; line-height: 1.1;
  margin: 0 0 .6rem;
}
.iy-panel__p {
  margin: 0;
  font-size: inherit; line-height: 1.6;
  color: var(--iy-fg-soft);
}
@media (max-width: 760px) {
  .iy-sectors__grid { grid-template-columns: 1fr; }
}

/* ═══════════════════════════════════════════════════════════════════════
   07 · engage — how we work  ⭐
   Geometry only. The rail draws left to right and the nodes light as it
   passes; on a phone the whole diagram rotates to a vertical timeline.
   ═══════════════════════════════════════════════════════════════════════ */


.iy-flow { position: relative; }
/* left/right 10% put the rail's ends at the first and last node CENTRES,
   because five equal columns make each column 20% wide. */
.iy-flow__rail {
  position: absolute; left: 10%; right: 10%; top: 8px; height: 1px;
  background: linear-gradient(to right,
    transparent, var(--iy-rule-2) 18%,
    var(--iy-rule-2) 82%, transparent);
  transform: scaleX(0); transform-origin: left center;
  transition: transform 1.5s var(--ease-out) .1s;
}
.iy-flow.is-revealed .iy-flow__rail { transform: scaleX(1); }

.iy-flow__steps {
  margin: 0; padding: 0; list-style: none;
  display: grid; grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: clamp(.8rem, 2vw, 1.6rem);
}
.iy-flow__step { position: relative; }
.iy-flow__node {
  display: block; width: 15px; height: 17px;
  margin: 0 auto 1.35rem;
  clip-path: var(--hex); background: var(--iy-mark);
  opacity: .18; transform: scale(.5);
  transition: opacity .5s var(--ease), transform .5s var(--ease-out);
  transition-delay: var(--d, 0ms);
}
.iy-flow.is-revealed .iy-flow__node { opacity: 1; transform: scale(1); }

/* This <h3> used to render at 13.6px — SMALLER than the site's own nav links
   at 14px, and barely above the eyebrow floor. A heading rendering at label
   size is most of why this chapter read as unfinished.
   Weight goes 300 -> 500 and tracking .2em -> .05em with the size: Archivo 300
   at 22px on a ground this size is too thin to hold, and .2em tracking on a
   14-character word at 22px is what would push IMPLEMENTATION out of its
   column. Both are measured by the gate at 1280 and 1024. */
.iy-flow__name {
  font-family: var(--display); font-weight: 500;
  text-transform: uppercase; letter-spacing: .05em;
  font-size: var(--fs-card);
  color: var(--iy-fg);
  text-align: center;
  text-wrap: balance;
  /* .05em -> .04em, measured. IMPLEMENTATION is fourteen characters and the
     only label that cannot break, so it sets the whole row's width. At .05em
     its ink was 212px inside a 213px column at 1440 — a one-pixel fit, which
     is a coincidence rather than a fit, and it went to zero at 1920. */
  letter-spacing: .04em;
  margin: 0 -.04em .55rem 0;
  /* ONGOING SUPPORT wraps to two lines; the other four do not. Vertically the
     row then puts five descriptors on two different baselines. Reserving the
     second line costs nothing and removes the whole class of defect - same
     reasoning as .iy-cell__desc and .iy-tile__desc. */
  min-height: 2.4em;
}
.iy-flow__desc {
  margin: 0; text-align: center;
  font-size: var(--fs-small); line-height: 1.5;
  color: var(--iy-fg-soft);
}

/* ⚠ 760 -> 1150, and it is measured. Five process steps across a grid means
   each column is (measure - 4 gaps) / 5, and IMPLEMENTATION is an unbreakable
   fourteen-character word that sets the floor. At 900px the column is 148px
   holding 183px of ink: the label ran 32px out of its own column and stopped
   4px short of PROCUREMENT's last letter. Not a clip and not an overflow — the
   grid was happy, scrollWidth was happy, and two labels nearly touched.
   The five-across frieze needs about 1150px before the arithmetic works, so
   below that it takes the vertical form, which keeps the device: the rail
   simply draws top-to-bottom instead of left-to-right. Same class of decision
   as the capabilities honeycomb handing over to cards at 860. */
@media (max-width: 1150px) {
  .iy-flow__steps { grid-template-columns: 1fr; gap: 1.55rem; }
  .iy-flow__step { padding-left: 2.4rem; }
  .iy-flow__node { position: absolute; left: 0; top: 1px; margin: 0; }
  .iy-flow__name, .iy-flow__desc { text-align: left; }
  /* ⚠ min-height: 0 is NOT tidying. The 2.4em reserve above exists so that
     ONGOING SUPPORT wrapping to two lines does not drop its descriptor below
     the other four — a ROW problem. Stacked, every label is one line, so the
     reserve becomes an empty line under each of them: measured at 1024, a
     label sat 57px from its OWN description and 36px from the NEXT label.
     Proximity inverted, so every description read as belonging to the step
     below it. Restated explicitly at matching (0,1,0) specificity per Trap 3;
     a reservation that is right in one layout is wrong in the other. */
  .iy-flow__name { margin-right: 0; min-height: 0; }
  /* The rail's centre is 7.5px in, which is where a 15px node centred on
     left:0 sits. Same geometry, rotated. */
  .iy-flow__rail {
    left: 7px; right: auto; top: 6px; bottom: 6px;
    width: 1px; height: auto;
    background: linear-gradient(to bottom,
      transparent, var(--iy-rule-2) 10%,
      var(--iy-rule-2) 90%, transparent);
    transform: scaleY(0); transform-origin: center top;
  }
  .iy-flow.is-revealed .iy-flow__rail { transform: scaleY(1); }
}

/* ═══════════════════════════════════════════════════════════════════════
   08 · ecosystem — technologies, quietly
   Text only, and deliberately the quietest chapter on the page. The row is a
   list of words with hairline separators, NOT chips and NOT badges: anything
   that looks like a container around a vendor name starts to read as a tier.
   ═══════════════════════════════════════════════════════════════════════ */

/* ⚠ A GRID, NOT A WRAPPING FLEX ROW, and the reason is a defect this project
   has already shipped once.

   As a flex row with a separator on every item bar the last, the row wrapped
   after "Networking" and left its separator DANGLING at the end of the line
   with nothing after it — a hairline pointing into empty space. That is
   character-for-character the defect the creed band hit on a phone in Phase 5,
   and :last-child cannot fix it, because the item at a line END is not the
   last item in the list. CSS has no "last on this line" selector.

   A grid removes the whole class of problem: the wrap points are DETERMINISTIC,
   so the item that ends each visual row is always :nth-child(3n) and can be
   targeted. The rules below still behave if the two conditional names are
   deleted — with 4 items the rows are [1,2,3][4], with 5 they are [1,2,3][4,5],
   and in both cases every line still ends on a 3n or on :last-child. */
.iy-eco__row {
  margin: 0; padding: 0; list-style: none;
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(1rem, 2.4vw, 1.8rem) 0;
  align-items: center;
}
.iy-eco__item {
  position: relative;
  font-family: var(--display); font-weight: 300;
  font-size: var(--fs-sub);
  letter-spacing: .06em;
  color: var(--iy-fg);
  padding-right: clamp(1rem, 2.4vw, 2.2rem);
}
/* NO separator glyph or hairline at all, and that is the second decision here.
   Once the grid made the columns align, a divider at the column EDGE sat far
   from both neighbours and read as a stray tick rather than as punctuation.
   The alignment is already doing the separating. This also means the row
   cannot regress into the dangling-separator defect by any route, at any
   width, however many items survive the Microsoft / Liquid Telecom decision.

   The copy deck writes this row as "Microsoft · Liquid Telecom · ..." but that
   is a description of a ROW, not a required glyph — same as the pillars, which
   the deck also lists with separators and which ship as four hexagon cells. */
@media (max-width: 900px) {
  .iy-eco__row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 560px) {
  .iy-eco__row { grid-template-columns: 1fr; gap: .75rem 0; }
  .iy-eco__item { font-size: var(--fs-card); padding-right: 0; }
}

/* ═══════════════════════════════════════════════════════════════════════
   09 · insights — the three real articles
   Light chapter. Three cards, no imagery (image-manifest.md §5). The date is
   set in the tracked display style so it reads as DATA rather than as
   decoration — that is what stops three text cards looking like a blog widget.
   ═══════════════════════════════════════════════════════════════════════ */

.iy-posts {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(1.4rem, 3vw, 2.6rem);
}
.iy-post {
  display: flex; flex-direction: column;
  /* Explicit. This is a grid child AND a flex container, which is precisely
     the combination that centred a whole row of cards off their baseline in
     Phase 5 when an align-items written for one context kept applying in the
     other. State both axes. */
  align-items: stretch;
  padding: clamp(1.3rem, 2.4vw, 1.9rem);
  background: var(--iy-bg-raised);
  border: 1px solid var(--iy-rule);
  border-top: 1px solid var(--iy-mark);
  transition: transform .35s var(--ease), border-color .35s var(--ease);
}
.iy-post__date {
  margin: 0 -.28em .9rem 0;
  font-family: var(--display); font-weight: 300;
  text-transform: uppercase; letter-spacing: .28em;
  font-size: var(--fs-micro);
  color: var(--iy-fg-accent);
}
.iy-post__h {
  margin: 0 0 .7rem;
  font-family: var(--display); font-weight: 800;
  font-size: var(--fs-card);
  letter-spacing: -.012em; line-height: 1.2;
  color: var(--iy-fg);
  text-wrap: balance;
  /* One title runs to three lines and two run to two, which starts the three
     standfirsts at three different heights across the row. The links already
     bottom-align via flex:1; this aligns the tops too. Reserve three lines. */
  min-height: 3.6em;
}
.iy-post__p {
  margin: 0 0 1.4rem;
  font-size: var(--fs-small); line-height: 1.55;
  color: var(--iy-fg-soft);
  /* Same reserved-space logic as .iy-cell__desc and .iy-tile__desc: the link
     below is pushed to the card foot by margin-top:auto, so without this the
     three standfirsts of different lengths put three links on three baselines. */
  flex: 1;
}
.iy-post__link {
  display: inline-flex; align-items: center; gap: .55rem;
  align-self: flex-start;
  font-size: var(--fs-micro); font-weight: 500; letter-spacing: .04em;
  color: var(--iy-fg-accent); text-decoration: none;
  border-bottom: 1px solid var(--iy-rule);
  padding-bottom: 2px;
  transition: border-color .3s var(--ease), gap .3s var(--ease);
}
.iy-post:hover { transform: translateY(-4px); border-color: var(--iy-rule); }
.iy-post:hover .iy-post__link { border-bottom-color: var(--iy-mark); gap: .8rem; }
.iy-post__link:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 3px; }

/* Tap target. Measured 136x24 at 390 — over the 24px WCAG 2.5.8 floor but well
   under the 44px this page holds everywhere else, and this is the only link in
   the chapter. The padding goes ABOVE the rule and is cancelled by a negative
   margin, so the hit box grows without the underline moving. */
@media (max-width: 760px) {
  /* Top-weighted for the same reason as .iy-detail a: this link's underline is
     a border-bottom and follows the padding box. */
  .iy-post__link { padding-top: 22px; margin-top: -14px; }
}

@media (max-width: 900px) {
  /* display stays grid — only the track count changes, so no cross-axis
     restatement is needed here. Noted explicitly because the sibling rule in
     chapter 05 DOES switch display and therefore does need one. */
  .iy-posts { grid-template-columns: 1fr; }
  .iy-post__p { flex: none; margin-bottom: 1.1rem; }
}

/* ═══════════════════════════════════════════════════════════════════════
   10 · contact — convert
   The page closes on the image it opened with. The lattice returns at roughly
   half the hero's opacity, and the scrim is driven by --p so it deepens as the
   footer approaches rather than sitting at one value.
   ═══════════════════════════════════════════════════════════════════════ */

.iy-contact { position: relative; overflow: hidden; }
.iy-contact__canvas {
  position: absolute; inset: 0;
  width: 100%; height: 100%; display: block;
}
.iy-contact__scrim {
  position: absolute; inset: 0; pointer-events: none;
  background:
    radial-gradient(95% 75% at 76% 42%, transparent 0%, rgba(5,5,6,.58) 58%, var(--iy-black) 100%),
    linear-gradient(to right, var(--iy-black) 0%, rgba(5,5,6,.62) 34%, transparent 62%);
  /* --p runs 0 at chapter entry to 1 at exit. The scrim is already doing its
     job at rest; this only closes it the last of the way, so the form never
     sits on a busy lattice while it is being read. */
  opacity: calc(.82 + .18 * var(--p, 1));
}
.iy-contact__inner {
  position: relative; z-index: 2;
  display: grid; grid-template-columns: minmax(0,.9fr) minmax(0,1.1fr);
  column-gap: clamp(2rem, 6vw, 5.5rem);
  align-items: start;
}

.iy-detail { margin: clamp(1.8rem, 4vh, 2.6rem) 0 0; display: grid; gap: 1.05rem; }
.iy-detail dt {
  font-family: var(--display); font-weight: 300;
  text-transform: uppercase; letter-spacing: .28em;
  margin-right: -.28em;
  font-size: var(--fs-eyebrow);
  color: var(--iy-fg-accent);
}
.iy-detail dd {
  margin: .3rem 0 0;
  font-size: var(--fs-small);
  color: var(--iy-fg);
  max-width: 34ch;
}
.iy-detail a { color: inherit; text-decoration: none; border-bottom: 1px solid var(--iy-rule); }
.iy-detail a:hover { border-bottom-color: var(--iy-mark); }

/* ── the form ───────────────────────────────────────────────────────────
   .u-field comes from primitives.css — mechanics only, brand-colour free, and
   toggled by OUR script.js rather than by the frozen engine. The engine's own
   form handler binds to [data-quote-form], which this markup deliberately does
   not have. See the block comment in index.html chapter 10.
   ───────────────────────────────────────────────────────────────────── */
.iy-form { display: grid; gap: 1.05rem; }
.u-field label {
  display: block; margin-bottom: .4rem;
  font-family: var(--display); font-weight: 300;
  text-transform: uppercase; letter-spacing: .24em;
  margin-right: -.24em;
  font-size: var(--fs-eyebrow);
  color: var(--iy-fg-accent);
}
/* Inputs keep primitives' 16px. That is a MECHANICAL iOS requirement — Safari
   zooms the viewport on focus for anything below 16px — not a type decision.
   Form controls are deliberately NOT on the type lattice. */
.iy-form__submit { justify-self: start; margin-top: .35rem; }
.iy-form__note {
  margin: 0;
  font-size: var(--fs-micro);
  color: var(--iy-fg-soft);
}

@media (max-width: 900px) {
  /* display switches grid -> block, so the cross axis is restated rather than
     left to change meaning underneath us. Trap 2. */
  .iy-contact__inner { display: block; align-items: start; }
  .iy-contact__r { margin-top: clamp(2rem, 5vh, 3rem); }
}

/* ═══════════════════════════════════════════════════════════════════════
   11 · site-footer
   The wordmark is ARCHITECTURE, not a heading: low-contrast gold, aria-hidden,
   and it opens its tracking as the page bottoms out.
   ═══════════════════════════════════════════════════════════════════════ */

.iy-foot { padding-block: clamp(3.5rem, 9vh, 6rem) clamp(2rem, 4vh, 3rem); }

.iy-foot__word-wrap { overflow: hidden; }
.iy-foot__word {
  margin: 0;
  font-family: var(--display); font-weight: 300;
  text-transform: uppercase;
  font-size: clamp(2.6rem, 14vw, 12.5rem);
  line-height: 1;
  /* --p, NOT transform. The wrapper carries [data-reveal] and primitives owns
     `transform` on that element — a transform here would be beaten at (0,2,0)
     and silently do nothing, which is the trap that cost two bugs in Phase 5.
     letter-spacing is not transform, so this is safe either way. */
  letter-spacing: calc(.26em + .08em * var(--p, 1));
  margin-right: -.34em;
  color: rgba(243,180,49,.14);
  user-select: none; white-space: nowrap;
}

.iy-foot__creed {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: .9rem;
  margin: clamp(1.4rem, 3vh, 2.2rem) 0 clamp(2.4rem, 5vh, 3.6rem);
  padding-top: clamp(1.4rem, 3vh, 2.2rem);
  border-top: 1px solid var(--iy-rule);
  font-family: var(--display); font-weight: 300;
  text-transform: uppercase; letter-spacing: .34em;
  font-size: var(--fs-eyebrow);
  color: var(--iy-fg-soft);
}

.iy-foot__cols {
  display: grid; grid-template-columns: 1.1fr 1fr 1.3fr;
  gap: clamp(1.6rem, 4vw, 3.5rem);
  align-items: start;
}
.iy-foot__h {
  margin: 0 0 .9rem;
  font-family: var(--display); font-weight: 300;
  text-transform: uppercase; letter-spacing: .28em;
  margin-right: -.28em;
  font-size: var(--fs-eyebrow);
  color: var(--iy-fg-accent);
}
.iy-foot__col ul { margin: 0; padding: 0; list-style: none; display: grid; gap: .5rem; }
.iy-foot__col li { font-size: var(--fs-small); color: var(--iy-fg-soft); line-height: 1.5; }
.iy-foot__col a { color: inherit; text-decoration: none; transition: color .3s var(--ease); }
.iy-foot__col a:hover { color: var(--iy-fg); }

.iy-foot__bar {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: 1rem;
  margin-top: clamp(2.4rem, 5vh, 3.6rem);
  padding-top: clamp(1.2rem, 2.5vh, 1.8rem);
  border-top: 1px solid var(--iy-rule);
}
.iy-foot__legal { margin: 0; font-size: var(--fs-micro); color: var(--iy-fg-soft); }
.iy-foot__social { margin: 0; padding: 0; list-style: none; display: flex; gap: 1.4rem; }
.iy-foot__social a {
  font-size: var(--fs-micro); color: var(--iy-fg-soft); text-decoration: none;
  transition: color .3s var(--ease);
}
.iy-foot__social a:hover { color: var(--iy-fg-accent); }

@media (max-width: 760px) {
  /* Only the track count changes; display stays grid. */
  .iy-foot__cols { grid-template-columns: 1fr; gap: 2rem; }
  .iy-foot__bar { flex-direction: column; align-items: flex-start; }

  /* ⚠ TAP TARGETS. Measured at 390: every link in this footer had a hit box
     16-17px tall — the nav links, all eight capability links, the telephone
     and the email. That is under even the 24x24 floor of WCAG 2.5.8, on the
     one surface where a phone user goes looking for the phone number.
     The links are inline, so their box is the line box and nothing else; they
     need to become blocks with real vertical padding. `display: block` also
     widens the target to the full column, so the whole row is tappable rather
     than just the glyphs. The list's own gap comes off to compensate, so the
     footer does not simply get taller by the padding it just gained. */
  .iy-foot__col ul { gap: 0; }
  .iy-foot__col a,
  .iy-foot__social a,
  .iy-foot__legal-links a {
    display: block;
    padding-block: .8rem;
    /* pull the first and last back so the column still starts and ends on the
       type, not on the padding */
    margin-block: -.15rem;
  }
  .iy-foot__social { gap: .4rem 1.4rem; }
  /* ⚠ The padding goes ABOVE, not evenly. These two links carry a
     border-bottom, and a border sits at the edge of the PADDING box — so
     symmetric padding moved the underline 11px away from its own text and it
     read as a detached rule. Top-weighted keeps the hit box at 48px and the
     underline where it was. */
  .iy-detail a { display: inline-block; padding: 22px 0 2px; margin: -22px 0 -2px; }
}

/* ═══════════════════════════════════════════════════════════════════════
   MOTION — the vanilla scroll system
   No GSAP. No ScrollTrigger. No Lenis. No AOS. Everything below is
   position:sticky, IntersectionObserver, one rAF loop writing --p, and CSS.

   Lenis is banned workspace-wide and the reason is mechanical, not
   preferential: it decouples window.scrollY from the rendered frame, which
   breaks every measurement the headless CDP gates take. The one thing this
   system genuinely gives up against a scrubbed GSAP timeline is a long
   multi-beat pinned sequence — and the page does not want one.

   MOTION BUDGET: no more than two devices active in any one chapter. That is
   what keeps this reading as a designed page rather than an effects demo.
   ═══════════════════════════════════════════════════════════════════════ */

/* --p must be a registered <number> or it cannot be interpolated and cannot be
   used inside calc() in an opacity. Registration also gives it a typed initial
   value, so a browser that never runs the rAF loop still computes valid CSS. */
@property --p { syntax: '<number>'; inherits: true; initial-value: 1; }

/* ── D1 · sticky cover ──────────────────────────────────────────────────
   The outgoing chapter is held while the incoming one scrolls up over it.
   Pure CSS, zero JavaScript — this is exactly how the B-TheOne flagship does
   its hero cover (index.html:295), and it is the one cinematic device that
   costs nothing at all.

   ⚠ This depends ENTIRELY on `html, body { overflow-x: clip }` in the base
   section. primitives.css sets overflow-x:hidden there, which makes the root a
   clip container, and sticky inside a clipped ancestor silently does not
   stick — it would just scroll past looking like an ordinary section, with no
   error anywhere. Proven by measurement in the gate against a static control.

   Used at exactly two transitions: 01 hero -> 02 creed (the opening) and
   09 insights -> 10 contact (the close, where the lattice returns). */
.iy-cover { position: relative; }
html.js-anim .iy-cover > .iy-cover__held { position: sticky; top: 0; z-index: 1; }
.iy-cover > .iy-cover__over { position: relative; z-index: 2; }

/* The glowing leading edge that rides the boundary as the cover closes. Its
   negative margin pulls the incoming chapter up over it so the glow sits ON
   the seam rather than above it. */
.iy-cover__edge {
  position: relative; z-index: 3;
  height: 2px; margin-bottom: -2px;
  background: linear-gradient(90deg,
    transparent, var(--iy-gold-200) 18%, var(--iy-gold-100) 50%,
    var(--iy-gold-200) 82%, transparent);
  box-shadow: 0 0 44px 7px rgba(243,180,49,.42), 0 0 14px 3px rgba(247,214,138,.5);
}

/* ── D3 · wipe ──────────────────────────────────────────────────────────
   A clip-path wipe, distinct from the engine's fade-and-rise, so the page has
   two reveal languages rather than one repeated eleven times.
   ⚠ Never put [data-wipe] on an element carrying a border or an inset
   box-shadow — clip-path cuts both (session-1 finding 1). */
[data-wipe] {
  clip-path: inset(0 100% 0 0);
  transition: clip-path 1.15s var(--ease-out);
}
[data-wipe="rtl"] { clip-path: inset(0 0 0 100%); }
[data-wipe].is-in { clip-path: inset(0 0 0 0); }

/* ── D6 · value-cut · the `seam` ────────────────────────────────────────
   The page now swaps value EIGHT times. Each cut has to read as deliberate
   rather than as two sections that happen to be different colours.

   Hard cuts throughout — no gradient blends between grounds. ThoroughTec cuts
   hard between every chapter, and a value gradient between two grounds is the
   visual signature of a template. The deliberateness comes from this device,
   not from softening the edge.

   The hexagon straddles the boundary: half on the outgoing ground, half on the
   incoming one. The page's own motif doing the joinery.

   ⚠ .iy-seam carries [data-reveal], and primitives owns `transform` on that
   element — a transform here would be beaten at (0,2,0) by
   [data-reveal].is-revealed and silently do nothing. That is the trap that
   cost two separate bugs in Phase 5. The scaleX therefore lives on ::before. */
.iy-seam { position: relative; height: 0; z-index: 4; }
.iy-seam::before {
  content: ""; position: absolute;
  left: var(--gutter); right: var(--gutter); top: 0; height: 1px;
  background: var(--iy-mark); opacity: .34;
  transform: scaleX(0); transform-origin: center;
  transition: transform 1.2s var(--ease-out);
}
.iy-seam.is-revealed::before { transform: scaleX(1); }
.iy-seam::after {
  content: ""; position: absolute;
  left: 50%; top: -8px; width: 14px; height: 16px; margin-left: -7px;
  clip-path: var(--hex); background: var(--iy-mark);
}

/* ── D6 · the BLEED, at capabilities -> sectors only ────────────────────
   One row of hexagons straddling the cut. The seam is a zero-height element
   sitting exactly on the boundary, so centring the row on its own top edge
   puts the boundary through the middle of every cell.

   ⚠ The two halves cannot be two elements. A hexagon split into an upper and
   a lower half-shape reads as two chevrons meeting, not as one hexagon lit
   differently — the whole device depends on the eye reading ONE shape that the
   value change passes through. So it is one clipped box carrying a hard-stop
   gradient at exactly 50%, which is where the boundary is.

   ⚠ And the colours cannot be one token. The paper half needs --iy-gold-600
   (3.94 on paper, the 3:1 non-text floor); --iy-gold-200 there measures 1.65
   and is invisible. The black half needs gold-200 (11.04); gold-600 on black
   is 4.63 and reads muddy. Two grounds, two golds, one shape. Same rule the
   semantic layer applies everywhere else on this page.

   Two stacked clipped layers, because clip-path cuts a border and an inset
   box-shadow both — session-1 finding 1, the same reason .iy-cell__frame and
   .iy-cell__face exist. */
.iy-seam--bleed { z-index: 5; }
/* ⚠ TRAP 1, INVERTED — and this is the safe direction of that fight.
   primitives owns `transform` on [data-reveal]: translateY(34px) until
   revealed, translateY(0) after, at (0,2,0). 34px is more than half a cell,
   so for the length of the reveal the row would sit BELOW the boundary and its
   gold halves would meet where the value does not change — the one thing this
   device cannot do. Both states are cancelled explicitly, and the second
   selector is (0,3,0) so it wins on specificity rather than on the load order
   of two stylesheets. The opacity half of the reveal is untouched. */
.iy-seam--bleed[data-reveal],
.iy-seam--bleed[data-reveal].is-revealed { transform: none; }
.iy-bleed {
  position: absolute; left: 0; right: 0; top: 0;
  display: flex; justify-content: center; align-items: center;
  gap: clamp(6px, 1.1vw, 14px);
  pointer-events: none;
}
.iy-bleed__cell {
  position: relative;
  flex: 0 0 auto;                       /* never let a flex row squash a hexagon */
  width: clamp(38px, 5.2vw, 74px);
  aspect-ratio: 1 / 1.1547;             /* regular pointy-top */
  /* the row's own centre lands on the seam, which is the value boundary */
  transform: translateY(-50%);
  clip-path: var(--hex);
  background: linear-gradient(to bottom,
    var(--iy-gold-600) 0 50%, var(--iy-gold-200) 50% 100%);
  opacity: .5;
}
/* the interior, inset by the hairline's width, carrying the two grounds */
.iy-bleed__cell::after {
  content: ""; position: absolute; inset: 1.5px;
  clip-path: var(--hex);
  background: linear-gradient(to bottom,
    var(--iy-paper) 0 50%, var(--iy-black) 50% 100%);
}
/* The outer cells fade so the row ends rather than stops — a hard last hexagon
   at each end would read as a cropped strip. */
.iy-bleed__cell:first-child,
.iy-bleed__cell:last-child  { opacity: .16; }
.iy-bleed__cell:nth-child(2),
.iy-bleed__cell:nth-child(6) { opacity: .32; }

/* Below the honeycomb breakpoint the capabilities chapter is squared cards and
   the row would be wider than the shapes it is rhyming with, so it thins to
   three cells' worth of width and drops the outermost pair. */
@media (max-width: 860px) {
  .iy-bleed__cell:first-child,
  .iy-bleed__cell:last-child { display: none; }
}

/* ── D5 · the context-aware nav ─────────────────────────────────────────
   Two orthogonal class sets, deliberately. The engine's own .is-scrolled now
   owns HEIGHT ONLY — its background and border declarations were removed and
   live here instead, keyed to which ground the bar is actually over. That way
   the engine stays frozen and the two concerns cannot collide. */
.iy-nav.is-dark {
  background: rgba(5,5,6,.82);
  backdrop-filter: blur(16px) saturate(140%);
  -webkit-backdrop-filter: blur(16px) saturate(140%);
  border-bottom-color: var(--iy-line-dark);
}
.iy-nav.is-light {
  background: rgba(244,242,238,.88);
  backdrop-filter: blur(16px) saturate(140%);
  -webkit-backdrop-filter: blur(16px) saturate(140%);
  border-bottom-color: rgba(20,20,20,.10);
}
.iy-nav.is-light .iy-nav__link { color: var(--iy-on-light-soft); }        /* 7.85 on paper */
.iy-nav.is-light .iy-nav__link:hover,
.iy-nav.is-light .iy-nav__link.is-active { color: var(--iy-on-light); }   /* 16.48 */
.iy-nav.is-light .iy-nav__link::before { background: var(--iy-gold-800); }/* 9.20 */
.iy-nav.is-light .iy-burger { border-color: rgba(20,20,20,.18); }
.iy-nav.is-light .iy-burger span { background: var(--iy-gold-800); }
.iy-nav.is-light .iy-btn--solid { border-color: var(--iy-gold-800); }

/* ⚠ THE LOGO, AND THE PHASE 4 BUG WAITING TO HAPPEN AGAIN.
   Both original lockups are keyed for a DARK ground — INYOSI is white and
   simply vanishes on a paper bar. filter:invert() is not an option; it would
   destroy the gold gradient in the mark. So there are four assets, and exactly
   one may ever be displayed.

   `.iy-nav__logo img` is specificity (0,1,1) and BEAT `.iy-nav__logo--compact
   { display:none }` at (0,1,0) in Phase 4, which put both lockups in the bar
   at once and rendered "INYOSI TECHNOLOGIES [mark] INYOSI". Lint was green.
   Do NOT add `display` to any descendant img rule. The variant rules below are
   the only thing that decides which asset shows. */
.iy-nav__logo--full-light,
.iy-nav__logo--compact-light { display: none; }
.iy-nav.is-light .iy-nav__logo--full          { display: none; }
.iy-nav.is-light .iy-nav__logo--compact       { display: none; }
.iy-nav.is-light .iy-nav__logo--full-light    { display: block; height: 52px; }
.iy-nav.is-light.is-scrolled .iy-nav__logo--full-light    { display: none; }
.iy-nav.is-light.is-scrolled .iy-nav__logo--compact-light { display: block; height: 40px; }

/* .iy-nav.is-hidden intentionally does not exist. The flagship auto-hides its
   bar on scroll-down; that is not ported here. See the note in script.js
   navContext(): it hid the bar during the exact interaction this page is built
   around, which made the light/dark swap unverifiable in every scrolled
   screenshot. */

@media (max-width: 900px) {
  /* Mobile always carries a compact lockup; the full one is a smear at this
     size. Listed at matching specificity for the same reason as above. */
  .iy-nav.is-light .iy-nav__logo--full-light { display: none; }
  .iy-nav.is-light .iy-nav__logo--compact-light { display: block; height: 36px; }
}

/* ── D2 · scroll-progress consumers ─────────────────────────────────────
   ⚠ Trap 1 governs every one of these: NEVER put a transform on an element
   carrying [data-reveal]. Each of these sits on a child or a wrapper. */

/* 03 — the macro counter-moves against the scroll. The transform is on the
   IMAGE, two levels inside the [data-reveal] element. */
html.js-anim .iy-philo__hex-in img {
  transform: translateY(calc((var(--p) - .5) * -5%)) scale(1.06);
}

/* 07 — the route DRAWS as you scroll rather than firing once on entry. This is
   the chapter that substitutes for a track record, so it earns the one
   scroll-coupled effect on the page.
   .iy-flow__node does not carry [data-reveal] (.iy-flow does), so scaling it
   here is safe. */
html.js-anim .iy-flow[data-progress] .iy-flow__rail {
  transform: scaleX(var(--p));
  transition: none;                    /* --p IS the timeline now */
}
html.js-anim .iy-flow[data-progress] .iy-flow__node {
  --lit: clamp(0, (var(--p) - .10 * var(--i, 0)) * 9, 1);
  opacity: calc(.18 + .82 * var(--lit));
  transform: scale(calc(.55 + .45 * var(--lit)));
  transition: none;
}
/* ⚠ THIS NUMBER MUST TRACK THE LAYOUT BREAKPOINT ABOVE (1150), not sit near
   it. The rail's geometry flips from horizontal to vertical when
   .iy-flow__steps goes single-column; if these two disagree the rail scales on
   the wrong axis for every width in between, so it would draw sideways across
   a vertical list — rendering perfectly, with no error, on a laptop. Moved
   from 760 together with the layout rule. */
@media (max-width: 1150px) {
  html.js-anim .iy-flow[data-progress] .iy-flow__rail { transform: scaleY(var(--p)); }
}

/* ══ THE STATIC PATH ══════════════════════════════════════════════════════
   One place, one rule set, covering all three cases together:
     • no JavaScript at all
     • prefers-reduced-motion: reduce
     • ?cap=1  (deterministic QA screenshots)

   Everything below must render FINISHED, not at its start state. The four
   declarations added for the new devices are marked. */
html:not(.js-anim) [data-progress] { --p: 1; }                       /* D2 */
html:not(.js-anim) [data-wipe]     { clip-path: none !important; }   /* D3 */
html:not(.js-anim) .iy-seam::before { transform: scaleX(1) !important; }  /* D6 */
html:not(.js-anim) .iy-cover > .iy-cover__held { position: static !important; }  /* D1 */
