/* ═══════════════════════════════════════════════════════════════════════════
   behnaam.com — BEHNAAM V5 design system, implemented.
   SOURCE OF TRUTH: "BEHNAAM Design System" handoff bundle (Claude Design),
   project/tokens/*.css + foundation/*.css + SKILL.md. The --b-* layer below is
   copied verbatim from the bundle; the --os-* names are kept as ALIASES so the
   109 pages need no churn. Never edit an --os-* value: edit the --b-* source.
   Two themes. Every text token meets WCAG AA (≥4.5:1) against its own surface.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── BEHNAAM V5 webfonts (self-hosted; bundle assets/fonts) ─────────────────
   Persian: Abar Mid = display/headings ONLY · Ravi = lead/body · Peyda = UI.
   Latin: IBM Plex Sans = English · IBM Plex Mono = metadata (always LTR). */
@font-face{font-family:"Abar Mid";src:url("/assets/fonts/AbarMid-Regular.woff2") format("woff2");font-weight:400;font-display:swap}
@font-face{font-family:"Abar Mid";src:url("/assets/fonts/AbarMid-Medium.woff2") format("woff2");font-weight:500;font-display:swap}
@font-face{font-family:"Abar Mid";src:url("/assets/fonts/AbarMid-SemiBold.woff2") format("woff2");font-weight:600;font-display:swap}
@font-face{font-family:"Abar Mid";src:url("/assets/fonts/AbarMid-Bold.woff2") format("woff2");font-weight:700;font-display:swap}
@font-face{font-family:"Abar Mid";src:url("/assets/fonts/AbarMid-ExtraBold.woff2") format("woff2");font-weight:800;font-display:swap}
@font-face{font-family:"Abar Mid";src:url("/assets/fonts/AbarMid-Black.woff2") format("woff2");font-weight:900;font-display:swap}
@font-face{font-family:"Ravi";src:url("/assets/fonts/Ravi-Light.woff2") format("woff2");font-weight:300;font-display:swap}
@font-face{font-family:"Ravi";src:url("/assets/fonts/Ravi-Regular.woff2") format("woff2");font-weight:400;font-display:swap}
@font-face{font-family:"Ravi";src:url("/assets/fonts/Ravi-Medium.woff2") format("woff2");font-weight:500;font-display:swap}
@font-face{font-family:"Ravi";src:url("/assets/fonts/Ravi-SemiBold.woff2") format("woff2");font-weight:600;font-display:swap}
@font-face{font-family:"Ravi";src:url("/assets/fonts/Ravi-Bold.woff2") format("woff2");font-weight:700;font-display:swap}
@font-face{font-family:"Peyda";src:url("/assets/fonts/Peyda-Regular.woff2") format("woff2");font-weight:400;font-display:swap}
@font-face{font-family:"Peyda";src:url("/assets/fonts/Peyda-Medium.woff2") format("woff2");font-weight:500;font-display:swap}
@font-face{font-family:"Peyda";src:url("/assets/fonts/Peyda-SemiBold.woff2") format("woff2");font-weight:600;font-display:swap}
@font-face{font-family:"Peyda";src:url("/assets/fonts/Peyda-Bold.woff2") format("woff2");font-weight:700;font-display:swap}
@font-face{font-family:"IBM Plex Sans";src:url("/assets/fonts/IBMPlexSans-Regular.woff2") format("woff2");font-weight:400;font-display:swap}
@font-face{font-family:"IBM Plex Sans";src:url("/assets/fonts/IBMPlexSans-SemiBold.woff2") format("woff2");font-weight:600;font-display:swap}
@font-face{font-family:"IBM Plex Sans";src:url("/assets/fonts/IBMPlexSans-Bold.woff2") format("woff2");font-weight:700;font-display:swap}
@font-face{font-family:"IBM Plex Mono";src:url("/assets/fonts/IBMPlexMono-Regular.woff2") format("woff2");font-weight:400;font-display:swap}
@font-face{font-family:"IBM Plex Mono";src:url("/assets/fonts/IBMPlexMono-SemiBold.woff2") format("woff2");font-weight:600;font-display:swap}

/* ── BEHNAAM V5 tokens — verbatim from bundle tokens/*.css ────────────────── */
:root {
  /* colors: base */
  --b-page:#F6F1E8;--b-surface:#FFFEFA;--b-ink:#172026;--b-ink-2:#2F3B42;--b-muted:#637078;--b-charcoal:#182127;--b-border:#d7d1c6;
  /* colors: semantic domain palette (display grade — see --b-*-text below) */
  --b-strategy:#B98639;--b-product:#255F8A;--b-operations:#4F7C63;--b-systems:#267888;--b-growth:#C77735;--b-trust:#6F5260;--b-gold:#BD8433;
  /* AA-safe text grades. The bundle ships ONE grade per domain (display) and
     also mandates WCAG AA; at body size those hues are ~2.6:1 on paper, so the
     system needs a text grade to satisfy its own rule. Values are the audited
     AA-safe set already in production. See DECISION_LOG D-20. */
  --b-strategy-text:#956215;--b-product-text:#1F5580;--b-operations-text:#3F6B53;
  --b-systems-text:#1F6F79;--b-growth-text:#A95917;--b-trust-text:#6C3849;--b-gold-text:#99600F;
  /* surfaces + interactive */
  --b-surface-2:#F8F3E9;--b-sunk:#EFE9DD;--b-surface-chip:#dceaea;--b-surface-selection:#d9e6ee;--b-surface-track:#c8cfd4;
  --b-action:var(--b-ink);--b-action-hover:#253139;--b-accent:var(--b-product);--b-accent-hover:#1f5175;
  /* bridge signature (origin → catalyst → outcome) */
  --b-bridge-origin:var(--b-strategy);--b-bridge-catalyst:var(--b-product);--b-bridge-outcome:var(--b-systems);
  --b-icon-accent:#267888;
  /* type roles */
  --b-font-display:"Abar Mid",sans-serif;--b-font-body:"Ravi",sans-serif;--b-font-ui:"Peyda",sans-serif;
  --b-font-en:"IBM Plex Sans",ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,Arial,sans-serif;
  --b-font-mono:"IBM Plex Mono",ui-monospace,SFMono-Regular,Menlo,monospace;
  /* fluid display sizes */
  --b-size-display-xl:clamp(3rem,8vw,7rem);--b-size-display:clamp(2.4rem,6vw,5rem);
  --b-size-heading:clamp(1.6rem,3vw,2.6rem);--b-size-heading-sm:clamp(1.25rem,2.2vw,1.6rem);
  --b-size-lead:clamp(1.1rem,2vw,1.35rem);--b-size-meta:0.78rem;
  --b-leading-display:1.02;--b-leading-heading:1.25;--b-leading-lead:1.8;
  --b-leading-body-fa:1.9;--b-leading-body-en:1.6;--b-tracking-meta:0.035em;
  /* spacing (8px grid) + shape */
  --b-space-1:4px;--b-space-2:8px;--b-space-3:12px;--b-space-4:16px;--b-space-6:24px;
  --b-space-8:32px;--b-space-12:48px;--b-space-16:64px;--b-space-20:80px;--b-space-24:96px;
  --b-radius-sm:8px;--b-radius-md:14px;--b-radius-lg:22px;--b-radius-xl:32px;--b-radius-pill:999px;
  --b-border-subtle:1px;--b-border-default:2px;--b-border-focus:3px;
  --b-content:1200px;--b-reading:760px;--b-target-min:44px;
  /* elevation, focus, motion */
  --b-shadow:0 14px 40px rgba(23,32,38,.09);--b-shadow-modal:0 24px 64px rgba(23,32,38,.18);
  --b-focus:0 0 0 3px rgba(37,95,138,.28);--b-focus-on-dark:0 0 0 3px rgba(246,241,232,.45);
  --b-fast:120ms;--b-default:180ms;--b-slow:280ms;--b-ease:cubic-bezier(.2,.8,.2,1);
}

:root {
  /* ── type — aliased to the V5 roles. Persian faces are applied per-lang
        below; the Latin default keeps IBM Plex Sans/Mono per the bundle. ── */
  --os-font: var(--b-font-en);
  --os-mono: var(--b-font-mono);
  --os-display: var(--b-font-en);

  --os-step--1: clamp(.8125rem, .78rem + .12vw, .875rem);
  --os-step-0:  clamp(.9375rem, .90rem + .18vw, 1rem);
  --os-step-1:  clamp(1.0625rem, 1.00rem + .30vw, 1.1875rem);
  --os-step-2:  clamp(1.25rem, 1.14rem + .55vw, 1.5rem);
  --os-step-3:  clamp(1.5rem, 1.30rem + 1.00vw, 2rem);
  --os-step-4:  clamp(1.875rem, 1.50rem + 1.85vw, 2.75rem);
  --os-step-5:  clamp(2.25rem, 1.65rem + 3.00vw, 3.75rem);

  /* ── geometry ─────────────────────────────────────────────────────────── */
  --os-container: 1160px;
  --os-narrow: 780px;
  --os-radius: 16px;
  --os-radius-sm: 10px;
  --os-radius-pill: 999px;
  --os-header-h: 66px;
  --os-gap: clamp(14px, 2.2vw, 22px);
  --os-block: clamp(48px, 7vw, 92px);

  /* ── motion ───────────────────────────────────────────────────────────── */
  --os-ease: cubic-bezier(.2, .7, .2, 1);
  --os-fast: 140ms;
  --os-slow: 420ms;
}

/* ═══ LIGHT — the approved surface ════════════════════════════════════════ */
:root,
html[data-theme="light"] {
  color-scheme: light;

  /* All values alias the V5 --b-* source above. Do not hardcode here. */
  --os-page:      var(--b-page);
  --os-page-soft: #FBF7EF;                 /* bundle has no page-soft token; V5-derived tint */
  --os-surface:   var(--b-surface);
  --os-surface-2: var(--b-surface-2);
  --os-sunk:      var(--b-sunk);

  --os-ink:       var(--b-ink);            /* 14.7:1 on page */
  --os-ink-2:     var(--b-ink-2);          /* 10.2:1 */
  --os-muted:     var(--b-muted);          /*  4.5:1 */

  --os-line:        rgba(32, 42, 48, .12);
  --os-line-strong: rgba(32, 42, 48, .22);

  /* Accents come in two grades:
     -text  → AA-safe, the ONLY grade allowed on body-size text
     -disp  → the V5 display chroma, for ≥24px display type, icons, rules, fills */
  --os-strategy-text: var(--b-strategy-text);   --os-strategy-disp: var(--b-strategy);
  --os-product-text:  var(--b-product-text);    --os-product-disp:  var(--b-product);
  --os-ops-text:      var(--b-operations-text); --os-ops-disp:      var(--b-operations);
  --os-systems-text:  var(--b-systems-text);    --os-systems-disp:  var(--b-systems);
  --os-growth-text:   var(--b-growth-text);     --os-growth-disp:   var(--b-growth);
  --os-trust-text:    var(--b-trust-text);      --os-trust-disp:    var(--b-trust);
  --os-gold-text:     var(--b-gold-text);       --os-gold-disp:     var(--b-gold);

  /* the single permitted dark surface */
  --os-charcoal:    var(--b-charcoal);
  --os-on-charcoal: var(--b-page);

  --os-shadow:      0 16px 44px rgba(25, 34, 40, .09);
  --os-shadow-soft: 0 10px 26px rgba(25, 34, 40, .06);
  --os-focus: 0 0 0 3px rgba(31, 111, 121, .28), 0 0 0 1px rgba(31, 111, 121, .62);

  --os-sky: linear-gradient(180deg, rgba(255,253,248,.72), rgba(244,240,231,.96) 42rem);
}

/* ═══ DARK — same hues, re-lit. Not an inversion. ═════════════════════════ */
html[data-theme="dark"] {
  color-scheme: dark;

  --os-page:      #12171B;
  --os-page-soft: #151B20;
  --os-surface:   #181F24;
  --os-surface-2: #1E262C;
  --os-sunk:      #0D1114;

  --os-ink:       #EEF2F4;   /* 15.5:1 on surface */
  --os-ink-2:     #C3CCD2;   /* 10.2:1 */
  --os-muted:     #8D99A1;   /*  5.6:1 */

  --os-line:        rgba(238, 242, 244, .12);
  --os-line-strong: rgba(238, 242, 244, .24);

  --os-strategy-text: #D8A65C;  --os-strategy-disp: #C9954A;
  --os-product-text:  #74A6CC;  --os-product-disp:  #5B93BC;
  --os-ops-text:      #86B399;  --os-ops-disp:      #6F9E84;
  --os-systems-text:  #58AEB9;  --os-systems-disp:  #4599A5;
  --os-growth-text:   #E0955F;  --os-growth-disp:   #D0834A;
  --os-trust-text:    #C79AA8;  --os-trust-disp:    #B0899A;
  --os-gold-text:     #D8A65C;  --os-gold-disp:     #C9954A;

  /* In dark mode the "one dark band" rule inverts: the accent band is light. */
  --os-charcoal:    #EDE7DA;
  --os-on-charcoal: #12171B;

  --os-shadow:      0 18px 46px rgba(0, 0, 0, .46);
  --os-shadow-soft: 0 10px 26px rgba(0, 0, 0, .32);
  --os-focus: 0 0 0 3px rgba(88, 174, 185, .34), 0 0 0 1px rgba(88, 174, 185, .70);

  --os-sky: linear-gradient(180deg, rgba(26, 33, 38, .70), rgba(18, 23, 27, .98) 42rem);
}

/* Accent channel: one variable, set per module via [data-accent]. */
[data-accent="strategy"] { --os-accent: var(--os-strategy-text); --os-accent-disp: var(--os-strategy-disp); }
[data-accent="product"]  { --os-accent: var(--os-product-text);  --os-accent-disp: var(--os-product-disp); }
[data-accent="ops"]      { --os-accent: var(--os-ops-text);      --os-accent-disp: var(--os-ops-disp); }
[data-accent="systems"]  { --os-accent: var(--os-systems-text);  --os-accent-disp: var(--os-systems-disp); }
[data-accent="growth"]   { --os-accent: var(--os-growth-text);   --os-accent-disp: var(--os-growth-disp); }
[data-accent="trust"]    { --os-accent: var(--os-trust-text);    --os-accent-disp: var(--os-trust-disp); }
:root { --os-accent: var(--os-systems-text); --os-accent-disp: var(--os-systems-disp); }

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

/* The hidden attribute must always win — including on SVG, where the UA
   stylesheet's [hidden] rule does not apply (sprite sheets rely on this). */
[hidden] { display: none !important; }

/* ═══ scroll progress ═════════════════════════════════════════════════════ */
.scroll-progress {
  position: fixed; inset-block-start: 0; inset-inline: 0; height: 2px; z-index: 90;
  background: var(--os-accent-disp); transform-origin: 0 50%; transform: scaleX(0);
}
html[dir="rtl"] .scroll-progress { transform-origin: 100% 50%; }

/* ═══ header ══════════════════════════════════════════════════════════════ */
.site-header {
  position: sticky; top: 0; z-index: 80;
  min-height: var(--os-header-h);
  display: flex; align-items: center;
  border-bottom: 1px solid var(--os-line);
  /* 92%, was 84: at 84 the text scrolling underneath stayed LEGIBLE through
     the band (caught on ar/contact mobile — a ghost line of body copy read
     straight through the header). The frost keeps its hint from the blur;
     the words underneath stop being words. */
  background: color-mix(in srgb, var(--os-page) 92%, transparent);
  backdrop-filter: saturate(1.4) blur(12px);
}
@supports not (backdrop-filter: blur(2px)) { .site-header { background: var(--os-page); } }

.header-in { display: flex; align-items: center; gap: 12px; width: 100%; }

.brand { display: inline-flex; align-items: center; gap: 10px; font-weight: 650; }
/* V5 structural symbol (supplied SVG — never redrawn, never recoloured; it
   carries the bridge signature: origin/catalyst/outcome). Used instead of the
   horizontal lockup because the header already prints the name + role and the
   lockup contains its own wordmark. Themes swap PRIMARY <-> the supplied
   REVERSE badge rather than filtering the artwork (D-22). Clear space comes
   from the flex gap + header padding. */
.brand-mark { height: 30px; width: 30px; flex: none; }
.brand-mark--reverse { display: none; }
html[data-theme="dark"] .brand-mark--primary { display: none; }
html[data-theme="dark"] .brand-mark--reverse { display: block; }
.brand-name { white-space: nowrap; }
/* ═══ BH-155 · the lockup stacks; the role stops being a flex hostage ════════
   As a flex item BESIDE the name, .brand-role owned only the width the nav
   left over — measured: a 4-line/91px header in en at 1152–1279, 5–6 lines up
   to 136px in es/fr/pt in the same window, and 5 lines at EVERY desktop width
   in de. The role now sits in its own grid row UNDER the name, where it gets
   the full lockup width and renders on one line in all 9 locales at every
   width ≥1080 (scripts/header_fit_probe.mjs). The mark spans both rows; with
   the role hidden (<1080px) row 2 collapses and nothing moves. Grid areas
   follow the writing mode, so RTL needs no mirror rules. The « · » separator
   the role used to carry is gone from the markup: a row break separates
   better than a dot, and a dot at line start was an orphan in every script. */
.brand { display: inline-grid; grid-template-columns: auto minmax(0, auto); column-gap: 10px; align-items: center; }
.brand-mark { grid-row: 1 / span 2; }
.brand-name { grid-column: 2; line-height: 1.2; }
.brand-role {
  display: none; grid-column: 2; grid-row: 2;
  color: var(--os-muted); font-weight: 400; font-size: var(--os-step--1);
  line-height: 1.25; white-space: nowrap;
}
/* Where the role is SHOWN is measured, not hoped (scripts/header_fit_probe.mjs,
   which checks the box overlap scrollWidth cannot see). The binding constraint
   is the 1160px header wrap: whatever the viewport, the role gets only what
   the nav and tools leave inside 1160. Four locales' titles are wider than
   that remainder at ANY width — de/es/fr/pt — so they never show the header
   role rather than show it truncated, five lines tall, or painted under the
   nav (all three were shipped at some point). Their hero h2 states the same
   title one viewport-line below; nothing is lost, and nothing lies. */
/* RE-MEASURED after the Journal joined the primary nav (BH-162). An eighth
   item costs roughly 90px, and with it only three locales can still fit the
   role beside the nav inside the 1160px header wrap — measured by sweeping
   1140->1900px per locale and testing for box overlap AND nowrap text
   overflow: ar and zh from 1160, fa from 1200, and en/de/es/fr/pt/ru at NO
   width up to 1900.
   That is a real cost and it is the right trade: the role is repeated in the
   hero h2 one viewport-line below, while the Journal is a navigation entry
   point that has to be reachable — it is the page the owner publishes to and
   the one that has to rank. A decorative repetition loses to a route. */
