/* ============================================================
   KORBAN STUDIO — MAIN CSS  (Slater)
   Order: 1. :root design tokens
          2. Global element styles
          3. Layout / structure
          4. Utilities
          5. Component blocks (one per Webflow section, marked)
          6. Theme overrides
   ============================================================ */

/* ------------------------------------------------------------
   0. FONT FACES
   Selna is the loader/display face — font-display: block because
   the loader counter is the first thing painted (no fallback flash;
   the woff2 is preloaded in the page head).
   ------------------------------------------------------------ */

@font-face {
  font-family: "Selna";
  src: url("/fonts/Selna-Regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: block;
}

@font-face {
  font-family: "F37 Jagger";
  src: url("/fonts/F37Jagger-Light.woff2") format("woff2");
  font-weight: 300;
  font-style: normal;
  font-display: swap;
}

/* ------------------------------------------------------------
   1. DESIGN TOKENS
   ------------------------------------------------------------ */

:root {
  /* Site fundamentals — unitless */
  --site--viewport-min: 20;        /* 20rem  ≈ 320px  — small mobile */
  --site--viewport-max: 120;       /* 120rem ≈ 1920px — large desktop */
  --site--column-count: 12;        /* 12-column grid */

  /* Max widths */
  --max-width--main: 80rem;        /* TODO: confirm against Figma */
  --max-width--medium: 60rem;
  --max-width--small: 40rem;

  /* Site layout — margin, gutter, column width */
  --site--margin: clamp(
    1 * 1rem,
    ((1 - ((3 - 1) / (var(--site--viewport-max) - var(--site--viewport-min)) * var(--site--viewport-min))) * 1rem
      + ((3 - 1) / (var(--site--viewport-max) - var(--site--viewport-min))) * 100vw),
    3 * 1rem
  );
  /* 1rem → 3rem — horizontal site margins */

  --site--gutter: clamp(
    1 * 1rem,
    ((1 - ((2 - 1) / (var(--site--viewport-max) - var(--site--viewport-min)) * var(--site--viewport-min))) * 1rem
      + ((2 - 1) / (var(--site--viewport-max) - var(--site--viewport-min))) * 100vw),
    2 * 1rem
  );
  /* 1rem → 2rem — column gutters */

  --site--column-width: calc(
    (min(var(--max-width--main), 100% - var(--site--margin) * 2)
      - (var(--site--gutter) * (var(--site--column-count) - 1)))
    / var(--site--column-count)
  );
  /* derived — updates with margin / gutter / max-width / column-count */

  /* Space */
  --space--section-main: clamp(
    4 * 1rem,
    ((4 - ((7 - 4) / (var(--site--viewport-max) - var(--site--viewport-min)) * var(--site--viewport-min))) * 1rem
      + ((7 - 4) / (var(--site--viewport-max) - var(--site--viewport-min))) * 100vw),
    7 * 1rem
  );
  /* 4rem → 7rem — standard vertical breathing room between sections */

  --space--section-page-top: clamp(
    10 * 1rem,
    ((10 - ((14 - 10) / (var(--site--viewport-max) - var(--site--viewport-min)) * var(--site--viewport-min))) * 1rem
      + ((14 - 10) / (var(--site--viewport-max) - var(--site--viewport-min))) * 100vw),
    14 * 1rem
  );
  /* 10rem → 14rem — top breathing room for the first section of a page */

  /* Text — families */
  --text--font-main: "Helvetica Neue", Helvetica, Arial, sans-serif;      /* TODO: brand font */
  --text--font-display: "Times New Roman", Times, serif;                  /* TODO: brand font */
  --text--font-selna: "Selna", "Times New Roman", Times, serif;           /* loader/display serif */
  --text--font-jagger: "F37 Jagger", "Helvetica Neue", Helvetica, Arial, sans-serif; /* wired, usage TBC from Figma */

  /* Text — sizes */
  --text--size-small: clamp(
    0.875 * 1rem,
    ((0.875 - ((1 - 0.875) / (var(--site--viewport-max) - var(--site--viewport-min)) * var(--site--viewport-min))) * 1rem
      + ((1 - 0.875) / (var(--site--viewport-max) - var(--site--viewport-min))) * 100vw),
    1 * 1rem
  );
  /* 14px → 16px */

  --text--size-main: clamp(
    1 * 1rem,
    ((1 - ((1.125 - 1) / (var(--site--viewport-max) - var(--site--viewport-min)) * var(--site--viewport-min))) * 1rem
      + ((1.125 - 1) / (var(--site--viewport-max) - var(--site--viewport-min))) * 100vw),
    1.125 * 1rem
  );
  /* 16px → 18px */

  --text--size-large: clamp(
    1.125 * 1rem,
    ((1.125 - ((1.25 - 1.125) / (var(--site--viewport-max) - var(--site--viewport-min)) * var(--site--viewport-min))) * 1rem
      + ((1.25 - 1.125) / (var(--site--viewport-max) - var(--site--viewport-min))) * 100vw),
    1.25 * 1rem
  );
  /* 18px → 20px */

  --text--size-h1: clamp(
    2.25 * 1rem,
    ((2.25 - ((4 - 2.25) / (var(--site--viewport-max) - var(--site--viewport-min)) * var(--site--viewport-min))) * 1rem
      + ((4 - 2.25) / (var(--site--viewport-max) - var(--site--viewport-min))) * 100vw),
    4 * 1rem
  );
  /* 36px → 64px */

  /* Text — line heights & tracking */
  --text--line-height-main: 1.5;
  --text--line-height-display: 1.1;
  --text--letter-spacing-eyebrow: 0.08em;

  /* Color — by role
     Phase-10 sweep: sampled from the Figma frames (pixel-band method).
     Ground: pure white (#ffffff everywhere sampled). Ink: pure black
     (title/statement/body glyph cores all #000000). Muted: the CONTACT
     eyebrow sampled #858a94-band → #8a8a8a. Watermark glyphs: #fafafa
     (About R/K) / #fcfcfc (Contact RK) → one token at #fafafa.
     Surface/border had no frame sample — neutralised to sit with the
     new ground (previous values were the phase-1 warm placeholders). */
  --color--bg: #ffffff;
  --color--surface: #f5f5f5;
  --color--text: #000000;
  --color--text-muted: #8a8a8a;
  --color--accent: #000000;
  --color--border: #e5e5e5;
  --color--watermark: #fafafa;
  --color--error: #a94438;    /* form validation ink — no frame sample; a
                                 restrained brick that sits with the mono
                                 palette (tunable) */

  /* Radius */
  --radius--main: 0.25rem;
  --radius--pill: 100vmax;

  /* Component tokens — HOME GRID
     Measured from Figma "Loader 7 - Images In Place" (1440×900 frame):
     item 270×345px, gaps 12px → 270/1440 = 18.75vw, gap = 12/270 of item.
     Prompt 25 feel pass: Josh chose HALF the frame gap (6/270 — the images
     almost touch); the frame's 12/270 survives on the /home-wide/ dev page
     for the customer comparison. */
  --home-grid--item-w: 18.75vw;
  --home-grid--item-ratio: 270 / 345;
  --home-grid--gap: calc(var(--home-grid--item-w) * (6 / 270));

  /* Component tokens — PROJECT HERO
     Measured from Figma "Project Page Hero" (1221×763): image left
     edge at 643px → right pane 47.34vw, full height */
  --project-hero--img-left: calc(100vw * (643 / 1221));
  --project-hero--title-outdent: 0.051em; /* prompt 21 — Selna's "E" carries
                                        ~0.051em more left side-bearing than
                                        the location line's glyph (canvas-
                                        measured); pulled left so the INK
                                        edges align, not just the boxes */
  --project-hint--gap: 0.375em;      /* prompt 21 — arrow tucked closer
                                        (was 0.75em) */
  --project-hint--arrow-w: 1.6em;
  --project-block--overscan: 6%;     /* prompt 20 — block-image mask overscan
                                        each side; must exceed the JS pan
                                        amplitude (PROJECT_SCROLL_FEEL
                                        .blockParallax, 5%) or the pan
                                        exposes the mask edge */
  --project-duo--gap: 0px;           /* prompt 22 — the duo full-bleed images
                                        touch (Josh: no seam); token kept as
                                        the tuning knob */

  /* Component tokens — PROJECT END (prompt 24)
     Measured from "Project Page End/Main Example.png" (964×601 frame):
     progress bar 142×1.5 centred at y27; struck project title ≈13px
     Selna; "The End" ≈26px Selna; ring cards ≈51×61 on a ≈101px-radius
     circle around the exact viewport centre. Vertical geometry and card
     size ride svh (the frame's own height axis); type rides the vw ramp
     like every other text token (964 frame → 1440-normalised). */
  --project-end--bar-w: calc(100vw * (212 / 1440));
  --project-end--bar-top: calc(100svh * (27 / 601));
  --project-end--head-top: calc(100svh * (56 / 601));
  --project-end--title-size: max(0.8125rem, calc(100vw * (19.4 / 1440)));
  --project-end--heading-size: calc(100vw * (39 / 1440));
  --project-end--card-w: calc(100svh * (51 / 601));
  --project-end--card-ratio: 51 / 61;
  --project-end--hit-d: calc(100svh * (300 / 601)); /* prompt 25 — the next-project
                                        link's circular hover/click area: the
                                        ring's outer edge (largest slot radius
                                        0.182vh + half a card) */

  /* Component tokens — MENU
     Measured from loader frame 7 + "Navigation Open - Background blur"
     (1440×900): bar 380×70 at 24px bottom offset; open panel 380×440;
     logo 116 wide; links ≈30px Selna; labels ≈11px Jagger uppercase */
  --menu--w: calc(100vw * (380 / 1440));         /* open width */
  --menu--w-closed: calc(100vw * (300 / 1440));  /* closed bar width (A6 two-part
                                                    motion; frames show near-equal
                                                    widths — flagged, tunable) */
  --menu--bar-h: calc(100vw * (70 / 1440));
  --menu--open-h: calc(100vw * (440 / 1440));
  --menu--offset: calc(100vw * (24 / 1440));
  --menu--logo-w: calc(100vw * (116 / 1440));
  --menu--link-size: calc(100vw * (30 / 1440));
  --menu--link-leading: 1.12; /* A4 — tightened link rhythm; the char-roll
                                 mask and travel derive from this token */
  --text--size-label: max(0.6875rem, calc(100vw * (11 / 1440)));
                              /* Jagger label ramp with an 11px floor
                                 (phase-10 housekeeping — laptops were
                                 rendering below legibility) */
  --menu--label-size: var(--text--size-label);
  --menu--bg: #000000;        /* sampled: the frames' nav bar is pure black */
  --menu--fg: #ffffff;
  --menu--blur: 16px;
  --menu--trail-ink-rgb: 0, 0, 0; /* canvas trail ink (JS reads this token) */

  /* Component tokens — LOADER
     Measured from Figma "Loader 1–4" frames (1440×900):
     logo 124px wide at the left margin, counter ≈42px serif,
     both on the vertical centre line */
  --loader--logo-w: calc(100vw * (124 / 1440));
  --loader--counter-size: calc(100vw * (42 / 1440));
  --loader--counter-gap: calc(100vw * (30 / 1440));

  /* Component tokens — CONTACT
     Lead measure + form column. Statement restored to the frame's
     scale (prompt 16): ≈26px in a ≈345px measure — a meaningful
     step above the sans intro (the phase-12 "small/dense/wide"
     reading is retired). The send icon geometry is read by the JS
     morph at runtime — those two tokens are the single source, no
     JS constant to keep in sync. The message cap bounds the
     auto-growing textarea: label landing pad + five text lines +
     base padding, then inner scroll takes over. */
  --contact--lead-w: calc(100vw * (395 / 1440));  /* prompt 23 — wider (was 345) */
  --contact--form-w: calc(100vw * (375 / 1440));  /* prompt 23 — wider (was 330) */
  --contact--statement-size: max(1.0625rem, calc(100vw * (26 / 1440)));
  --contact--statement-leading: 1.3;
  /* (--contact--send-icon-* retired — prompt 23 replaced the send pill
     with the arrow-shift text control) */
  --contact--message-max-h: calc(1.5em + 0.55em + (1.4em * 5));

  /* Component tokens — ABOUT CARDS (prompt 15)
     Measured from "About Part 2" frames 1–4 (1440×900): display
     heading fills the margin-to-margin line (~1350px for the word
     "Recognition" — size tuned render-fit); press cards 328×356
     images + serif title ≈24px + sans byline; 120% overscan
     wrapper for the image-in-mask parallax (±8% drift stays
     inside the ±10% overscan margin — no blank gaps by
     construction). */
  --about-cards--display-size: calc(100vw * (238 / 1440));
  --about-cards--card-w: calc(100vw * (328 / 1440));
  --about-cards--ratio: 328 / 356;
  --about-cards--overscan: 120%;
  --about-cards--title-size: calc(100vw * (24 / 1440));

  /* Component token — ABOUT HERO STATEMENT WAVE (prompt 17): the light
     grey the words resolve FROM as the blur wave passes; destination is
     the element's own ink. Monochrome by design — no accent colour. */
  --about--wave-from: #d9d9d9;

  /* Component tokens — ABOUT (prompt 18): tightened statement leading for
     the locked-hero composition; body tuck = distance from the hero's
     centre line down to where the biography begins (the hero's empty
     lower half is pulled over). */
  --about--statement-leading: 1.25;
  --about--body-tuck: 21rem;
  --about--lock-rise: 8svh;          /* prompt 25: the JS OVERRIDES this inline
                                        (px) per viewport so the locked pair +
                                        statement group is dead-centre — this
                                        value only serves reduced motion / no-JS.
                                        prompt 20 — the locked hero composition
                                        rises this far across the pin scrub;
                                        keep in sync with ABOUT_TIMING.lockRiseVh */
  --about--hero-text-gap: 0.375rem;  /* prompt 19 — the lines start closer
                                        to the portrait (was gutter/2) */
  --about--bio-gap: 3.25rem;         /* prompt 19 — equal rhythm: statement
                                        → para 1 → para 2 share this beat
                                        (tuck lands the first gap the same) */
  --about-cards--hover-inset: 3.5%;  /* prompt 19 — press-card hover: the
                                        image masks in a little */

  /* Component tokens — ABOUT OUTRO (frame 5): centred Selna
     statement ≈30px in a ~660px measure, sans paragraph in a
     ~440px measure, 200px hairline, Contact arrow link. */
  --about-outro--statement-size: calc(100vw * (30 / 1440));
  --about-outro--statement-w: calc(100vw * (820 / 1440));
  --about-outro--para-w: calc(100vw * (440 / 1440));
  --about-outro--rule-w: calc(100vw * (200 / 1440));
  /* Arrow-shift link (prompt 20, 5.1 — Details.so button-05 hover ported
     to the house curve): on hover the trailing arrow exits right, a twin
     enters from the left and the copy shifts into the vacated slot.
     em-based — the geometry scales with each host's type size. */
  --arrow-shift--time: 0.62s;
  --arrow-shift--lag: 0.08s;         /* the second mover's head-start delay */
  --arrow-shift--arrow-w: 1.6em;
  --arrow-shift--gap: 0.4em;         /* prompt 25 — arrow tucked closer to the copy (was 0.75) */
  --arrow-shift--bleed: 0.25em;      /* hidden-side clearance off the clip edge */
  /* Prompt 25: each arrow slot is its own overflow clip for the blur-loop —
     the loop's drift can never leak out of the host clip (the lead arrow
     used to creep INTO view before hover) nor be cut off by it (the tail's
     exit kick). The slot is one run-out wider than the glyph so the exit
     drift has room inside the host. Keep in sync with
     PROJECT_HINT_ARROW.travelPct (25% of the glyph width) in main.js. */
  --arrow-shift--runout: calc(var(--arrow-shift--arrow-w) * 0.25);
  /* Prompt 25 (About 2.2.1): the Recognition heading decelerates into its
     pin instead of stopping dead — it lags its natural flow by this much
     by the time it rests, so the cards sit this much further down to keep
     the heading→card geometry (and the cards-over-heading exit) exactly as
     before. MUST equal recogApproachVh × (recogApproachStretch − 1) in
     ABOUT_TIMING (22 × 0.7 = 15.4). */
  --about-recog--lag: 15.4svh;
}

/* ------------------------------------------------------------
   2. GLOBAL ELEMENT STYLES
   ------------------------------------------------------------ */

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

html {
  overscroll-behavior: none; /* iOS 16+ honours this — ends the rubber-band
                                fight with the grid drag (research §2 Q6) */
}

body {
  margin: 0;
  overscroll-behavior: none; /* both axes — was x-only (history swipe);
                                vertical rubber-banding fights the grid too */
  font-family: var(--text--font-main);
  font-size: var(--text--size-main);
  line-height: var(--text--line-height-main);
  color: var(--color--text);
  background-color: var(--color--bg);
  -webkit-font-smoothing: antialiased;
}

h1,
h2,
h3,
p {
  margin: 0;
}

img {
  max-width: 100%;
  display: block;
}

a {
  color: inherit;
  text-decoration: none;
}

/* ------------------------------------------------------------
   3. LAYOUT / STRUCTURE
   ------------------------------------------------------------ */

.page-wrap {
  position: relative;
  min-height: 100svh;
}

.main-wrap {
  position: relative;
  min-height: 100svh;
  background-color: var(--color--bg);
}

.padding-global {
  padding-left: var(--site--margin);
  padding-right: var(--site--margin);
}

.container-small  { max-width: var(--max-width--small);  width: 100%; margin: 0 auto; }
.container-medium { max-width: var(--max-width--medium); width: 100%; margin: 0 auto; }
.container-large  { max-width: var(--max-width--main);   width: 100%; margin: 0 auto; }
.container-full   { max-width: 100%;                     width: 100%; margin: 0 auto; }

.padding-section-main {
  padding-top: var(--space--section-main);
  padding-bottom: var(--space--section-main);
}

.padding-section-page-top {
  padding-top: var(--space--section-page-top);
  padding-bottom: var(--space--section-main);
}

/* ------------------------------------------------------------
   4. UTILITIES
   ------------------------------------------------------------ */

.text-style-h1 {
  font-family: var(--text--font-display);
  font-size: var(--text--size-h1);
  line-height: var(--text--line-height-display);
  font-weight: 400;
}

.text-style-large { font-size: var(--text--size-large); }
.text-style-main  { font-size: var(--text--size-main); }
.text-style-small { font-size: var(--text--size-small); }

.text-style-eyebrow {
  font-size: var(--text--size-small);
  text-transform: uppercase;
  letter-spacing: var(--text--letter-spacing-eyebrow);
}

.text-color-muted { color: var(--color--text-muted); }

/* Screen-reader-only utility (form labels etc.) */
.u-sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Underline-link utility (A5) — attribute-driven animated underlines.
   The "alt" variant starts underlined; on hover the line exits, then
   redraws in (double pseudo-element). Ease matches the stagger button. */
/* Unbreakable word groups (prompt 23): SplitText's word delimiter is \s,
   which swallows no-break spaces — a nowrap wrapper survives the split
   (its word masks are inline-blocks that cannot wrap apart). */
.u-nowrap {
  white-space: nowrap;
}

[data-underline-link] {
  text-decoration: none;
  position: relative;
}

[data-underline-link]::before,
[data-underline-link="alt"]::before,
[data-underline-link="alt"]::after {
  content: "";
  position: absolute;
  bottom: -0.0625em;
  left: 0;
  width: 100%;
  height: 0.0625em;
  background-color: currentColor;
  transition: transform 0.735s cubic-bezier(0.625, 0.05, 0, 1);
  transform-origin: right;
  transform: scaleX(0) rotate(0.001deg);
}

[data-underline-link="alt"]::before {
  transform-origin: left;
  /* --email-line: entrance hook (prompt 23) — the contact compose scales
     the resting line from 0 and draws it left→right once the email word
     has landed; everywhere else the variable defaults to 1 (line shown) */
  transform: scaleX(var(--email-line, 1)) rotate(0.001deg);
  transition-delay: 0.3s;
}

/* while GSAP drives the variable per-frame, the hover transition must not
   re-interpolate every step (prompt 23) */
[data-underline-link="alt"][data-email-entering]::before {
  transition: none;
}

[data-underline-link="alt"]::after {
  transform-origin: right;
  transform: scaleX(0) rotate(0.001deg);
  transition-delay: 0s;
}

@media (hover: hover) and (pointer: fine) {
  [data-underline-link="alt"]:hover::before {
    transform-origin: right;
    transform: scaleX(0) rotate(0.001deg);
    transition-delay: 0s;
  }

  [data-underline-link="alt"]:hover::after {
    transform-origin: left;
    transform: scaleX(1) rotate(0.001deg);
    transition-delay: 0.3s;
  }
}

/* ============================================================
   MENU  —  menu
   Used on: all pages (persistent — outside the Barba container)
   Black bar per the frames; expands upward into the panel, with
   a backdrop-blur layer over the page while open. All motion GSAP.
   ============================================================ */

.menu {
  position: fixed;
  bottom: var(--menu--offset);
  left: 50%;
  transform: translateX(-50%);
  z-index: 100;
}

.menu_inner {
  display: flex;
  flex-direction: column-reverse; /* bar sits at the visual bottom; the panel
                                     grows upward out of the overflow clip */
  overflow: hidden;
  width: var(--menu--w-closed);
  height: var(--menu--bar-h);
  background-color: var(--menu--bg);
  color: var(--menu--fg);
  will-change: width, height;
}

.menu_bar {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  height: var(--menu--bar-h);
  padding: 0 calc(var(--menu--bar-h) * 0.3);
}

.menu_logo {
  display: block;
  width: var(--menu--logo-w);
  color: var(--menu--fg);
}

.menu_logo svg {
  display: block;
  width: 100%;
  height: auto;
}

.menu_toggle {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: calc(var(--menu--bar-h) * 0.11);
  width: calc(var(--menu--bar-h) * 0.66);
  height: calc(var(--menu--bar-h) * 0.66);
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--menu--fg);
  cursor: pointer;
}

