/* ═══════════════════════════════════════════════════════════════════════
   Inyosi Technologies — bespoke stylesheet
   Layers ON TOP of primitives.css. Load order: primitives.css, then this.

   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-
   ═══════════════════════════════════════════════════════════════════════ */

: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 and no two levels
     resolve to near-identical sizes.

     The MAXIMA are sized for desktop and the MINIMA hold at phone width,
     which keeps the scale fairly flat across widths.

     The comment after each token is its resolved value at 1440px / 390px.
     Nine of the ten pin between 1424 and 1440px, so a 1920 monitor renders
     type identical to a 1440 laptop and the extra width goes to the layout.

     INFRASTRUCTURE is 14 characters; 1.9rem with 7.4vw is the measured floor
     at which it fits a 390px screen without overflowing. */
  --fs-hero-heavy: clamp(1.9rem,    7.4vw,  5.24rem);   /* 83.84 · 30.40 */
  --fs-chapter:    clamp(1.625rem,  3.34vw, 3rem);      /* 48.00 · 26.00 */
  --fs-h3:         clamp(1.35rem,   2.14vw, 1.92rem);   /* 30.72 · 21.60  ← the mid-tier */
  --fs-hero-light: var(--fs-h3);
  --fs-sub:        clamp(1.25rem,   1.53vw, 1.375rem);  /* 22.00 · 20.00 */
  --fs-card:       clamp(1.15rem,   1.37vw, 1.23rem);   /* 19.68 · 18.40 */
  --fs-lead:       clamp(1.0625rem, 1.23vw, 1.1rem);    /* 17.60 · 17.00 */
  --fs-small:      clamp(.85rem,    .98vw,  .88rem);    /* 14.08 · 13.60 */
  --fs-micro:      clamp(.78rem,    .875vw, .7875rem);  /* 12.60 · 12.48 */
  --fs-eyebrow:    clamp(.69rem,    .79vw,  .703rem);   /* 11.25 · 11.04 */

  /* ── 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;
  /* ⚠ .50 measures 3.33:1 over --input-bg, clearing the 3:1 WCAG 1.4.11 floor
     for a UI component boundary. .42 measures 2.71:1 and fails. Do not lower
     it by eye. */
  --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 (see .iy-light and .iy-invert below).

     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.
     ═════════════════════════════════════════════════════════════════════ */

  /* 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 contrast figure in this file is
   measured against THIS paper, so changing it would invalidate all of them.
   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. .78 is the value
     that measures above 3:1. 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. Chapter 05 depends on it: the eight capability tiles carry dark
   abstract textures authored to read bright-on-dark, so at 18% over white
   they become grey smudges. The tile stays dark and the GROUND changes
   around it.
   ─────────────────────────────────────────────────────────────────────── */
.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 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.
   Pages load primitives.css THEN this file, and both selectors are
   (0,0,1)+(0,0,1), so this wins on source order without !important.

   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);
  /* Flat 1rem at every width: body copy does not scale with the viewport. */
  font-size: 1rem;                                    /* 16.00 · 16.00 */
  line-height: 1.65;
}

::selection { background: var(--iy-gold-400); color: #1A1200; }

/* ══ grounds ═══════════════════════════════════════════════════════════════
   A chapter's ground is decided in exactly one place: the ground class on
   its <section> (.iy-ground--dark, .iy-ground--ink or .iy-light).

   ⚠ No chapter rule may set `background`. `.iy-light` is specificity (0,1,0),
   the same as the chapter classes (.iy-creed, .iy-philo, .iy-pillars, ...),
   and those are declared LATER in this file — so a chapter-level `background`
   silently wins on source order and adding .iy-light produces NO VISIBLE
   CHANGE AT ALL. Moving rules or raising specificity only re-runs the same
   race with a different winner.
   ═══════════════════════════════════════════════════════════════════════ */
.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. Every chapter composes 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. */
.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. An off-scale size
   such as .875rem lands within 2% of --fs-micro, which puts the "Send enquiry"
   button and the explanatory note UNDER it at visually the same size — a call
   to action smaller than its own footnote.
   Form CONTROLS are exempt from the lattice (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 engine.js'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 page alternates paper and black under a fixed bar, so the
   bar's ground depends on WHICH chapter is underneath rather than merely on
   whether the page has scrolled. It lives on .is-dark / .is-light in the
   MOTION section. Two orthogonal class sets, no collision, and engine.js
   needs no changes. */
.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. 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 — otherwise this "back
   to top" control is 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),
   putting both lockups in the bar at once. 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: var(--fs-small); 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, because
   engine.js has no section-spy hook. 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: var(--fs-micro); }

/* ── 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.25rem; 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(.95rem, 4.4vw, 1.32rem); 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 phone number and email sit
   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; without min-height these two
   collapse to ~25px — and they are the phone number and the email address, the
   one-tap route out that this block exists to provide. */
.iy-mobile__foot a {
  color: var(--iy-gold-200); text-decoration: none; font-size: var(--fs-small);
  display: flex; align-items: center; gap: .6rem;
  min-height: 46px;
}

/* ── the bar over the hero ─────────────────────────────────────────────────
   Over the hero the bar carries the logo only; the links and CTA appear once
   the page is in the other sections.

   `.is-atop` is written by navContext() in script.js and is ADDED to hide, so
   the no-JS default is a complete, focusable nav. Below 900px both of these are
   already display:none and the burger is the navigation, so this is a
   desktop-only effect by construction -- the burger is deliberately NOT
   included: it is the only way a phone visitor can navigate, and hiding it
   over the hero strands them at the top of the page.

   ⚠ DO NOT USE `visibility: hidden` HERE: IT BREAKS THE KEYBOARD.
   visibility:hidden removes an element from the tab order, so a keyboard
   visitor landing at the top of the page -- which is where every visitor
   lands -- would tab straight past the entire navigation.

   So the links stay FOCUSABLE and only the mouse is locked out
   (`pointer-events: none`). Tabbing into the bar brings it back, which is the
   same pattern a skip-link uses: hidden until a keyboard needs it, then
   present. Do not "tidy" this by adding visibility back. */
.iy-nav__links,
.iy-nav__cta {
  transition: opacity .4s var(--ease), transform .4s var(--ease-out);
}
.iy-nav.is-atop .iy-nav__links,
.iy-nav.is-atop .iy-nav__cta {
  opacity: 0;
  transform: translateY(-6px);
  pointer-events: none;
}
.iy-nav.is-atop:focus-within .iy-nav__links,
.iy-nav.is-atop:focus-within .iy-nav__cta {
  opacity: 1;
  transform: none;
  pointer-events: auto;
}

@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.
   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 carries 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 the
   URL does not carry ?cap=1 (static capture mode). 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. */
    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. Without it the bottom third reads as empty, 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%);
  }
}

/* ═══════════════════════════════════════════════════════════════════════
   CHAPTERS 02 creed · 03 philosophy · 04 pillars ·
            05 capabilities · 06 sectors · 07 engage

   Composed from the shared devices above: .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                     (static capture mode: the lattice does not
                                   animate and reveals are forced static)
     • 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 is guarded the same way above; this
   extends the guard over every chapter, without touching engine.js.
   ───────────────────────────────────────────────────────────────────── */
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 ──────────────────────────────────────────────────────
   A single narrow block (e.g. `max-width: 48rem`, 768px inside a 1168px
   measure at 1440) wraps the headline to short lines and leaves ~400px of
   empty ground beside it, which reads as left over, not composed. The
   --centre and --split variants below fill the measure instead.
   ─────────────────────────────────────────────────────────────────────── */