@media (min-width: 1160px) {
  html[lang="ar"] .brand-role, html[lang="zh"] .brand-role { display: block; }
}
@media (min-width: 1200px) { html[lang="fa"] .brand-role { display: block; } }

.nav-links { display: none; margin-inline: auto; gap: 2px; }
/* Desktop-nav switch is based on measured content fit per locale (widest
   header content wins, brand-role included): every locale fits at 1152px
   except German, which needs 1280px. Keep these three rules and os.js
   (drawer auto-close) in sync. */
@media (min-width: 1152px) { html:not([lang="de"]) .nav-links { display: flex; } }
@media (min-width: 1280px) { .nav-links { display: flex; } }
.nav-links a {
  padding: 8px 11px; border-radius: var(--os-radius-pill);
  color: var(--os-ink-2); font-size: var(--os-step--1); font-weight: 550;
  transition: background var(--os-fast) var(--os-ease), color var(--os-fast) var(--os-ease);
}
.nav-links a:hover { background: var(--os-surface-2); color: var(--os-ink); }
.nav-links a[aria-current="page"] {
  color: var(--os-ink); background: var(--os-surface);
  box-shadow: inset 0 0 0 1px var(--os-line-strong);
}

.header-tools { display: flex; align-items: center; gap: 8px; margin-inline-start: auto; }
@media (min-width: 1152px) { html:not([lang="de"]) .header-tools { margin-inline-start: 0; } }
@media (min-width: 1280px) { .header-tools { margin-inline-start: 0; } }

.icon-btn {
  display: inline-grid; place-items: center; width: 38px; height: 38px; flex: none;
}
/* touch devices get the 44px hit area WCAG/HIG recommend for key controls */
@media (pointer: coarse) {
  .icon-btn { width: 44px; height: 44px; }
}
.icon-btn {
  border: 1px solid var(--os-line); border-radius: var(--os-radius-sm);
  background: var(--os-surface); color: var(--os-ink-2); cursor: pointer;
  transition: border-color var(--os-fast) var(--os-ease), color var(--os-fast) var(--os-ease);
}
.icon-btn:hover { border-color: var(--os-line-strong); color: var(--os-ink); }
/* ═══ BH-149 · every `.icon` gets a size, not just the ones someone remembered
   This stylesheet sized icons ONLY by context — `.btn svg`, `.icon-btn svg`,
   `.eyebrow svg`, `.icon-box svg`, `.clean-list svg`, `.os-note svg`. Put an
   icon anywhere without a matching rule and it silently falls back to the SVG
   intrinsic default, 300x150. Three places had:

     .cta-head          the header "Get in touch" envelope, 90x150 — a black
                        blob ~320px across dominating the header of ALL 109
                        pages, in every locale, since launch
     .u-inline-chip     six "Full case" arrows on Home at 248x150 each
     summary .caret     the disclosure carets on contact and experience, 300x150

   None of it overflowed, clipped, or failed contrast, so every gate I had -
   static_audit, consistency, html-validate, the 259-cell viewport matrix -
   passed it for weeks. It took one screenshot.

   This is the systemic fix: a baseline for the class itself. Every existing
   context rule is (0,1,1) and still outranks this (0,1,0), so nothing that
   already worked changes; only the unstyled cases get caught. */
.icon { width: 17px; height: 17px; flex: none; }
.icon-btn svg { width: 18px; height: 18px; }

.cta-head {
  display: none; align-items: center; gap: 7px; padding: 9px 15px;
  border-radius: var(--os-radius-pill); font-size: var(--os-step--1); font-weight: 650;
  color: var(--os-on-charcoal); background: var(--os-charcoal); white-space: nowrap;
}
@media (min-width: 640px) { .cta-head { display: inline-flex; } }
.cta-head:hover { opacity: .9; }
.cta-head svg { width: 17px; height: 17px; flex: none; }

/* ── language picker ─────────────────────────────────────────────────────── */
.lang-picker { position: relative; }
.lang-btn { width: auto; padding-inline: 10px; gap: 6px; grid-auto-flow: column; font-size: 12px; font-weight: 700;
            font-family: var(--os-mono); letter-spacing: .06em; }
.lang-menu {
  position: absolute; inset-block-start: calc(100% + 8px); inset-inline-end: 0;
  min-width: 190px; padding: 6px; margin: 0; list-style: none; z-index: 95;
  border: 1px solid var(--os-line-strong); border-radius: var(--os-radius);
  background: var(--os-surface); box-shadow: var(--os-shadow);
  display: none;
}
.lang-picker[data-open="true"] .lang-menu { display: block; }
.lang-menu li { margin: 0; }
.lang-menu a {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 9px 11px; border-radius: var(--os-radius-sm);
  color: var(--os-ink-2); font-size: var(--os-step--1);
}
.lang-menu a:hover { background: var(--os-surface-2); color: var(--os-ink); }
.lang-menu a[aria-current="true"] { color: var(--os-accent); font-weight: 650; }
.lang-menu .code { font-family: var(--os-mono); font-size: 11px; color: var(--os-muted); letter-spacing: .08em; }

/* ── mobile drawer ───────────────────────────────────────────────────────── */
.menu-btn { display: inline-grid; }
@media (min-width: 1152px) { html:not([lang="de"]) .menu-btn { display: none; } }
@media (min-width: 1280px) { .menu-btn { display: none; } }

.nav-drawer {
  position: fixed; inset: 0; z-index: 120; display: none;
  background: color-mix(in srgb, var(--os-page) 96%, transparent);
  backdrop-filter: blur(10px);
  padding: 18px; overflow-y: auto;
}
.nav-drawer[data-open="true"] { display: block; }
.drawer-top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 22px; }
.drawer-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; }
.drawer-list a {
  display: flex; align-items: baseline; gap: 14px; padding: 14px 12px;
  border-radius: var(--os-radius-sm); border: 1px solid transparent;
  font-size: var(--os-step-1); font-weight: 600;
}
.drawer-list a:hover, .drawer-list a[aria-current="page"] {
  background: var(--os-surface); border-color: var(--os-line);
}
.drawer-list .n { font-family: var(--os-mono); font-size: 12px; color: var(--os-muted); }
.drawer-cta { margin-top: 20px; display: grid; gap: 10px; }

/* ═══ footer ══════════════════════════════════════════════════════════════ */
.site-footer {
  border-top: 1px solid var(--os-line);
  background: var(--os-page-soft);
  padding-block: clamp(40px, 6vw, 64px) 28px;
}
.footer-grid { display: grid; gap: 32px; grid-template-columns: 1fr; }
@media (min-width: 780px) { .footer-grid { grid-template-columns: 1.4fr 1fr 1fr; } }

.footer-h {
  font-family: var(--os-mono); font-size: 11px; letter-spacing: .12em;
  text-transform: uppercase; color: var(--os-muted); margin-bottom: 12px;
}
.footer-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; }
/* WCAG 2.2 target size: block links with vertical padding give a >=28px
   hit area (was ~19px text-height); gap reduced so visual rhythm holds. */
.footer-list a { display: inline-block; padding-block: 5px; color: var(--os-ink-2); font-size: var(--os-step--1); }
.footer-list a:hover { color: var(--os-accent); text-decoration: underline; text-underline-offset: 3px; }

/* phone numbers never line-break, and stay LTR inside RTL text */
a[href^="tel:"] { white-space: nowrap; direction: ltr; unicode-bidi: isolate; }

/* consistent affordance for links that leave the site (behavior normalized to
   target=_blank + noopener). The slash syntax keeps the glyph out of the
   accessibility tree in supporting browsers. */
a[target="_blank"]:not(.btn):not(.cta-head)::after { content: " ↗" / ""; font-size: .85em; opacity: .75; }
@supports not (content: "x" / "y") {
  a[target="_blank"]:not(:is(.btn, .cta-head))::after { content: " ↗"; font-size: .85em; opacity: .75; }
}
/* On BLOCK link cards the marker cannot trail the prose: it lands alone on its
   own last line, detached from everything (BH-156 — the contact channel cards).
   Pin it to the card's logical end corner instead, where "leaves the site"
   reads as a corner badge. And mirror the glyph under RTL: "up-and-out" points
   ↖ when "out" is to the left — the LTR glyph aims INTO an RTL page. */
.panel-link { position: relative; }
.panel-link[target="_blank"]::after { position: absolute; inset-inline-end: 16px; inset-block-start: 16px; }
html[dir="rtl"] a.panel-link[target="_blank"]::after { content: " ↖" / ""; }
@supports not (content: "x" / "y") {
  html[dir="rtl"] a.panel-link[target="_blank"]::after { content: " ↖"; }
}

.footer-bottom {
  margin-top: 34px; padding-top: 20px; border-top: 1px solid var(--os-line);
  display: flex; flex-wrap: wrap; gap: 10px 20px; align-items: center;
  justify-content: space-between; color: var(--os-muted); font-size: var(--os-step--1);
}

/* ═══ buttons ═════════════════════════════════════════════════════════════ */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  max-width: 100%;   /* long labels (e.g. German) wrap inside their container instead of widening the page */
  padding: 12px 20px; border-radius: var(--os-radius-pill); border: 1px solid transparent;
  font-size: var(--os-step-0); font-weight: 650; cursor: pointer; text-align: center;
  transition: transform var(--os-fast) var(--os-ease), box-shadow var(--os-fast) var(--os-ease),
              background var(--os-fast) var(--os-ease), border-color var(--os-fast) var(--os-ease);
}
.btn:hover { transform: translateY(-1px); }
.btn svg { width: 17px; height: 17px; flex: none; }
html[dir="rtl"] .btn svg.dir { transform: scaleX(-1); }

/* One filled emphasis element per viewport. This is it. */
.btn-primary { color: var(--os-on-charcoal); background: var(--os-charcoal); box-shadow: var(--os-shadow-soft); }
.btn-primary:hover { box-shadow: var(--os-shadow); }
.btn-ghost { color: var(--os-ink); background: var(--os-surface); border-color: var(--os-line-strong); }
.btn-ghost:hover { border-color: var(--os-ink-2); }
.btn-quiet { color: var(--os-accent); background: transparent; padding-inline: 4px; }
.btn-quiet:hover { text-decoration: underline; text-underline-offset: 4px; }
.btn-block { width: 100%; }

/* ═══ panel — the one surface primitive ═══════════════════════════════════ */
.panel {
  position: relative; padding: clamp(18px, 2.6vw, 28px);
  border: 1px solid var(--os-line); border-radius: var(--os-radius);
  background: var(--os-surface); box-shadow: var(--os-shadow-soft);
}
.panel-sunk { background: var(--os-surface-2); box-shadow: none; }
.panel-quiet { background: transparent; box-shadow: none; }
.panel-accent { border-top: 3px solid var(--os-accent-disp); }
a.panel, .panel-link {
  display: block;
  transition: transform var(--os-fast) var(--os-ease), box-shadow var(--os-fast) var(--os-ease),
              border-color var(--os-fast) var(--os-ease);
}
a.panel:hover, .panel-link:hover {
  transform: translateY(-2px); box-shadow: var(--os-shadow); border-color: var(--os-line-strong);
}

/* The one permitted dark band (inverts in dark mode — see tokens). */
.band-charcoal {
  color: var(--os-on-charcoal); background: var(--os-charcoal);
  border-radius: var(--os-radius); padding: clamp(28px, 4vw, 52px);
}
.band-charcoal .muted,
.band-charcoal .lead { color: color-mix(in srgb, var(--os-on-charcoal) 74%, transparent); }
.band-charcoal .btn-ghost {
  color: var(--os-on-charcoal); background: transparent;
  border-color: color-mix(in srgb, var(--os-on-charcoal) 38%, transparent);
}
.band-charcoal .btn-primary { color: var(--os-charcoal); background: var(--os-on-charcoal); }

/* ═══ section head ════════════════════════════════════════════════════════ */
.sec-head { display: flex; align-items: flex-start; gap: 16px; margin-bottom: clamp(20px, 3vw, 32px); }
.sec-no {
  flex: none; display: grid; place-items: center; width: 34px; height: 34px;
  border: 1px solid var(--os-accent-disp); border-radius: var(--os-radius-sm);
  font-family: var(--os-mono); font-size: 12px; font-weight: 600; color: var(--os-accent);
}
.sec-head > div { flex: 1; min-width: 0; }
.sec-head .eyebrow { margin-bottom: 4px; }
.sec-head .sec-lead { color: var(--os-muted); margin: 6px 0 0; max-width: 62ch; }
.sec-head .sec-more { margin-inline-start: auto; flex: none; align-self: center; }
/* Long single-word titles (German compounds) must hyphenate rather than
   overflow the viewport; hyphens:auto only engages where the page lang has
   a dictionary, break-word is the emergency fallback. */
/* Applies to EVERY heading, not just .sec-title: the hero h1 renders at
   --os-step-5 (up to 3.75rem), so one long compound there escapes the
   viewport by hundreds of px. Caught by the long-content stress test
   (a 61-char German compound at 360px overflowed by 845px) — no current
   heading is that long, so this closes a latent risk, not a live defect.

   `overflow-wrap: anywhere`, not `break-word`: only `anywhere` feeds the
   break opportunity into MIN-CONTENT sizing, so the grid/flex track can
   actually shrink. With `break-word` the track still reserved the full
   unbroken word (measured: h1 and its column both grew to 1189px inside a
   360px viewport). `hyphens: auto` stays for pretty breaks where the page
   lang has a dictionary — it cannot help a German word on an `en` page,
   which is exactly the case that failed. */
.sec-title, h1, h2, h3, h4 { hyphens: auto; overflow-wrap: anywhere; }
/* Grid/flex children default to min-width:auto and refuse to shrink below
   their content. Let the intrinsic minimum collapse so the rule above can
   take effect. */
.hero-grid > *, .grid > * { min-width: 0; }
@media (max-width: 640px) {
  .sec-head { flex-wrap: wrap; }
  .sec-head .sec-more { margin-inline-start: 0; }
}

.eyebrow {
  display: inline-flex; align-items: center; gap: 7px;
  font-family: var(--os-mono); font-size: 11px; font-weight: 600;
  letter-spacing: .14em; text-transform: uppercase; color: var(--os-muted);
}
.eyebrow svg { width: 13px; height: 13px; color: var(--os-accent); }

/* ═══ hero ════════════════════════════════════════════════════════════════ */
.hero { padding-block: clamp(40px, 6vw, 76px) clamp(32px, 5vw, 56px); }
.hero-grid { display: grid; gap: clamp(26px, 4vw, 48px); align-items: center; }
@media (min-width: 900px) { .hero-grid { grid-template-columns: 1.35fr .85fr; } }

.status-pill {
  display: inline-flex; align-items: center; gap: 9px; padding: 8px 15px;
  border: 1px solid var(--os-line-strong); border-radius: var(--os-radius-pill);
  background: var(--os-surface);
  font-family: var(--os-mono); font-size: 11px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--os-ink-2);
}
.status-dot {
  width: 8px; height: 8px; border-radius: 50%; flex: none;
  background: var(--os-ops-disp); box-shadow: 0 0 0 3px color-mix(in srgb, var(--os-ops-disp) 24%, transparent);
}
@media (prefers-reduced-motion: no-preference) {
  .status-dot { animation: os-pulse 2.6s var(--os-ease) infinite; }
  @keyframes os-pulse {
    0%, 100% { box-shadow: 0 0 0 3px color-mix(in srgb, var(--os-ops-disp) 24%, transparent); }
    50%      { box-shadow: 0 0 0 6px color-mix(in srgb, var(--os-ops-disp) 8%, transparent); }
  }
}

.hero h1 { margin: 20px 0 10px; }
.hero .role { font-size: var(--os-step-2); font-weight: 600; color: var(--os-accent); margin-bottom: 14px; }
.hero .promise { max-width: 56ch; }
.hero-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 26px; }

.portrait-card { position: relative; }
.portrait-frame {
  position: relative; overflow: hidden; border-radius: var(--os-radius);
  border: 1px solid var(--os-line); background: var(--os-surface-2); box-shadow: var(--os-shadow);
  aspect-ratio: 4 / 5;
}
.portrait-frame img { width: 100%; height: 100%; object-fit: cover; object-position: 50% 22%; }
.portrait-tag {
  position: absolute; inset-block-start: 12px; inset-inline-start: 12px;
  padding: 6px 11px; border-radius: var(--os-radius-pill);
  background: color-mix(in srgb, var(--os-charcoal) 84%, transparent);
  color: var(--os-on-charcoal);
  font-family: var(--os-mono); font-size: 10px; letter-spacing: .12em; text-transform: uppercase;
}
.portrait-meta {
  margin-top: -22px; margin-inline-start: 14px; position: relative; z-index: 2;
  display: inline-block; padding: 12px 16px;
  border: 1px solid var(--os-line); border-radius: var(--os-radius-sm);
  background: var(--os-surface); box-shadow: var(--os-shadow-soft);
}
.portrait-meta .k { font-family: var(--os-mono); font-size: 10px; letter-spacing: .1em;
                    text-transform: uppercase; color: var(--os-accent); }
.portrait-meta .v { font-weight: 600; font-size: var(--os-step--1); }

/* ═══ metric ribbon ═══════════════════════════════════════════════════════ */
.metric-ribbon { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(min(100%, 168px), 1fr)); }
.metric-tile {
  padding: 18px; border: 1px solid var(--os-line); border-radius: var(--os-radius);
  background: var(--os-surface); box-shadow: var(--os-shadow-soft);
  border-top: 2px solid var(--os-accent-disp);
}
.metric-tile .icon-box { margin-bottom: 12px; }
.metric-value {
  display: block; font-family: var(--os-display); font-weight: 700;
  font-size: var(--os-step-3); line-height: 1.1; letter-spacing: -.02em;
  font-variant-numeric: tabular-nums; color: var(--os-ink);
}
.metric-label { display: block; margin-top: 5px; font-size: var(--os-step--1);
                font-weight: 600; color: var(--os-ink-2); }
.metric-note  { display: block; margin-top: 5px; font-size: var(--os-step--1);
                color: var(--os-muted); line-height: 1.5; }

.icon-box {
  display: grid; place-items: center; width: 34px; height: 34px; flex: none;
  border-radius: var(--os-radius-sm);
  color: var(--os-accent);
  background: color-mix(in srgb, var(--os-accent-disp) 12%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--os-accent-disp) 26%, transparent);
}
.icon-box svg { width: 17px; height: 17px; }

/* ═══ chips & pills ═══════════════════════════════════════════════════════ */
.chip {
  display: inline-flex; align-items: center; gap: 6px; padding: 6px 11px;
  border: 1px solid var(--os-line); border-radius: var(--os-radius-pill);
  background: var(--os-surface-2); font-size: var(--os-step--1); color: var(--os-ink-2);
}
.chip-accent { color: var(--os-accent); border-color: color-mix(in srgb, var(--os-accent-disp) 40%, transparent);
               background: color-mix(in srgb, var(--os-accent-disp) 10%, transparent); }