.menu_toggle-bar {
  display: block;
  flex: none;
  width: calc(var(--menu--bar-h) * 0.5);
  height: 1px;
  background-color: currentColor;
}

.menu_panel {
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  width: var(--menu--w); /* fixed at open width so text never reflows
                            while the bar width animates (A6) */
  height: calc(var(--menu--open-h) - var(--menu--bar-h));
  padding: calc(var(--menu--bar-h) * 0.35) calc(var(--menu--bar-h) * 0.3);
  visibility: hidden; /* opened by GSAP */
}

.menu_label {
  font-family: var(--text--font-jagger);
  font-size: var(--menu--label-size);
  text-transform: uppercase;
  letter-spacing: 0.14em;
  opacity: 0.65;
}

.menu_list {
  list-style: none;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  margin: calc(var(--menu--bar-h) * 0.2) 0 0;
  padding: 0;
}

.menu_item {
  display: block;
  overflow: hidden; /* the mask each link reveals through */
}

.menu_link {
  display: block;
  font-family: var(--text--font-selna);
  font-size: var(--menu--link-size);
  line-height: var(--menu--link-leading);
  color: var(--menu--fg);
}

.menu_link[aria-current="page"] {
  opacity: 0.55;
}

/* Stagger-button hover: each character carries a duplicate glyph one line
   below via text-shadow; hover rolls the line up, per-char delays set
   inline by JS. The roll distance derives from the leading token so the
   mask always matches the line box exactly. */