.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 at 48rem, the four pillars headline breaks onto
   three lines, which stacks a tall type block above a tall geometry block and
   leaves 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.

   align-items is deliberately NOT set on the container: "end" would also push
   the eyebrow down to sit on the headline. The baseline pairing wanted is
   between the headline and the lead only, so it is expressed as align-self on
   those two items 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` plus a hand-tuned padding-bottom is NOT the same
     thing as a shared baseline. `end` aligns the two BOXES; the last lines then
     sit apart by the difference in half-leading and descent between the display
     face and the body face, and by far more at narrower widths, where the two
     columns wrap to different line counts. `last baseline` asks the browser for
     the thing actually wanted, at every width. 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 a container pseudo-element would need 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; }

/* ⚠ Two traps live in this one block.
   (a) `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.
   (b) 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 reset
   at higher specificity (e.g. a :first-child one at (0,3,0)) is one a later
   media query cannot override, and it leaves a 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 (a) 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 ────────────────────────────────────────────────────────────
   An orphan is one word alone on a block's last line, whatever width it
   happens to be — so it is a matter of word count, not line width.

   `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 Chromium (Edge 151) it does nothing
   at all. On a block built with a deliberate orphan:

     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. Using it would leave the orphans in place behind a rule that looks
   like it 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 — light chapters are handled by the semantic token layer at the top of
   this file (see also the grounds comment in the base section). The few
   devices 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.

   Sized 12x14 rather than 9x10: at 9px, and dimmed, the shape reads as a
   BULLET. The separator is a hexagon precisely so it is not a bullet, 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 break as "CONNECT ⬡ PROTECT ⬡ SUPPORT ⬡" / "GROW" — a
   separator dangling at the end of the first line with nothing after it, which
   reads as a loose bullet. 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 so it can 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 can distort an image-bearing child. 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 —
   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 (the same reason
   .iy-cell__frame/__face exist). */
.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 can break 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; }

  /* ⚠⚠ A media query adds NO specificity. 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), so none of it can
     reach the grid — without this rule the chapter stays TWO COLUMNS on a
     phone, the copy running in a ~155px measure beside the honeycomb band.
     There is no overflow and no error; it is visible only in the render.

     Reset at MATCHING (0,2,0) specificity, and the placement stated rather
     than left to source order so the band lands above the copy — the copy is
     first in the DOM, so without grid-row the band would sit below it. */
  .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. */
.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 ─────────────────────────────────────────────────────── */

/* The field lattice (canvas behind the pillars).
   `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. */
.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; }
/* ⚠ Pillar cell sizing: cell width, padding and name tracking are tuned
   together, against two failure modes.
     · Too little type in too big a cell reads as four empty outlines with a
       caption floating in the middle. The text block should fill roughly
       55-60% of the hexagon's usable band — BIGGER TYPE, not more words.
     · Too much ink for the width collides. Archivo 300 uppercase measures
       ~0.74em per character, and .iy-cell__body is not the clipped layer, so
       an over-wide name does not clip: it runs out of the hexagon into its
       neighbour (CONNECTED and INTELLIGENT fuse into one word). Tracking is
       where the room is — .16em adds ~22% to an 11-character word — so the
       pillar names drop to .08em, which fits from 640 to 1920 with room to
       spare while keeping the type at --fs-sub.
   If --fs-sub changes, scale the CELL and its PADDING by the same factor:
   type-down alone empties the cell, and a rem padding does not shrink with a
   px cell, so scaling one without the other worsens the fit. */
.iy-comb--pillars { --cw: clamp(167px, 14.08vw, 199px); gap: var(--comb-gap); }
.iy-comb--pillars .iy-cell__body { padding-inline: clamp(.4rem, .88vw, .75rem); gap: .48rem; }
.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);
  /* Without balance a descriptor can break 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. Reserving the second
     line costs nothing and removes the whole class of defect. */
  min-height: 3em;
}