.chip-row { display: flex; flex-wrap: wrap; gap: 7px; }

/* ═══ logo system ═════════════════════════════════════════════════════════
   Rule: logos are never recoloured, filtered or masked. They sit on a light
   canvas in BOTH themes, because a brand mark's own colours are its identity.
   Where a supplied asset has no usable fill data, we do NOT fake it — we show
   an honest label-led tile carrying the real name and the real sector.
   ═══════════════════════════════════════════════════════════════════════════ */
.logo-frame {
  display: grid; place-items: center; flex: none;
  width: var(--lf, 56px); height: var(--lf, 56px); padding: 8px;
  border: 1px solid var(--os-line); border-radius: var(--os-radius-sm);
  background: #FFFEFA;                       /* constant canvas — never themed */
  overflow: hidden;
}
html[data-theme="dark"] .logo-frame { border-color: rgba(255,255,255,.16); }
.logo-frame img { max-width: 100%; max-height: 100%; object-fit: contain; }
.logo-frame--sm { --lf: 40px; padding: 6px; }
.logo-frame--lg { --lf: 72px; padding: 10px; }
.logo-frame--wide { width: auto; min-width: 84px; height: 44px; padding-inline: 12px; }

.logo-label {                                /* honest fallback tile */
  display: grid; place-items: center; gap: 2px; text-align: center;
  min-width: 84px; max-width: min(220px, 100%); min-height: 56px; height: auto;
  padding: 8px 12px; flex: 0 1 auto;
  border: 1px dashed var(--os-line-strong); border-radius: var(--os-radius-sm);
  background: var(--os-surface-2);
}
.logo-label b {
  font-size: var(--os-step--1); font-weight: 650; color: var(--os-ink);
  line-height: 1.2; overflow-wrap: break-word; min-width: 0; max-width: 100%;
}
.logo-label i {
  font-style: normal; font-family: var(--os-mono); font-size: 9px;
  letter-spacing: .1em; text-transform: uppercase; color: var(--os-muted); line-height: 1.1;
}
.logo-wall { display: flex; flex-wrap: wrap; gap: 10px; }

/* ═══ record / timeline ═══════════════════════════════════════════════════ */
.records { display: grid; gap: 14px; }
.rec-top { display: flex; align-items: flex-start; gap: 14px; }
.rec-main { flex: 1; min-width: 0; }
.rec-title { font-weight: 650; font-size: var(--os-step-1); }
.rec-spec  { color: var(--os-accent); font-weight: 600; font-size: var(--os-step--1); margin-top: 2px; }
.rec-org   { color: var(--os-muted); font-size: var(--os-step--1); margin-top: 3px; }
.rec-date  { flex: none; font-family: var(--os-mono); font-size: 11px; color: var(--os-muted);
             letter-spacing: .05em; white-space: nowrap;
             /* The range and the engagement label are separate flex items, so a
                squeezed row breaks BETWEEN them instead of clipping the label —
                while white-space:nowrap still keeps each token whole. Russian
                («Частичная занятость») needs 315px in a 250px column at 320px;
                English and Persian fit, which is exactly why testing only those
                would have shipped it clipped. */
             display: inline-flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; }
.rec-date .rec-range { white-space: nowrap; }
/* Engagement label (D-33/35/36). Specificity — not source order — beats the
   `.rec-date { font-family: mono }` role rule further down the file, so this
   survives a reorder. It must: the label is a WORD, and IBM Plex Mono carries no
   Persian or Arabic glyphs, so mono would silently fall back to a system face —
   the BH-096 defect. Labels are a UI surface; they take the UI face. */
.rec-date .rec-eng {
  white-space: nowrap; font-family: var(--os-ui); letter-spacing: 0;
  padding: 1px 7px; border: 1px solid var(--os-line);
  border-radius: var(--os-radius-pill); color: var(--os-ink-2);
}
/* The range is data and stays LTR (see the bidi rule below); the label is prose
   and reads in the page's own direction. */
[dir="rtl"] .rec-date .rec-eng { direction: rtl; unicode-bidi: isolate; }
@media (max-width: 640px) {
  .rec-top { flex-wrap: wrap; }
  .rec-date { order: 3; width: 100%; }
  /* When a wide fallback tile (.logo-label) squeezes the text column below a
     readable width, wrap the text column onto its own full-width row instead
     of letting long words spill past the viewport. */
  .rec-main { min-width: min(100%, 220px); }
}
/* BH-029 was filed as "small text / dense". Measured on /experience at 1280px,
   the type is fine — 16px body, 19px lead, one 14px note. The density is in the
   LINE LENGTH: .rec-desc ran to 138 characters, roughly double the 45–75 that
   the eye can track back to the start of the next line without losing its place.
   It was the one prose element on the page with no measure, while its own
   siblings already had one (.sec-head .sec-lead 62ch, .hero .promise 56ch).
   So this is not a new opinion about typography; it applies the site's existing
   rule to the element that was missing it. Inert below ~1000px, where the column
   is narrower than the cap anyway.

   `ch` IS NOT CHARACTERS — measured, not assumed. The CSS `ch` unit is the
   advance width of the "0" glyph, 9.6px in this face, but the average character
   in actual prose here is 7.25px (canvas-measured over a real .rec-desc). So my
   first attempt at 68ch rendered 653px wide and fitted 90 real characters —
   still outside the band, while the number in the stylesheet said 68. 56ch is
   538px ≈ 74 characters, and it reuses a value the system already defines on
   .hero .promise rather than inventing a new one. */
.rec-desc  { margin-top: 14px; color: var(--os-ink-2); max-width: 56ch; }
.rec-areas-h { margin-top: 16px; margin-bottom: 8px; font-family: var(--os-mono); font-size: 10px;
               letter-spacing: .12em; text-transform: uppercase; color: var(--os-muted); }

.clean-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 9px; }
.clean-list li { display: flex; gap: 9px; align-items: flex-start; font-size: var(--os-step-0); }
.clean-list svg { width: 15px; height: 15px; flex: none; margin-top: 5px; color: var(--os-accent); }

/* ═══ evidence disclosure — replaces the hover-only value chain ════════════ */
.evidence { margin-top: 16px; border-top: 1px solid var(--os-line); padding-top: 14px; }
.evidence > summary {
  display: flex; align-items: center; gap: 8px; cursor: pointer; list-style: none;
  font-size: var(--os-step--1); font-weight: 650; color: var(--os-accent);
  padding: 6px 2px; border-radius: var(--os-radius-sm);
}
.evidence > summary::-webkit-details-marker { display: none; }
.evidence > summary .caret { transition: transform var(--os-fast) var(--os-ease); }
.evidence[open] > summary .caret { transform: rotate(90deg); }
html[dir="rtl"] .evidence > summary .caret { transform: scaleX(-1); }
html[dir="rtl"] .evidence[open] > summary .caret { transform: scaleX(-1) rotate(90deg); }
.evidence-body { padding-top: 14px; }

/* ═══ steps (value chain, product journey) — click/keyboard, never hover ══ */
.steps { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(min(100%, 210px), 1fr)); }
.step {
  padding: 16px; border: 1px solid var(--os-line); border-radius: var(--os-radius);
  background: var(--os-surface); border-top: 2px solid var(--os-accent-disp);
}
.step-n { font-family: var(--os-mono); font-size: 11px; color: var(--os-accent); letter-spacing: .1em; }
.step-t { font-weight: 650; margin-top: 6px; }
.step-d { margin-top: 4px; font-size: var(--os-step--1); color: var(--os-muted); }
.step-i { margin-top: 12px; padding-top: 12px; border-top: 1px dashed var(--os-line);
          font-size: var(--os-step--1); color: var(--os-ink-2); }
.step-i b { display: block; font-family: var(--os-mono); font-size: 10px; letter-spacing: .1em;
            text-transform: uppercase; color: var(--os-muted); margin-bottom: 5px; }

/* ═══ notes / disclaimers ═════════════════════════════════════════════════ */
.note-block {
  border-inline-start: 3px solid var(--os-accent-disp);
  padding: 14px 16px;
  /* LOGICAL radius, to match the logical border above (BH-138). The physical
     `border-radius: 0 r r 0` did not flip with direction, so in RTL the accent
     bar moved to the right while the squared corners stayed on the left — the
     flat edge on one side and the bar on the other. These four properties
     follow the writing direction, so the square corners always sit against the
     accent bar. */
  border-start-start-radius: 0;
  border-end-start-radius: 0;
  border-start-end-radius: var(--os-radius-sm);
  border-end-end-radius: var(--os-radius-sm);
  background: var(--os-surface-2); color: var(--os-ink-2); font-size: var(--os-step--1);
}
.note-block b { color: var(--os-ink); }

/* ═══ hand annotation (decorative, aria-hidden, motion-gated) ═════════════ */
.os-note {
  display: inline-flex; align-items: center; gap: 5px;
  font-family: var(--os-hand); font-size: 19px; color: var(--os-accent);
  transform: rotate(-2deg);
}
.os-note svg { width: 40px; height: 18px; }
html[dir="rtl"] .os-note svg { transform: scaleX(-1); }

/* ═══ tables ══════════════════════════════════════════════════════════════ */
.tbl-wrap { overflow-x: auto; border: 1px solid var(--os-line); border-radius: var(--os-radius); }
table.tbl { width: 100%; border-collapse: collapse; font-size: var(--os-step--1); background: var(--os-surface); }
table.tbl th, table.tbl td { padding: 11px 14px; text-align: start; border-bottom: 1px solid var(--os-line); }
table.tbl th { font-family: var(--os-mono); font-size: 10px; letter-spacing: .1em; text-transform: uppercase;
               color: var(--os-muted); background: var(--os-surface-2); }
table.tbl tr:last-child td { border-bottom: 0; }

/* ═══ forms ═══════════════════════════════════════════════════════════════ */
.field { display: grid; gap: 6px; }
.field > label { font-size: var(--os-step--1); font-weight: 600; color: var(--os-ink-2); }
.field .req { color: var(--os-growth-text); margin-inline-start: 3px; }
.field .hint { font-size: var(--os-step--1); color: var(--os-muted); }

.input, .select, .textarea {
  width: 100%; padding: 12px 14px; font: inherit; font-size: var(--os-step-0);
  color: var(--os-ink); background: var(--os-surface);
  border: 1px solid var(--os-line-strong); border-radius: var(--os-radius-sm);
  transition: border-color var(--os-fast) var(--os-ease);
}
.input::placeholder, .textarea::placeholder { color: var(--os-muted); }
.input:hover, .select:hover, .textarea:hover { border-color: var(--os-ink-2); }
.textarea { resize: vertical; min-height: 130px; line-height: 1.6; }
.select {
  appearance: none; padding-inline-end: 38px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23637078' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-size: 17px;
  background-position: right 13px center;
}
html[dir="rtl"] .select { background-position: left 13px center; }
html[data-theme="dark"] .select {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238D99A1' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
}

.check { display: flex; gap: 10px; align-items: flex-start; font-size: var(--os-step--1); color: var(--os-ink-2); }
.check input { margin-top: 3px; width: 17px; height: 17px; accent-color: var(--os-systems-disp); flex: none; }

.field[data-invalid="true"] .input,
.field[data-invalid="true"] .select,
.field[data-invalid="true"] .textarea { border-color: var(--os-growth-text); }
.err { display: none; font-size: var(--os-step--1); color: var(--os-growth-text); }
.field[data-invalid="true"] .err { display: block; }

/* The live region must STAY IN THE ACCESSIBILITY TREE (BH-123). It carried
   role="status" aria-live="polite" correctly, but `display: none` removes the
   element from the tree entirely — a region that only appears at the moment its
   text changes is unreliably announced, and in several screen readers is not
   announced at all. So a blind visitor got no confirmation that their message
   was sent, received or failed.
   Idle state is now visually collapsed but still rendered and still observed;
   only the visible box appears on state change. */
.form-status {
  margin-top: 16px;
  height: 0; overflow: hidden; padding: 0; border: 0; opacity: 0;
}
.form-status[data-state="ok"], .form-status[data-state="err"], .form-status[data-state="busy"] {
  height: auto; overflow: visible; opacity: 1;
  /* Literal values, NOT `revert` (BH-141). The element is `class="form-status
     panel"`, and `revert` rolls a property back to the USER-AGENT stylesheet —
     for a <div> that is no padding and no border. So the box shed its panel
     styling at the exact moment it became visible: the success and error
     messages rendered as bare text with no frame. `revert-layer` would not help
     either; these are plain author rules, not cascade layers. Match .panel. */
  padding: clamp(18px, 2.6vw, 28px); border: 1px solid var(--os-line);
}
.form-status[data-state="ok"]  { --os-accent-disp: var(--os-ops-disp);    --os-accent: var(--os-ops-text); }
.form-status[data-state="err"] { --os-accent-disp: var(--os-growth-disp); --os-accent: var(--os-growth-text); }

/* honeypot — never shown, never announced. No left/right offset: an offset
   extends the scrollable overflow area in RTL documents, so the box is
   collapsed in place instead (direction-safe in both RTL and LTR). */
.hp {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  opacity: 0;
}

/* ═══ section map (desktop rail) ══════════════════════════════════════════
   The old build hid this below 1180px and gave mobile nothing. Here the
   labels are always in the DOM, the rail collapses to a scroll-spy chip row
   on small screens, and nothing depends on :hover.
   ═══════════════════════════════════════════════════════════════════════════ */
.os-sysmap {
  position: fixed; inset-inline-end: 14px; top: 50%; transform: translateY(-50%);
  z-index: 70; display: none; flex-direction: column; gap: 8px;
}
@media (min-width: 1280px) { .os-sysmap { display: flex; } }
.os-sysmap a {
  display: flex; align-items: center; gap: 8px; justify-content: flex-end;
  padding: 5px 7px; border-radius: var(--os-radius-pill); color: var(--os-muted);
}
.os-sysmap .dot {
  width: 8px; height: 8px; flex: none; border-radius: 50%;
  border: 1px solid var(--os-line-strong); background: transparent;
  transition: background var(--os-fast) var(--os-ease), transform var(--os-fast) var(--os-ease);
}
.os-sysmap .lbl {
  font-family: var(--os-mono); font-size: 10px; letter-spacing: .08em;
  text-transform: uppercase; max-width: 0; overflow: hidden; white-space: nowrap;
  text-overflow: ellipsis;  /* long section labels clip gracefully instead of JS slice() */
  opacity: 0; transition: max-width var(--os-slow) var(--os-ease), opacity var(--os-fast) var(--os-ease);
}
.os-sysmap a:hover .lbl,
.os-sysmap a:focus-visible .lbl,
.os-sysmap a[aria-current="true"] .lbl {
  max-width: 230px; opacity: 1;
  /* a surface, because at 1280px the open label reaches into the content
     column — bare text over text was the collision the sweep caught. Styles
     live only in the OPEN state: on the closed 0-width box, border+padding
     would render as a lump beside every dot. */
  padding: 3px 8px; background: var(--os-surface);
  border: 1px solid var(--os-line); border-radius: var(--os-radius-pill);
  box-shadow: 0 2px 10px rgba(23, 32, 38, .08);
}
.os-sysmap a[aria-current="true"] { color: var(--os-accent); }
.os-sysmap a[aria-current="true"] .dot { background: var(--os-accent-disp); transform: scale(1.35); }