.menu_link {
  --menu--link-roll: calc(var(--menu--link-leading) * 1em);
}

[data-menu-link-chars] {
  display: inline-block;
  overflow: hidden;
  position: relative;
}

[data-menu-link-chars] > span {
  display: inline-block;
  position: relative;
  text-shadow: 0 var(--menu--link-roll) currentColor;
  transform: translateY(0) rotate(0.001deg);
  transition: transform 0.6s cubic-bezier(0.625, 0.05, 0, 1);
}

@media (hover: hover) and (pointer: fine) {
  .menu_link:hover [data-menu-link-chars] > span {
    transform: translateY(calc(-1 * var(--menu--link-roll))) rotate(0.001deg);
  }
}

.menu_legal {
  margin-top: calc(var(--menu--bar-h) * 0.45);
  font-family: var(--text--font-jagger);
  font-size: var(--menu--label-size);
  text-transform: uppercase;
  letter-spacing: 0.14em;
}

.menu-trail {
  position: fixed;
  inset: 0;
  z-index: 85; /* between the page and the blur — the blur softens it further */
  width: 100vw;
  height: 100vh;
  pointer-events: none;
}

.menu-blur {
  position: fixed;
  inset: 0;
  z-index: 90; /* above the grid, below the nav */
  opacity: 0;
  visibility: hidden; /* toggled by GSAP autoAlpha — also gates pointer events */
  -webkit-backdrop-filter: blur(var(--menu--blur));
  backdrop-filter: blur(var(--menu--blur));
}

/* end: MENU */

/* ============================================================
   HOME GRID  —  section_home-grid
   Used on: Home
   Source markup (4 rows × 8 items) is the Webflow contract.
   The engine (main.js) clones items at runtime, positions every
   tile absolutely and drives them from one ticker. The section
   stays invisible until the engine reveals it (status attribute).
   ============================================================ */

.section_home-grid {
  position: relative;
  width: 100%;
  height: 100svh;
  overflow: clip;
  touch-action: none;
  cursor: grab;
  user-select: none;
  -webkit-user-select: none;
  background-color: var(--color--bg);
  opacity: 1;
  transition: opacity 0.5s ease;
}

.section_home-grid[data-home-grid-status="loading"] {
  opacity: 0;
}

.section_home-grid[data-home-grid-status="dragging"] {
  cursor: grabbing;
}

.home-grid_collection {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  gap: var(--home-grid--gap);
}

.home-grid_row {
  display: flex;
  gap: var(--home-grid--gap);
}

.home-grid_item {
  position: relative;
  display: block;
  flex: none;
  width: var(--home-grid--item-w);
  cursor: inherit; /* links must show grab/grabbing, not the pointer cursor */
  will-change: transform;
  backface-visibility: hidden;
}

.home-grid_mask {
  overflow: hidden;
  width: 100%;
  aspect-ratio: var(--home-grid--item-ratio);
  will-change: transform;
}

.home-grid_img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;        /* with draggable="false" and the section's */
  -webkit-user-drag: none;     /* dragstart guard — kills native drag ghosts */
  user-select: none;
  -webkit-user-select: none;
  will-change: transform;
}

/* end: HOME GRID */

/* ============================================================
   LOADER  —  loader
   Used on: Home (first-load only — hidden by default, shown by
   the Barba `once` timeline per the loader-once doc; the beats
   choreograph the home grid's own rows through the engine API)
   ============================================================ */

.loader {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: none; /* the once timeline sets display: block */
  pointer-events: none;
  background-color: var(--color--bg);
  color: var(--color--text);
}

.loader_logo {
  position: absolute;
  top: 50%;
  left: var(--site--margin);
  transform: translateY(-50%);
  width: var(--loader--logo-w);
  overflow: hidden; /* the mask the logo exits through */
}

.loader_logo svg {
  display: block;
  width: 100%;
  height: auto;
}

.loader_counter {
  position: absolute;
  top: 50%;
  left: 0;
  transform: translateY(-50%);
  overflow: hidden; /* the mask the counter exits through */
  font-family: var(--text--font-selna);
  font-size: var(--loader--counter-size);
  line-height: 1.1;
  white-space: nowrap;
}

.loader_logo-inner,
.loader_counter-inner {
  display: block;
  will-change: transform;
}

/* Digit cells (phase 8 counter redesign) — each character sits in its own
   mask so step changes roll upward per digit (the menu-character language)
   and the exit rises through the same masks. Fixed width keeps the number's
   rhythm steady while digits roll (Selna has no tabular figures) — keep in
   sync with counterDigitEm in HOME_LOADER_TIMING. */
.loader_digit {
  display: inline-block;
  position: relative;
  overflow: hidden;
  width: 0.62em;
  height: 1.1em; /* matches the counter line-height */
  text-align: center;
  vertical-align: top;
}

.loader_digit--sym {
  width: auto;
}

.loader_digit-char {
  display: block;
  height: 100%;
}