/* ── 05 capabilities ────────────────────────────────────────────────── */
.iy-comb--caps {
  /* ⚠ 28vw is set by the descriptions, not the hexagons. Any narrower (e.g.
     24vw) and the longest description ("Modernise, secure and optimise your
     technology environment.") sets to THREE lines while the other seven stay
     at two. The body is vertically centred in the cell, so one three-line
     description lifts that whole tile's name ~9px above the two beside it.
     A three-line min-height reserve would fix the symptom and cost every other
     tile a dead line. Widening the cell removes the CAUSE: at 28vw every
     description is two lines from 861px up, and names share one baseline.
     The 300px ceiling applies from 1071px up. */
  --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 LIGHT, but the eight tiles stay DARK. That is deliberate.

   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, and lowering the opacity makes them worse.

   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 the tiles separate clearly from their
   ground (dark tiles on a dark ground barely would). */

/* 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. */
.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 the height/width are stated explicitly rather than assumed, so a
   specificity change elsewhere cannot silently undo them. */
.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, one
     level up. "Network Infrastructure" sets to two lines and "Connectivity" to
     one, and with the body vertically centred in the hexagon that would put
     their names ~13px apart on one row. 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.

     ⚠ The container-shift version does not work: the container carries
     [data-reveal], so primitives' own
     `[data-reveal].is-revealed { transform: translateY(0) }` at (0,2,0) BEATS
     `.iy-comb--pillars { transform: ... }` at (0,1,0) and the counter-shift
     silently never applies, leaving the cluster clipped and off-centre.
     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.

   ⚠ 860 is a MEASURED boundary, not a guess about phones. At 780 the
   three-across honeycomb puts each cell at 187px, which sets "Modernise,
   secure and optimise your technology environment." to FOUR lines and drives
   the body past the hexagon's usable band — content running out into the
   taper, with the row's three names on different baselines. From 861 up,
   cells at 28vw hold every description in two lines. 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. Left alone, the two cards in a row
       differ in height and neither their tops nor their bottoms line up,
       because the shorter one is centred in the row. */
    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. */
@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 would keep its quarter-pitch
     shift in the list form and hang ~20px outside the measure. 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
   ═══════════════════════════════════════════════════════════════════════ */

.iy-sectors__grid {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(1.6rem, 4vw, 3.5rem);
}
/* This chapter is DARK, and that changes what a panel has to be.
   Both photographs are dark and gold-lit. Dropped straight onto black they
   become 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 are already 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 does
   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, 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); }

/* A heading size, not a label size: at label size this <h3> would render
   smaller than the site's own nav links and the chapter reads as unfinished.
   Weight 500 and tight tracking go with the size: Archivo 300 at this size is
   too thin to hold, and wide (.2em) tracking on a 14-character word is what
   would push IMPLEMENTATION out of its column. */
.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;
  /* .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 fits
     its column at 1440 by a single pixel, which is a coincidence rather than a
     fit, and not at all 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);
}

/* ⚠ 1150 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 runs 32px out of its own column to within
   4px of PROCUREMENT's last letter. Not a clip and not an overflow — the
   grid and scrollWidth report nothing, and two labels nearly touch.
   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 and a label sits further
     from its OWN description than from the NEXT label: proximity inverts, and
     every description reads as belonging to the step below it. Restated
     explicitly at matching (0,1,0) specificity, because a media query adds
     none; 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.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── the shared item type, used by BOTH bands ────────────────────────────
   No separator glyph or hairline anywhere in this chapter. A divider between
   wordmarks reads as a stray tick, and a wrapping row with per-item separators
   dangles one at every line break (the same defect the creed band avoids on a
   phone), which :last-child cannot fix because the item at a line END is not
   the last item in the list. The spacing does the separating. */
/* A deliberately compact chapter. Its height is not one number: it is
   .iy-chapter's shared padding-block, plus .iy-chapter__head's shared
   margin-bottom, plus the two bands' own offsets. The SHARED rules are not
   touched — they set the rhythm for every chapter — so both are overridden
   scoped to .iy-eco. This is the quietest chapter on the page by design (a
   marquee and six names, no ornament), so it is the one that can afford the
   least air. */
.iy-eco { padding-block: clamp(3rem, 7vh, 5rem); }
.iy-eco .iy-chapter__head { margin-bottom: clamp(1.6rem, 3.4vh, 2.6rem); }

.iy-eco__item {
  position: relative;
  font-family: var(--display); font-weight: 300;
  font-size: var(--fs-sub);
  letter-spacing: .06em;
  color: var(--iy-fg);
  white-space: nowrap;
}

/* ═══ THE MOVING BAND ═════════════════════════════════════════════════════
   Band 1. Full-bleed by design — a marquee that stops at the text gutter
   reads as a broken carousel rather than as a band, so .iy-marquee sits
   OUTSIDE .iy-wrap in the markup.

   ⚠ Never put a transform on a [data-reveal] element. The reveal transform is
   on .iy-marquee; the scroll animation is on the CHILD .iy-marquee__track. If
   they were ever the same element, primitives'
   [data-reveal].is-revealed{transform:translateY(0)} at (0,2,0) would beat the
   animation's own transform and the band would sit still — rendering
   perfectly, with no error.

   ⚠ overflow:hidden here means scrollWidth reports NO overflow even while
   content is clipped away, so scrollWidth cannot detect a broken band;
   measure the track itself. */
.iy-marquee {
  position: relative;
  overflow: hidden;
  margin-top: clamp(1.3rem, 2.6vw, 2.1rem);
  padding-block: clamp(.5rem, 1.2vw, .9rem);
  /* Hairlines top and bottom so the band reads as a BAND — a defined
     horizontal register — rather than as a row of words that happens to
     move. At 16% gold on ink this is texture, not a rule you notice. */
  border-block: 1px solid var(--iy-line-dark);
  /* Fade both ends so items enter and leave instead of hard-cutting at the
     viewport edge. Masked, not a gradient overlay, because the ground under
     this chapter is --iy-ink and an overlay would have to know that. */
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 9%, #000 91%, transparent 100%);
          mask-image: linear-gradient(to right, transparent 0, #000 9%, #000 91%, transparent 100%);
}
.iy-marquee__track {
  display: flex;
  width: max-content;          /* ← both sets on one line, so -50% is exactly one set */
  will-change: transform;
}
.iy-marquee__set {
  display: flex;
  flex: 0 0 auto;              /* ⚠ never let a set shrink; -50% assumes equal halves */
  align-items: center;
  gap: clamp(2rem, 4.5vw, 4rem);
  margin: 0; padding: 0 clamp(1rem, 2.25vw, 2rem) 0 0; list-style: none;
}
/* Travels LEFT → RIGHT. The track holds two identical sets, so
   translating from -50% (showing set B) to 0 (showing set A) is visually
   seamless at the loop point. Reverse the two keyframe values to flip
   direction — that is the only change needed. */
@keyframes iy-marquee-ltr {
  from { transform: translateX(-50%); }
  to   { transform: translateX(0); }
}
@media (prefers-reduced-motion: no-preference) {
  html.js-anim .iy-marquee__track {
    animation: iy-marquee-ltr 42s linear infinite;
  }
  /* Pause on hover AND on focus-within. Hover alone would make this a device
     a keyboard user can never stop, and the items are long enough to be worth
     reading. :focus-within also catches a screen-reader user tabbing past. */
  html.js-anim .iy-marquee:hover .iy-marquee__track,
  html.js-anim .iy-marquee:focus-within .iy-marquee__track {
    animation-play-state: paused;
  }
}

/* ── band 2 · who we work with ───────────────────────────────────────────
   Small, static, and under its own label, because these are a DIFFERENT KIND
   OF THING from the band above. Different size, different colour, different
   register. */
.iy-eco__vendors {
  margin-top: clamp(1.5rem, 2.9vw, 2.25rem);
  display: flex; flex-wrap: wrap; align-items: baseline;
  gap: .5rem clamp(1.25rem, 3vw, 2.5rem);
}
.iy-eco__label {
  margin: 0;
  font-family: var(--display); font-weight: 300;
  font-size: var(--fs-eyebrow);
  letter-spacing: .22em; text-transform: uppercase;
  color: var(--iy-gold-200);
}
.iy-eco__names {
  margin: 0; padding: 0; list-style: none;
  display: flex; flex-wrap: wrap;
  gap: .35rem clamp(1rem, 2.4vw, 2rem);
}
.iy-eco__name {
  font-family: var(--display); font-weight: 300;
  font-size: var(--fs-small);
  letter-spacing: .08em;
  color: var(--iy-on-dark-soft);
}

@media (max-width: 560px) {
  .iy-eco__item { font-size: var(--fs-card); }
  .iy-marquee__set { gap: clamp(1.4rem, 6vw, 2.2rem); }
}

/* ═══════════════════════════════════════════════════════════════════════
   09 · insights — three articles
   Light chapter. Three cards, no imagery. 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 where an align-items written for one context keeps
     applying in the other and knocks a row of cards off their baseline.
     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;
}
/* Shared by chapter 09's "Continue Reading" and chapter 03's onward link to
   /the-hive/. One rule set rather than two, so the tap-target measurement and
   the underline behaviour below cannot drift apart between them. */
.iy-post__link,
.iy-philo__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,
.iy-philo__link:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 3px; }

/* The link sits in its own block so it starts a line of its own rather
   than running on from the pull quote. Margin stated here and not on the
   anchor, because the anchor grows a negative top margin at <=760px to
   buy its tap target and the two would fight. */
.iy-philo__more { margin: 1.75rem 0 0; }

/* Tap target. Unpadded the link is ~24px tall at phone width — 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,
  .iy-philo__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 script.js rather than by engine.js. The engine's own form
   handler binds to [data-quote-form], which this markup deliberately does
   not have.
   ───────────────────────────────────────────────────────────────────── */
/* ── form layout ────────────────────────────────────────────────────────
   ⚠ DOUBLE SPACING, invisible in either file on its own. `.iy-form` sets a
   grid `gap` AND primitives.css sets `.u-field { margin-bottom: 1.1rem }` on
   the very same elements, because .u-field IS the grid item. Left alone every
   field carries ~2.15rem of stack — about double what either rule reads as.
   The margin is zeroed here rather than in primitives.css, which is shared.

   Name/Organisation and Email/Telephone pair into two columns, so six rows
   become four. The pairs are the two that belong together semantically, and
   both halves of each pair are short inputs — pairing a short input with the
   textarea would make a ragged row.
   ───────────────────────────────────────────────────────────────────── */
.iy-form {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: clamp(.75rem, 1.6vw, 1.1rem);
  row-gap: .7rem;
}
/* Zeroing primitives' own field margin is what makes the row-gap mean what it
   says. Without this the two rules stack and the gap is a lie. */
.iy-form .u-field { margin-bottom: 0; }
/* Everything that is not half of a pair spans the full width. Stated as an
   explicit list rather than a `:not()` on the pairs, because a new field added
   later must make a deliberate choice, not inherit one. */
.iy-form > .u-field--wide,
.iy-form > .iy-form__submit,
.iy-form > .iy-form__note,
.iy-form > .iy-form__hp { grid-column: 1 / -1; }

/* Below 560 the pairing has nowhere to go: two inputs in ~150px each is worse
   than one in 310. Restated explicitly, not left to a media query's specificity
   (a media query adds none). */
@media (max-width: 560px) {
  .iy-form { grid-template-columns: 1fr; }
  .iy-form > .u-field { grid-column: 1 / -1; }
}
/* ⚠ REQUIRED, not belt-and-braces. `display: grid` above BEATS the UA
   stylesheet's [hidden] { display: none }, so without this rule setting
   f.hidden = true sets the attribute and changes nothing on screen: the
   success panel appears with the filled-in form still sitting above it,
   inviting a second submit — which is the exact failure the success panel
   exists to prevent. The .hidden IDL property reads true either way; only
   the rendered box shows whether the form is really gone. */
.iy-form[hidden] { display: none !important; }
.u-field label {
  display: block; margin-bottom: .3rem;
  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. */
/* primitives.css reserves 110px for a textarea. This is the tallest single
   element in the form and three rows is enough to start an enquiry in — the
   field scrolls, and nobody writes their brief in the box. The 16px font-size
   and 48px min-height on the INPUTS are untouched: those are the iOS anti-zoom
   floor and the tap-target floor, both mechanical, neither a type decision. */
.iy-form textarea { min-height: 84px; }
.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. */
  .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(2.6rem, 6.4vh, 4.2rem) clamp(1.5rem, 3vh, 2.2rem);
  /* isolation, so the bloom's z-index:-1 stays INSIDE this element. Without a
     stacking context here a negative z-index child escapes behind the section
     background and paints nothing. */
  position: relative;
  isolation: isolate;
}

/* ── the bloom ───────────────────────────────────────────────────────────
   A warm colour layer with the PAGE'S OWN GROUND laid over it as two vertical
   gradients, one fading in at the top edge and one at the bottom, so the bloom
   has no seam anywhere -- it simply stops being. That is why it reads as light
   in the page rather than as a panel with a gradient in it.

   The colour is Inyosi's gold, rising from below the wordmark: the hive glow,
   at the one place on the page where the lattice canvas does not run. Three
   layers, painted top to bottom in source order:
     1  black feathering DOWN from the top edge -> hides the seam at the cut
     2  black feathering UP from the bottom edge -> the page closes on black
     3  the gold itself, a wide low ellipse centred at 78% of the footer

   ⚠ The peak has to sit BETWEEN the two feathers. Centred below the bottom
   edge (e.g. 108%) it sits exactly where layer 2 is at full strength, so the
   gradient masks its own brightest part and reads as a faint warm cast rather
   than as light. 78% puts the peak just above the legal bar.

   ⚠ The bloom is brightest exactly where .iy-foot__bar sits, so the legal line
   and the social links are the text most at risk from it. Their contrast must
   hold at the bloom's brightest point AND its darkest, because a gradient
   turns one contrast question into two. Do not raise the gold stops by eye. */
.iy-foot::before {
  content: '';
  position: absolute; inset: 0; z-index: -1;
  pointer-events: none;
  background:
    linear-gradient(180deg, var(--iy-black) 0%, rgba(5,5,6,0) 34%),
    linear-gradient(0deg,   var(--iy-black) 0%, rgba(5,5,6,0) 20%),
    radial-gradient(122% 66% at 50% 78%,
      rgba(209,139,7,.34) 0%,
      rgba(209,139,7,.12) 48%,
      rgba(209,139,7,0)   78%);
}

.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.4rem, 12.4vw, 11rem);   /* 176.00 at 1440 */
  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.
     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(1rem, 2.2vh, 1.6rem) 0 clamp(1.8rem, 3.6vh, 2.6rem);
  padding-top: clamp(1rem, 2.2vh, 1.6rem);
  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(1.8rem, 3.6vh, 2.6rem);
  padding-top: clamp(1rem, 2vh, 1.4rem);
  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; flex-wrap: wrap; align-items: center;
  gap: .9rem;
}
/* Pipe separators between the social links. ::before on every item but the
   first, so a wrap never dangles a separator at the start of a line. */