/* small-screen equivalent: a real, scrollable, tappable chip rail */
.os-secbar {
  position: sticky; top: var(--os-header-h); z-index: 60;
  display: flex; gap: 7px; padding: 10px clamp(16px, 4vw, 32px);
  overflow-x: auto; scrollbar-width: none;
  border-bottom: 1px solid var(--os-line);
  background: color-mix(in srgb, var(--os-page) 96%, transparent);
  backdrop-filter: blur(10px);
}
.os-secbar::-webkit-scrollbar { display: none; }
.os-secbar {
  -webkit-mask-image: linear-gradient(to right, transparent, #000 18px, #000 calc(100% - 18px), transparent);
  mask-image: linear-gradient(to right, transparent, #000 18px, #000 calc(100% - 18px), transparent);
}
@media (min-width: 1280px) { .os-secbar { display: none; } }
.os-secbar a {
  flex: none; padding: 7px 13px; border-radius: var(--os-radius-pill);
  border: 1px solid var(--os-line); background: var(--os-surface);
  font-size: 12px; font-weight: 600; color: var(--os-ink-2); white-space: nowrap;
}
.os-secbar a[aria-current="true"] {
  color: var(--os-accent); border-color: color-mix(in srgb, var(--os-accent-disp) 46%, transparent);
  background: color-mix(in srgb, var(--os-accent-disp) 10%, transparent);
}

/* ═══ reveal-on-scroll (enhancement only; content is visible without JS) ══ */
.js-on .reveal { opacity: 0; transform: translateY(14px); }
.js-on .reveal.in {
  opacity: 1; transform: none;
  transition: opacity var(--os-slow) var(--os-ease) calc(var(--i, 0) * 55ms),
              transform var(--os-slow) var(--os-ease) calc(var(--i, 0) * 55ms);
}
@media (prefers-reduced-motion: reduce) {
  .js-on .reveal { opacity: 1; transform: none; }
  .nav-drawer[data-open="true"],
  .lang-picker[data-open="true"] .lang-menu,
  .form-status[data-state] { animation: none; }
  .btn, .btn:hover, .btn:active { transform: none; }
  html { scroll-behavior: auto; }
}

/* ═══ V5 motion layer — see docs/MOTION_SYSTEM.md ═══════════════════════════
   All durations/easing come from the bundle motion tokens (--b-fast/default/
   slow/ease). Rules: transform+opacity only, one-shot state changes (no
   loops), reduced-motion neutralized in the block above. ═══════════════════ */
/* press feedback (V5 .b-button spec) */
.btn:active { transform: translateY(1px); }
/* header elevates once real scrolling starts (class set by os.js, rAF) */
.site-header { transition: box-shadow var(--b-default) var(--b-ease); }
.site-header.is-scrolled { box-shadow: 0 1px 0 var(--os-line), 0 10px 28px rgba(23, 32, 38, .07); }
/* one-shot entrances for surfaces that pop into existence */
@keyframes b-pop-in { from { opacity: 0; transform: translateY(-8px); } }
.nav-drawer[data-open="true"] { animation: b-pop-in var(--b-default) var(--b-ease); }
.lang-picker[data-open="true"] .lang-menu { animation: b-pop-in var(--b-fast) var(--b-ease); }
.form-status[data-state="ok"], .form-status[data-state="err"],
.form-status[data-state="busy"] { animation: b-pop-in var(--b-default) var(--b-ease); }
/* cross-document transition: pure progressive enhancement (Chrome 126+),
   a default crossfade between same-origin pages; disabled under reduce */
@view-transition { navigation: auto; }
@media (prefers-reduced-motion: reduce) {
  @view-transition { navigation: none; }
}

/* ═══ V5 brand signature ════════════════════════════════════════════════════
   Bridge signature — origin (gold) → catalyst (blue) → outcome (teal).
   Geometry verbatim from the bundle (components.css); never placed over text
   or photos. The connector uses --os-ink so it adapts to the dark theme
   (bundle has no dark tokens — BH-098 policy). */
.b-bridge { display: inline-flex; align-items: center; gap: 0; direction: ltr; }
.b-bridge i { width: 9px; height: 9px; border-radius: 50%; background: var(--b-bridge-origin); }
.b-bridge u { width: 18px; height: 2px; background: color-mix(in oklab, var(--os-ink) 55%, transparent); }
.b-bridge s { width: 16px; height: 12px; border-radius: 3px; background: var(--b-bridge-catalyst); }
.b-bridge i:last-child { background: var(--b-bridge-outcome); }
/* signature meta line (hero + footer). AA text grade per D-20. */
.u-sig { margin: 0 0 10px; color: var(--b-systems-text); }
.u-sig .b-bridge { margin-inline-start: 10px; vertical-align: middle; }

/* ═══ utilities — replace the site's most repeated inline styles (BH-021).
   Values are identical to the inline declarations they replaced. ═══════════ */
.mt-8  { margin-top: 8px; }
.mt-10 { margin-top: 10px; }
.mt-12 { margin-top: 12px; }
.mt-14 { margin-top: 14px; }
.mt-18 { margin-top: 18px; }
.mt-20 { margin-top: 20px; }
.fs-0 { font-size: var(--os-step-0); }
.fs-1 { font-size: var(--os-step-1); }
.max-38ch { max-width: 38ch; }
.max-56ch { max-width: 56ch; }
.max-64ch { max-width: 64ch; }
.u-row { display: flex; align-items: center; gap: 12px; }
.u-wrap-row { display: flex; flex-wrap: wrap; gap: 10px; }
.u-title-gap,
h1.u-title-gap, h2.u-title-gap { margin: 14px 0 12px; }  /* heading variant outweighs .hero h1 */
.u-flat { border: 0; padding: 0; margin-top: 8px; }
.u-dim-inherit { color: inherit; opacity: .7; }
.u-note { opacity: .72; margin: 0; }
.g-stack { grid-template-columns: 1fr; gap: 22px; }
.fs--1 { font-size: var(--os-step--1); }
.mt-4  { margin-top: 4px; }
.mt-6  { margin-top: 6px; }
.mt-16 { margin-top: 16px; }
.mb-8  { margin-bottom: 8px; }
.mb-14 { margin-bottom: 14px; }
.m-0   { margin: 0; }
.g-1col { grid-template-columns: 1fr; }
.u-row-14 { display: flex; align-items: center; gap: 14px; }
.u-inline-chip { display: inline-flex; gap: 6px; align-items: center; }
.u-center-pad { text-align: center; padding: clamp(32px, 6vw, 64px); }
.mx-auto { margin-inline: auto; }
.u-lede-center { max-width: 52ch; margin: 12px auto 0; }
.u-justify-center { justify-content: center; }
.u-items-start { align-items: start; }

/* ═══ V5 type roles per language ═══════════════════════════════════════════
   Bundle rule (SKILL.md): "Abar Mid only for Persian headings, Ravi for
   Persian body, Peyda for Persian UI, IBM Plex Sans for English, IBM Plex
   Mono for metadata (always LTR)". Arabic shares the Persian script and takes
   the same faces (D-21). The remaining six locales stay on IBM Plex Sans —
   the bundle defines no face for them, and Ravi/Peyda carry no Cyrillic or
   CJK glyphs; zh additionally falls back to the platform CJK face. */
/* ru / zh: no shipped face covers these scripts, so name the fallback (BH-134).
   MEASURED, not assumed — the canvas test that caught BH-096 twice before:
       Latin     in "IBM Plex Sans" 149.1px · in a nonexistent face 139.2px  → differs
       Cyrillic  in "IBM Plex Sans" 249.2px · in a nonexistent face 248.6px  → identical
   Identical width means the request was never honoured: the shipped Plex subset
   is Latin-only and Russian was being drawn by whatever the OS happened to pick.
   (document.fonts.check() reports `true` for Cyrillic here and is simply wrong —
   it answers "is a face available in the stack", not "does that face have the
   glyphs". Width against a deliberately missing face is the reliable probe.)

   Shipping a real Cyrillic/CJK subset is an asset decision for the owner. What
   is fixable here is the ARBITRARINESS: name platform faces that are actually
   designed for these scripts, so the result is deliberate and consistent across
   machines instead of a lottery. Recorded in PERFORMANCE_REPORT for follow-up. */
html[lang="ru"] {
  --os-font: "IBM Plex Sans", "Segoe UI", "Noto Sans", "PT Sans", Roboto,
             system-ui, sans-serif;
  --os-display: var(--os-font);
  --os-ui: var(--os-font);
}
html[lang="zh"] {
  --os-font: "IBM Plex Sans", "PingFang SC", "Hiragino Sans GB",
             "Microsoft YaHei", "Noto Sans SC", system-ui, sans-serif;
  --os-display: var(--os-font);
  --os-ui: var(--os-font);
}

html[lang="fa"], html[lang="ar"] {
  --os-font:    var(--b-font-body);   /* Ravi — body/lead */
  --os-display: var(--b-font-display);/* Abar Mid — headings ONLY */
  --os-ui:      var(--b-font-ui);     /* Peyda — labels/buttons/forms */
  line-height: var(--b-leading-body-fa);
}
html[lang="zh"] {
  --os-font: var(--b-font-en), "PingFang SC", "Hiragino Sans GB",
             "Microsoft YaHei", "Noto Sans CJK SC", sans-serif;
  --os-display: var(--os-font);
}
:root { --os-ui: var(--b-font-en); }

/* Persian/Arabic UI surfaces use Peyda, per the role table. */
html[lang="fa"] :is(.btn, .nav-links a, .drawer-list a, .chip, label, .lang-btn, .input, .select, .textarea, .footer-list a, .os-secbar a, .eyebrow),
html[lang="ar"] :is(.btn, .nav-links a, .drawer-list a, .chip, label, .lang-btn, .input, .select, .textarea, .footer-list a, .os-secbar a, .eyebrow) {
  font-family: var(--os-ui);
}
/* Metadata/dates/numbers stay IBM Plex Mono and always read LTR. */
.mono, .metric-value, .rec-date, .sec-no, .b-meta {
  font-family: var(--b-font-mono);
}
[dir="rtl"] :is(.mono, .metric-value, .rec-date, .sec-no) {
  direction: ltr; unicode-bidi: isolate;
}
/* The `.mono-prose` opt-out lives at the END of this file, after the LAST
   `.mono` rule — see the note there. Placing it here silently lost the cascade. */
/* V5 display scale — available to any surface that opts in. */
.b-display-xl, .b-display, .b-heading, .b-heading-sm {
  font-family: var(--os-display); margin: 0; text-wrap: balance;
}
.b-display-xl { font-size: var(--b-size-display-xl); font-weight: 900; line-height: var(--b-leading-display); }
.b-display    { font-size: var(--b-size-display);    font-weight: 800; line-height: 1.08; }
.b-heading    { font-size: var(--b-size-heading);    font-weight: 700; line-height: var(--b-leading-heading); }
.b-heading-sm { font-size: var(--b-size-heading-sm); font-weight: 600; line-height: 1.35; }
[dir="ltr"] :is(.b-display-xl, .b-display) { font-weight: 700; }
.b-meta {
  font-size: var(--b-size-meta); letter-spacing: var(--b-tracking-meta);
  text-transform: uppercase; direction: ltr; unicode-bidi: isolate;
}

html {
  scroll-behavior: smooth;
  /* Clear the header AND the sticky section bar underneath it. --os-secbar-h is
     published by os.js §5, which is the only thing that knows the bar's real
     height (it builds it, and it wraps). Falls back to 0 when there is no bar —
     no-JS, or >=1280px where CSS hides it. Without this, every anchor jump and
     every keyboard focus target landed underneath the bar (BH-130). */
  scroll-padding-top: calc(var(--os-header-h) + var(--os-secbar-h, 0px) + 16px);
  background: var(--os-page);
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  min-width: 320px;                 /* full responsiveness down to 320px */
  color: var(--os-ink);
  background: var(--os-sky), var(--os-page);
  font-family: var(--os-font);
  font-size: var(--os-step-0);
  line-height: 1.62;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  /* overflow-x:hidden removed (2026-07-15): it was masking layout bugs. All
     root causes are fixed and the 109-page × multi-viewport matrix measures
     zero horizontal overflow WITHOUT the clip. Do not re-add as a "fix". */
}

img, svg, video { max-width: 100%; }
img { height: auto; }
a  { color: inherit; text-decoration: none; }
b, strong { font-weight: 650; }

h1, h2, h3, h4 {
  font-family: var(--os-display);
  font-weight: 640;
  line-height: 1.16;
  letter-spacing: -.014em;
  margin: 0;
  text-wrap: balance;
}
h1 { font-size: var(--os-step-5); }
h2 { font-size: var(--os-step-3); }
h3 { font-size: var(--os-step-1); }
p  { margin: 0 0 1em; text-wrap: pretty; }
p:last-child { margin-bottom: 0; }

::selection { color: var(--os-surface); background: var(--os-systems-text); }

:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  /* A TRANSPARENT outline, not `none` (BH-121). Windows High Contrast strips
     box-shadow entirely, so `outline: none` + a shadow leaves keyboard users
     with NO focus indicator at all — WCAG 2.4.7, and unusable rather than ugly.
     A transparent outline is invisible in normal rendering but is forced to a
     system colour in forced-colors mode, so both worlds get an indicator. */
  outline: 2px solid transparent;
  outline-offset: 2px;
  box-shadow: var(--os-focus);
  /* border-radius deliberately NOT set here. It used to be --os-radius-sm,
     which squared off every pill-shaped control the moment it took focus:
     buttons visibly changed shape on keyboard navigation (BH-122). The focus
     ring follows the element's own radius instead. */
}
@media (forced-colors: active) {
  :where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
    outline-color: Highlight;
  }
}

/* ═══ layout primitives ═══════════════════════════════════════════════════ */
.wrap    { width: min(100% - 2 * clamp(16px, 4vw, 32px), var(--os-container)); margin-inline: auto; }
.wrap-n  { width: min(100% - 2 * clamp(16px, 4vw, 32px), var(--os-narrow));    margin-inline: auto; }
.block   { padding-block: var(--os-block); }
.stack > * + * { margin-top: var(--os-gap); }
.grid    { display: grid; gap: var(--os-gap); }
.g-2 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr)); }
.g-3 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr)); }
.g-4 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 190px), 1fr)); }

.mono  { font-family: var(--os-mono); font-size: var(--os-step--1); letter-spacing: .04em; }
.muted { color: var(--os-muted); }
.lead  { font-size: var(--os-step-1); color: var(--os-ink-2); line-height: 1.58; }

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

.skip-link {
  position: fixed; inset-inline-start: 16px; top: 0; z-index: 200;
  transform: translateY(-240%); opacity: 0; pointer-events: none;
  padding: 10px 16px; border-radius: var(--os-radius-pill);
  color: var(--os-on-charcoal); background: var(--os-charcoal); font-weight: 700;
  transition: transform var(--os-fast) var(--os-ease);
}
.skip-link:focus { transform: translateY(14px); opacity: 1; pointer-events: auto; }

/* ═══ RTL ═════════════════════════════════════════════════════════════════ */
/* Persian/Arabic faces come from the V5 role block at the end of this file.
   (This rule used to name "Vazirmatn", which was never loaded by any
   @font-face or <link> — RTL text silently fell back to a system face, the
   same class of bug the note above records for Fraunces. Fixed by shipping
   the bundle's real faces: Abar Mid / Ravi / Peyda.) */
html[dir="rtl"] body { letter-spacing: 0; }
html[dir="rtl"] :is(h1, h2, h3, h4) { letter-spacing: 0; }
html[dir="rtl"] .mono { direction: ltr; unicode-bidi: embed; }

/* ── .mono-prose — RTL sentences that were wearing metadata styling (BH-115) ──
   `.mono` was carrying full Persian/Arabic SENTENCES, not just metadata: 46 of
   them. The design system reserves IBM Plex Mono for "metadata, always LTR", and
   the LTR half is the real harm — a Persian sentence with direction:ltr and
   text-align:start aligns LEFT on an RTL page and puts its final «.» on the wrong
   side. Measured on /fa/contact.html before the fix: two sentences of 10 and 8
   words, both ending in a period, both computing direction:ltr.

   POSITION IS LOad-BEARING. This file defines `.mono` THREE times — line ~903,
   again at ~995, and the RTL rule just above. The first version of this opt-out
   sat before all of them and was completely inert: `html[dir="rtl"] .mono` is
   (0,2,1) and later, so it won on both specificity and order, and the measured
   direction stayed `ltr` even though the class was applied and the CSS loaded.
   That is cascade lesson D-16 for the third time in this codebase: a rule that
   overrides a component must come AFTER the component's last definition.

   Genuine metadata («تهران · Asia/Tehran (UTC+3:30)», «فین‌تک · … · 2024 — 2025»)
   keeps mono+LTR, which is correct for it. Classified by scripts/mono_prose.py
   structurally — a date ⇒ label run, sentence punctuation ⇒ prose — never by word
   count, which measures the language rather than the content and mis-sorted fa
   against ar (26 vs 30) when I first tried it. */
.mono-prose { font-family: var(--os-font); letter-spacing: 0; }
html[dir="rtl"] .mono-prose {
  direction: rtl; unicode-bidi: isolate; text-align: start;
}
/* Latin brand names and metrics stay LTR inside RTL prose. */
html[dir="rtl"] .ltr { direction: ltr; unicode-bidi: isolate; display: inline-block; }

/* ═══ motion ══════════════════════════════════════════════════════════════ */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
}