/* the incoming character of a roll — parked one line below its mask */
.loader_digit-char + .loader_digit-char {
  position: absolute;
  top: 100%;
  left: 0;
  width: 100%;
}

/* end: LOADER */

/* ============================================================
   ABOUT  —  section_about
   Used on: About
   Vertical document page to the About frames: centred portrait
   hero (title above, caption below), fixed watermark glyphs
   bleeding off-canvas, then a centred text sequence. Pre-hides
   for the entrance live here (attribute-scoped) so nothing
   flashes before the JS takes over; reduced motion neutralises
   them and the JS fades instead.
   ============================================================ */

.section_about {
  position: relative;
  overflow: clip; /* the watermark glyphs bleed past the edges */
}

/* Component tokens — measured from "About Page Hero" (1440×900):
   portrait 375×490 centred; title ≈28px Selna; caption ≈15px;
   watermark glyphs ≈400px Selna at #fafafa */
.section_about,
.section_contact {
  --page--watermark-size: calc(100vw * (400 / 1440));
}

.about_watermarks {
  position: fixed; /* holds while the page scrolls (both frames agree) */
  inset: 0;
  z-index: 0;
  pointer-events: none;
}

.about_watermark {
  position: absolute;
  font-family: var(--text--font-selna);
  font-size: var(--page--watermark-size);
  line-height: 1;
  color: var(--color--watermark);
}

.about_watermark.is-r { top: -0.12em; left: -0.04em; }
.about_watermark.is-k { bottom: -0.14em; right: -0.05em; }

.about_hero {
  position: relative;
  z-index: 1;
  min-height: 100svh;
  display: flex;
  align-items: center;
  justify-content: center;
}

.about_hero-inner {
  width: calc(100vw * (375 / 1440));
  min-width: 16rem;
}

/* The converging lines and the statement travel OVER the portrait during
   the pinned scrub — explicit layers (transforms would otherwise leave
   paint order to DOM position, and the portrait comes between the two). */
.about_hero .about_reveal-mask {
  position: relative;
  z-index: 2;
}

.about_title {
  font-family: var(--text--font-selna);
  font-size: calc(100vw * (28 / 1440));
  font-weight: 400;
  line-height: 1.2;
  /* spacing lives on the mask wrapper below — a margin INSIDE the mask
     makes the mask taller than the text, and the 110% pre-hide shift then
     leaves the ascenders peeking through the extra strip (prompt 17 fix) */
}

.about_hero-inner > .about_reveal-mask:first-child {
  margin-bottom: var(--about--hero-text-gap);
}

/* Statement inside the pinned hero (prompt 17 — 4.2): anchored to the hero
   centre; the scrub drives its y so it always sits clear below the
   portrait's current bottom edge, arriving under the converged lines. */
.about_hero-statement {
  position: absolute;
  top: 50%;
  left: 0;
  right: 0;
  z-index: 2;
  display: flex;
  justify-content: center;
  padding: 0 var(--site--margin);
}

[data-about-statement-wave] {
  opacity: 0; /* invisible until the blur wave plays — JS pins opacity 1 inline */
}

.about_portrait {
  position: relative;
  z-index: 1;
  width: 100%;
  aspect-ratio: 375 / 490;
  overflow: hidden;
}

.about_portrait img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.about_caption {
  margin-top: var(--about--hero-text-gap);
  font-family: var(--text--font-selna);
  font-size: var(--text--size-small);
  text-align: right;
}

.about_body {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  /* Prompt 18: the hero's locked composition sits at ITS centre, leaving
     the hero's lower half empty — the tuck pulls the biography up so it
     sits immediately below the statement (body top = hero centre + tuck). */
  /* the lock-rise keeps the paragraphs the same distance below the
     (now higher) locked statement — prompt 20 */
  margin-top: calc(var(--about--body-tuck) - 50svh - var(--about--lock-rise));
  padding: 0 var(--site--margin) var(--space--section-page-top);
  gap: var(--space--section-main);
}

/* (.about_intro / .about_name / .about_role retired in prompt 17 — the
   pinned hero's converged lines ARE that composition now.) */

.about_statement {
  font-family: var(--text--font-selna);
  font-size: calc(100vw * (30 / 1440));
  line-height: var(--about--statement-leading); /* prompt 18 — tightened;
                                                   the locked-hero read wants
                                                   the lines closer together */
  text-align: center;                           /* prompt 18 — centre-aligned */
  max-width: calc(100vw * (670 / 1440));
  min-width: min(100%, 20rem);
}

.about_bio {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--about--bio-gap); /* prompt 19 — paragraph rhythm, tighter than
                                 the section gap the body keeps for its
                                 larger blocks */
}

.about_paragraph {
  font-family: var(--text--font-main);
  font-size: var(--text--size-small);
  line-height: 1.6;
  max-width: calc(100vw * (440 / 1440));
  min-width: min(100%, 18rem);
}

/* Entrance pre-hides — the JS animates to rest; reduced motion clears */
.about_reveal-mask { overflow: hidden; }

/* Outro pre-hides (prompt 19): statement waves in, paragraph words rise
   (split at init), the rule draws left→right, the contact link rises. */
[data-about-outro-wave] { opacity: 0; }
[data-about-outro-para] { visibility: hidden; }
[data-about-outro-rule] {
  transform: scaleX(0);
  transform-origin: 0% 50%;
}
[data-about-outro-cta] { transform: translateY(110%); }
[data-about-portrait] { clip-path: inset(50% 0% 50% 0%); }
[data-about-rise],
[data-about-scroll] { transform: translateY(110%); }

/* Hero per-character entrance (prompt 16 — the project-title / nav
   language): each WORD is its own overflow clip so wrapped text still
   masks per line-fragment; the chars are the animation targets. The
   spans exist only after the JS split — the element-level pre-hide
   above covers every frame before that. */
[data-about-word] {
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
}

[data-about-char] {
  display: inline-block;
}

@media (prefers-reduced-motion: reduce) {
  [data-about-portrait] { clip-path: inset(0% 0% 0% 0%); }
  [data-about-rise],
  [data-about-scroll] { transform: none; }
  /* No pin, no scrub, no wave — the hero reads as a static column with
     the statement in flow beneath it. */
  [data-about-statement-wave] { opacity: 1; }
  [data-about-outro-wave] { opacity: 1; }
  [data-about-outro-para] { visibility: visible; }
  [data-about-outro-rule],
  [data-about-outro-cta] { transform: none; }
  .about_hero { flex-direction: column; }
  .about_hero-statement {
    position: static;
    margin-top: var(--space--section-main);
  }
}

/* end: ABOUT */

/* ============================================================
   ABOUT CARDS  —  about_recognition / about-card
   Used on: About (below the biography — prompt 15 frames 1–4)
   A display heading, then press cards scattered in a diagonal
   cascade. Each card is ONE self-contained component block
   (Webflow slot contract): outer div = parallax trigger carrying
   the per-card speed attributes; first child = the move wrapper
   (target) so the whole card drifts; inside it the media mask is
   its own trigger with a 120% overscan wrapper (target) so the
   image counter-drifts within a fixed frame. Document order is
   load-bearing: the move wrapper must be the FIRST
   [data-parallax="target"] inside the card so the outer trigger
   binds to it, not to the image wrapper. Positions repeat per
   nth-child(4n+k) — a customer dropping a fifth card into the
   Webflow slot continues the cascade with no code.
   Speed menu (authoring reference): subtle ±10 · medium ±20
   (the no-attribute default) · deep ±35.
   ============================================================ */

.about_recognition {
  /* prompt 19 — pulled up by a third of the section gap so the heading
     is met sooner after the biography */
  margin-top: calc(var(--space--section-main) * -1 / 3);
  position: relative;
  z-index: 1;
  padding-top: calc(100vw * (240 / 1440));
}

/* The pinned heading sits BENEATH the cards (they travel over it as it
   scrubs out); its chars exit upward through their word mask. Prompt 25:
   the PIN is on a wrapper — the heading inside it carries the approach
   transform (decelerating into place), so ScrollTrigger never measures a
   transformed element. */
.about_display-pin {
  position: relative;
  z-index: 1;
}

.about_display {
  position: relative;
  z-index: 1;
  margin-left: var(--site--margin);
  font-family: var(--text--font-selna);
  font-size: var(--about-cards--display-size);
  line-height: 1.05;
  font-weight: 400;
}

.about_cards {
  position: relative;
  z-index: 2; /* over the pinned heading */
  position: relative;
  /* prompt 23 — heading→card-1 distance at 150% of the prompt-19 value;
     prompt 25 — plus the heading's approach lag (it now rests later in the
     scroll), so card 1 meets the resting heading exactly where it did */
  padding-top: calc(100vw * (210 / 1440) + var(--about-recog--lag));
  /* the cascade's last card bottoms out ≈82vw below the list top */
  padding-bottom: calc(100vw * (120 / 1440));
}

.about-card {
  width: var(--about-cards--card-w);
}

/* The cascade (frames 2–4 composed into one strip, 1440 basis):
   A (143, 0) · B (850, +191) · C (378, +522) · D (968, +762).
   Expressed as flow margins so the pattern repeats for any card
   count; negative top margins pull each card up beside its
   predecessor (card total height ≈ 28.9vw at 1440). */
.about-card:nth-child(4n + 1) { margin-left: calc(100vw * (143 / 1440)); margin-top: 0; }
.about-card:nth-child(4n + 2) { margin-left: calc(100vw * (850 / 1440)); margin-top: calc(100vw * (-225 / 1440)); }
.about-card:nth-child(4n + 3) { margin-left: calc(100vw * (378 / 1440)); margin-top: calc(100vw * (-85 / 1440)); }
.about-card:nth-child(4n)     { margin-left: calc(100vw * (968 / 1440)); margin-top: calc(100vw * (-176 / 1440)); }