.iy-foot__social li + li::before {
  content: '';
  display: inline-block;
  width: 1px; height: .78em;
  margin-right: .9rem;
  vertical-align: -.08em;
  background: var(--iy-rule);
}
.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. As plain inline text every link in this footer has a hit
     box only 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 PIPE AND THE TAP TARGET INTERACT.
     The separator above is an inline `li + li::before`. The tap-target rule
     in this same block turns the anchor into a BLOCK, and a block child
     forces a preceding inline pseudo onto its own line -- so the pipe would
     render ABOVE its link and the links would drop out of line with each
     other. Neither rule is wrong on its own; the defect is only in their
     interaction, and only at phone width.
     Making the li a flex row puts the pseudo back beside the anchor and
     keeps the 46px hit box. This is the same shape as `.iy-crumb li`. */
  .iy-foot__social li { display: flex; align-items: center; }

  /* ⚠ `text-wrap: balance` EARNS ITS PLACE AT 1440 AND BACKFIRES AT 390.
     The tile blurbs carry balance so a two-line blurb sets as two even lines
     instead of a long line and a stub -- at 1440 it turns "Protect your
     business against modern / threats." into "Protect your business /
     against modern threats.", which is the whole reason it is there.
     In the ~150px tile column on a phone there is no slack left to balance,
     and on Connectivity it produces "Telecommunications / and / business
     connectivity." -- the conjunction alone on its own line, because the
     18-character first word fixes the maximum and the algorithm distributes
     what is left around it. Measured from 320 to 760px, balance breaks only
     at 390 and greedy wrapping is clean at every width; at 1440 balance is
     clean and greedy wrapping is visibly worse on six of eight blurbs.
     It is a knife-edge, but 390 is one of the most common phone widths.
     Greedy wrapping below the breakpoint, balance above it. */
  .iy-tile__desc { text-wrap: auto; }
  /* ⚠ 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 would move the underline ~11px away from its own text
     and it would 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.

   No smooth-scroll library, and the reason is mechanical, not preferential:
   one like Lenis decouples window.scrollY from the rendered frame, which
   breaks headless screenshots and any scroll-position measurement of the
   page. 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; }

/* ── sticky cover ───────────────────────────────────────────────────────
   The outgoing chapter is held while the incoming one scrolls up over it.
   Pure CSS, zero JavaScript — 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.

   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);
}

/* ── 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. */
[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); }

/* ── value-cut · the `seam` ─────────────────────────────────────────────
   The page 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. 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. 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);
}

/* ── 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 — the same reason .iy-cell__frame and .iy-cell__face
   exist. */