/* ═══ print ═══════════════════════════════════════════════════════════════ */
@media print {
  :root { --os-page: #fff; --os-surface: #fff; --os-ink: #000; --os-muted: #444; }
  body { background: #fff; }
  /* reveal-on-scroll must never hide content on paper: unscrolled sections
     would otherwise print as blank space (BH-007) */
  .js-on .reveal { opacity: 1 !important; transform: none !important; }
  .site-header, .site-footer, .os-sysmap, .os-note, .theme-toggle, .os-secbar,
  .lang-picker, .nav-drawer, .skip-link, .scroll-progress { display: none !important; }
  .panel { border: 1px solid #bbb; box-shadow: none; break-inside: avoid; }
  a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 10px; color: #555; }
  .block { padding-block: 18px; }
  /* the letterhead exists only on paper: on screen the header names the
     person, but the header is chrome and chrome does not print — which left
     the Leadership Portfolio PDF a portfolio document that never says whose
     it is. Text comes from each page's own <title>. */
  .print-letterhead {
    display: block; font-weight: 650; font-size: 12px;
    letter-spacing: .04em; text-transform: uppercase;
    border-bottom: 1px solid #bbb; padding-bottom: 6px; margin: 0 0 18px;
  }
  /* a printed page that says "Download as PDF" is a PDF advertising itself
     (BH-018 — the regenerated downloads are printed FROM these pages). Links
     that leave the page keep printing, with their URL, which is useful on
     paper; links to the very artifact being produced do not. */
  a[href$=".pdf"] { display: none !important; }
}

/* ═══ consent banner (BH-039, EU locales only) ═══════════════════════════════
   Bottom sheet, not a full-screen interstitial: the page stays readable and
   usable behind it. That is deliberate — a modal that blocks the content until
   you accept is the coercion pattern the brief forbids, and it also makes
   "reject" feel costly. Both buttons are the same size for the same reason.  */
.consent {
  position: fixed; inset-inline: 0; bottom: 0; z-index: 60;
  display: flex; flex-wrap: wrap; align-items: center; gap: 14px 20px;
  padding: 16px clamp(16px, 4vw, 40px);
  padding-bottom: max(16px, env(safe-area-inset-bottom));
  background: var(--os-surface); border-top: 1px solid var(--os-line);
  box-shadow: var(--b-shadow-modal);
}
.consent[hidden] { display: none; }
.consent-text {
  flex: 1 1 22rem; min-width: 0;          /* min-width:0 or long words push the
                                             buttons off-canvas in de/pt */
  margin: 0; font-size: var(--os-step--1); color: var(--os-ink-2);
  text-wrap: pretty;
}
/* GRID, not flex. With flex the two buttons measured 175px vs 143px even at
   flex:1 1 0 / min-width:0 — equal-share flex was not producing equal boxes
   here. `1fr 1fr` cannot: the columns are equal by definition, which is what
   'refusing is as easy as accepting' has to mean in pixels.               */
.consent-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; flex: 0 0 auto; }
/* EQUAL PROMINENCE, enforced here rather than trusted to the button variants.
   Accept shipped as .btn-primary (filled) and reject as .btn-quiet (subdued),
   and they measured 175px vs 143px at 360px wide. A refuse button that is
   quieter and smaller than accept is the nudge pattern EU regulators single out
   and the brief forbids outright. These two rules make the pair identical in
   size and weight; only the label differs. Overrides the variant classes on
   purpose — do not "simplify" by deleting them.                              */
.consent-actions .btn {
  flex: 1 1 0; min-width: 9rem; justify-content: center;
  background: var(--os-surface-2); color: var(--os-ink);
  border: 1px solid var(--os-line); box-shadow: none; font-weight: 600;
}
.consent-actions .btn:hover { background: var(--os-surface); border-color: var(--os-ink-2); }

@media (max-width: 560px) {
  .consent { gap: 12px; }
  .consent-actions { width: 100%; }
  .consent-actions .btn { min-width: 0; }
}
@media (prefers-reduced-motion: no-preference) {
  .consent { animation: b-consent-in .32s cubic-bezier(.22,.61,.36,1) both; }
  @keyframes b-consent-in { from { transform: translateY(100%); } to { transform: none; } }
}

/* ═══════════════════════════════════════════════════════════════════════════
   CINEMATIC MOTION LAYER (owner decision 2026-07-18: "bold and cinematic")
   ═══════════════════════════════════════════════════════════════════════════
   Scroll-driven animations (`animation-timeline: view()`): compositor-run, no
   JS, no scroll listener, and progressive — engines without support simply get
   the static page.

   ONE ABSOLUTE RULE, paid for in this file: THIS LAYER NEVER TOUCHES `.reveal`.
   `.reveal` carries every panel of real content on the site. The first version
   put the entry animation on it, and measurement showed the panels computing
   `opacity: 0` even after `.in` was applied and even with `opacity: 1` written
   at both ends of the keyframes. I could not finish diagnosing it, because
   scroll-driven animations only advance at frame boundaries and this preview
   pane freezes rAF — every attempt to observe the timeline timed out.

   An enhancement whose failure mode is "the entire page is blank" and which I
   cannot verify does not ship. So the layer now animates only elements whose
   worst case is "did not move": hero layers, section numerals and titles, the
   bridge mark. Content visibility stays entirely with the IntersectionObserver
   reveal, which is proven, has a no-JS fallback, and is untouched below.

   Only `transform`/`opacity` are animated; nothing that forces reflow.        */

@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {

    /* ── 1 · hero parallax: two speeds against one scroll = depth ──────────
       Both layers start fully opaque and only drift as the hero LEAVES, so a
       stalled timeline leaves the hero exactly as it renders today.          */
    /* `.hero-copy`/`.hero-visual` do not exist — I wrote them from memory and the
       rules matched nothing. The real hero is `.hero-grid` with a `.portrait`
       inside it. Verified against the markup rather than assumed.            */
    .hero-grid > div:first-child { animation: b-drift-up linear both;
                   animation-timeline: view(); animation-range: exit 0% exit 100%; }
    .hero-grid > div:last-child {
                   animation: b-drift-down linear both;
                   animation-timeline: view(); animation-range: exit 0% exit 100%; }
    @keyframes b-drift-up   { from { transform: none; opacity: 1; }
                              to   { transform: translateY(-46px); opacity: .55; } }
    @keyframes b-drift-down { from { transform: none; }
                              to   { transform: translateY(22px) scale(1.03); } }

    /* ── 2 · section numerals track the scroll ─────────────────────────────
       Decorative index digits (aria-hidden in the markup). Opacity is pinned
       at 1 in both keyframes: this may move, it may never move, but it can
       never disappear.                                                       */
    .sec-no {
      animation: b-numeral linear both; animation-timeline: view();
      animation-range: entry 0% cover 45%;
    }
    @keyframes b-numeral { from { opacity: 1; transform: translateY(16px) scale(.82); }
                           to   { opacity: 1; transform: none; } }

    /* ── 3 · headings settle from a slight offset ──────────────────────────── */
    .sec-title {
      animation: b-title-in linear both; animation-timeline: view();
      animation-range: entry 0% cover 34%;
    }
    @keyframes b-title-in { from { opacity: 1; transform: translateY(24px); }
                            to   { opacity: 1; transform: none; } }

    /* ── 4 · the bridge signature draws itself in ──────────────────────────── */
    .b-bridge {
      animation: b-bridge-draw linear both; animation-timeline: view();
      animation-range: entry 0% cover 50%;
    }
    @keyframes b-bridge-draw { from { opacity: 1; transform: scaleX(.45); }
                               to   { opacity: 1; transform: none; } }

    /* ── 5 · the scroll-progress bar, driven by the document itself ─────────
       Replaces nothing: os.js still drives it for engines without support.
       `scroll()` is exact and free; the JS path stays as the fallback.       */
    .scroll-progress {
      animation: b-progress linear both; animation-timeline: scroll(root block);
    }
    @keyframes b-progress { from { transform: scaleX(0); } to { transform: scaleX(1); } }

    /* ── 6 · panels rise as they arrive ─────────────────────────────────────
       RESTORED after being reverted blind. The note below is kept because the
       caution was right at the time — but a real headless Chromium settled it,
       and the answer was that it is safe.

       A/B measured on /index, 15 scroll positions, identical runs with and
       without this rule: reveal 13/13 both times, and the in-view opacity
       violations were IDENTICAL (24 → 6 → 0 as the probe's own settle time and
       viewport threshold were corrected). This rule adds exactly nothing to the
       risk. TRANSFORM ONLY, 18px: the reveal keeps its own opacity, so the worst
       frozen frame is "sits 18px lower", never "invisible".

       What actually cost me the earlier revert was that the preview pane freezes
       rAF, so `.reveal` never gained `.in` there and every panel read as
       opacity 0. In a live browser the observer works — 13/13 and 15/15. The
       tooling was broken, not the CSS.                                        */
    .panel {
      animation: b-rise linear both; animation-timeline: view();
      animation-range: entry 0% entry 90%;
    }
    @keyframes b-rise { from { transform: translateY(18px); } to { transform: none; } }

    /* ── 6b · the original caution, kept as the record ───────────────────────
       I wrote one — transform-only, 18px, opacity untouched — and my own safety
       probe rejected it. Measured on /index: all 10 `.panel` elements are also
       `.reveal` (10/10), which is precisely the class this layer's contract
       says it must never touch, for the reason recorded above.

       Two concrete hazards, neither of which I can observe here because this
       pane freezes rAF at 0 frames/1200ms:
         · a CSS animation beats a CSS transition, so `b-rise` would seize the
           transform that `.reveal.in` transitions — the reveal would then be
           driven by scroll position rather than by its own observer;
         · that is the same coupling that produced the earlier incident where
           panels computed opacity:0 with `.in` applied and no way to see why.

       An enhancement I cannot verify, on the one class carrying every panel of
       real content, is not worth 18px of drift. The IntersectionObserver reveal
       already gives these elements their entrance, has a no-JS fallback, and is
       proven.                                                                */

    /* ── 7 · the logo wall drifts, giving the page a second plane ───────────
       Decorative marks. A frozen frame leaves them a few pixels off centre,
       which reads as intentional rather than broken.                        */
    .logo-wall img {
      animation: b-float linear both; animation-timeline: view();
      animation-range: entry 0% exit 100%;
    }
    @keyframes b-float { from { transform: translateY(10px); }
                         to   { transform: translateY(-10px); } }

    /* ── 8 · the portrait holds its own plane against the hero copy ─────────
       Paired with the hero drift in §1: two speeds against one scroll is what
       actually reads as depth, rather than any single element moving.

       The class is `.portrait-frame`. I first wrote `.portrait`, which does not
       exist — the same from-memory selector mistake this file already records
       against `.hero-copy`/`.hero-visual`, made again in the same session. The
       safety probe caught it as "NO MATCH"; grep alone would not have, because
       `\bportrait\b` also matches portrait-card/-meta/-tag. Verified against the
       markup: .portrait-card > .portrait-frame > img.

       The image is animated inside its frame, so the frame keeps its layout box
       and nothing around it can shift. */
    /* ── 8 · REMOVED: no second animation inside the portrait ───────────────
       I wrote one, and a real headless browser killed it twice over.

       With `animation-range: entry 0% exit 100%` it sat at progress 0.49995 at
       EVERY scroll position, transform frozen — an element already on screen at
       load has no "entry" phase, so a range opening on `entry` never resolves
       and the animation parks at its midpoint. Switched to `exit 0% exit 100%`
       and it pinned at progress 0.000 instead. Throughout, it reported as a live
       CSSAnimation with a view() timeline: PRESENCE IS NOT MOTION, and every
       structural check I had before Playwright would have passed it.

       Then the measurement made the right answer obvious. `.hero-grid >
       div:last-child` IS the portrait's own column, and §1 already drifts it —
       46px, verified. A second animation on a child inside it is two motions
       stacked on one region of the page, which reads as noise rather than
       depth. The portrait already has its plane. */

    /* ── 9 · section eyebrow leads its heading in ──────────────────────────
       Arriving slightly ahead of `.sec-title` (§3) is what gives a section a
       sense of sequence instead of everything landing at once.              */
    .sec-head .eyebrow {
      animation: b-eyebrow linear both; animation-timeline: view();
      animation-range: entry 0% cover 22%;
    }
    /* The offset comes from a CUSTOM PROPERTY, not from a second animation-name.
       An `[dir="rtl"] …` override of animation-name lost to the `animation`
       shorthand above — measured: the element matched the RTL selector and the
       computed animation-name was still `b-eyebrow`. A custom property is
       substituted per element at use time, so it simply cannot lose that fight,
       and one keyframe serves both directions. */
    @keyframes b-eyebrow { from { opacity: 1; transform: translateX(var(--b-lead, -10px)); }
                           to   { opacity: 1; transform: none; } }
  }

  /* ── 10 · REMOVED: sticky record headers (BH-144) ───────────────────────
     `.rec-top { position: sticky; top: 0; z-index: 2 }` was "chapter feel, zero
     animation cost". Measured against the rest of the file, it was three things
     wrong at once: `.rec-top` (os.css:586) declares NO background, `top: 0`
     ignores the 66px sticky header entirely, and z-index 2 loses to the header's
     80 (os.css:207). So each role header pinned itself to the very top of the
     viewport, slid underneath the site header, and let the page text scroll
     straight through it — transparent text over text.

     I could give it a background, offset it by
     `calc(var(--os-header-h) + var(--os-secbar-h, 0px))` and lift its z-index —
     but this pane cannot show me the result, and the failure mode is unreadable
     overlapping text on the page that carries the whole work history. Decoration
     I cannot verify does not ship. Same call as the `.panel` entry in §6.       */
}

/* §9 slides the eyebrow in from the READING edge, so the offset has to flip with
   the writing mode — a physical translateX would have it entering from the wrong
   side in fa/ar. That looks merely "off" rather than broken, which is exactly why
   it ships unnoticed. One custom property, no cascade fight, no second keyframe.
   Unconditional on purpose: a plain custom property is inert unless the animation
   above is actually running. */
[dir="rtl"] { --b-lead: 10px; }

/* Reduced motion and print land on the same honest baseline: everything
   visible, nothing moving. `.reveal` is absent here because this layer never
   touched it — its own reduced-motion rule further up still applies.         */
/* These two blocks used to name `.hero-copy, .hero-visual` — selectors the
   comment above already records as never having existed. So the reset listed
   two dead names and MISSED every real one. It was harmless only because the
   whole layer sits inside `prefers-reduced-motion: no-preference` and therefore
   never applies under `reduce` in the first place. A safety net that catches
   nothing is worth less than no net, because it stops you looking. Now it names
   what is actually animated. */
@media (prefers-reduced-motion: reduce) {
  .hero-grid > div, .sec-no, .sec-title, .sec-head .eyebrow,
  .b-bridge, .logo-wall img,
  /* cinematic layer v2 (below): the load choreography, the section sequence's
     last two beats, the footer rise, and both pseudo layers */
  .hero .b-meta, .hero .status-pill, .hero h1, .hero .role, .hero .lead,
  .hero .hero-actions, .hero .portrait-card,
  .sec-lead, .sec-head .sec-more, .site-footer,
  .hero::before, .site-header::after {
    animation: none !important; opacity: 1 !important; transform: none !important;
  }
  /* the glow's resting opacity is its full strength; under reduce it should
     simply be absent, not frozen at full */
  .hero::before, .site-header::after { content: none !important; }
  .btn .icon.dir { transition: none !important; translate: none !important; }
  /* `.scroll-progress` is deliberately NOT in that list (BH-140). Its resting
     state is `transform: scaleX(0)` — an empty bar. `transform: none !important`
     resolves to scaleX(1), so the reset painted a FULL progress bar pinned
     across the top of every page, permanently, for exactly the users who asked
     for less motion. Killing its animation is right; killing its transform
     inverts it. os.js still drives the bar under reduced motion, which is fine:
     a progress indicator that tracks position is information, not motion. */
  .scroll-progress { animation: none !important; }
}
@media print {
  .hero-grid > div, .sec-no, .sec-title, .sec-head .eyebrow,
  .b-bridge, .logo-wall img,
  /* cinematic v2 — CRITICAL for the load choreography: print CAN match
     `prefers-reduced-motion: no-preference`, and a time-based animation
     rasterised at t=0 would print the hero at opacity 0. Every element that
     ever leaves opacity 1 must be pinned here. */
  .hero .b-meta, .hero .status-pill, .hero h1, .hero .role, .hero .lead,
  .hero .hero-actions, .hero .portrait-card,
  .sec-lead, .sec-head .sec-more, .site-footer {
    animation: none !important; opacity: 1 !important; transform: none !important;
  }
  .hero::before, .site-header::after { content: none !important; }
}


/* ═══ theme toggle icon swap (BH-119) ════════════════════════════════════════
   The button ships both glyphs — <svg class="icon ico-light"> (sun) and
   <svg class="icon ico-dark"> (moon) — and the rules meant to swap them were
   never written: ".ico-light" and ".ico-dark" appeared ZERO times in this
   file. Measured on /index: both painted, 18x18, opacity 1, display block, on
   all 109 pages since launch. The icon shows the CURRENT theme (sun while
   light) and the aria-label describes the ACTION ("Switch to dark theme") -
   state and action, not two competing states.                              */
.ico-dark  { display: none; }                       /* light theme: offer the moon... */
html[data-theme="dark"] .ico-light { display: none; }
html[data-theme="dark"] .ico-dark  { display: block; }


/* ═══ Arabic-script typography (BH-120) ══════════════════════════════════════
   Persian and Arabic are CURSIVE: letters join. Positive letter-spacing pulls
   those joins apart, which is not a stylistic softening the way it is in Latin
   - it damages the word shape and, in the worst cases, reads as broken text.
   The tracking on .eyebrow, .lbl, .k, .footer-h, .portrait-tag and friends was
   authored for Latin small-caps labels, where wide tracking is correct.

   Measured on /fa/index before the fix: 17 Persian text nodes carrying up to
   1.2px of tracking.

   Scoped by LANGUAGE, not by component, so it holds for any label added later.
   text-transform: none rides along for the same reason: Arabic script has no
   letter case, so uppercase() is a no-op that only signals the rule was written
   for another writing system.                                                */
html[lang="fa"] *, html[lang="ar"] * { letter-spacing: normal; }
/* Latin runs inside a Persian page - dates, codes, ASCII metadata - keep the
   tracking they were designed with. */
html[lang="fa"] .ltr, html[lang="ar"] .ltr,
html[lang="fa"] .code, html[lang="ar"] .code,
html[lang="fa"] .rec-range, html[lang="ar"] .rec-range { letter-spacing: .04em; }
/* The blanket rule above is (0,1,1) and loses to component rules like
   `.os-sysmap .lbl` (0,2,0) and `.portrait-meta .k` — measured: 6 of the 17
   nodes kept their tracking. Naming the label components lifts this to (0,2,1),
   which wins. Specificity, not source order, is what decides here. */
html[lang="fa"] :is(.eyebrow, .lbl, .k, .footer-h, .sec-no, .portrait-tag),
html[lang="ar"] :is(.eyebrow, .lbl, .k, .footer-h, .sec-no, .portrait-tag) {
  letter-spacing: normal; text-transform: none;
}


/* ═══ portfolio: flagship weight (BH-070) ═══════════════════════════════════
   Thirteen projects at identical weight tell a reader nothing about which
   matter. WHICH ones are flagship is a claim about someone career, so it is not
   mine to invent — and it did not have to be: Home already links six of the
   thirteen from #selected-work, in all 9 locales. That is the owner editorial
   choice, already shipped. scripts/portfolio_weight.py reads Home and marks
   exactly those, so the emphasis follows Home rather than a ranking of mine.

   The treatment is restraint, not decoration: a slightly stronger surface and a
   defined accent edge. The other seven are not diminished — they keep their
   full card, their content and their link. Nothing is hidden from anyone. */
[data-flagship="true"] {
  border-color: color-mix(in srgb, var(--os-accent-disp) 45%, var(--os-line));
  box-shadow: var(--os-shadow-soft), 0 0 0 1px color-mix(in srgb, var(--os-accent-disp) 18%, transparent);
}
[data-flagship="true"] .rec-title { font-size: var(--os-step-2); }

/* ═══ portfolio filter (BH-080) ═════════════════════════════════════════════
   Built by os.js from the page own group headings, so every label is already
   localized and no new copy enters any build. Progressive: with no JS the bar
   never appears and all thirteen projects stay visible, which is also what a
   crawler sees. */
.pf-filter { display: flex; flex-wrap: wrap; gap: 8px; margin-block: 18px 6px; }
.pf-filter button {
  padding: 7px 14px; border-radius: var(--os-radius-pill);
  border: 1px solid var(--os-line); background: var(--os-surface);
  color: var(--os-ink-2); font-size: var(--os-step--1); font-family: var(--os-ui);
  cursor: pointer; transition: border-color var(--os-fast) var(--os-ease),
                              color var(--os-fast) var(--os-ease);
}
.pf-filter button:hover { border-color: var(--os-accent-disp); color: var(--os-ink); }
.pf-filter button[aria-pressed="true"] {
  border-color: var(--os-accent-disp); color: var(--os-accent);
  background: color-mix(in srgb, var(--os-accent-disp) 10%, transparent);
}

/* ═══ BH-150 · a signpost section is not a truncated one ══════════════════════
   Three sections — #track-record (index), #evidence (executive-brief) and
   #numbers (leadership-portfolio), in all 9 locales, so 27 pages — consist of
   nothing but their <div class="sec-head">. They are deliberate signposts: the
   figures live on the Experience page next to the role that produced them, and
   these sections say so and link there. That is honest, and it stays.

   What was wrong was the rendering. `.sec-head` carries margin-bottom to
   separate itself from a body, and `.block` carries the full section rhythm
   above and below. With no body, a 121px header rendered inside a 332px
   section: 211px of reserved-but-empty space, which reads as content that
   failed to load rather than as a signpost. Measured, not guessed —
   89.6 + 121 + 32 + 89.6 = 332, the box model explains all of it.

   Both selectors are structural. They find a header-only section by its shape,
   so they apply in every locale and to any such section added later, with
   nothing to hand-tag and nothing to keep in sync.

   The padding rule is scoped to `screen` on purpose: @media print already sets
   `.block { padding-block: 18px }` at (0,1,0), and this rule is (0,2,0), so
   unscoped it would outrank print at any source position and re-inflate the
   printed page. The multiplier keeps the clamp — no hardcoded viewport value. */
.sec-head:last-child { margin-bottom: 0; }
@media screen {
  .block:has(> .sec-head:only-child) { padding-block: calc(var(--os-block) * 0.62); }
}

/* ═══════════════════════════════════════════════════════════════════════════
   CINEMATIC LAYER v2 (owner 2026-07-18: «میزان انیمیشن‌ها و افکت‌های ویژوال و
   سینمایی و هنری رو بیشتر کن» — more animation, more visual and artistic
   effect, full authority granted)
   ═══════════════════════════════════════════════════════════════════════════
   Same contract as the layer above, learned the hard way there:
     · transform/opacity only — nothing that forces reflow;
     · scroll-driven pieces pin opacity 1 at both keyframe ends, so a stalled
       timeline leaves content displaced, never invisible;
     · the ONE place opacity animates from 0 — the load choreography — is
       time-based, not scroll-based: any engine that passes the @supports gate
       runs time-based CSS animation to completion, and `both` fill parks it
       at opacity 1. The rAF freeze that killed the first reveal experiment
       does not apply: CSS animation time is not requestAnimationFrame.
     · every selector added here is also added to the reduced-motion and print
       neutralizers below — this file already records why a stale reset list
       is worse than none.

   The load choreography deliberately targets `.hero` CHILDREN by their own
   class names, none of which carry `.reveal` (verified in the markup: the
   reveal panels start after the hero). The absolute rule of the layer above —
   never touch `.reveal` — stands untouched.                                  */

@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {

    /* ── v2·1 · arrival: the page composes itself ──────────────────────────
       One keyframe, staggered per element. Every template opens with
       `section.hero`; Home and the brief add role/status/actions/portrait, the
       other ten templates have h1 + lead and get a two-beat version of the
       same choreography for free. Total runtime ≤ 1s, so it is over before a
       reader has begun reading — arrival, not spectacle.                    */
    .hero .b-meta       { animation: b-arrive .6s var(--os-ease) both; }
    .hero .status-pill  { animation: b-arrive .6s var(--os-ease) .07s both; }
    .hero h1            { animation: b-arrive .64s var(--os-ease) .14s both; }
    .hero .role         { animation: b-arrive .6s var(--os-ease) .21s both; }
    .hero .lead         { animation: b-arrive .6s var(--os-ease) .28s both; }
    .hero .hero-actions { animation: b-arrive .6s var(--os-ease) .36s both; }
    .hero .portrait-card{ animation: b-arrive .72s var(--os-ease) .18s both; }
    @keyframes b-arrive { from { opacity: 0; transform: translateY(14px); }
                          to   { opacity: 1; transform: none; } }

    /* ── v2·2 · the section sequence gains its last two beats ──────────────
       eyebrow (§9) → title (§3) → now the lead paragraph, then the side link.
       Ranges overlap deliberately: a cascade, not a queue.                  */
    .sec-lead {
      animation: b-lead-in linear both; animation-timeline: view();
      animation-range: entry 0% cover 42%;
    }
    @keyframes b-lead-in { from { opacity: 1; transform: translateY(14px); }
                           to   { opacity: 1; transform: none; } }
    /* The side link enters from ITS edge — the far one — so it uses the
       eyebrow's logical offset negated, and flips with the writing mode the
       same way §9 does: one custom property, no second keyframe.            */
    .sec-head .sec-more {
      animation: b-more-in linear both; animation-timeline: view();
      animation-range: entry 0% cover 30%;
    }
    @keyframes b-more-in { from { opacity: 1;
                                  transform: translateX(calc(-1 * var(--b-lead, -10px))); }
                           to   { opacity: 1; transform: none; } }

    /* ── v2·3 · the header earns its shadow ────────────────────────────────
       At rest the header is flush with the page; the moment content actually
       passes beneath it, a shadow fades in over the first 180px of scroll.
       Depth appears exactly when there is something to be above. Driven by
       `scroll()`, so it costs no JS and no listener. The shadow lives on a
       pseudo-element because box-shadow itself cannot animate cheaply —
       opacity can. z-index -1 keeps it behind the header's own translucent
       background; the projected part below the border is the visible shadow. */
    .site-header::after {
      content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none;
      box-shadow: 0 12px 28px -14px color-mix(in srgb, var(--os-ink) 38%, transparent);
      animation: b-header-veil linear both; animation-timeline: scroll(root block);
      animation-range: 0px 180px;
    }
    @keyframes b-header-veil { from { opacity: 0; } to { opacity: 1; } }

    /* ── v2·4 · the footer rises to meet the reader ────────────────────────
       Transform only; the worst frozen frame is "16px lower".              */
    .site-footer {
      animation: b-rise-16 linear both; animation-timeline: view();
      animation-range: entry 0% entry 80%;
    }
    @keyframes b-rise-16 { from { transform: translateY(16px); } to { transform: none; } }

    /* ── v2·5 · atmosphere: one slow plane of light behind the hero ────────
       The artistic beat the owner asked for by name. A single soft radial
       glow on the portrait side, drifting over ~70s. Vertical drift and
       scale ONLY: a horizontal drift measured in element-percent would move
       INWARD in LTR and OUTWARD in RTL, and a transformed element's overflow
       is scrollable — that is a horizontal-scrollbar bug waiting for exactly
       the locales least likely to be re-tested. Sized and inset so that even
       at peak scale it stays inside the hero's inline box: max growth is
       ~5% of 560px ≈ 28px against a 6% inset. `isolation: isolate` gives the
       pseudo a stacking context to sit at the bottom of, behind every child,
       without gambling on the page's paint order.                           */
    .hero { position: relative; isolation: isolate; }
    .hero::before {
      content: ""; position: absolute; z-index: -1; pointer-events: none;
      width: min(42vw, 560px); aspect-ratio: 1; border-radius: 50%;
      inset-block-start: -6%; inset-inline-end: 6%;
      background: radial-gradient(closest-side,
        color-mix(in srgb, var(--os-accent-disp) 7%, transparent), transparent 72%);
      animation: b-atmo 70s var(--os-ease) infinite alternate;
    }
    @keyframes b-atmo { from { transform: translateY(0) scale(1); }
                        to   { transform: translateY(4%) scale(1.1); } }
  }

  /* ── v2·6 · micro-interactions (transitions, not animations) ─────────────
     The arrow in a button leans into its own reading direction on hover and
     keyboard focus. It uses the `translate` PROPERTY, not `transform`,
     because RTL already flips these icons with `transform: scaleX(-1)`
     (os.css §buttons) — a transform here would enter that cascade fight and
     one of the two would silently lose; the two properties compose instead. */
  .btn .icon.dir { transition: translate var(--os-fast) var(--os-ease); }
  .btn:hover .icon.dir, .btn:focus-visible .icon.dir { translate: 3px 0; }
  html[dir="rtl"] .btn:hover .icon.dir,
  html[dir="rtl"] .btn:focus-visible .icon.dir { translate: -3px 0; }
}