.about-card_link {
  display: block;
  color: inherit;
  text-decoration: none;
  cursor: pointer;
}

.about-card_media {
  clip-path: inset(0%);
  transition: clip-path 0.45s cubic-bezier(0.625, 0.05, 0, 1); /* osmo */
  /* the image-in-mask trigger: fixed frame, overflow hidden */
  position: relative;
  overflow: hidden;
  width: 100%;
  aspect-ratio: var(--about-cards--ratio);
}

.about-card_link:hover .about-card_media,
.about-card_link:focus-visible .about-card_media {
  clip-path: inset(var(--about-cards--hover-inset)); /* the image masks in a little */
}

.about-card_img-wrap {
  /* the overscan target: taller than the mask, centred, drifts inside */
  position: absolute;
  left: 0;
  right: 0;
  top: calc((var(--about-cards--overscan) - 100%) / -2);
  height: var(--about-cards--overscan);
}

.about-card_img-wrap img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.about-card_title {
  margin-top: calc(100vw * (16 / 1440));
  font-family: var(--text--font-selna);
  font-size: var(--about-cards--title-size);
  line-height: 1.15;
  font-weight: 400;
}

.about-card_byline {
  margin-top: calc(100vw * (4 / 1440));
  font-family: var(--text--font-main);
  font-size: var(--text--size-small);
  line-height: 1.4;
  color: var(--color--text-muted);
}

/* end: ABOUT CARDS */

/* ============================================================
   ABOUT OUTRO  —  about_outro
   Used on: About (frame 5): centred Selna statement, sans
   paragraph, short hairline, Contact arrow link. Text rises use
   the page's existing data-about-scroll mechanism.
   ============================================================ */

.about_outro {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding-top: calc(100vw * (420 / 1440));
  padding-bottom: calc(100vw * (260 / 1440));
}

.about_outro-statement {
  max-width: var(--about-outro--statement-w);
  font-family: var(--text--font-selna);
  font-size: var(--about-outro--statement-size);
  line-height: 1.4;
  font-weight: 400;
}

.about_outro-para {
  max-width: var(--about-outro--para-w);
  margin-top: calc(100vw * (72 / 1440));
  font-family: var(--text--font-main);
  font-size: var(--text--size-small);
  line-height: 1.55;
}

.about_outro-rule {
  width: var(--about-outro--rule-w);
  height: 1px;
  margin-top: calc(100vw * (66 / 1440));
  background-color: var(--color--border);
}

.about_outro-contact {
  margin-top: calc(100vw * (66 / 1440));
  font-family: var(--text--font-main);
  font-size: var(--text--size-small);
  color: var(--color--text);
  text-decoration: none;
}

/* Arrow-shift hover (prompt 20, 5.1 — Details.so button-05, house curve).
   The HOST clip (the link's reveal mask on About, overflow:hidden on the
   contact button and the project-end inner) is the horizontal clip: the
   trailing arrow exits through it, the leading twin waits outside it. The
   copy reserves the arrow slot as padding, so the link never changes width
   mid-hover. Prompt 25: the hover can come from an ANCESTOR host
   ([data-arrow-shift-host] — the project-end circle), and every arrow slot
   is its own overflow clip for the blur-loop (see --arrow-shift--runout). */
[data-arrow-shift] {
  position: relative;
  display: inline-flex;
  align-items: center;
}

[data-arrow-shift] .arrow-shift_copy {
  position: relative;
  z-index: 1;
  padding-right: calc(var(--arrow-shift--arrow-w) + var(--arrow-shift--gap) + var(--arrow-shift--runout));
  transition: transform var(--arrow-shift--time) cubic-bezier(0.625, 0.05, 0, 1);
}

[data-arrow-shift] .arrow-shift_arrow {
  position: absolute;
  top: 50%;
  width: calc(var(--arrow-shift--arrow-w) + var(--arrow-shift--runout));
  overflow: hidden; /* the loop's own mask — its drift never leaves the slot */
  line-height: 0;
  transform: translate(0, -50%);
  transition: transform var(--arrow-shift--time) cubic-bezier(0.625, 0.05, 0, 1);
}

[data-arrow-shift] .arrow-shift_arrow svg {
  display: block;
  width: 100%;
  height: auto;
}

/* the blur-loop rides an INNER span (prompt 23) — GSAP's x/alpha/blur on
   it can never fight the hover shift's CSS transition on the outer arrow.
   The inner is glyph-wide (not slot-wide) so the loop's xPercent travel is
   a fraction of the GLYPH — the run-out room is what it drifts into. */
[data-arrow-shift] .arrow-shift_arrow-inner {
  display: block;
  width: var(--arrow-shift--arrow-w);
}

[data-arrow-shift] .arrow-shift_arrow.is-tail {
  right: 0;
  transition-delay: var(--arrow-shift--lag);
}

[data-arrow-shift] .arrow-shift_arrow.is-lead {
  left: 0;
  transform: translate(calc(-100% - var(--arrow-shift--bleed)), -50%);
  transition-delay: 0s;
}

:is([data-arrow-shift]:hover, [data-arrow-shift]:focus-visible,
    [data-arrow-shift-host]:hover [data-arrow-shift],
    [data-arrow-shift-host]:focus-visible [data-arrow-shift]) .arrow-shift_arrow.is-tail {
  transform: translate(calc(100% + var(--arrow-shift--gap) + var(--arrow-shift--bleed)), -50%);
  transition-delay: 0s;
}

:is([data-arrow-shift]:hover, [data-arrow-shift]:focus-visible,
    [data-arrow-shift-host]:hover [data-arrow-shift],
    [data-arrow-shift-host]:focus-visible [data-arrow-shift]) .arrow-shift_arrow.is-lead {
  transform: translate(0, -50%);
  transition-delay: var(--arrow-shift--lag);
}

:is([data-arrow-shift]:hover, [data-arrow-shift]:focus-visible,
    [data-arrow-shift-host]:hover [data-arrow-shift],
    [data-arrow-shift-host]:focus-visible [data-arrow-shift]) .arrow-shift_copy {
  transform: translate3d(calc(var(--arrow-shift--arrow-w) + var(--arrow-shift--gap)), 0, 0);
}

/* end: ABOUT OUTRO */

/* ============================================================
   CONTACT  —  section_contact
   Used on: Contact
   Single-viewport page — height-locked, never scrolls. Lead
   column top-left (dense statement + intro), underline form at
   the foot. On short "letterbox" viewports the page re-lays to
   two columns: lead left, form right. Field labels float on
   focus, underlines draw in, validation errors roll into
   reserved strips with a shake, and Send is a pill that
   contracts to a spinner dot then expands to "Sent"
   (annnimate-derived, rebuilt for this stack — no MorphSVG).
   Centred RK watermark behind. Pre-hides as on About.
   ============================================================ */

.section_contact {
  position: relative;
  overflow: clip;
  height: 100svh; /* hard lock — this page never scrolls */
  display: flex;
  flex-direction: column;
  padding: var(--site--margin);
  padding-bottom: calc(var(--menu--offset) + var(--menu--bar-h) + var(--site--gutter));
}

.contact_watermark {
  position: fixed;
  inset: 0;
  z-index: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  font-family: var(--text--font-selna);
  font-size: calc(100vw * (300 / 1440));
  line-height: 1;
  color: var(--color--watermark);
}

.contact_inner {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0; /* children must never force the locked height open */
  width: 100%;
}

.contact_lead {
  width: var(--contact--lead-w);
  min-width: min(100%, 20rem);
  max-width: 100%;
}

.contact_statement {
  font-family: var(--text--font-selna);
  font-size: var(--contact--statement-size);
  line-height: var(--contact--statement-leading);
}

.contact_intro {
  margin-top: calc(var(--site--gutter) * 1.5);
  font-family: var(--text--font-main);
  font-size: var(--text--size-small);
  line-height: 1.6;
}

.contact_form-block {
  width: var(--contact--form-w);
  min-width: min(100%, 18rem);
  max-width: 100%;
  margin-top: auto; /* the form sits at the page foot, per the frame */
  padding-top: calc(var(--site--gutter) * 1.5);
}

.contact_label {
  font-family: var(--text--font-jagger);
  font-size: var(--text--size-label);
  text-transform: uppercase;
  letter-spacing: var(--text--letter-spacing-eyebrow);
  color: var(--color--text-muted);
}

.contact_field {
  margin-top: calc(var(--site--gutter) * 0.75); /* the reserved error strip
                                                   below each field carries
                                                   the rest of the rhythm */
}

.contact_field-control {
  position: relative;
}

.contact_input {
  display: block;
  width: 100%;
  padding: 1.5em 0 0.55em; /* top pad is the floated label's landing zone */
  border: 0; /* the resting line is .contact_line below — an element, so
                the entrance can draw it left→right (prompt 19) */
  border-radius: 0;
  background: transparent;
  font-family: var(--text--font-main);
  font-size: var(--text--size-small);
  line-height: 1.4;
  color: var(--color--text);
  resize: none;
  appearance: none;
  -webkit-appearance: none;
}

.contact_input:focus-visible {
  outline: none; /* the drawing underline is the focus indicator */
}

/* Message grows with its content (JS autosize — prompt 16) up to the
   cap, then scrolls inside itself; data-lenis-prevent in the markup
   keeps that inner scroll away from Lenis. */
textarea.contact_input {
  max-height: var(--contact--message-max-h);
  overflow-y: auto;
}