.iy-seam--bleed { z-index: 5; }
/* ⚠ The reveal transform, CANCELLED — 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; }
}

/* ── the context-aware nav ──────────────────────────────────────────────
   Two orthogonal class sets, deliberately. The engine's own .is-scrolled
   owns HEIGHT ONLY; the bar's background and border live here instead, keyed
   to which ground the bar is actually over. That way engine.js needs no
   changes 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 ON A LIGHT BAR.
   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 would BEAT
   `.iy-nav__logo--compact { display:none }` at (0,1,0), putting two lockups
   in the bar at once. 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 bar does not auto-hide
   on scroll-down. Hiding it would hide the light/dark swap during exactly the
   scrolling this page is built around. */

@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; }
}

/* ── scroll-progress consumers ──────────────────────────────────────────
   ⚠ One rule 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.
   .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. Change
   both together. */
@media (max-width: 1150px) {
  html.js-anim .iy-flow[data-progress] .iy-flow__rail { transform: scaleY(var(--p)); }
}

/* ── the chamber (scroll-opened doors) ──────────────────────────────────
   The chapter arrives CLOSED and opens from its own centre line as it is
   scrolled into view. Two opaque leaves meet on that line, each carrying the
   chapter's own ground, and part vertically.

   WHY LEAVES OVER THE TOP AND NOT A CLIP ON THE CONTENT. Clipping the
   chapter's contents needs one wrapper around the canvas, the scrim and
   .iy-wrap together — and .iy-chapter's own padding means such a wrapper
   cannot be inset:0, so the lattice would stop short of the section edge and
   the ground would show through a chamber that is supposed to be shut. An
   overlay solves the same problem without touching the chapter's box at all.

   ⚠ NEVER on the <section>. The leaves live in an absolutely-positioned
   overlay INSIDE it. A transform or a clip on the section makes it the
   containing block for every descendant, which is how a sticky element
   silently stops sticking, and .iy-chapter is the box the seam and the
   lattice are both measured against.

   ⚠ A percentage in translate() resolves against the element's OWN border box,
   never the container's. Every moving part here is therefore sized to the
   travel it needs: a leaf is half the chamber and moves -100% of itself; an
   edge is half the chamber and moves -100% of itself. A 2px line could not be
   moved this way at all — it would travel 2px.

   ⚠ The gold edge is a SIBLING of the leaf, not a child: it has to paint
   identically whatever mechanism moves the leaves, and clip-path cuts a child
   along with its parent.

   The overlay carries [data-progress] with a SUB-RANGE. The default 0..1
   traversal only completes once the chapter's bottom has passed the top of
   the window: a chamber on that timeline finishes opening after it has gone,
   which is to say it is never seen open. The sub-range puts the whole opening
   into the part of the scroll where the chapter is actually on screen.
   ─────────────────────────────────────────────────────────────────────── */
.iy-chamber {
  position: absolute; inset: 0; z-index: 6;
  pointer-events: none;
  overflow: hidden;                 /* the leaves travel out of frame */
  contain: paint;                   /* and the glow stays off the seam */
  /* --p's registered initial value is 1, which for this device means OPEN.
     scrollProgress() does not write the real 0 until DOMContentLoaded, so
     without this the chamber paints open for a frame and then slams shut.
     Declared here, beaten by the inline style JS writes, and beaten by the
     static-path rule at the foot of this file. */
  --p: 0;
}

/* +1px, not a flat 50%. Two leaves at exactly half can leave a sub-pixel line
   of the chapter showing along the centre at fractional device ratios and at
   125% Windows scaling — a bright hairline across a black chapter, visible
   only on the machine that has it. The 2px overlap is invisible while shut
   and gone once open. */
.iy-chamber__leaf {
  position: absolute; left: 0; right: 0;
  height: calc(50% + 1px);
  background: var(--iy-bg);
  will-change: transform;
}
.iy-chamber__leaf--t { top: 0; }
.iy-chamber__leaf--b { bottom: 0; }

/* The leading edge riding each leaf's inner lip, so the chamber reads as light
   escaping rather than as two rectangles moving. Same language as
   .iy-cover__edge, the page's other opening device. It fades as the doors
   clear or it would end up as two stray rules across an open chapter.

   Sized to HALF the chamber and laid against its own lip: that is what lets it
   travel on transform alone. The visible line is the ::after. */
.iy-chamber__edge {
  position: absolute; left: 0; right: 0;
  height: 50%;
  opacity: calc(1 - var(--p));
  will-change: transform, opacity;
}
.iy-chamber__edge::after {
  content: ""; position: absolute; left: 0; right: 0; height: 2px;
  background: linear-gradient(90deg,
    transparent, var(--iy-mark) 16%, var(--iy-fg-lead) 50%,
    var(--iy-mark) 84%, transparent);
}
.iy-chamber__edge--t { top: 0; }
.iy-chamber__edge--b { bottom: 0; }
.iy-chamber__edge--t::after { bottom: 0; box-shadow: 0  12px 40px 2px rgba(243,180,49,.30); }
.iy-chamber__edge--b::after { top: 0;    box-shadow: 0 -12px 40px 2px rgba(243,180,49,.30); }

/* On paper there is no light to spill. The tokens above already resolve to
   gold-600 / gold-800 in .iy-light, so the line reads as a hairline rule at
   3.94 and 9.20 — but a warm bloom on a paper ground reads as a printing
   fault, not as a chamber opening. The rule stays; the glow goes.

   ⚠ CURRENTLY UNREACHABLE, and deliberately kept. Chapter 04 is the only
   chapter with a chamber and it is dark, so nothing matches this selector
   today. It is retained because the chamber is a general device and the next
   one placed on a paper chapter needs this or it ships a printing fault —
   not because it is doing work right now. Do not read it as evidence that a
   light chamber exists. */
.iy-light .iy-chamber__edge::after { box-shadow: none; }

/* ── the mechanism ──────────────────────────────────────────────────────
   transform: translate3d, chosen by measurement. The alternative is the same
   leaves cut back from the centre line with `clip-path: inset()`, the page's
   other opening idiom (the wipe). Over the same 700px/s scroll gesture through
   chapter 04, from a devtools timeline trace, median of three:

       baseline (no chamber)   191 paints    25.07ms paint
       transform               183 paints    32.83ms   ZERO extra paints
       clip-path               457 paints    64.26ms   +266 paints

   The leaf is a flat rectangle of one colour, so both mechanisms paint the
   IDENTICAL result and the choice costs nothing visually. It also avoids
   clip-path cutting borders and inset box-shadows, which would make the gold
   edge impossible as a child. */
html.js-anim .iy-chamber__leaf--t { transform: translate3d(0, calc(-100% * var(--p)), 0); }
html.js-anim .iy-chamber__leaf--b { transform: translate3d(0, calc( 100% * var(--p)), 0); }
html.js-anim .iy-chamber__edge--t { transform: translate3d(0, calc(-100% * var(--p)), 0); }
html.js-anim .iy-chamber__edge--b { transform: translate3d(0, calc( 100% * var(--p)), 0); }