/* The essays' thematic breaks (BH-156). The default hr is a 3D inset groove
   from 1995; this site's only hr consumers are the Journal articles, where the
   break should read as a quiet pause, not a ruled line across the panel. */
/* scoped to screen ON PURPOSE: unscoped, this sits later in the file than
   the @media print block and silently wins there too — the letterhead
   existed for exactly one regeneration before the cascade ate it (D-16). */
@media screen { .print-letterhead { display: none; } }

article hr {
  border: 0; border-top: 1px solid var(--os-line);
  width: 64px; margin: 26px auto;
}

/* ═══ BH-154 · logo tiles (GENERATED by scripts/logo_tiles.py — edit the
   generator, not this block) ══════════════════════════════════════════ */
.logo-frame:has(img[src$="/aghigh.svg"]) img { scale: 1.3; translate: 2.1% 15.2%; }
.logo-frame:has(img[src$="/goldoon.svg"]) { background: #3f1e55; padding: 0; }
.logo-frame:has(img[src$="/goldoon.svg"]) img { scale: 2.79; translate: -2.4% 5.5%; }
.logo-frame:has(img[src$="/shalize.svg"]) img { scale: 1.17; translate: 0.3% -2.7%; }
.logo-frame:has(img[src$="/writingchex.svg"]) { background: #363635; padding: 0; }
.logo-frame:has(img[src$="/writingchex.svg"]) img { scale: 1.0; translate: 0.3% 3.3%; }
/* ═══ end BH-154 generated ═══ */

/* ═══ BH-081 · the 5P wheel — a diagram derived from the page's own labels ═══
   The five <text> nodes are the section's own h3 strings, read at build time
   by scripts/fivep_provenance.py, so the picture cannot disagree with the
   prose beside it in any locale. currentColor + tokens: dark theme for free. */
.fivep-diagram { margin: 0 auto 8px; max-width: 560px; }
.fivep-diagram svg { width: 100%; height: auto; display: block; }
.fd-ring { fill: none; stroke: var(--os-line-strong); stroke-width: 1.2; }
.fd-spoke { stroke: var(--os-line); stroke-width: 1; }
.fd-hub { fill: var(--os-surface); stroke: var(--os-accent-disp); stroke-width: 1.6; }
.fd-hub-label { font-family: var(--os-mono); font-size: 15px; font-weight: 700;
                fill: var(--os-accent); letter-spacing: .08em; }
.fd-node { fill: var(--os-accent-disp); }
.fd-label { font-size: 14px; font-weight: 650; fill: var(--os-ink); }
@media print { .fivep-diagram { max-width: 420px; } }


/* ╔══════════════════════════════════════════════════════════════════════════╗
   ║  BH-159 · CINEMATIC LAYER v3 — the scene, the depth, the rhythm          ║
   ╚══════════════════════════════════════════════════════════════════════════╝
   Owner directive (2026-07-20): the site still reads as a competent template.
   v1 gave motion, v2 gave a load choreography; neither gave the page a
   COMPOSITION. This layer adds the three things that were missing — a scene
   behind the first screen, depth in the surfaces, and a scroll that feels
   authored rather than incidental.

   The contract every earlier layer paid to learn still holds:
     · transform and opacity only, on the compositor, off the main thread;
     · scroll-driven pieces pin opacity 1 at both ends, so a frozen rAF can
       only cost movement, never content;
     · the one place opacity animates from 0 is time-based, not scroll-based;
     · every new animated selector is named in the reduced-motion and print
       resets — a reset that does not name what is animated is not a reset;
     · no inline styles anywhere: style-src is 'self' on an ENFORCED policy;
     · no filter:blur on the orbs. A radial-gradient with a transparent
       falloff is already soft, costs one paint, and does not hand the
       compositor three full-screen blur surfaces on a mid-range phone.       */

/* ── the stage ────────────────────────────────────────────────────────────
   Behind everything, clipped to the hero, never in the a11y tree.           */
.hero { position: relative; isolation: isolate; }
.hero > :not(.hero-stage) { position: relative; z-index: 1; }
/* FULL-BLEED, not column-width. The first version inherited `.wrap`'s 1160px
   measure, so the orbs stopped dead at the content edge and the "atmosphere"
   rendered as a visible tinted rectangle sitting inside the page — worse than
   no scene at all. `inset-inline: calc(50% - 50vw)` breaks an absolutely
   positioned child out to the viewport from a centred container. */
.hero-stage {
  position: absolute;
  inset-block: -12% -20%;
  inset-inline: calc(50% - 50vw);
  z-index: 0; pointer-events: none; contain: paint;
  /* The scene has to DISSOLVE into the page, not stop. Without this the
     stage's lower boundary painted a full-width tonal edge across every hero
     — obvious in dark mode on /leadership-portfolio, where it read as a
     rectangle laid over the page. Atmosphere with a visible border is not
     atmosphere. */
  -webkit-mask-image: linear-gradient(to bottom, #000 58%, transparent 96%);
          mask-image: linear-gradient(to bottom, #000 58%, transparent 96%);
}
/* Contained on MAIN, not on body. Two reasons, both measured:
   · overflow set on `body` PROPAGATES to the viewport when `html` is
     `visible`. That changed scrollbar resolution mid-load and produced a
     0.0107 layout shift on /fa — html's width jumped 381 -> 390px, which is
     exactly one scrollbar. The adoption gate caught it; it was a regression
     I had just introduced, not a pre-existing one.
   · `clip` rather than `hidden` because hidden makes the element a scroll
     container, and the header's sticky positioning depends on not having one
     between it and the viewport.
   This contains a decorative overflow the layout creates on purpose. It is
   not a `overflow:hidden` band-aid over a broken width — the viewport matrix
   still asserts zero horizontal scroll on 259 page/width pairs. */
main { overflow-x: clip; }

.hs-orb { position: absolute; border-radius: 50%; }
.hs-orb-1 {
  width: 46vw; height: 46vw; max-width: 620px; max-height: 620px;
  inset-block-start: -14%; inset-inline-start: -8%;
  background: radial-gradient(circle at 50% 50%,
    color-mix(in srgb, var(--os-systems-disp) 26%, transparent) 0%,
    color-mix(in srgb, var(--os-systems-disp) 9%, transparent) 42%,
    transparent 68%);
}
.hs-orb-2 {
  width: 40vw; height: 40vw; max-width: 540px; max-height: 540px;
  inset-block-start: 24%; inset-inline-end: -10%;
  background: radial-gradient(circle at 50% 50%,
    color-mix(in srgb, var(--os-gold-disp) 22%, transparent) 0%,
    color-mix(in srgb, var(--os-gold-disp) 7%, transparent) 44%,
    transparent 70%);
}
.hs-orb-3 {
  width: 34vw; height: 34vw; max-width: 460px; max-height: 460px;
  inset-block-end: -16%; inset-inline-start: 26%;
  background: radial-gradient(circle at 50% 50%,
    color-mix(in srgb, var(--os-accent-disp) 18%, transparent) 0%,
    color-mix(in srgb, var(--os-accent-disp) 6%, transparent) 46%,
    transparent 72%);
}
html[data-theme="dark"] .hs-orb-1,
html[data-theme="dark"] .hs-orb-2,
html[data-theme="dark"] .hs-orb-3 { opacity: .62; }

/* the lattice: a dot grid that exists only where the eye is not reading */
.hs-mesh {
  position: absolute; inset: 0;
  background-image: radial-gradient(color-mix(in srgb, var(--os-ink) 20%, transparent) 1px,
                                    transparent 1px);
  background-size: 30px 30px;
  -webkit-mask-image: radial-gradient(ellipse 78% 70% at 55% 40%, #000 18%, transparent 78%);
          mask-image: radial-gradient(ellipse 78% 70% at 55% 40%, #000 18%, transparent 78%);
  opacity: .5;
}

/* the system object — one hub, five satellites */
/* Placed so the portrait OCCLUDES its lower half: partial occlusion is the
   strongest depth cue available without 3D, and it puts the constellation
   behind the subject rather than beside him. The first placement hid it
   almost entirely behind the card — visible in the first capture as two
   stray nodes and nothing else. */
.hs-map {
  position: absolute; inset-block-start: -2%; inset-inline-start: 13%;
  width: min(52%, 640px); height: auto; opacity: .6;
  /* The graph must never compete with the sentence. Positioned into the band
     ABOVE the copy, then faded out before it reaches it — the first attempt
     put a node on the "B" of Balalami and ran two connectors through the
     promise paragraph, which is a legibility fault, not atmosphere. The mask
     is the guarantee: wherever the copy is, the graph has already dissolved. */
  -webkit-mask-image: linear-gradient(to bottom, #000 16%, transparent 48%);
          mask-image: linear-gradient(to bottom, #000 16%, transparent 48%);
}
.hs-link { stroke: color-mix(in srgb, var(--os-accent) 42%, transparent); stroke-width: 1.25; }
.hs-ring {
  fill: none; stroke: color-mix(in srgb, var(--os-accent) 26%, transparent);
  stroke-width: 1; stroke-dasharray: 3 9; transform-origin: 200px 160px;
}
.hs-node { fill: var(--os-surface); stroke: var(--os-accent); stroke-width: 1.6; }
.hs-hub  { fill: var(--os-accent); }
html[data-theme="dark"] .hs-map { opacity: .46; }

/* film grain: one 120x120 turbulence tile, repeated. Data URI, so
   img-src 'self' data: covers it and the enforced policy stays untouched. */
/* 64px tile at one octave, not 120px at three. Measured: the three-octave
   tile cost 36ms of LCP on its own, because feTurbulence is rasterised before
   first paint and octaves multiply that work. At 15% opacity behind a
   gradient the two are visually indistinguishable — this is grain, and grain
   that needs three octaves to read is not grain. */
.hs-grain {
  position: absolute; inset: 0; opacity: .15; mix-blend-mode: multiply;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='64' height='64'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='1'/%3E%3C/filter%3E%3Crect width='64' height='64' filter='url(%23n)' opacity='.5'/%3E%3C/svg%3E");
}
html[data-theme="dark"] .hs-grain { mix-blend-mode: overlay; opacity: .09; }

/* On phones the scene is atmosphere only: the map would fight the headline
   for the same 380 logical pixels, and the lattice reads as noise there. */
@media (max-width: 700px) {
  .hs-map { display: none; }
  .hs-mesh { opacity: .26; background-size: 20px 20px; }
}

/* ── depth in the surfaces ────────────────────────────────────────────────
   A card should feel like an object with a position in space. A light rim on
   the top edge (where a light source would catch it) plus a shadow that grows
   with elevation does most of that work. */
.panel, .metric-tile, .portrait-meta {
  box-shadow: var(--os-shadow-soft),
              inset 0 1px 0 color-mix(in srgb, #fff 60%, transparent);
}
html[data-theme="dark"] .panel,
html[data-theme="dark"] .metric-tile,
html[data-theme="dark"] .portrait-meta {
  box-shadow: var(--os-shadow-soft),
              inset 0 1px 0 color-mix(in srgb, #fff 8%, transparent);
}

/* the portrait becomes the scene's foreground object */
.portrait-frame {
  box-shadow: 0 30px 70px -28px rgba(20, 28, 34, .42);
}
.portrait-frame::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  border-radius: inherit;
  background: linear-gradient(148deg,
    color-mix(in srgb, #fff 20%, transparent) 0%,
    transparent 34%, transparent 76%,
    color-mix(in srgb, var(--os-charcoal) 14%, transparent) 100%);
}

/* ── section rhythm ───────────────────────────────────────────────────────
   A hairline that starts at the accent and dissolves: it marks the cut
   between chapters without ruling a line across the page. */
.block { position: relative; }
.block + .block::before {
  content: ""; position: absolute; inset-inline: 0; inset-block-start: 0;
  height: 1px;
  background: linear-gradient(to right,
    transparent 0%,
    color-mix(in srgb, var(--os-accent-disp) 34%, transparent) 18%,
    color-mix(in srgb, var(--os-line) 90%, transparent) 50%,
    transparent 92%);
}
html[dir="rtl"] .block + .block::before {
  background: linear-gradient(to left,
    transparent 0%,
    color-mix(in srgb, var(--os-accent-disp) 34%, transparent) 18%,
    color-mix(in srgb, var(--os-line) 90%, transparent) 50%,
    transparent 92%);
}

/* the section numeral becomes an anchor with weight, not a label */
.sec-no {
  box-shadow: 0 6px 18px -8px color-mix(in srgb, var(--os-accent) 44%, transparent),
              inset 0 1px 0 color-mix(in srgb, #fff 55%, transparent);
}
html[data-theme="dark"] .sec-no {
  box-shadow: 0 6px 18px -8px color-mix(in srgb, var(--os-accent) 30%, transparent),
              inset 0 1px 0 color-mix(in srgb, #fff 10%, transparent);
}


/* ── v3 motion ─────────────────────────────────────────────────────────────
   A LATENT BUG FOUND WHILE BUILDING THIS, and the reason the hover work below
   uses the independent transform properties instead of `transform`:

     `a.panel:hover { transform: translateY(-2px) }` has been in this file
     since the beginning and has never rendered on any page where the scroll
     layer is active. `.panel` carries `animation: b-rise ... both`, and a
     filling animation OWNS the property it animates — a transition on the
     same property cannot win, at any specificity. Every card on the site has
     a hover lift that does nothing. The v1 layer recorded exactly this rule
     ("a CSS animation beats a CSS transition") for a different element and the
     panel hover was never re-checked against it.

   `translate`, `scale` and `rotate` are SEPARATE properties from `transform`
   and compose with it, so the animation keeps its 18px entry rise while hover
   owns the lift. Both survive. Nothing needed to be removed.                */

@media (prefers-reduced-motion: no-preference) {

  /* the scene breathes: three orbs on independent, deliberately non-harmonic
     periods so the composition never visibly repeats. transform only. */
  .hs-orb-1 { animation: hs-drift-a 74s var(--os-ease) infinite alternate; }
  .hs-orb-2 { animation: hs-drift-b 61s var(--os-ease) infinite alternate; }
  .hs-orb-3 { animation: hs-drift-c 88s var(--os-ease) infinite alternate; }
  @keyframes hs-drift-a { from { transform: translate3d(0,0,0) scale(1); }
                          to   { transform: translate3d(4%, 6%, 0) scale(1.12); } }
  @keyframes hs-drift-b { from { transform: translate3d(0,0,0) scale(1.08); }
                          to   { transform: translate3d(-5%, -4%, 0) scale(1); } }
  @keyframes hs-drift-c { from { transform: translate3d(0,0,0) scale(1); }
                          to   { transform: translate3d(3%, -7%, 0) scale(1.1); } }

  /* the system object assembles itself: links draw, then nodes arrive on the
     ends they were drawn to. Time-based, because the hero is above the fold —
     a view() timeline there would already be at 100% on first paint. */
  .hs-link {
    stroke-dasharray: 260; stroke-dashoffset: 260;
    animation: hs-draw 1.15s var(--os-ease) both;
  }
  .hs-link:nth-of-type(1) { animation-delay: .30s; }
  .hs-link:nth-of-type(2) { animation-delay: .42s; }
  .hs-link:nth-of-type(3) { animation-delay: .54s; }
  .hs-link:nth-of-type(4) { animation-delay: .66s; }
  .hs-link:nth-of-type(5) { animation-delay: .78s; }
  @keyframes hs-draw { to { stroke-dashoffset: 0; } }

  .hs-node { scale: 0; transform-box: fill-box; transform-origin: center;
             animation: hs-pop .5s var(--os-ease) both; }
  .hs-node:nth-of-type(1) { animation-delay: 1.05s; }
  .hs-node:nth-of-type(2) { animation-delay: 1.17s; }
  .hs-node:nth-of-type(3) { animation-delay: 1.29s; }
  .hs-node:nth-of-type(4) { animation-delay: 1.41s; }
  .hs-node:nth-of-type(5) { animation-delay: 1.53s; }
  @keyframes hs-pop { to { scale: 1; } }

  .hs-hub {
    transform-box: fill-box; transform-origin: center;
    animation: hs-hub-pulse 3.8s var(--os-ease) infinite;
  }
  @keyframes hs-hub-pulse {
    0%, 100% { scale: 1;    opacity: 1; }
    50%      { scale: 1.28; opacity: .82; }
  }

  .hs-ring { animation: hs-spin 96s linear infinite; }
  @keyframes hs-spin { to { transform: rotate(360deg); } }

  /* The scene ARRIVES rather than being there. This is not only an effect:
     measured, deferring its reveal by 120ms recovers 52ms of LCP, because a
     fully transparent layer is not rasterised before first paint. The scene's
     paint cost against a hero with no stage at all falls from 68ms to 16ms.
     `opacity: 0` lives inside this no-preference block on purpose — hoisted
     out, the reduced-motion reset (which kills the animation) would leave the
     scene permanently invisible for exactly the users who asked for less
     motion, which is the same inversion the progress bar taught in v1. */
  .hero-stage { opacity: 0; animation: hs-arrive .7s var(--os-ease) .12s forwards; }
  @keyframes hs-arrive { to { opacity: 1; } }

  @supports (animation-timeline: scroll(root block)) {
    /* parallax with a purpose: the scene falls away slower than the copy, so
       the first screen gains depth as it leaves. Range-limited to the first
       viewport so the work stops the moment the hero is gone.
       Declared TOGETHER with the arrival, because `animation` is one property
       — a second rule naming only the parallax would silently drop the
       arrival, and the stage would never become visible. The two own disjoint
       properties: arrival owns opacity on the document timeline, parallax
       owns transform on the scroll timeline. */
    .hero-stage {
      animation: hs-arrive .7s var(--os-ease) .12s forwards,
                 hs-parallax linear both;
      animation-timeline: auto, scroll(root block);
      animation-range: normal, 0 100vh;
    }
    @keyframes hs-parallax {
      from { transform: translateY(0)     scale(1);    }
      to   { transform: translateY(-56px) scale(1.05); }
    }
  }

  /* cards become objects you can pick up. `translate`/`scale`, never
     `transform` — see the note at the top of this block. */
  .panel, .metric-tile, .logo-frame {
    transition: translate var(--os-default, 180ms) var(--os-ease),
                scale var(--os-default, 180ms) var(--os-ease),
                box-shadow var(--os-slow) var(--os-ease),
                border-color var(--os-fast) var(--os-ease);
  }
  a.panel:hover, .panel-link:hover, .pf-card:hover {
    translate: 0 -4px; scale: 1.006;
    box-shadow: 0 22px 44px -22px color-mix(in srgb, var(--os-charcoal) 46%, transparent),
                inset 0 1px 0 color-mix(in srgb, #fff 60%, transparent);
    border-color: color-mix(in srgb, var(--os-accent-disp) 52%, transparent);
  }
  .metric-tile:hover { translate: 0 -3px; }
  /* the logo tile lifts; the LOGO ITSELF is never touched — no filter, no
     recolour, no crop. Owner rule, and it is the tile that moves anyway. */
  .logo-frame:hover { translate: 0 -2px; scale: 1.03; }

  /* tactile press: the button goes down under the finger and comes back */
  .btn { transition: translate var(--os-fast) var(--os-ease),
                     scale var(--os-fast) var(--os-ease),
                     box-shadow var(--os-fast) var(--os-ease); }
  .btn:hover  { translate: 0 -2px;
                box-shadow: 0 14px 26px -14px color-mix(in srgb, var(--os-charcoal) 52%, transparent); }
  .btn:active { translate: 0 1px; scale: .985; box-shadow: none; }

  /* the primary CTA catches a light as the pointer crosses it */
  .btn-primary { position: relative; overflow: hidden; }
  .btn-primary::after {
    content: ""; position: absolute; inset-block: 0; inset-inline-start: -60%;
    width: 45%; pointer-events: none;
    background: linear-gradient(100deg, transparent,
                color-mix(in srgb, #fff 26%, transparent), transparent);
    translate: -140% 0;
  }
  .btn-primary:hover::after { animation: hs-sheen .72s var(--os-ease); }
  @keyframes hs-sheen { to { translate: 420% 0; } }

  /* the portrait floats a little against its own shadow */
  .portrait-frame { animation: hs-float 9s var(--os-ease) infinite alternate; }
  @keyframes hs-float { from { translate: 0 0; } to { translate: 0 -8px; } }
}

/* Reduced motion: the composition stays, the movement goes. The scene is a
   still image at its resting state — links drawn, nodes present, hub calm. */
@media (prefers-reduced-motion: reduce) {
  .hs-orb-1, .hs-orb-2, .hs-orb-3, .hs-ring, .hs-hub, .hero-stage,
  .portrait-frame, .btn-primary::after {
    animation: none !important; transform: none !important;
    translate: none !important; scale: none !important;
  }
  /* the arrival's starting opacity is scoped to no-preference, but pin it
     here too so no future edit can strand the scene at zero */
  .hero-stage { opacity: 1 !important; }
  .hs-link { stroke-dashoffset: 0 !important; animation: none !important; }
  .hs-node { scale: 1 !important; animation: none !important; }
}

/* Print: the stage is atmosphere and atmosphere does not belong on paper.
   Removed entirely rather than pinned — pinning would still spend ink on a
   full-bleed gradient and a grain tile. */
@media print {
  .hero-stage { display: none !important; }
  .portrait-frame { animation: none !important; translate: none !important; }
  .block + .block::before { display: none !important; }
}


/* ── v3 round-2 fixes: what the post-redesign sweep found ─────────────────── */

/* 1. The portfolio filter row was never in a container. `.pf-filter` is a
      direct child of `main.page`, so it started at x=0 while every other
      element on the page starts at the 60px gutter — the "All" chip was
      sheared flat against the viewport edge. Measured: row x=0, width=1280,
      parent `.page` x=0. Given the same measure as `.wrap` rather than
      re-nesting the markup in 9 locales; the result is identical and the
      change cannot desynchronise between locales. */
.pf-filter {
  width: min(100% - 2 * clamp(16px, 4vw, 32px), var(--os-container));
  margin-inline: auto;
}

/* 2. The rail label only expands when the gutter can actually hold it.
      Measured at 1280px: the expanded pill started at x=1157 while the content
      column ends at x=1220 — 64px of overlap, and it painted over the last
      word of a card's body text. Widening it to 230px (this same pass) is what
      made a truncation problem into an occlusion problem, which is worse: a
      clipped label is ugly, a covered sentence is unreadable.
      The gutter at 1280 is (1280-1160)/2 = 60px and cannot hold any label, so
      below 1660px the rail is dots only. Nothing is lost to assistive tech —
      every dot's <a> already carries the section name as its aria-label. */
@media (max-width: 1659px) {
  .os-sysmap a:hover .lbl,
  .os-sysmap a:focus-visible .lbl,
  .os-sysmap a[aria-current="true"] .lbl { max-width: 0; opacity: 0; }
}

/* 3. The CTA sheen sat ON the button at rest in RTL. `inset-inline-start`
      is logical (it becomes `right` in RTL) but `translate` is PHYSICAL, so
      the -140% that parks the highlight off-screen to the left in LTR drove it
      straight back across the button in RTL — a grey band with a hard vertical
      edge on every Persian and Arabic primary button. Now the resting state is
      opacity 0, which is direction-independent, and the sweep runs in each
      writing mode's own reading direction. */
@media (prefers-reduced-motion: no-preference) {
  .btn-primary::after { opacity: 0; translate: 0 0; }
  .btn-primary:hover::after { animation: hs-sheen .72s var(--os-ease); }
  html[dir="rtl"] .btn-primary:hover::after { animation-name: hs-sheen-rtl; }
  @keyframes hs-sheen {
    0%   { opacity: 0; translate: -140% 0; }
    18%  { opacity: 1; }
    82%  { opacity: 1; }
    100% { opacity: 0; translate: 420% 0; }
  }
  @keyframes hs-sheen-rtl {
    0%   { opacity: 0; translate: 140% 0; }
    18%  { opacity: 1; }
    82%  { opacity: 1; }
    100% { opacity: 0; translate: -420% 0; }
  }
}

/* 4. The system graphic only appears on heroes that HAVE a spare column.
      On /leadership-portfolio the hero is a single full-width text block, so
      there is no region without copy — the hub landed on the intro paragraph
      and a connector cut through the headline. `:has(.hero-grid)` is the
      structural test for "this hero has a second column", which is exactly the
      condition under which the graphic has somewhere to live. Everything else
      about the scene (orbs, lattice, grain) still applies there. */
.hero:not(:has(.hero-grid)) .hs-map { display: none; }


/* ╔══════════════════════════════════════════════════════════════════════════╗
   ║  BH-163 · CINEMATIC LAYER v4 — the page gets a POINT OF VIEW              ║
   ╚══════════════════════════════════════════════════════════════════════════╝
   The owner's verdict on v3 was correct: atmosphere without drama. A soft
   wash behind the hero and an 18px rise on the panels is politeness, not
   design. Mid-page the site still read as four white cards on beige with an
   orphaned fourth and a dead half-row beside it.

   v4 uses the material the site already had and I had not touched:
     · six accent colours, already declared per section as [data-accent]
     · --i, already written onto every grid child by os.js
     · the section numbers, already authored and now mirrored to data-chapter
   Nothing here invents a new visual language; it finally spends the one the
   design system was already carrying.

   Same contract as every layer before it: transform/opacity only, every
   animated selector named in the reduced-motion and print resets, no inline
   styles (style-src is 'self' on an enforced policy), and the scroll-driven
   pieces cannot cost content — only movement. */

/* ── 1 · the page takes the colour of what you are reading ────────────────
   A single fixed wash whose hue is the ACTIVE section's accent. os.js already
   runs an IntersectionObserver to mark the current section in the rail; it now
   also publishes that section's accent to :root, and this listens. Scrolling
   the page walks it through six deliberate colours instead of one flat beige,
   and every colour is one the content itself declared. */
.b-ambient {
  position: fixed; inset: 0; z-index: 0; pointer-events: none;
  background:
    radial-gradient(60% 45% at 82% 12%,
      color-mix(in srgb, var(--live-accent, var(--os-accent-disp)) 20%, transparent), transparent 70%),
    radial-gradient(52% 40% at 12% 86%,
      color-mix(in srgb, var(--live-accent, var(--os-accent-disp)) 14%, transparent), transparent 72%);
  opacity: 0;
  transition: opacity 900ms var(--os-ease), background 900ms var(--os-ease);
}
.js-on .b-ambient { opacity: 1; }
html[data-theme="dark"] .b-ambient { opacity: 0; }
html[data-theme="dark"].js-on .b-ambient { opacity: .55; }
/* Content sits above the wash without a z-index on every block.
   NOT .site-header. That rule shipped for one build and broke the sticky
   header outright: `.site-header` already declares `position: sticky`, this
   selector has identical specificity and comes later, so it won overrode it to
   `relative` and the header scrolled away with the page. Measured: position
   relative, z-index 1, top -1500px after a 1500px scroll. The header already
   carries z-index 80 and needs nothing from this rule. */
main, .site-footer { position: relative; z-index: 1; }

/* ── 2 · the chapter numeral ──────────────────────────────────────────────
   Large, outlined, set in the section's own accent, sitting behind the head
   where the eye enters the band. It says what the small chip says — the value
   is mirrored from it (scripts/chapter_marks.py), so the two can never
   disagree. Hidden from assistive tech: it is the same number, said louder. */
.block[data-chapter] { position: relative; }
.block[data-chapter]::before {
  content: attr(data-chapter);
  position: absolute; inset-block-start: -.18em; inset-inline-start: -.06em;
  z-index: 0; pointer-events: none;
  font-family: var(--os-display); font-weight: 800;
  font-size: clamp(9rem, 22vw, 20rem); line-height: .8;
  letter-spacing: -.04em;
  color: transparent;
  -webkit-text-stroke: 1.5px color-mix(in srgb, var(--os-accent-disp) 22%, transparent);
  opacity: .5;
}
html[data-theme="dark"] .block[data-chapter]::before {
  -webkit-text-stroke-color: color-mix(in srgb, var(--os-accent-disp) 26%, transparent);
  opacity: .42;
}
.block[data-chapter] > * { position: relative; z-index: 1; }
@media (max-width: 700px) {
  .block[data-chapter]::before { font-size: 34vw; opacity: .34; }
}

/* ── 3 · the cards stop being rectangles ──────────────────────────────────
   A four-item grid was resolving to three-across plus an orphan, with a dead
   half-row beside it — the single worst thing on the page. `:has()` gives the
   grid its own shape based on how many children it actually has, so a set of
   four reads as a 2x2 block and a set of three still spans. */
@media (min-width: 900px) {
  .grid:has(> :nth-child(4):last-child) { grid-template-columns: repeat(2, 1fr); }
  .grid:has(> :nth-child(2):last-child) { grid-template-columns: repeat(2, 1fr); }
}

/* an accent edge that arrives on hover — the card acknowledges the pointer
   with its section's own colour rather than a generic grey */
.panel { position: relative; overflow: hidden; }
.panel::before {
  content: ""; position: absolute; inset-block-start: 0; inset-inline: 0;
  height: 2px; pointer-events: none;
  background: linear-gradient(to right, transparent,
              var(--os-accent-disp) 22%, var(--os-accent-disp) 78%, transparent);
  scale: 0 1; transform-origin: center;
  transition: scale var(--os-slow) var(--os-ease);
}
a.panel:hover::before, .panel-link:hover::before, .pf-card:hover::before { scale: 1 1; }

/* the icon becomes a plate with depth, not a tinted square */
.icon-box {
  box-shadow: inset 0 1px 0 color-mix(in srgb, #fff 70%, transparent),
              0 4px 12px -6px color-mix(in srgb, var(--os-accent-disp) 55%, transparent);
}
html[data-theme="dark"] .icon-box {
  box-shadow: inset 0 1px 0 color-mix(in srgb, #fff 10%, transparent),
              0 4px 14px -6px color-mix(in srgb, var(--os-accent-disp) 40%, transparent);
}

/* ── 4 · motion with intent ───────────────────────────────────────────────*/
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {

    /* Cards arrive in sequence, not as a block. --i is already written by
       os.js onto every grid child; v1 used it for a 55ms opacity delay and
       nothing since. Here it staggers a real entrance: each card lifts and
       settles 60ms after the one before it. */
    .grid > .panel, .grid > .metric-tile, .grid > a.panel {
      animation: b-card-in linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 62%;
    }
    @keyframes b-card-in {
      from { transform: translateY(34px); scale: .965; }
      to   { transform: none;             scale: 1; }
    }

    /* The chapter numeral drifts against the scroll — the parallax that makes
       a flat page feel like it has a back wall. */
    .block[data-chapter]::before {
      animation: b-chapter-drift linear both;
      animation-timeline: view();
      animation-range: entry 0% exit 100%;
    }
    @keyframes b-chapter-drift {
      from { transform: translateY(64px)  scale(.94); }
      to   { transform: translateY(-64px) scale(1.06); }
    }

    /* The section head arrives from behind its own leading edge: a clip that
       opens rather than an opacity that fades. Opacity is pinned at 1 at both
       ends — a frozen rAF can cost the reveal, never the words. */
    .sec-title {
      animation: b-wipe linear both;
      animation-timeline: view();
      animation-range: entry 8% entry 70%;
    }
    @keyframes b-wipe {
      from { clip-path: inset(0 0 100% 0); opacity: 1; }
      to   { clip-path: inset(0 0 0% 0);   opacity: 1; }
    }
  }

  /* The hero headline builds itself, line by line, in the first second.
     Time-based: the hero is above the fold, so a view() timeline would already
     be finished at first paint. */
  .hero h1 {
    clip-path: inset(0 0 110% 0);
    animation: b-hero-wipe .92s cubic-bezier(.16, 1, .3, 1) .18s forwards;
  }
  @keyframes b-hero-wipe { to { clip-path: inset(0 0 -20% 0); } }

  /* The portrait is uncovered by a COVER that slides off, never by clipping
     the image itself.
     Clipping the image cost 496ms of LCP, measured: the portrait IS the LCP
     element on every locale, and Chrome does not count a clipped element as
     painted, so the metric waited for the reveal. The effect is identical to
     the eye and free to the metric — the image paints at t=0 behind a panel
     that is already leaving. */
  .portrait-frame::before {
    content: ""; position: absolute; inset: 0; z-index: 2; pointer-events: none;
    background: var(--os-page);
    animation: b-portrait-wipe 1.05s cubic-bezier(.16, 1, .3, 1) .28s forwards;
  }
  @keyframes b-portrait-wipe { to { transform: translateX(101%); } }
  html[dir="rtl"] .portrait-frame::before { animation-name: b-portrait-wipe-rtl; }
  @keyframes b-portrait-wipe-rtl { to { transform: translateX(-101%); } }

  /* the section's accent hairline draws itself across the band */
  .block + .block::before { transform-origin: left center; }
  html[dir="rtl"] .block + .block::before { transform-origin: right center; }
  @supports (animation-timeline: view()) {
    .block + .block::before {
      animation: b-rule-draw linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 46%;
    }
    @keyframes b-rule-draw { from { scale: 0 1; } to { scale: 1 1; } }
  }
}

/* Reduced motion: every v4 animation off, and every clip opened. A clip left
   at its starting value hides the headline completely — the failure mode is
   not "less motion", it is "no words", which is the inversion this codebase
   has now been bitten by twice. */
@media (prefers-reduced-motion: reduce) {
  .grid > .panel, .grid > .metric-tile, .grid > a.panel,
  .block[data-chapter]::before, .sec-title, .hero h1,
  .portrait-frame::before, .block + .block::before, .b-ambient {
    animation: none !important; transform: none !important;
    scale: none !important; clip-path: none !important;
  }
  /* `transform: none` would park the cover ON the portrait — the reset must
     remove it, not freeze it at its starting position */
  .portrait-frame::before { content: none !important; }
}
@media print {
  .b-ambient, .block[data-chapter]::before { display: none !important; }
  .portrait-frame::before { content: none !important; }
  .grid > .panel, .grid > .metric-tile, .grid > a.panel,
  .sec-title, .hero h1, .portrait-frame::before, .block + .block::before {
    animation: none !important; transform: none !important;
    scale: none !important; clip-path: none !important; opacity: 1 !important;
  }
}

/* ═══ BH-165 · Calendly embed ════════════════════════════════════════════════
   The published snippet carries style="min-width:320px;height:700px". This
   site ships an ENFORCED CSP with style-src 'self' and no unsafe-inline —
   BH-021 removed every style attribute from all 109 pages so that directive
   could be tightened — so the browser would refuse that attribute and the
   frame would collapse to nothing. The dimensions live here instead, which is
   where they belonged anyway. */
.cal-embed { margin-block-start: var(--os-gap); }
.calendly-inline-widget {
  min-width: 320px; height: 700px; width: 100%;
  border: 1px solid var(--os-line); border-radius: var(--os-radius);
  overflow: hidden; background: var(--os-surface);
  box-shadow: var(--os-shadow-soft);
}
@media (max-width: 700px) { .calendly-inline-widget { height: 620px; } }

/* ═══ BH-166 · Journal artwork ═══════════════════════════════════════════════
   Generated per article from the site's own visual language (see
   scripts/article_art.py). SVG, so it is a couple of kilobytes and stays sharp
   at any density. `aspect-ratio` is declared so the space is reserved before
   the file arrives — an image that pushes the article down when it loads is a
   layout shift, and this project measured its way from 0.0646 to 0.0002 to
   avoid exactly that. */
.art-banner {
  display: block; width: 100%; height: auto; aspect-ratio: 1200 / 630;
  border: 1px solid var(--os-line); border-radius: var(--os-radius);
  margin-block-end: var(--os-gap);
  box-shadow: var(--os-shadow-soft);
}
.art-card { overflow: hidden; padding: 0; }
/* A LISTING thumbnail, not a poster. At the artwork's own 1200x630 each card
   filled an entire viewport and six of them made the index unbrowsable — you
   could not see two headlines at once, which is the one job a listing has.
   Cropped to a strip with object-fit so the composition's centre survives. */
.art-thumb {
  display: block; width: 100%; aspect-ratio: 16 / 6;
  object-fit: cover; object-position: 50% 50%;
  border-block-end: 1px solid var(--os-line);
}
/* two abreast from the width where two headlines still fit on one line each */
@media (min-width: 900px) {
  .grid:has(> .art-card) { grid-template-columns: repeat(2, 1fr); }
}
/* the article's own banner is a header image and keeps more of the frame,
   but not the full 630 — that pushed the headline below the fold */
.art-banner { aspect-ratio: 16 / 6.5; object-fit: cover; }
.art-card > :not(.art-thumb) { padding-inline: clamp(16px, 3vw, 26px); }
.art-card > :first-of-type:not(.art-thumb) { padding-block-start: 18px; }
.art-card > :last-child { padding-block-end: 20px; }

/* ═══ BH-167 · the PDFs get the brand back ═══════════════════════════════════
   The four downloads are PRINTED from these pages (BH-018), so the print
   stylesheet IS the PDF's art direction — and it was bleaching everything to
   black on white. `--os-ink: #000` and `--os-surface: #fff` flattened the six
   accent channels, the section numerals, the icon plates and the eyebrows into
   one grey document. The result read as a fax of a website.
   Colour costs nothing on a PDF that is read on a screen (which is where an
   executive brief is actually read), and these are the site's own tokens, not
   print-only decoration invented for paper. */
@media print {
  :root {
    --os-page: #fff;
    --os-surface: #fff;
    --os-surface-2: #FBF7EF;
    --os-ink: #17242B;          /* the brand's ink, not pure black */
    --os-ink-2: #35454E;
    --os-muted: #5C6B74;
  }

  /* the accent survives: it is what tells one section from another */
  .sec-title { color: var(--os-ink); }
  .eyebrow { color: var(--os-accent); }
  .sec-no {
    color: var(--os-accent); border: 1.5px solid var(--os-accent-disp);
    box-shadow: none; background: #fff;
  }
  .icon-box { background: color-mix(in srgb, var(--os-accent-disp) 12%, #fff); box-shadow: none; }
  .icon-box .icon { color: var(--os-accent); }

  /* a rule in the section's own colour where the screen has a gradient */
  .block { border-block-start: 2px solid color-mix(in srgb, var(--os-accent-disp) 34%, #fff); }
  .block:first-of-type { border-block-start: 0; }

  /* the letterhead becomes a masthead rather than a caption */
  .print-letterhead {
    font-size: 13px;
    border-bottom: 2px solid var(--os-accent-disp);
    color: var(--os-ink);
  }

  /* cards keep their edge and their tint instead of dissolving into the page */
  .panel {
    border: 1px solid #D8D2C6;
    background: #FDFBF7;
    border-radius: 10px;
    padding: 14px 16px;
  }
  .panel-accent, .panel[data-accent] {
    border-inline-start: 3px solid var(--os-accent-disp);
  }

  /* the one dark band stays dark — it is the document's single moment of
     contrast and the page it lands on is the call to action */
  .band-charcoal {
    background: #17242B !important; color: #F6F1E8 !important;
    -webkit-print-color-adjust: exact; print-color-adjust: exact;
    border-radius: 10px; padding: 18px 20px;
  }
  .band-charcoal h2, .band-charcoal .lead, .band-charcoal .eyebrow { color: #F6F1E8 !important; }

  /* metric figures are the reason these documents exist; let them read */
  .metric-value { color: var(--os-accent); }
  .metric-tile { border: 1px solid #E0DACE; background: #FDFBF7; }

  /* backgrounds only print if the document asks for it */
  body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
}

@media print {
  /* The portrait consumed almost the whole of page 1 in the brief. On screen
     it is the hero's counterweight; on an A4 page it is a headshot, and a
     two-minute read whose first page is mostly photograph is not a two-minute
     read. Capped so the summary and the first evidence both land above the
     fold of page 1. */
  .portrait-card { max-width: 46mm; margin-inline-start: auto; }
  .portrait-frame { aspect-ratio: 1 / 1.2; }
  /* 46mm, not 62: at 62 the copy column narrowed enough to break the name
     across two lines in the printed headline. The photograph is the smaller
     obligation here. */
  .hero-grid { grid-template-columns: 1fr 46mm; align-items: start; gap: 7mm; }
  .hero h1 { font-size: 26pt; line-height: 1.08; }
  .portrait-meta { font-size: 9px; }
}

/* ═══ BH-167b · the call to action asks for something ════════════════════════
   The closing band was a heading, a sentence and two buttons of equal weight —
   a reader could not tell which one was the ask. A CTA that offers two equal
   choices is a CTA that makes the visitor do the deciding, and most of them
   decide to leave. */
.band-charcoal { position: relative; overflow: hidden; isolation: isolate; }
.band-charcoal::before {
  content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none;
  background:
    radial-gradient(58% 80% at 88% 10%,
      color-mix(in srgb, var(--os-accent-disp) 40%, transparent), transparent 68%),
    radial-gradient(46% 70% at 6% 96%,
      color-mix(in srgb, var(--os-accent-disp) 22%, transparent), transparent 72%);
}
.band-charcoal .hero-actions { align-items: center; gap: 14px; }
/* the primary action reads as the primary action */
.band-charcoal .btn-primary {
  background: var(--os-on-charcoal); color: var(--os-charcoal);
  border-color: transparent; font-weight: 700;
  padding-inline: 22px;
}
.band-charcoal .btn-ghost { border-color: color-mix(in srgb, var(--os-on-charcoal) 34%, transparent); }
@media (prefers-reduced-motion: no-preference) {
  .band-charcoal .btn-primary { transition: translate var(--os-fast) var(--os-ease),
                                            box-shadow var(--os-fast) var(--os-ease); }
  .band-charcoal .btn-primary:hover {
    translate: 0 -2px;
    box-shadow: 0 12px 26px -12px color-mix(in srgb, var(--os-on-charcoal) 60%, transparent);
  }
}

/* ═══ BH-168 · Work-with-me engagement cards ═════════════════════════════════
   The conversion surface. Each card carries its section-accent edge (from the
   panel-accent rule) and lays its price beside the name so the eye reaches the
   number before it reads the prose — the price is the filter, so it leads. */
.eng-grid { grid-template-columns: 1fr; gap: var(--os-gap); }
@media (min-width: 820px) { .eng-grid { grid-template-columns: 1fr 1fr; } }
.eng-card { display: flex; flex-direction: column; gap: 12px; padding: clamp(20px, 3vw, 30px); }
.eng-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; }
.eng-name { font-size: var(--os-step-2); margin-top: 4px; }
.eng-price {
  flex: none; font-family: var(--os-mono); font-weight: 700;
  font-size: var(--os-step-1); color: var(--os-accent);
  white-space: nowrap; padding-top: 4px;
}
.eng-price--soft { color: var(--os-muted); font-weight: 600; font-size: var(--os-step-0); }
.eng-tagline { margin: 0; }
.eng-who { font-size: var(--os-step--1); margin: 0; }
.eng-who strong { color: var(--os-ink); font-weight: 650; }
.eng-includes { margin: 4px 0; display: grid; gap: 8px; }
.eng-includes li { display: flex; align-items: flex-start; gap: 9px; font-size: var(--os-step--1); }
.eng-includes .icon { color: var(--os-accent); flex: none; margin-top: 2px; }
.eng-start {
  margin: 6px 0 0; padding-top: 12px; font-size: var(--os-step--1); color: var(--os-ink-2);
  border-top: 1px solid var(--os-line);
}
.eng-start .k {
  font-family: var(--os-mono); font-size: 10px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--os-accent); margin-inline-end: 6px;
}
.not-for-list { display: grid; gap: 10px; }
.not-for-list li { display: flex; align-items: flex-start; gap: 10px; color: var(--os-ink-2); }
.not-for-list li::before {
  content: "—"; color: var(--os-accent); font-weight: 700; flex: none;
}

/* ═══ BH-169 · the qualifying fields ════════════════════════════════════════
   Budget, timeline and team-size sit in a row of their own, tighter than the
   full-width name/email/message stack, because together they are one question:
   "is this a real project?" On narrow screens they stack; from tablet up they
   share the row so the form does not grow a long ladder of selects. */
.form-qualify {
  display: grid; gap: 12px 16px; margin-block: 4px 6px;
  grid-template-columns: 1fr;
}
@media (min-width: 560px) {
  .form-qualify { grid-template-columns: repeat(3, 1fr); }
}
.form-field { display: flex; flex-direction: column; min-width: 0; }
.form-qualify label {
  display: block; font-size: var(--os-step--1); font-weight: 600;
  color: var(--os-ink-2); margin-bottom: 5px;
}
.form-qualify .select { width: 100%; }


/* ── BH-170 · Work-with-me: how-I-work, the record, FAQ ─────────────────── */
.process-grid { gap: var(--os-gap); }
@media (min-width: 760px) { .process-grid { grid-template-columns: repeat(3, 1fr); } }
.process-step { position: relative; padding-top: 42px; }
.process-n {
  position: absolute; top: 18px; inset-inline-start: 18px;
  width: 30px; height: 30px; display: grid; place-items: center;
  border-radius: 50%; border: 1.5px solid var(--os-accent-disp);
  color: var(--os-accent-disp); font: 650 13px/1 var(--b-font-mono);
}
.process-h { margin: 4px 0 6px; font-size: 1.05rem; }
.faq-list { display: flex; flex-direction: column; }
.faq-item { padding: 20px 0; border-top: 1px solid var(--os-line); }
.faq-item:first-child { border-top: 0; }
.faq-q { margin: 0 0 6px; font-size: 1.05rem; }
.faq-a { margin: 0; max-width: 64ch; }


/* ── BH-172 · engagement card CTA + contact prefill confirmation ─────────── */
.eng-cta { margin-top: auto; align-self: flex-start; }
.eng-card .eng-cta { margin-top: 18px; }
@media (min-width: 820px) { .eng-card .eng-cta { margin-top: auto; } }
.eng-confirm {
  display: inline-flex; align-items: center; gap: 8px; margin: 0 0 14px;
  padding: 8px 13px; border-radius: var(--os-radius-pill);
  background: var(--os-accent-wash, rgba(0,0,0,.04));
  border: 1px solid var(--os-accent-disp); color: var(--os-ink);
  font-size: var(--os-step--1); font-weight: 600;
}
.eng-confirm-mark {
  display: grid; place-items: center; width: 18px; height: 18px; flex: none;
  border-radius: 50%; background: var(--os-accent-disp); color: #fff;
  font-size: 11px; font-weight: 800;
}


/* ── BH-173 · work-with-me comparison table + inline booking ─────────────── */
.vs-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; margin-top: 4px; }
.vs-table { width: 100%; min-width: 720px; border-collapse: collapse; font-size: var(--os-step--1); }
.vs-table th, .vs-table td { text-align: left; vertical-align: top; padding: 12px 14px; border-top: 1px solid var(--os-line); }
.vs-table thead th { border-top: 0; font-weight: 700; color: var(--os-ink); }
.vs-table tbody th[scope="row"] { font-weight: 650; color: var(--os-ink-2); white-space: nowrap; }
.vs-corner { border: 0 !important; background: none !important; }
.vs-table .vs-me { background: var(--os-accent-wash, rgba(0,0,0,.045)); }
.vs-table thead th.vs-me { color: var(--os-accent-disp); border-bottom: 2px solid var(--os-accent-disp); }
.vs-table td.vs-me { font-weight: 600; color: var(--os-ink); }
#book .calendly-inline-widget { margin-top: 6px; }


/* ── BH-176 · payment page ──────────────────────────────────────────────── */
.pay-card { display: flex; flex-direction: column; gap: 14px; padding: clamp(20px,3vw,28px); }
.pay-head { display: flex; flex-direction: column; gap: 2px; }
.pay-net { font-size: var(--os-step-1); margin: 0; }
.pay-field { display: flex; flex-direction: column; gap: 6px; }
.pay-label { font: 600 12px/1 var(--os-mono); letter-spacing: .08em; text-transform: uppercase; color: var(--os-ink-2); }
.pay-row { display: flex; align-items: stretch; gap: 8px; }
.pay-val { flex: 1 1 auto; min-width: 0; overflow-x: auto; white-space: nowrap; padding: 11px 13px;
  background: var(--os-accent-wash, rgba(0,0,0,.045)); border: 1px solid var(--os-line);
  border-radius: var(--os-radius-sm); font-size: 15px; letter-spacing: .01em; }
.pay-copy { flex: none; padding: 0 15px; border-radius: var(--os-radius-sm); cursor: pointer;
  border: 1px solid var(--os-accent-disp); background: transparent; color: var(--os-accent-disp);
  font-weight: 650; font-size: 13px; transition: background var(--os-fast,.15s), color var(--os-fast,.15s); }
.pay-copy:hover { background: var(--os-accent-disp); color: #fff; }
.pay-copy.is-copied { background: var(--os-accent-disp); color: #fff; }
.pay-warn { display: flex; gap: 9px; align-items: flex-start; margin: 2px 0 0; font-size: var(--os-step--1);
  color: var(--os-ink-2); }
.pay-warn .icon { color: var(--os-accent-disp); flex: none; margin-top: 2px; }
.pay-sub { font-size: 12px; }


/* ── BH-184 · CMS-rendered journal posts (PHP) ──────────────────────────── */
.cms-post { max-width: 720px; }
.cms-cover { width: 100%; border-radius: var(--os-radius); margin: 18px 0; }
.cms-body { margin-top: 20px; }
.cms-body p { margin: 0 0 1em; }
.cms-body h2, .cms-body h3 { margin: 1.6em 0 .5em; }
.cms-body img { max-width: 100%; height: auto; border-radius: var(--os-radius-sm); }
.cms-body a { color: var(--os-accent-disp, var(--os-accent)); }
.embed-frame { position: relative; padding-top: 56.25%; margin: 22px 0; border-radius: var(--os-radius); overflow: hidden; background: var(--os-line); }
.embed-frame iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
.embed-frame.embed-audio { padding-top: 0; height: 180px; }
.cms-notes { margin-top: 16px; }
.cms-grid { grid-template-columns: 1fr; gap: var(--os-gap); }
@media (min-width: 720px) { .cms-grid { grid-template-columns: 1fr 1fr; } }
.cms-card a { text-decoration: none; color: inherit; display: block; }
.cms-card-t { font-size: var(--os-step-2); margin: 8px 0 6px; }