/* Autofill: repaint the UA yellow with the page ground, keep our ink, and
   fire a no-op keyframe the JS listens for to float the label (autofilled
   values are hidden from scripts until first interaction, so value checks
   can't detect them). */
.contact_input:-webkit-autofill {
  -webkit-text-fill-color: var(--color--text);
  caret-color: var(--color--text);
  box-shadow: inset 0 0 0 1000px var(--color--bg);
  transition: background-color 999999s step-end;
  animation-name: contact-autofill-in;
  animation-duration: 0.001s;
}

@keyframes contact-autofill-in {
  from { opacity: 1; }
  to { opacity: 1; }
}

.contact_field-label {
  position: absolute;
  left: 0;
  top: 1.5em; /* sits exactly on the input's text line */
  transform-origin: 0% 50%;
  font-size: var(--text--size-small);
  line-height: 1.4;
  color: var(--color--text-muted);
  pointer-events: none;
}

.contact_line {
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 1px;
  background-color: var(--color--border);
  transform: scaleX(0); /* the entrance draws it; it stays drawn */
  transform-origin: 0% 50%;
}

.contact_underline {
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 1px;
  background-color: var(--color--text);
  transform: scaleX(0);
  transform-origin: 0% 50%;
}

/* Reserved strip — an error appearing must never reflow the fields
   below it; only the masked message rolls. */
.contact_error {
  overflow: hidden;
  height: 1.4em;
  margin-top: 0.3em;
  font-size: calc(var(--text--size-small) * 0.85);
  line-height: 1.4;
  color: var(--color--error);
}

.contact_error-inner {
  display: block;
  transform: translateY(110%);
}

.contact_send {
  /* prompt 23 — the pill is retired: Send is the About CTA's standalone
     arrow-shift text, dark on the page. overflow:hidden doubles as the
     arrow-shift clip (the tail exits through it, the lead waits outside). */
  position: relative;
  display: inline-flex;
  align-items: center;
  overflow: hidden;
  margin-top: calc(var(--site--gutter) * 1.25);
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--color--text);
  font-family: var(--text--font-main);
  font-size: var(--text--size-small);
  line-height: 1.2;
  cursor: pointer;
}

.contact_send:focus-visible {
  outline: 1px solid var(--color--text);
  outline-offset: 3px;
}

.contact_send-mask {
  position: relative;
  display: block;
  overflow: hidden;
  height: 1.2em;
}

.contact_send-label {
  display: block;
  white-space: nowrap;
}

.contact_send-label-next {
  position: absolute;
  top: 0;
  left: 0;
  transform: translateY(100%);
}


/* Entrance pre-hides */
.contact_reveal-mask {
  overflow: hidden;
  /* Side + top padding (with equal negative margins) so the error shake
     (x) and the floated label — which rises just past the control's top
     edge — don't clip against the mask. Deliberately NO bottom padding:
     the rows are short, and any bottom pad would let the parked reveal
     (110% of its own height down) peek through the strip. */
  padding: 0.5rem 0.5rem 0;
  margin: -0.5rem -0.5rem 0;
}

[data-contact-statement] { opacity: 0; }  /* blur-waves in */
[data-contact-intro] { visibility: hidden; } /* split at init, words rise */
[data-contact-row] { opacity: 0; }           /* fades in behind its line */

@media (prefers-reduced-motion: reduce) {
  [data-contact-statement],
  [data-contact-row] { opacity: 1; }
  [data-contact-intro] { visibility: visible; }
  .contact_line { transform: none; }
}

/* "Really thin" (short) viewports: the column stack can't fit inside the
   height lock, so the page re-lays — the lead stays left, the form moves
   to the right and anchors to the foot. Column widths switch to % here:
   the vw tokens assume a tall page and starve at small widths.
   (Raw breakpoint values — media queries can't read custom properties.) */
@media (max-height: 40rem) and (min-width: 40rem) {
  .contact_inner {
    flex-direction: row;
    align-items: flex-end;
    justify-content: space-between;
    gap: var(--site--gutter);
  }

  .contact_lead {
    align-self: flex-start;
    width: 46%;
  }

  .contact_form-block {
    width: 42%;
    margin-top: 0;
    padding-top: 0;
  }
}

/* end: CONTACT */

/* ============================================================
   PROJECT HERO  —  section_project-hero
   Used on: Projects template
   Full-height hero: text left, image pinned to the right of the
   measured Figma line (643/1221 ≈ 52.66vw — deliberately NOT a
   round 50vw; the frames win). The image rect is deterministic
   from layout — the home→project Flip transition lands on it.
   Keep --project-hero--img-left in sync with PROJECT_FLIP in JS.
   ============================================================ */

.section_project-hero {
  position: relative;
  height: 100svh;
}

.project-hero_text {
  position: absolute;
  top: var(--site--margin);
  left: var(--site--margin);
  width: calc(var(--project-hero--img-left) - var(--site--margin) * 2);
}

.project-hero_title {
  font-family: var(--text--font-selna);
  font-size: var(--text--size-h1);
  line-height: 1.15;
  font-weight: 400;
}

/* Ink-edge alignment with the location line (prompt 21): the outdent goes
   on the title's MASK (the first one in the text stack) — shifting the
   title inside its own overflow clip would crop the ink it aligns. The
   em resolves against the title inside, so set the mask's font-size. */
.project-hero_text .project-hero_reveal-mask:first-child {
  font-size: var(--text--size-h1);
  margin-left: calc(-1 * var(--project-hero--title-outdent));
}

.project-hero_location {
  margin-top: calc(var(--site--gutter) / 4); /* prompt 21 — closer (was /2) */
  font-family: var(--text--font-jagger);
  font-size: var(--text--size-small);
  color: var(--color--text-muted);
}

/* Masked-rise reveals — same language as the nav panel: elements rise
   from below an overflow clip (yPercent driven by JS). */
.project-hero_reveal-mask {
  overflow: hidden;
}

/* Per-character title entrance (transitioned arrivals — prompt 12):
   the title splits into word wrappers > char spans, menu-hover
   language. Each WORD is its own overflow clip so a wrapped title
   still masks per line-fragment (chars can never ghost across a
   line below). Word heights are uniform (one line-height), so the
   inline-block bottom-edge baselines stay consistent. */
.project-hero_title [data-project-title-word] {
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
}

.project-hero_title [data-project-title-char] {
  display: inline-block;
}

.project-hero_hint-wrap {
  position: absolute;
  bottom: var(--site--margin);
  left: var(--site--margin);
  display: flex;
  align-items: center;
  gap: var(--project-hint--gap);
}

.project-hero_hint {
  font-family: var(--text--font-jagger);
  font-size: var(--text--size-small);
  color: var(--color--text-muted);
}

/* The looping arrow beside the hint (prompt 20, 2.3) — GSAP owns its
   motion (x / autoAlpha / blur); CSS only parks it invisible so no
   arrival path ever flashes it. */
.project-hero_hint-arrow {
  display: block;
  width: var(--project-hint--arrow-w);
  font-size: var(--text--size-small);
  color: var(--color--text-muted);
  opacity: 0;
}

.project-hero_hint-arrow svg {
  display: block;
  width: 100%;
  height: auto;
}

/* Transitioned arrivals (attribute set at beforeEnter, before first paint):
   text and hero stay hidden until the flight choreography takes over —
   kills the paint→hide→animate flash. Direct loads never carry this. */
[data-project-arrival="flip"] [data-project-hero-reveal],
[data-project-arrival="flip"] [data-project-hero-img] {
  visibility: hidden;
}

.project-hero_media {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: var(--project-hero--img-left);
  overflow: hidden;
}

.project-hero_img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* end: PROJECT HERO */

/* ============================================================
   TRANSITION SHADE  —  transition-shade
   Used on: all pages (persistent chrome — the home↔project
   layered-parallax transition darkens the receding home under
   it). One solid token-black layer, opacity-animated only — a
   CSS filter on the ticking grid would re-rasterise 60+ tiles
   per frame; this is compositor-flat. z sits between the two
   Barba containers, which get z 1 / z 3 inline per direction
   during the transition (cleared at resetPage). Sits BELOW the
   flip layer (clone rides above the shade by construction) and
   below the nav (persistent chrome stays crisp — flagged).
   ============================================================ */

.transition-shade {
  position: fixed;
  inset: 0;
  z-index: 2;
  background-color: var(--color--accent);
  opacity: 0;
  visibility: hidden; /* toggled by GSAP autoAlpha */
  pointer-events: none;
}

/* end: TRANSITION SHADE */

/* ============================================================
   FLIP LAYER  —  flip-layer
   Used on: all pages (persistent — home→project image transition;
   the clone lives here, outside the Barba wrapper, so container
   swaps can never touch it)
   ============================================================ */

.flip-layer {
  position: fixed;
  inset: 0; /* never dvh — iOS toolbar tracking would resize it mid-flight */
  z-index: 80; /* above page content, below the blur layer and nav */
  pointer-events: none;
  /* Scopes the flight's layout writes to this layer's subtree (research
     §2 Q4/Q5). ON THE LAYER ITSELF ONLY: contain on any ANCESTOR of a
     fixed element would capture it as containing block — the complete
     trap list (transform/filter/will-change/contain/content-visibility)
     is in the research report §2 Q4. */
  contain: layout;
}

.flip-clone {
  position: fixed;
  overflow: hidden;
}

.flip-clone img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* end: FLIP LAYER */