/* ── the sectors card-stack ─────────────────────────────────────────────
   Two full-width cards that pin under the bar and stack: card one holds while
   card two rises and covers it. The two sectors stop being a pair of tiles
   read side by side and become two statements read one after the other.
   `position: sticky` with a `top` just under the bar, and NOTHING sticky at
   all below the 900px breakpoint.

   ⚠ Sticky is available here ONLY because style.css's base section overrides
   primitives' `overflow-x: hidden` with `overflow-x: clip`. `hidden` makes the
   root a scroll container and sticky inside one silently does not stick — no
   error, no warning, it simply scrolls past looking like an ordinary section.
   Do not "tidy" that line. .iy-sectors itself must also never gain an
   `overflow` or a `transform`, for the same reason: either one becomes the
   containing block and the cards stop pinning. That is why chapter 06 is the
   one chapter that does NOT get the chamber overlay.

   NO rAF LOOP AND NO SCROLL LISTENER. The whole device is `position: sticky`.
   The travel is a consequence of the cards' own height plus the gap, not a
   number written on a wrapper: card two reaches its pin point after exactly
   (card height + gap) of scroll.
   ─────────────────────────────────────────────────────────────────────── */
.iy-stack {
  --iy-stack-top: calc(var(--nav-h-c) + clamp(1rem, 3vh, 1.5rem));
  display: flex; flex-direction: column;
  gap: clamp(1.6rem, 4vw, 3rem);
}

.iy-stack__card {
  position: relative;
  background: var(--iy-bg-raised);
  border: 1px solid var(--iy-rule);
  overflow: hidden;                /* the media's own corner, not a clip on type */
}
/* Card two paints over card one. Source order already decides this between two
   positioned siblings; it is stated because the whole device depends on it and
   a later reorder of the markup would silently invert the stack. */
.iy-stack__card--a { z-index: 1; }
.iy-stack__card--b { z-index: 2; }

.iy-stack__media { position: relative; overflow: hidden; background: var(--iy-media-bg); }
/* ⚠ object-fit: cover crops on ONE axis only — whichever way the aspect
   mismatch runs. These two frames are wider than 4:3 at desktop and taller at
   phone, so the crop flips axis at the breakpoint and half of any
   object-position value is dead code on either side of it. Centre is correct
   for both photographs: the subject is the whole frame in each. */
.iy-stack__media img {
  position: absolute; inset: 0;
  display: block; width: 100%; height: 100%;
  object-fit: cover; object-position: center;
}

.iy-stack__body {
  display: flex; flex-direction: column; justify-content: center;
  padding: clamp(1.4rem, 3vw, 2.6rem);
}
.iy-stack__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-stack__p { margin: 0; line-height: 1.6; color: var(--iy-fg-soft); }
.iy-stack__body .iy-rule-node { margin-top: 0; }

/* The lit lip on card two, so its arrival over card one reads as an event
   rather than as a rectangle appearing. Same vocabulary as .iy-cover__edge and
   the chamber edge — the page opens things one way.

   Both cards carry the same raised ground on purpose: on a near-black chapter
   a second dark tint would not read as a different object anyway, and the
   things that DO read are the inverted layout, the moving photograph and this
   edge. Card two is not a different colour; it is a different arrival. */
.iy-stack__card--b::before {
  content: ""; position: absolute; left: 0; right: 0; top: 0; height: 1px;
  background: linear-gradient(90deg,
    transparent, var(--iy-mark) 14%, var(--iy-fg-lead) 50%,
    var(--iy-mark) 86%, transparent);
  z-index: 3;
}

/* ── desktop and tablet: the split card, and the pin ────────────────────
   900 and not 760. The card only becomes a two-column media/text split when
   there is width for both, and a 600px-tall card pinning inside a 700px
   viewport has nowhere to travel. Below this the cards are ordinary stacked
   panels in flow — media over text. */
@media (min-width: 900px) {
  .iy-stack__card {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    /* ⚠ Restated because the display changed. `align-items` written for the
       flex column above keeps applying under grid — it just means something
       else. Stretch is what makes the photograph fill the card's height. */
    align-items: stretch;
    min-height: min(58vh, 520px);
  }
  /* Card two inverts: media right, text left. Stated with grid-column rather
     than `order`, so the DOM order that decides the stacking above is never
     the thing that moves. */
  .iy-stack__card--b > .iy-stack__media { grid-column: 2; grid-row: 1; }
  .iy-stack__card--b > .iy-stack__body  { grid-column: 1; grid-row: 1; }

  /* ⚠ The divider is on the BODY, never on the media. The media carries
     [data-wipe], and clip-path cuts borders and inset box-shadows — the same
     reason .iy-cell__frame and .iy-cell__face are two stacked layers. A border here would be sliced off for the length of the wipe and
     then snap into existence, which reads as a rendering fault rather than as
     a reveal. */
  .iy-stack__body { border-inline-start: 1px solid var(--iy-mark); }
  .iy-stack__card--b > .iy-stack__body {
    border-inline-start: 0; border-inline-end: 1px solid var(--iy-mark);
  }

  html.js-anim .iy-stack__card { position: sticky; top: var(--iy-stack-top); }
}

/* Below the breakpoint the media needs its own shape back — with no grid row
   to stretch into, a height:100% absolute image inside an auto-height box
   collapses to nothing. */
@media (max-width: 899.98px) {
  .iy-stack__media { aspect-ratio: 4 / 3; }
  .iy-stack__body { border-top: 1px solid var(--iy-mark); }   /* on the body: clip-path would cut it on the media */
}

/* ── the static path for the card-stack ─────────────────────────────────
   Sticky is gated on html.js-anim above rather than switched off here, so
   no-JS, reduced motion and ?cap=1 all get two composed cards in flow. There
   is nothing to "finish": the fallback IS the layout, which is the whole
   reason this device was built on sticky instead of a scrubbed transform. */

/* ═══════════════════════════════════════════════════════════════════════
   09 · products — innovation from the hive
   Dark (#050506) after chapter 08's ink (#0E0E10): a value STEP, not a swap.
   The chapter earns its weight from the lattice behind it and from one card
   with real presence, NOT from a second card or a screenshot.

   ⛔ No product screenshot here. A picture of an already-designed UI cannot
   be a background or a cropped fill — bled behind type it fights the type,
   and under object-fit:cover it slices its own headlines. This chapter is
   deliberately image-free.
   ═══════════════════════════════════════════════════════════════════════ */
.iy-products { position: relative; overflow: hidden; }
.iy-products__canvas {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  display: block; pointer-events: none;
  opacity: .5;   /* quieter than the hero; this chapter is a coda, not an opening */
}
.iy-products__inner { position: relative; z-index: 1; }

.iy-product {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: clamp(1.25rem, 3vw, 2.5rem);
  /* 34rem wraps the description to two lines, which FILLS the card. Any wider
     and the copy ends well short of the card's right edge, leaving its right
     third empty — the card reads as a wide box with text pushed into its left
     corner rather than as a composed object. */
  max-width: 34rem;
  margin: clamp(1.8rem, 3.6vw, 2.8rem) auto 0;
  padding: clamp(1.1rem, 2.2vw, 1.6rem);
  text-decoration: none;
  background: rgba(14,14,16,.72);
  border: 1px solid var(--iy-line-dark);
  border-radius: 2px;
  /* The gold bloom is deliberate and it is a quotation: Verify360's own
     interface is black with a gold glow on its cards. The card on the website
     should look like the product it opens. */
  box-shadow: 0 0 0 1px rgba(243,180,49,.04), 0 24px 60px -30px rgba(243,180,49,.35);
  transition: border-color .32s var(--ease), box-shadow .32s var(--ease), background-color .32s var(--ease);
}
/* ⚠ NO transform on .iy-product — it carries [data-reveal], and primitives'
   [data-reveal].is-revealed{transform:translateY(0)} at (0,2,0) beats a plain
   class. Hover feedback goes on the card's own paint and on DESCENDANTS. */
.iy-product:hover,
.iy-product:focus-visible {
  border-color: rgba(243,180,49,.42);
  background: rgba(22,22,26,.82);
  box-shadow: 0 0 0 1px rgba(243,180,49,.10), 0 28px 70px -28px rgba(243,180,49,.55);
}
.iy-product__mark {
  width: clamp(38px, 4.7vw, 48px);
  aspect-ratio: 1 / 1.1547;          /* a regular pointy-top hexagon */
  flex: 0 0 auto;
  clip-path: var(--hex);
  background: var(--iy-metal);
  /* ⚠ clip-path cuts a border AND an inset box-shadow — never try to outline
     this. If an outline is ever wanted it needs two stacked clipped layers. */
}
.iy-product__body { display: block; min-width: 0; }
.iy-product__name {
  display: block;
  font-family: var(--display); font-weight: 800;
  font-size: var(--fs-h3);
  letter-spacing: .01em;
  color: var(--iy-on-dark);
}
.iy-product__desc {
  display: block;
  margin-top: .35rem;
  font-family: var(--body);
  font-size: var(--fs-lead);
  line-height: 1.5;
  color: var(--iy-on-dark-soft);
  /* ⚠ NO text-wrap:balance here, deliberately. Inside a card this narrow,
     balance evens the two lines by SHORTENING the first one, which widens the
     dead space on the right instead of closing it. Balance is right for a
     centred display headline and wrong for left-aligned body copy in a box
     that is already sized to the text. (And note `pretty` is not an option
     either — measured in Edg/151 it parses, reports CSS.supports true,
     computes to "pretty" and changes nothing.) */
}
.iy-product__go {
  display: inline-flex; align-items: center; gap: .55rem;
  margin-top: clamp(.6rem, 1.3vw, .85rem);
  font-family: var(--display); font-weight: 500;
  font-size: var(--fs-small);
  letter-spacing: .16em; text-transform: uppercase;
  color: var(--iy-gold-200);
}
/* The affordance moves, not the card. Same idiom as the capability tiles. */
.iy-product__go .iy-btn__hex {
  transition: transform .32s var(--ease-out);
}
.iy-product:hover .iy-product__go .iy-btn__hex,
.iy-product:focus-visible .iy-product__go .iy-btn__hex {
  transform: translateX(5px);
}
@media (max-width: 620px) {
  .iy-product {
    grid-template-columns: 1fr;
    justify-items: start;
    gap: 1rem;
  }
}

/* ═══════════════════════════════════════════════════════════════════════
   THE HEXAGON PIPS — one loading idiom, used in two places
   It reuses the --hex token and .iy-btn__hex's 9x10 sizing so it reads as the
   same family as the hex glyph already inside every button.

   currentColor, so it inherits whatever it sits on — gold on the boot overlay,
   the button's own ink on the submit button. One rule, two grounds.
   ═══════════════════════════════════════════════════════════════════════ */
.iy-pips { display: inline-flex; align-items: center; gap: .34rem; }
.iy-pips > i {
  width: 9px; height: 10px;
  flex: none;
  clip-path: var(--hex);
  background: currentColor;
  opacity: .22;
}
@keyframes iy-pip {
  0%, 100% { opacity: .22; transform: scale(.78); }
  42%      { opacity: 1;   transform: scale(1); }
}
@media (prefers-reduced-motion: no-preference) {
  html.js-anim .iy-pips > i { animation: iy-pip 1.15s var(--ease) infinite; }
  html.js-anim .iy-pips > i:nth-child(2) { animation-delay: .13s; }
  html.js-anim .iy-pips > i:nth-child(3) { animation-delay: .26s; }
  html.js-anim .iy-pips > i:nth-child(4) { animation-delay: .39s; }
}

/* ═══════════════════════════════════════════════════════════════════════
   THE FORM — in-flight, success and error states
   An async submission with a success state, rather than a mailto: that pops
   the visitor's mail client open. The pips above are the in-flight state.
   ═══════════════════════════════════════════════════════════════════════ */
/* Off-screen, NOT display:none. A bot that computes styles skips a hidden
   field; one that reads the DOM fills this in and gets rejected. */
.iy-form__hp {
  position: absolute !important;
  width: 1px; height: 1px;
  overflow: hidden; clip-path: inset(50%);
  white-space: nowrap;
}
.iy-form__submit { position: relative; }
.iy-form__pips { display: none; }
.iy-form.is-sending .iy-form__submit { pointer-events: none; opacity: .82; }
.iy-form.is-sending .iy-form__submit-label { visibility: hidden; }
.iy-form.is-sending .iy-form__submit-hex   { display: none; }
.iy-form.is-sending .iy-form__pips {
  display: inline-flex;
  position: absolute; inset: 0;
  align-items: center; justify-content: center;
}

/* Success and error REPLACE the form rather than sitting under it — a filled
   form still on screen under a "thank you" invites a second submit. */
/* Explicit, not left to the UA sheet: any later `display` on .iy-form__panel
   would silently beat [hidden]'s UA rule and show both panels at once. */
.iy-form__panel[hidden] { display: none !important; }
.iy-form__panel {
  border: 1px solid var(--iy-line-dark);
  border-radius: 2px;
  padding: clamp(1.5rem, 3.4vw, 2.5rem);
  background: rgba(14,14,16,.66);
}
.iy-form__panel--ok { border-color: rgba(243,180,49,.34); }
.iy-form__panel-mark {
  display: block;
  width: 34px; aspect-ratio: 1 / 1.1547;
  clip-path: var(--hex);
  background: var(--iy-metal);
  margin-bottom: 1rem;
}
.iy-form__panel-h {
  margin: 0;
  font-family: var(--display); font-weight: 800;
  font-size: var(--fs-h3);
  color: var(--iy-on-dark);
}
/* script.js gives this tabindex="-1" and focuses it so a keyboard or screen
   reader user is not stranded on a submit button that no longer exists. It is
   NOT an interactive control and the user did not navigate here — so the ring
   is wrong: a gold box drawn around the word "Thank you." reads as a defect. Removing the indicator on a programmatic -1 target
   is the correct behaviour; the panel appearing IS the visible feedback.
   (Only :focus — a real :focus-visible on anything else keeps its ring.) */
.iy-form__panel-h:focus { outline: none; }
.iy-form__panel-p {
  margin: .6rem 0 0;
  font-family: var(--body);
  font-size: var(--fs-lead);
  line-height: 1.55;
  color: var(--iy-on-dark-soft);
}
.iy-form__panel-a { margin-top: 1.4rem; }

/* ═══════════════════════════════════════════════════════════════════════
   THE BOOT OVERLAY
   The gold hexagon bee mark flies left→right on an arc, igniting lattice
   cells in its wake, over four hexagon pips and the wordmark.

   ⚠ THE SAFETY CONTRACT, and it matters more than the animation:
   a preloader that sticks is a dead site. Three independent guarantees —
     1. It only EXISTS under html.iy-boot-on, set pre-paint. No-JS,
        reduced-motion and ?cap=1 never render a frame of it.
     2. script.js arms a hard-kill timeout BEFORE it does anything else, so a
        throw anywhere later still tears the overlay down.
     3. Removing .iy-boot-on releases display AND the scroll lock together —
        they are the same class, so they cannot get out of step.
   ═══════════════════════════════════════════════════════════════════════ */