/* ============================================================
   VIEWPORT BLOCKER  —  viewport-blocker
   Used on: all pages (persistent chrome — prompt 24). The site is
   not yet responsive: below 1200px a full-viewport card covers
   everything (above the menu and the lightbox) and asks for a
   larger screen. Pure CSS gate — no JS, nothing to tear down;
   delete this section (and the div on each page) when the
   responsive pass lands.
   ============================================================ */

.viewport-blocker {
  position: fixed;
  inset: 0;
  z-index: 300; /* above every chrome layer (menu 100, lightbox 95) */
  display: none;
  background-color: var(--color--bg);
  color: var(--color--text);
  text-align: center;
  padding: var(--site--margin);
  cursor: default;
}

@media (max-width: 1199.98px) {
  .viewport-blocker {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 1.25rem;
  }
}

.viewport-blocker_label {
  margin: 0;
  font-family: var(--text--font-jagger);
  font-size: var(--text--size-label);
  text-transform: uppercase;
  letter-spacing: var(--text--letter-spacing-eyebrow);
  color: var(--color--text-muted);
}

.viewport-blocker_text {
  margin: 0;
  max-width: 22ch;
  font-family: var(--text--font-selna);
  font-size: 1.375rem;
  line-height: 1.45;
}

/* end: VIEWPORT BLOCKER */

/* ============================================================
   PROJECT SCROLL  —  section_project-scroll
   Used on: Projects template
   The whole page is one horizontal track: the hero is the first
   viewport-width panel, the body blocks continue rightward. The
   track transform is driven by the JS controller (Observer +
   lerp); nothing here scrolls natively.
   ============================================================ */

.project-scroll {
  position: relative;
  width: 100%;
  height: 100svh;
  overflow: clip;
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
  background-color: var(--color--bg);
}

.project-scroll_track {
  display: flex;
  height: 100%;
  width: max-content;
  will-change: transform;
}

/* the hero keeps its own section CSS; in the track it is panel one */
.project-scroll_track .section_project-hero {
  flex: none;
  width: 100vw;
  height: 100%;
}

/* end: PROJECT SCROLL */

/* ============================================================
   PROJECT BLOCKS  —  project-block-*
   Used on: Projects template (horizontal panels) + Components
   page (vertical reference list)
   Eight self-contained compositions measured from the Figma
   "Project Block 1–8" frames. Each block locks its frame's
   aspect via --pb-aspect; every figure is placed in % of the
   block box, so the same markup renders identically as a
   full-height horizontal panel or a width-fitted vertical card.
   Each block is a future Webflow component: no cross-block
   dependencies; images are the swap points.
   ============================================================ */

.project-block {
  position: relative;
  flex: none;
  background-color: var(--color--bg);
}

/* horizontal context: full-height panel, width from the frame aspect */
.project-scroll_track .project-block {
  height: 100%;
  width: calc(100svh * var(--pb-aspect, 1.48));
  /* Prompt 24 (1): twice the breathing room between sections — each block
     carries margins equal to its own composition's internal edge insets
     (the --pb-pad-* fractions mirror the nth-child geometry below), so
     every between-section gap is exactly 2× the frame-measured
     whitespace. Full-bleed blocks stay at zero and keep meeting the flow
     edge-to-edge. Scoped to the horizontal track — the components page
     keeps its vertical rhythm. */
  margin-left: calc(100svh * var(--pb-aspect, 1.48) * var(--pb-pad-l, 0));
  margin-right: calc(100svh * var(--pb-aspect, 1.48) * var(--pb-pad-r, 0));
}

.project-scroll_track .project-block-full {
  width: 100vw; /* the full-bleed image owns the whole viewport */
}

.project-block_media {
  position: absolute;
  margin: 0;
  /* prompt 20 (1.1): the media box is the MASK — the image inside is
     overscanned and pans laterally as the panel crosses the viewport */
  overflow: hidden;
}

.project-block_btn {
  display: block;
  width: 100%;
  height: 100%;
  padding: 0;
  border: 0;
  background: transparent;
  cursor: pointer;
}

.project-block_btn:focus-visible {
  outline: 1px solid var(--color--text);
  outline-offset: 3px;
}

.project-block_img {
  /* overscanned each side so the JS pan (5% of mask width) never exposes
     the mask edge — prompt 20 */
  width: calc(100% + var(--project-block--overscan) * 2);
  max-width: none;
  margin-left: calc(-1 * var(--project-block--overscan));
  height: 100%;
  object-fit: cover;
  -webkit-user-drag: none;
}

/* --- 01 · Triptych Row — three portraits, even rhythm (frame 1) --- */
.project-block-triptych { --pb-aspect: 2.4; --pb-pad-l: 0.045; --pb-pad-r: 0.045; }
.project-block-triptych .project-block_media { top: 17.5%; height: 65%; width: 21%; }
.project-block-triptych .project-block_media:nth-child(1) { left: 4.5%; }
.project-block-triptych .project-block_media:nth-child(2) { left: 39.5%; }
.project-block-triptych .project-block_media:nth-child(3) { left: 74.5%; }

/* --- 02 · Overlap Pair, Raised Right — large left running the full
       height (prompt 21: to the bottom of the page), smaller portrait
       riding its right shoulder (frame 2) --- */
.project-block-pair-raised { --pb-aspect: 1.32; --pb-pad-l: 0.08; --pb-pad-r: 0.10; }
.project-block-pair-raised .project-block_media:nth-child(1) { left: 8%; top: 0; width: 59%; height: 100%; }
.project-block-pair-raised .project-block_media:nth-child(2) { left: 62%; top: 7%; width: 28%; height: 57%; z-index: 2; }

/* --- 03 · Full-Bleed Image — one image, edge to edge (frame 3).
       Prompt 22: pans 2.8× the standard amount (data-pan-scale="2.8" in
       the HTML — toned down from 4) — the overscan override must cover
       blockParallax × scale (0.05 × 2.8 = 14%) or the pan exposes the
       mask edge. --- */
.project-block-full { --pb-aspect: 1.48; --project-block--overscan: 15%; }
.project-block-full .project-block_media { inset: 0; width: 100%; height: 100%; }

/* --- 04 · Quartet Row — four portraits, tight gaps (frame 4) --- */
.project-block-quartet { --pb-aspect: 2.42; --pb-pad-l: 0.043; --pb-pad-r: 0.051; }
.project-block-quartet .project-block_media { top: 17%; height: 63.5%; width: 21.3%; }
.project-block-quartet .project-block_media:nth-child(1) { left: 4.3%; }
.project-block-quartet .project-block_media:nth-child(2) { left: 27.4%; }
.project-block-quartet .project-block_media:nth-child(3) { left: 50.5%; }
.project-block-quartet .project-block_media:nth-child(4) { left: 73.6%; }

/* --- 05 · Diagonal Stagger Pair — top-right meets bottom-left at the
       centre line (frame 5) --- */
.project-block-stagger { --pb-aspect: 1.06; --pb-pad-l: 0.105; --pb-pad-r: 0.104; }
.project-block-stagger .project-block_media:nth-child(1) { left: 50%; top: 0; width: 39.6%; height: 57.6%; }
.project-block-stagger .project-block_media:nth-child(2) { left: 10.5%; top: 42.4%; width: 39.6%; height: 57.6%; }

/* --- 06 · Overlap Pair, Anchored Left — large right running the full
       height (prompt 21: to the bottom of the page), smaller portrait
       low on its left flank (frame 6) --- */
.project-block-pair-anchored { --pb-aspect: 1.14; --pb-pad-l: 0.095; --pb-pad-r: 0.096; }
.project-block-pair-anchored .project-block_media:nth-child(1) { left: 24.8%; top: 0; width: 65.6%; height: 100%; }
.project-block-pair-anchored .project-block_media:nth-child(2) { left: 9.5%; top: 49%; width: 31%; height: 47%; z-index: 2; }

/* --- 07 · Feature Landscape Pair — small portrait upper-left, wide
       landscape overlapping to the right (frame 7) --- */
.project-block-feature-landscape { --pb-aspect: 1.22; --pb-pad-l: 0.09; --pb-pad-r: 0.09; }
.project-block-feature-landscape .project-block_media:nth-child(1) { left: 9%; top: 11%; width: 28.6%; height: 43%; }
.project-block-feature-landscape .project-block_media:nth-child(2) { left: 33.6%; top: 22.3%; width: 57.4%; height: 55.7%; z-index: 2; }

/* --- 08 · Cascade Pair — small portrait at the top edge, taller one
       stepping down to the right (frame 8) --- */
.project-block-cascade { --pb-aspect: 0.79; --pb-pad-l: 0.144; --pb-pad-r: 0.141; }
.project-block-cascade .project-block_media:nth-child(1) { left: 14.4%; top: 0; width: 46%; height: 44.7%; }
.project-block-cascade .project-block_media:nth-child(2) { left: 34.7%; top: 40.5%; width: 51.2%; height: 55.6%; z-index: 2; }

/* --- 09 · Duo Full-Bleed — two images sharing the whole viewport,
       edge to edge with one thin seam (prompt 21, new section 2 —
       screenshot only, no frame: seam width is a first-guess token) --- */
.project-block-duo-full { --pb-aspect: 1.48; }
.project-scroll_track .project-block-duo-full { width: 100vw; }
.project-block-duo-full .project-block_media { top: 0; height: 100%; width: calc(50% - var(--project-duo--gap) / 2); }
.project-block-duo-full .project-block_media:nth-child(1) { left: 0; }
.project-block-duo-full .project-block_media:nth-child(2) { left: calc(50% + var(--project-duo--gap) / 2); }

/* --- 10 · Portrait + Landscape — small portrait upper-left, the larger
       landscape seated centre-right over its shoulder (prompt 21, new
       section 3 — measured from the screenshot) --- */