.iy-boot { display: none; }
html.iy-boot-on .iy-boot {
  display: flex;
  position: fixed; inset: 0;
  z-index: 9999;
  flex-direction: column; align-items: center; justify-content: center;
  gap: clamp(1.5rem, 4vh, 2.75rem);
  background: var(--iy-black);
  opacity: 1;
  transition: opacity .42s var(--ease);
}
/* Scroll lock. Tied to the SAME class as the overlay, on purpose: there is no
   state in which one is present and the other is not. */
html.iy-boot-on, html.iy-boot-on body { overflow: hidden; }
html.iy-boot-on .iy-boot.is-out { opacity: 0; pointer-events: none; }

.iy-boot__canvas {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  display: block; pointer-events: none;
  opacity: .62;
}
.iy-boot__stage {
  position: relative; z-index: 1;
  width: min(680px, 82vw);
  height: 96px;
}
/* ⚠ WIDTH 100% IS LOAD-BEARING.
   A percentage in `translate()` resolves against the ELEMENT'S OWN border box,
   not against its parent. With this span shrink-wrapped to the 44px SVG,
   translateX(104%) would move the bee ~45 pixels across the whole flight on a
   680px stage — rendering perfectly and looking like a bee twitching in place.

   Full-width, so 1% is 1% of the STAGE and the arc means what it says. */
.iy-boot__bee {
  position: absolute; top: 50%; left: 0;
  display: block;
  width: 100%;
  margin-top: -22px;
  pointer-events: none;
}
/* Rotation lives on the SVG, not on the mover. Rotating a full-width box
   pivots about the stage's centre and would swing the mark through an arc
   hundreds of pixels tall. */
.iy-boot__bee svg {
  display: block;
  filter: drop-shadow(0 0 18px rgba(243,180,49,.45));
}
/* The arc. Two axes so the bee rises and settles rather than sliding flat —
   a straight horizontal translate reads as a progress bar with a logo on it. */
@keyframes iy-boot-fly {
  0%   { transform: translate(-10%, 14px); opacity: 0; }
  12%  { opacity: 1; }
  30%  { transform: translate(20%, -16px); }
  55%  { transform: translate(46%, 10px); }
  78%  { transform: translate(72%, -12px); }
  90%  { opacity: 1; }
  100% { transform: translate(102%, 0); opacity: 0; }
}
/* Small, and eased back to level so the mark is UPRIGHT while it is most
   legible — a logo tumbling across the screen reads as a toy. */
@keyframes iy-boot-tilt {
  0%   { transform: rotate(-9deg); }
  30%  { transform: rotate(5deg); }
  55%  { transform: rotate(-4deg); }
  78%  { transform: rotate(4deg); }
  100% { transform: rotate(0deg); }
}
html.iy-boot-on .iy-boot__bee {
  animation: iy-boot-fly 2.2s var(--ease) forwards;
}
html.iy-boot-on .iy-boot__bee svg {
  animation: iy-boot-tilt 2.2s var(--ease) forwards;
}
.iy-boot__word {
  position: relative; z-index: 1;
  margin: 0;
  font-family: var(--display); font-weight: 300;
  font-size: var(--fs-small);
  letter-spacing: .42em; text-transform: uppercase;
  color: var(--iy-on-dark-soft);
  /* .42em of tracking adds a trailing space the eye reads as off-centre. */
  text-indent: .42em;
}
/* Bigger than the button pips, and deliberately so. At the shared 9x10 they
   read as four dots on a full-screen black field — which defeats the point,
   since these are hexagons INSTEAD of dots. In a button the hex sits
   beside 14px type and 9px is right; here it is the only loading signal on a
   1440px canvas. Same component, different scale. */
.iy-boot__pips { position: relative; z-index: 1; color: var(--iy-gold-200); gap: .55rem; }
.iy-boot__pips > i { width: 14px; height: 16px; }

/* ══ THE STATIC PATH ══════════════════════════════════════════════════════
   One place, one rule set, covering all three cases together:
     • no JavaScript at all
     • prefers-reduced-motion: reduce
     • ?cap=1  (static capture mode)

   Everything below must render FINISHED, not at its start state. Each
   declaration is labelled with the device it finishes. */
html:not(.js-anim) [data-progress] { --p: 1; }                       /* scroll progress */
html:not(.js-anim) [data-wipe]     { clip-path: none !important; }   /* wipe */
html:not(.js-anim) .iy-seam::before { transform: scaleX(1) !important; }  /* seam */
html:not(.js-anim) .iy-cover > .iy-cover__held { position: static !important; }  /* sticky cover */
/* The chamber — and for a pair of covering leaves, "finished" means ABSENT.
   Leaving them in place at --p's initial value would give a shut chamber: a
   blank rectangle where the chapter's content is, which is exactly the failure
   this section exists to prevent. The card-stack needs no line here at all —
   its sticky is gated on html.js-anim, so the fallback IS the composed
   two-card layout. */
html:not(.js-anim) .iy-chamber { display: none; }                    /* chamber */

/* The moving band — and this one is NOT just "switch the animation off".
   A stopped marquee is a max-content flex row eight items wide inside an
   overflow:hidden box: items 5 to 8 are simply CLIPPED AWAY, invisible, with
   no overflow reported anywhere. That is not a static fallback, it is a
   chapter that silently loses half its content for every reduced-motion or
   no-JS visitor.

   So the static path RELAYOUTS the band: the track wraps and centres, the
   clone is removed from the flow, and the edge mask comes off because there is
   nothing running out of frame to fade. A composed block, not a frozen film. */
html:not(.js-anim) .iy-marquee {
  -webkit-mask-image: none;
          mask-image: none;
}
html:not(.js-anim) .iy-marquee__track {
  width: auto;
  flex-wrap: wrap;
  justify-content: center;
  transform: none !important;
}
html:not(.js-anim) .iy-marquee__set {
  /* ⚠ flex:1 1 100% and min-width:0 are BOTH load-bearing. The base rule sets
     .iy-marquee__set to `flex: 0 0 auto` so the two halves can never shrink —
     which is correct for the moving band and exactly wrong here: without them
     the set keeps its full ~2178px content width, ignores flex-wrap entirely,
     and half or more of the items sit outside the box with no overflow
     reported anywhere, because the parent is overflow:hidden. */
  flex: 1 1 100%;
  min-width: 0;
  flex-wrap: wrap;
  justify-content: center;
  row-gap: clamp(.6rem, 1.6vw, 1rem);
  padding-right: 0;
}
/* The clone exists ONLY to make translateX(-50%) loop seamlessly. With no
   animation it is a duplicated list — visible, and read by nothing. */
html:not(.js-anim) .iy-marquee__set[aria-hidden="true"] { display: none; }

/* The boot overlay — nothing to neutralise. The overlay is gated on html.iy-boot-on, which
   the pre-paint script only sets when js-anim is also set, so on this path it
   was never in the layout at all. Recorded here so the absence is deliberate
   rather than an oversight the next reader has to re-derive. */