.project-block-portrait-landscape { --pb-aspect: 1.33; --pb-pad-l: 0.09; --pb-pad-r: 0.10; }
.project-block-portrait-landscape .project-block_media:nth-child(1) { left: 9%; top: 14%; width: 23%; height: 40%; }
.project-block-portrait-landscape .project-block_media:nth-child(2) { left: 30.5%; top: 24%; width: 59.5%; height: 60%; z-index: 2; }

/* end: PROJECT BLOCKS */

/* ============================================================
   PROJECT END  —  project-end
   Used on: Projects template (the last panel of the horizontal
   track). The closing composition from the "Project Page End"
   frame: a short scroll-on bar top-centre, the finished project's
   name struck through above "The End", and the project's own
   images gathered into a ring around the next-project link. The
   ring cards are cloned at runtime from the page's block images
   (data-project-end-ring stays empty in the markup — nothing to
   author per project); all ring/bar/title motion is JS-owned
   (PROJECT_END in main.js). Pre-hides live here so no arrival
   path ever flashes the resolved state.
   ============================================================ */

.project-end {
  position: relative;
  flex: none;
  width: 100vw;
  height: 100%;
  background-color: var(--color--bg);
}

/* The scroll-on bar — reads as a short rule until the user keeps
   scrolling past the end; the black fill is the commitment meter
   (scaleX driven by JS from the overscroll progress). */
.project-end_progress {
  position: absolute;
  top: var(--project-end--bar-top);
  left: 50%;
  transform: translateX(-50%);
  width: var(--project-end--bar-w);
  height: 1.5px;
  background-color: var(--color--border);
}

.project-end_progress-fill {
  position: absolute;
  inset: 0;
  background-color: var(--color--text);
  transform: scaleX(0);
  transform-origin: 0% 50%;
}

.project-end_head {
  position: absolute;
  top: var(--project-end--head-top);
  left: 0;
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  z-index: 3;
}

.project-end_title {
  margin: 0;
  font-family: var(--text--font-selna);
  font-size: var(--project-end--title-size);
  line-height: 1.2;
}

/* The struck title: blurs in as ink, the line draws across it, and the
   text resolves to muted as the line crosses its centre (JS-owned).
   currentColor on the strike means the one colour tween carries both. */
.project-end_title-inner {
  position: relative;
  display: inline-block;
  opacity: 0; /* pre-hide — the head sequence owns the entrance */
  color: var(--color--text);
}

.project-end_strike {
  position: absolute;
  left: -0.15em;
  right: -0.15em;
  top: 52%;
  height: 1px;
  background-color: currentColor;
  transform: scaleX(0);
  transform-origin: 0% 50%;
}

.project-end_heading {
  margin: 0.1em 0 0;
  font-family: var(--text--font-selna);
  font-size: var(--project-end--heading-size);
  font-weight: 400;
  line-height: var(--text--line-height-display);
  visibility: hidden; /* pre-hide — split chars rise through word masks */
}

/* The End — word-mask > char grammar (project-title language) */
.project-end_heading [data-end-word] {
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
}

.project-end_heading [data-end-char] {
  display: inline-block;
}

/* The ring — JS positions every card from the viewport centre; the wrap
   carries the pointer-tilt perspective. Decorative: never interactive. */
.project-end_ring {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
}

.project-end_card {
  position: absolute;
  left: 50%;
  top: 50%;
  width: var(--project-end--card-w);
  aspect-ratio: var(--project-end--card-ratio);
  overflow: hidden;
}

.project-end_card-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  -webkit-user-drag: none;
}

/* The next-project link — the house arrow-shift control, dead centre,
   revealed by the JS as the ring pulls clear of it. Prompt 25: the link
   is the whole RING'S circle (data-arrow-shift-host — hovering anywhere
   among the orbiting images arms the arrow shift and clicking anywhere
   there navigates); the inner span carries the arrow-shift clip. The ring
   cards are pointer-events:none, so the hover falls through to this. */
.project-end_next {
  position: absolute;
  left: 50%;
  top: 50%;
  width: var(--project-end--hit-d);
  height: var(--project-end--hit-d);
  transform: translate(-50%, -50%);
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  font-family: var(--text--font-main);
  font-size: var(--text--size-small);
  color: var(--color--text);
  text-decoration: none;
  opacity: 0;              /* JS resolves it in with the ring opening */
  pointer-events: none;    /* enabled once the centre is clear */
}

.project-end_next-inner {
  overflow: hidden;        /* the arrow-shift clip (contact-send pattern) —
                              the lead arrow waits outside it */
}

@media (prefers-reduced-motion: reduce) {
  /* Static closing state: struck muted title, The End shown, ring formed
     (JS lays it out once), link live, no scroll-on bar (the overscroll
     zone never arms — the link is the way forward). */
  .project-end_title-inner { opacity: 1; color: var(--color--text-muted); }
  .project-end_strike { transform: none; }
  .project-end_heading { visibility: visible; }
  .project-end_next { opacity: 1; pointer-events: auto; }
  .project-end_progress { display: none; }
}

/* end: PROJECT END */

/* ============================================================
   LIGHTBOX  —  lightbox
   Used on: Projects template + Components page
   Overlay lives on <body> (the project track is a transform
   context — fixed positioning inside it would break). The clone
   flies between the source rect and the centred rect; the
   backdrop dims beneath it. All motion GSAP.
   ============================================================ */

.lightbox {
  position: fixed;
  inset: 0;
  z-index: 95; /* above the flip layer/trail/blur, below the nav */
  visibility: hidden; /* opened by GSAP */
}

.lightbox_backdrop {
  position: absolute;
  inset: 0;
  background-color: var(--menu--bg);
  opacity: 0;
}

.lightbox_img {
  position: fixed;
  overflow: hidden;
}

.lightbox_img img {
  width: 100%;
  height: 100%;
  object-fit: cover; /* the box tweens toward the natural ratio, so the
                        crop releases continuously — no jump at rest */
  display: block;
}

/* end: LIGHTBOX */

/* ============================================================
   COMPONENTS  —  section_components
   Used on: Components (internal component-library page)
   The client's reference page: every project block in sequence,
   labelled, scrolling VERTICALLY (the horizontal system never
   initialises here — no [data-project-scroll] wrapper exists).
   Blocks keep their frame aspect, fitted to the page width and
   capped so tall blocks don't overwhelm the viewport.
   ============================================================ */

.components {
  display: flex;
  flex-direction: column;
  gap: calc(var(--site--gutter) * 1.5);
}

.components_list {
  display: flex;
  flex-direction: column;
  gap: var(--space--section-main);
  margin-top: var(--space--section-main);
}

.components_label {
  font-family: var(--text--font-jagger);
  font-size: var(--text--size-small);
  text-transform: uppercase;
  letter-spacing: var(--text--letter-spacing-eyebrow);
  color: var(--color--text-muted);
  margin-bottom: calc(var(--site--gutter) / 2);
}

.components_item .project-block {
  width: min(100%, calc(85svh * var(--pb-aspect, 1.48)));
  aspect-ratio: var(--pb-aspect, 1.48);
  outline: 1px solid var(--color--border); /* shows each block's true bounds */
}

/* end: COMPONENTS */

/* ------------------------------------------------------------
   6. THEME OVERRIDES
   ------------------------------------------------------------ */

[data-page-theme="dark"] {
  --color--bg: #000000;
  --color--surface: #141414;
  --color--text: #ffffff;
  --color--text-muted: #8a8a8a;
  --color--accent: #ffffff;
  --color--border: #262626;
  --color--watermark: #0a0a0a;
}

/* ============================================================
   LENIS  —  smooth scroll (site-wide library CSS)
   Used on: all pages (the instance is created once in the
   boilerplate's initLenis; it only has anything to move on
   pages whose document actually overflows — About, Components).
   These are the lenis@1.2.3 distribution rules inlined so the
   Slater stylesheet stays the single CSS delivery — no extra
   CDN <link>. [data-lenis-prevent] marks nested scrollers
   (e.g. the contact message field) as native.
   ============================================================ */

html.lenis,
html.lenis body {
  height: auto;
}

.lenis.lenis-smooth {
  scroll-behavior: auto !important;
}

.lenis.lenis-smooth [data-lenis-prevent] {
  overscroll-behavior: contain;
}

.lenis.lenis-stopped {
  overflow: hidden;
}

.lenis.lenis-smooth iframe {
  pointer-events: none;
}

/* end: LENIS */

/* ============================================================
   TEXT REVEAL  —  [data-reveal-01] (Details.so Slide Up Text Reveal)
   Used on: About biography paragraphs — page-generic (any element
   carrying the attribute). This block is the resource's integration
   contract verbatim: the pre-hide stops the text flashing before
   the split runs, and the generated masks need the clip margins or
   descenders/ascenders shear at the mask edges. The .line/.word/
   .char and *-mask class names are generated by SplitText — never
   rename them.
   ============================================================ */

[data-reveal-01] {
  visibility: hidden;
}

[data-reveal-01] > * {
  margin-bottom: -0.18em;
}

.word-mask,
.char-mask {
  vertical-align: top;
}

.line-mask,
.word-mask,
.char-mask {
  overflow: clip;
  overflow-clip-margin: 0.06em;
}

.line-mask > *,
.word-mask > *,
.char-mask > * {
  padding-bottom: 0.18em;
  will-change: transform;
}

@media (prefers-reduced-motion: reduce) {
  [data-reveal-01] {
    visibility: visible; /* no split, no motion — the text is simply there */
  }
}

/* end: TEXT REVEAL */
