/* ============================================================
   site-fixes.css — hand-written overrides for this site.
   Loaded LAST, after all generated CSS.
   ============================================================ */

/* ------------------------------------------------------------
   FIX — the horizontal scroll bug
   ------------------------------------------------------------
   CAUSE
   The original design canvas is 1920px wide. Decorative layers are
   absolutely positioned against that canvas and bleed past the
   viewport — e.g. `.studio-1wf7js2` is 2240px wide and ends at
   x=1760 in a 1280px viewport. Nothing clips them, so the
   document's scrollWidth grows past the viewport (measured 1760
   at 1280px) and a horizontal scrollbar appears.

   FIX
   Clip the overflow on the ROOT element only.

   WHY THIS EXACT SELECTOR — three other approaches were tried
   and each one broke the page. Do not "simplify" this:

   1. `body { overflow-x: clip }` and
      `.studio-CYG9j { overflow-x: clip }` (the tall page wrapper)
      BOTH make the entire hero backdrop stack — the rotating
      technical-drawing layers — stop painting. Verified by A/B
      toggling: every box keeps its exact geometry (identical
      getBoundingClientRect on all ancestors), only the paint
      disappears. Clipping a composited, transform-animated
      subtree at those levels drops it.

   2. `html { overflow-x: clip }` does NOT stop the scroll here.
      `clip` leaves the root scrollable, so the scrollbar remained.

   `overflow-x: hidden` on `html` alone stops the scroll AND keeps
   the backdrop painting.

   STICKY IS SAFE
   The usual objection to `overflow-x: hidden` is that it breaks
   `position: sticky` — this site leans on sticky heavily (the
   4000px "UFO" scroll sequence). That failure mode applies to
   INTERMEDIATE ancestors, which become the sticky scrollport.
   On the root element the scrollport is still the viewport, so
   sticky behaves exactly as before. Verified by scrolling the
   UFO section and confirming its Sticky child pins.
   ------------------------------------------------------------ */

html {
  overflow-x: hidden;
}

/* The vendor badge that used to sit at the bottom-right is gone from the
   markup and from the bundle entirely, so no hiding rule is needed here. */

/* "DESIGNER" hero display type — swapped from Inter Tight to the VHS-intro's
   pixel font so the giant background word matches the site's glitch identity.
   Visitor TT1 BRK is much narrower per-glyph than Inter Tight Black, so the
   size is scaled up to keep the same left-to-right span as the original. */
@font-face {
  font-family: "Visitor TT1 BRK";
  src: url("../../vhs-intro/vhs-assets/visitor-brk/visitor1.woff2") format("woff2"),
       url("../../vhs-intro/vhs-assets/visitor-brk.visitor-tt1-brk.ttf") format("truetype");
  font-display: swap;
}

.studio-1k8s2xl h1.studio-text,
.studio-1wwnokt h1.studio-text {
  font-family: "Visitor TT1 BRK", "Courier New", monospace !important;
  font-size: 340px !important;
}

/* The giant hero word — "DESIGNER" -> "CRAFTSMAN" -> now "DIRECTOR" (2026-08-06).
   The site's own fit-text logic auto-scales this to fill its container width, so
   font-size alone cannot shrink it; a transform scale applied AFTER that auto-fit
   is the only lever.
   ⚠️ THE HORIZONTAL OFFSET IS GONE, AND IT MUST STAY GONE. It was
   `translate(-211px, -40px)`: a hand-tuned left shift that anchored the word to
   the nav column when it read "CRAFTSMAN". That number was measured for a
   NINE-letter word — "DIRECTOR" is eight, so the same shift pushed the shorter
   word visibly off-centre. Ehsan asked for it centred on the screen, so the X
   offset is 0 and the word now sits wherever its own centred box puts it.
   Any future change of this word must NOT reintroduce a fixed px offset: it is
   only ever correct for the exact string it was measured against. */
.studio-1k8s2xl h1.studio-text,
.studio-1wwnokt h1.studio-text {
  transform: translate(0, -40px) scale(0.585);
  transform-origin: center;
  text-align: center !important;
}

/* "Craftsman" is one letter longer than "DESIGNER" — its trailing "N" was
   getting clipped by the SVG's own viewBox box (browsers default <svg> to
   overflow:hidden regardless of the foreignObject inside it already being
   overflow:visible). Forcing it visible so the full word always paints. */
svg.studio-1k8s2xl,
svg.studio-1wwnokt {
  overflow: visible !important;
}

/* Nav links (ABOUT/WORKS/MY APPROACH/CONTACT) and the "THE ART OF PORTFOLIO"
   button copy switched to the same Visitor TT1 BRK pixel font as the hero
   word, for a consistent glitch/VHS identity. Scoped by ancestor container
   since .studio-text is a generic class used all over the page. */
[class*="9fvcu1"] .studio-text,
[class*="u1i29m"] .studio-text {
  font-family: "Visitor TT1 BRK", "Courier New", monospace !important;
}

/* Hero navbar ticker ("Elevating Digital Excellence. Specialize in design
   elements...") removed per direction. Its own element carried a horizontal
   mask-image fade (transparent -> opaque -> transparent) that read as a dark
   overlay/shadow on both ends against the black backdrop — hiding the whole
   element removes the text and that fade together, since they were the same
   node. Matched by attribute-substring so it survives the framer->studio
   class rename at runtime. */
[class*="1n40vvh-container"] {
  display: none !important;
}

/* Root background. This is only a backstop — the colour you actually SEE on
   this site comes from opaque page-tall wrappers (.studio-CYG9j, #about-01),
   which paint over html/body no matter what is set here. Measured on the live
   page; see paintUpperBackground() in ln-hero.js. Kept in sync with the
   below-the-character-section colour so any gap can't flash a wrong shade. */
html {
  background: #10141A !important;
}

/* Fin-Tech card carousel: the rotating cards + label were deleted, but the "BG"
   panel behind them stays and gets a top-to-bottom gradient. Its real background
   source couldn't be pinned down (no CSS rule or inline style defines a color on
   this container in either markup or the compiled bundle), so this forces it
   directly rather than guessing at a JS-computed value. */
.framer-x0r1bo,
.studio-x0r1bo {
  background: linear-gradient(180deg, #020d1d 0%, #00040a 100%) !important;
}

/* HERO background (see SECTION-MAP.md). #my-hero-section has no background of
   its own; what you see is a full-size backdrop layer INSIDE it —
   `data-framer-name="Variant 1"`, h1343 starting at doc y -299, so it spans the
   whole hero — carrying an INLINE `style="background-color:rgb(16, 20, 26)"` in
   index.html. Being inline it paints over the page-level #000307 gradient that
   paintUpperBackground() puts on .studio-CYG9j, which is why the hero read
   lighter than the black immediately below it.
   `!important` is required here specifically to beat that inline style. */
.studio-e6z611,
.framer-e6z611 {
  background-color: #000307 !important;
}

/* WORK-SECTION fade divider (see SECTION-MAP.md). The gradient does NOT live
   on #work-section — it lives on its sticky child #oblasss / .studio-1yjldl3,
   whose compiled rule is:
     linear-gradient(180deg, rgba(16,20,26,0) 0%, rgb(16,20,26) 41.157%,
                     rgb(16,20,26) 100%)
   i.e. transparent at the top ramping to a solid single colour by ~41% down.
   Recoloured #10141A -> #000307 on request. The stop positions are copied
   byte-for-byte from the original so only the colour changes; if you need to
   move the ramp, change the percentages here, not in the bundle. */
.studio-1yjldl3,
.framer-1yjldl3 {
  background: linear-gradient(180deg,
              rgba(0, 3, 7, 0) 0%,
              rgb(0, 3, 7) 41.15672916541066%,
              rgb(0, 3, 7) 100%) !important;
}

/* Landonorris.com-style layered hero (cloned mechanism, own 3 Character
   images + Dither WebGL background). Built at the exact spot the "View All
   Works" rotating card carousel used to sit (deleted 2026-08-03, see
   HANDOFF.md). No frame/border/radius by request — sits directly on canvas. */
.ln-hero-wrap {
  position: relative;
  /* Full-bleed edge-to-edge breakout: the parent section (.studio-19qw8we)
     has 24px side padding, so width:100% was resolving to the padded
     content box (1422px of a 1470px viewport), not the real screen edge.
     Standard vw-breakout cancels that regardless of the parent's padding
     value. Confirmed live: rectLeft was 24px before this fix, must be 0. */
  width: 100vw;
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
  /* 300vh -> 600vh (2026-08-05): the burn now finishes in the first 42% and the
     character holds the screen ALONE for ~2 scrolls before the INVEST IN
     ACTUATORS headline draws on. ⚠️ BURN_END/TEXT_START in ln-hero.js are
     fractions OF THIS HEIGHT — change them together. */
  /* ⚠️ 600vh -> 450vh (2026-08-07, Ehsan) AND THE CUT IS FROM THE BOTTOM, NOT
     THE TOP. His words: "the actual long scroll is actually on the character
     itself when you see the character, it takes you couple of scrolls until the
     text (INVEST IN ACTUATORS) appears". So the burn is NOT what drags — the
     HOLD after it is. The burn keeps its absolute length (252vh, unchanged);
     the hold is halved from 180vh to 90vh. See BURN_END / TEXT_START in
     ln-hero.js, which are FRACTIONS of this height and must be recomputed
     whenever it changes, or the burn silently retimes. */
  height: 450vh;
  padding: 0;
  border: none;
  border-radius: 0;
}

.ln-hero-sticky {
  position: sticky;
  top: 0;
  width: 100%;
  height: 100vh;
  overflow: hidden;
  margin: 0;
  padding: 0;
  border: none;
  border-radius: 0;
  background: transparent; /* no section background — the page's own
    html/body background (#00040a, set globally above) shows through */
}

.ln-hero-layers {
  position: absolute;
  inset: 0;
}

.ln-hero-face {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  object-position: center;
  border: none;
  border-radius: 0;
  box-shadow: none;
  pointer-events: none;
}

/* Face/Hover: as of 2026-08-03 (later same session) these source files were
   reprocessed into REAL alpha-channel cutouts (Python/PIL, luma-threshold +
   soft ramp, corner-verified alpha=0) — see HANDOFF.md. mix-blend-mode:
   screen is no longer needed (that was a render-time workaround for
   opaque-black source files) and would distort color where layers overlap
   now that real transparency exists, so it's removed here. Wireframe wave
   layer deleted entirely per explicit instruction (same date). */
.ln-hero-face {
  opacity: 1;
}

/* HALO CURSOR MASK DELETED (2026-08-04, Ehsan's request). The Hover layer
   used to be revealed inside a `radial-gradient(circle 320px at var(--hx)
   var(--hy))` mask that tracked the pointer, and .ln-hero-crt carried the
   identical mask so the CRT overlay stayed inside the same circle. Both
   masks, the --hx/--hy custom properties that drove them, the .ln-hero-hover
   img layer and the whole .ln-hero-crt element are gone.

   Replaced by a dither-shader reveal: .ln-hero-reveal (below) renders BOTH
   character layers plus the entire CRT treatment in WebGL2, dissolving
   Face -> Hover through an ordered-dither stipple on hover, panel-wide
   instead of in a cursor spotlight. Everything the old CSS documented — the
   cool-vs-cool channel split that can never resolve to white, the 3px
   screen-space pixel grid, the red-pinned-at-zero stipple that fixed the
   white halo, no blur/bloom — is preserved as shader code and comments in
   assets/scripts/ln-reveal.js. Don't reintroduce a mask here. */
.ln-hero-reveal {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  pointer-events: none;
  border: none;
  border-radius: 0;
}

/* Burning-page effect canvas — sits above the character layers so the panel
   burns away to reveal the page beneath as the hero's sticky runway
   scrolls. Rendered by ln-burn.js (original shader; technique referenced
   from shopify.com/editions/winter2026, no code or assets taken). */
.ln-hero-burn {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  pointer-events: none;
  border: none;
  border-radius: 0;
}

/* Split page background: header down to the CHARACTER section is #000307 (the
   same colour as the burn layer's unburned panel, so the burn canvas is
   invisible against the page until it starts burning); the character section
   onward is #10141A. There is NO CSS rule for it here on purpose — it is a
   hard-stop gradient applied to two page-tall wrappers from JS, because the
   boundary is a pixel offset inside one of them. See paintUpperBackground()
   in ln-hero.js, which documents why an .ln-bg-upper band at z-index:-1 could
   never work on this site. */

@media (prefers-reduced-motion: reduce) {
  .ln-hero-wrap {
    height: 100vh;
  }
  .ln-hero-sticky {
    position: relative;
  }
}

/* Xelnium branding pass on the fixed "Home" nav + footer (2026-08-03).
   Hide the native Framer hover-swap (SVG vector shape) — replaced by the
   real Xelnium animated logo, injected by ln-xelnium-nav.js.
   ROOT CAUSE (verified live via DOM scan, not guessed): this component has TWO
   native logo containers, not one — `studio-lnihvz-container` AND
   `studio-jswwm3-container` (confirmed: the bundle styles them together in
   a single rule, `.studio-1whQZ .studio-lnihvz-container,
   .studio-1whQZ .studio-jswwm3-container { width:61px; height:63px }`).
   Earlier passes only hid `jswwm3`, so `lnihvz` kept rendering — that was
   the second logo Ehsan saw. Rather than chase variant classnames one by
   one, hide EVERY `*-container` child of the Home link: that catches both
   present variants plus any future one. The "Home" text wrapper
   (`studio-1dcln91`) has no `-container` suffix so it is untouched, and our
   own overlay is appended to #cikdf6 itself (sibling of the <a>), so it is
   outside this selector entirely. */
#cikdf6 a > div[class*="-container"] {
  display: none !important;
}

/* Home nav gated to section 3+ (ln-xelnium-nav.js). #cikdf6 itself has a
   LIVE Web Animations API instance driving its own opacity (Framer's own
   scroll-linked fade, confirmed via element.getAnimations()) — that beats
   any CSS we write on the element directly, !important or not. Fix: these
   rules target a plain wrapper div ln-xelnium-nav.js inserts AROUND
   #cikdf6, which Framer's animation never touches. */
.ln-home-gate-wrapper {
  opacity: 0;
  transition: opacity 200ms ease-out;
}
.ln-home-gate-wrapper:not(.ln-home-visible) {
  pointer-events: none;
}
.ln-home-gate-wrapper.ln-home-visible {
  opacity: 1;
}

.ln-xel-hover-logo {
  /* Sit centered INSIDE #cikdf6, exactly where the native logo used to
     render — not offset outside it. Was `left: calc(100% + 12px)`, which
     pushed it clear of the container (measured live: overlay at x=222 while
     the container spans x=48..210), so it floated off to the right of the
     nav instead of replacing the Home mark in place. */
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  /* 64 -> 78px (2026-08-06, Ehsan: "make it just a little bit bigger"). */
  width: 78px;
  height: 78px;
  opacity: 0;
  transition: opacity 180ms ease-out;
  pointer-events: none;
}
.ln-xel-hover-logo.is-visible {
  opacity: 1;
}
.ln-xel-hover-logo-frame {
  width: 100%;
  height: 100%;
  border: none;
  background: transparent;
  pointer-events: none;
}

/* ⚠️ NAV OVER A LIGHT SECTION — the fix for "on white frames YOU CAN NOT SEE IT".
   `ln-nav-on-light` is set on <html> by ln-xelnium-nav.js, which samples the real
   painted background under the nav rather than trusting a scroll range.
   The X mark is animated INSIDE AN IFRAME, so its colours cannot be restyled from
   this document — `filter: invert()` on the frame is what turns the white mark
   black, and it is applied to the iframe element itself, which needs no access to
   the child document. hue-rotate compensates so the blue split does not invert to
   orange. The Home label and the language toggle carry the identical defect for
   the identical reason, so they flip on the same signal. */
html.ln-nav-on-light .ln-xel-hover-logo-frame {
  filter: invert(1) hue-rotate(180deg);
}
/* ⚠️ HOME AND THE LANGUAGE TOGGLE MUST STAY THE SAME SIZE (Ehsan, 2026-08-06:
   "match the font SIZE of HOME and Language toggle (BOTH)"). They are the two
   fixed marks in the top corners and read as a pair, so a mismatch is obvious.
   HOME had no size rule of its own — it inherited Framer's — so it is pinned
   here explicitly. The toggle's matching `font-size: 21px` lives in the
   `.ln-lang-toggle` block inside index.html. NOTHING LINKS THE TWO NUMBERS:
   change one and you must change the other. */
.ln-home-gate-wrapper a,
.ln-home-gate-wrapper a * {
  font-size: 21px !important;
  line-height: 1 !important;
  letter-spacing: .10em !important;
}

html.ln-nav-on-light .ln-home-gate-wrapper a,
html.ln-nav-on-light .ln-home-gate-wrapper a * {
  color: #0B1220 !important;
  -webkit-text-fill-color: #0B1220 !important;
  text-shadow: none !important;
}
html.ln-nav-on-light .ln-lang-toggle {
  color: #0B1220;
  text-shadow: -1px 0 0 rgba(9, 78, 188, .38), 1px 0 0 rgba(0, 40, 120, .25);
  opacity: 1;
}

.ln-xel-footer-logo {
  display: inline-block;
  width: 28px;
  height: 28px;
  vertical-align: middle;
  margin-right: 10px;
}
.ln-xel-footer-logo-frame {
  width: 100%;
  height: 100%;
  border: none;
}

/* BIKE section (2026-08-04). Replaces the deleted MOBILE-APPS-BAND. Full-bleed
   left-to-right on a transparent background, so the page colour shows through.
   The vw-breakout cancels the parent section's 24px side padding — same trick as
   .ln-hero-wrap, which is why width:100% is not enough here.
   There is ONE canvas and nothing hidden: three.js renders the scene to a render
   target and the DAVID shader draws that to this canvas, which also owns the
   pointer events for click-drag orbit. (An earlier build used a hidden
   model-viewer and could not be dragged — it only redraws when it decides to,
   and a hidden element never does. See HANDOFF.md.)

   SCROLL LOCK (2026-08-04): same runway pattern as .ln-hero-wrap — a tall
   wrapper with a sticky 100vh child, so the bike holds the viewport for a couple
   of scroll ticks instead of flying past. Room to extend this later. */
.ln-bike-wrap {
  position: relative;
  width: 100vw;
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
  /* Scroll runway; the sticky child is what stays on screen. 300vh -> 1000vh on
     2026-08-05, when the section became a three-act story: free bike, then the
     move-left + THRESHOLD dissolve, then 16 sheets stacking on the right.
     ⚠️ TOTAL_VH in ln-bike.js must match this number — nothing links them. */
  height: 500vh;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
  /* NO overflow:hidden here — `overflow` on any ancestor of a sticky element
     silently kills the stickiness. It lives on .ln-bike-sticky instead. */
}

.ln-bike-sticky {
  position: sticky;
  top: 0;
  width: 100%;
  height: 100vh;
  overflow: hidden;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
}

.ln-bike-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  background: transparent;
  cursor: grab;          /* signals that it is draggable */
  touch-action: pan-y;   /* vertical swipe still scrolls the page on touch */
}

.ln-bike-canvas:active { cursor: grabbing; }

/* The BIKE card deck (.ln-bike-stack*) is DELETED — Ehsan, 2026-08-05:
   "DELETE THEM ALL 16". Replaced by .ln-bike-headline below. */

/* SORA — Ehsan's chosen headline face, vendored at assets/Sora/. The VARIABLE
   font is used so one file covers the whole weight range. */
@font-face {
  font-family: "Sora";
  src: url("../Sora/Sora-VariableFont_wght.ttf") format("truetype-variations");
  font-weight: 100 800;
  font-display: swap;
}

/* BIKE headline — BEHIND the 3D bike (z-index 0 against the canvas's 1).
   Font size is computed in ln-bike.js from the viewport rather than hard-coded,
   so it fills the frame without clipping. */
.ln-bike-headline {
  position: absolute;
  inset: 0;
  z-index: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: flex-end;
  padding: 0 5vw;
  font-family: "Sora", "Inter", sans-serif;
  font-weight: 700;
  line-height: .96;
  letter-spacing: -.02em;
  color: #F4F7FD;
  text-align: right;
  text-transform: uppercase;
  pointer-events: none;
  opacity: 0;
  white-space: nowrap;
}
.ln-bike-headline span { display: block; }

.ln-bike-canvas { z-index: 1; }

/* PIXEL-CARDS section (2026-08-04). Replaces the 9 stacked project cards.
   Deliberately EMPTY and fully TRANSPARENT — the page background shows straight
   through; the only content is the 16 invader cards, which are thrown into the
   pinned viewport one by one. Structure mirrors CHARACTER/BIKE: tall wrapper =
   scroll runway, sticky child = what stays on screen. */
.ln-cards-wrap {
  position: relative;
  width: 100vw;
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
  /* height set in JS from RUNWAY_VH so the runway length lives in one place */
  padding: 0;
  border: 0;
  background: transparent;
  /* NO overflow here — overflow on a sticky ancestor kills the stickiness. */
}

.ln-cards-sticky {
  position: sticky;
  top: 0;
  width: 100%;
  height: 100vh;
  overflow: hidden;      /* clips cards while they are still below the fold */
  background: transparent;
}

/* COMING SOON — the still backdrop the invader cards fly across (2026-08-06).
   DELIBERATELY INERT: no transition, no transform, no animation, no hover, no
   pointer events. It is the one motionless thing in a section that is nothing
   but motion, which is exactly why it reads.
   z-index 0 (NOT negative): `.ln-cards-sticky` creates the stacking context, so
   a negative index would put it behind the section itself. Cards share z-index
   0..15 and are appended after it, so DOM order alone keeps every card in front.
   Sized in vw so it stays one line at any width — this string must never wrap. */
.ln-cards-soon {
  position: absolute;
  inset: 0;
  z-index: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  user-select: none;
  /* Framer's reset puts `transition: all` on everything. Nothing mutates this
     element so it could never actually animate, but Ehsan asked for a dead
     backdrop — pinned off so a later global rule cannot quietly animate it. */
  transition: none !important;
  animation: none !important;
  font-family: "Sora", "Inter", sans-serif;
  font-weight: 800;
  font-size: clamp(2.6rem, 10.5vw, 15rem);
  line-height: 1;
  letter-spacing: -0.03em;
  white-space: nowrap;
  color: #FFFFFF;
}

/* --- one Space-Invader trading card ---
   Styled to ~/Desktop/GLOW EFFECT.webp, MEASURED not eyeballed:
     page background      rgb(12,16,26)   (ours is #10141A = 16,20,26)
     card AT REST         rgb(33,30,62)   barely above the page, faint accent tint
     rest border          rgb(49,41,85)   dim and desaturated, NOT the accent
     card ON HOVER        rgb(114,79,168) bright, saturated
     bloom outside edge   rgb(97,72,157) -> rgb(85,53,143) far out (wide halo)
     grain on the lit face  sigma ~33-44 per channel (heavy, fine)
   So: at rest the card is nearly invisible with a hairline dark bezel; the
   accent only takes over on hover, where a wide soft bloom pushes well past the
   card edge. */
.ln-inv-card {
  position: absolute;
  top: 0;
  left: 0;               /* real placement comes from the JS transform */
  display: block;
  overflow: hidden;
  border-radius: 16px;
  isolation: isolate;
  user-select: none;
  cursor: pointer;
  /* REST border: the accent crushed most of the way to black, so it reads as a
     dark bezel rather than a neon outline (Ehsan: "thinner and less coloured,
     towards the black, until you hover"). */
  border: 1px solid color-mix(in srgb, var(--accent) 16%, #05070c);
  /* Rest face is measured against the reference: its resting card sits at
     rgb(33,30,62) over a rgb(12,16,26) page — a lift of about +21/+14/+36. A
     first pass at #14162a came out ~10 too dark on every channel. */
  background:
    radial-gradient(115% 82% at 50% 0%,
      color-mix(in srgb, var(--accent) 12%, transparent) 0%,
      transparent 72%),
    linear-gradient(180deg, #1a1c34 0%, #0e1020 100%);
  /* REST glow: present but quiet — the reference's resting cards barely lift off
     the page. Ehsan's own snippet shape, dialled down. */
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .045),
    0 6px 20px rgba(0, 0, 0, .5),
    0 0 11px 2px color-mix(in srgb, var(--accent) 12%, transparent);
  will-change: transform;
  backface-visibility: hidden;
  transform-origin: center center;
  transition:
    box-shadow 260ms cubic-bezier(.22, 1, .36, 1),
    border-color 260ms cubic-bezier(.22, 1, .36, 1),
    background 260ms cubic-bezier(.22, 1, .36, 1);
}

/* HOVER: the bloom. Two wide soft shadows push the halo far past the edge the
   way the reference does, plus an inner rim so the card reads as lit glass. */
.ln-inv-card:hover,
.ln-inv-card:focus-visible {
  border-color: color-mix(in srgb, var(--accent) 85%, transparent);
  /* The reference's hovered face is bright across the WHOLE card
     (rgb(114,79,168) at its centre), not just under a top highlight. A gradient
     anchored at 50% 0% and ending by 78% left my lower strip at rgb(23,24,40) —
     effectively unlit. Anchoring lower and carrying colour to 100% fixes it. */
  background:
    radial-gradient(135% 115% at 50% 32%,
      color-mix(in srgb, var(--accent) 52%, transparent) 0%,
      color-mix(in srgb, var(--accent) 30%, transparent) 52%,
      color-mix(in srgb, var(--accent) 12%, transparent) 100%),
    linear-gradient(180deg, #24264c 0%, #15172e 100%);
  box-shadow:
    inset 0 0 0 1px color-mix(in srgb, var(--accent) 45%, transparent),
    inset 0 1px 0 rgba(255, 255, 255, .10),
    0 0 11px 2px color-mix(in srgb, var(--accent) 55%, transparent),
    0 0 46px 10px color-mix(in srgb, var(--accent) 38%, transparent),
    0 0 120px 34px color-mix(in srgb, var(--accent) 20%, transparent);
}

/* GRAIN + a very mild chromatic aberration, both on one overlay element.
   The reference's lit face is heavily speckled; this is an inline SVG
   feTurbulence (no network request — the site must work offline) held at low
   opacity at rest and lifted on hover. The 1px red/cyan edge offsets are the
   aberration: deliberately tiny, they should read as a lens artefact, not a
   glitch effect. */
.ln-inv-card::after {
  content: "";
  position: absolute;
  inset: -1px;
  z-index: 2;
  pointer-events: none;
  border-radius: inherit;
  /* feTurbulence alone emits COLOUR noise with a random ALPHA channel, so most
     of it composites straight back out and the grain measured sigma ~8 against
     the reference's ~27. feColorMatrix flattens it to grayscale AND forces
     alpha opaque; feComponentTransfer then expands it around mid-grey so the
     overlay blend actually has contrast to work with. */
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='120' height='120'><filter id='g' x='0' y='0' width='100%25' height='100%25'><feTurbulence type='fractalNoise' baseFrequency='1.15' numOctaves='2' stitchTiles='stitch'/><feColorMatrix type='matrix' values='0.33 0.33 0.33 0 0 0.33 0.33 0.33 0 0 0.33 0.33 0.33 0 0 0 0 0 0 1'/><feComponentTransfer><feFuncR type='linear' slope='2.4' intercept='-0.7'/><feFuncG type='linear' slope='2.4' intercept='-0.7'/><feFuncB type='linear' slope='2.4' intercept='-0.7'/></feComponentTransfer></filter><rect width='100%25' height='100%25' filter='url(%23g)'/></svg>");
  background-size: 120px 120px;
  mix-blend-mode: overlay;
  /* Grain is measured, not eyeballed. Strength: the reference's lit face has a
     per-channel sigma of ~25-30; opacity .74 lands this at ~15-20 (see the
     feColorMatrix note above for why the first attempt sat at 8).
     SIZE: baseFrequency 1.15 with 2 octaves, halved from 0.62/4 at Ehsan's
     request. Verified by high-passing the card (a Gaussian blur removes the
     background gradient, which otherwise dominates and made a naive measurement
     report the grain as getting COARSER): lag-1 autocorrelation fell 0.64 -> 0.30,
     i.e. the grain feature size went ~2px -> ~1px with sigma held at ~12.
     Note the reference's own grain is COARSER than this (feature size ~7px) —
     this is finer on purpose. */
  opacity: .14;
  transition: opacity 260ms ease;
}
.ln-inv-card:hover::after { opacity: .74; }

/* the aberration fringe — a separate hairline pair so it never tints the grain */
.ln-inv-card::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  border-radius: inherit;
  box-shadow:
    inset 1px 0 0 rgba(255, 40, 60, .16),
    inset -1px 0 0 rgba(0, 190, 255, .16);
  opacity: .8;
  transition: opacity 260ms ease;
}
.ln-inv-card:hover::before { opacity: 1; }

.ln-inv-card .pixel-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  opacity: .5;           /* shimmer sits behind the art, never competes with it */
  pointer-events: none;
  z-index: 0;
}

/* Art window: the invader sits in the upper area, leaving room for the name
   band below it — the trading-card arrangement Ehsan asked for. */
.ln-inv-card .ln-inv-art {
  position: absolute;
  left: 8%;
  right: 8%;
  top: 9%;
  height: 58%;
  display: grid;
  place-items: center;
  z-index: 1;
  pointer-events: none;
}
.ln-inv-card .ln-inv-art img {
  /* width is set in JS, proportional to the card — max-* alone cannot upscale */
  max-width: 100%;
  max-height: 100%;
  /* ~70-90px pixel-art GIFs scaled up: keep the pixels hard-edged instead of
     letting the browser smooth them into mush */
  image-rendering: pixelated;
  filter: drop-shadow(0 0 6px color-mix(in srgb, var(--accent) 45%, transparent));
  transition: filter 300ms cubic-bezier(.22, 1, .36, 1);
}
/* The invader BURNS on hover: the glow swells and breathes. Two stacked
   drop-shadows (tight core + wide halo) read as heat rather than a flat blur. */
.ln-inv-card:hover .ln-inv-art img {
  animation: ln-inv-burn 1.5s ease-in-out infinite;
}
@keyframes ln-inv-burn {
  0%, 100% {
    filter:
      drop-shadow(0 0 8px color-mix(in srgb, var(--accent) 90%, transparent))
      drop-shadow(0 0 22px color-mix(in srgb, var(--accent) 55%, transparent))
      brightness(1.12);
  }
  50% {
    filter:
      drop-shadow(0 0 14px color-mix(in srgb, var(--accent) 100%, transparent))
      drop-shadow(0 0 38px color-mix(in srgb, var(--accent) 70%, transparent))
      brightness(1.3);
  }
}

/* Name sits ABOVE the bottom edge, not flush to it. */
.ln-inv-card .ln-inv-name {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 13%;
  text-align: center;
  z-index: 3;
  pointer-events: none;
  font-family: "Visitor TT1 BRK", "Courier New", monospace;
  /* proportional to the card (--cw, set in JS) so the locked look survives
     the much larger card size */
  font-size: calc(var(--cw, 170px) * 0.082);
  letter-spacing: .12em;
  color: #cdd6e6;
  text-shadow: 0 0 8px color-mix(in srgb, var(--accent) 30%, transparent);
  transition: color 260ms ease, text-shadow 260ms ease;
}
.ln-inv-card:hover .ln-inv-name {
  color: #f2f6ff;
  text-shadow: 0 0 12px color-mix(in srgb, var(--accent) 80%, transparent);
}
.ln-inv-card .ln-inv-name::after {
  content: "";
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  bottom: -7px;
  width: 34%;
  height: 1px;
  background: color-mix(in srgb, var(--accent) 30%, transparent);
  transition: background 260ms ease, width 260ms ease;
}
.ln-inv-card:hover .ln-inv-name::after {
  width: 46%;
  background: color-mix(in srgb, var(--accent) 90%, transparent);
}

@media (prefers-reduced-motion: reduce) {
  .ln-inv-card:hover .ln-inv-art img { animation: none; }
}

/* CARD-RISE — fanned CARD STACK (rebuilt 2026-08-05). See ln-bike-cards.js.
   Ehsan rejected the previous "rising belt" version outright and asked for this
   section to be rebuilt on the card-stack fan component he supplied. The belt's
   rules (.ln-bikecards-layer / -back / -front, .ln-bikecard and its hover growth,
   blur planes and glow) are DELETED — nothing on the page uses them any more.

   The component is React + Tailwind; this is the vanilla equivalent of its
   classes, kept visually faithful: rounded-2xl (16px), a 4px translucent light
   border, shadow-xl, object-cover image, a bottom gradient for grounding, and the
   two blurred background washes above and below the stack. */

.ln-cardrise-wrap {
  position: relative;
  width: 100vw;
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
  /* height comes from TOTAL_VH in JS */
  padding: 0;
  border: 0;
  background: transparent;
  /* NO overflow here — overflow on a sticky ancestor kills the stickiness. */
}
.ln-cardrise-sticky {
  position: sticky;
  top: 0;
  width: 100%;
  height: 100vh;
  overflow: hidden;
  background: transparent;
}

/* The component's two spotlight washes: a soft pool up top, a heavier one under
   the stack. Without them the fan reads as floating on nothing. */
/* StrokeText — the "Brands I Developed" draw-on that owns the head of CARD-RISE.
   Sits centred in the sticky, BEHIND the fan (z-index 0 vs the stage's cards) so
   a late card can never be hidden by it. Pointer-events off: the stage below
   owns drag. */
.ln-fan-stroketext {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: min(88vw, 1500px);
  height: auto;
  overflow: visible;
  pointer-events: none;
  z-index: 0;
}

/* DepthCarousel is DELETED (build 4). Replaced by DriftWall below. */

/* ---- DriftWall, CSS ported from Ehsan's DriftWall.css ----
   Kept faithfully, including the two-layer mask that fades the wall's edges.
   ⚠️ `mask-composite` needs BOTH spellings: the unprefixed value is `intersect`,
   the -webkit- one is `source-in`. Writing only one silently drops the second
   mask layer in one engine or the other. */
.drift-wall {
  position: absolute;
  inset: 0;
  overflow: hidden;
  perspective: var(--dw-perspective, 1200px);
  perspective-origin: 50% 50%;
  --dw-tile-w: 152px; --dw-tile-h: 257px; --dw-gap: 14px; --dw-radius: 14px;
  --dw-lift: 36px; --dw-dim: .45; --dw-gray: 1; --dw-overlay: #7e93ff; --dw-edge: 40%;
  -webkit-mask-image:
    radial-gradient(ellipse 78% 82% at 50% 46%, #000 var(--dw-edge), transparent 100%),
    linear-gradient(to top, #000 var(--dw-edge), transparent 100%);
  -webkit-mask-composite: source-in;
  mask-image:
    radial-gradient(ellipse 78% 82% at 50% 46%, #000 var(--dw-edge), transparent 100%),
    linear-gradient(to top, #000 var(--dw-edge), transparent 100%);
  mask-composite: intersect;
}
.drift-wall__plane {
  position: absolute; top: 50%; left: 50%;
  display: flex; flex-direction: row;
  transform-style: preserve-3d;
  cursor: pointer; transform-origin: 50% 50%; will-change: transform;
}
.drift-wall__col {
  position: relative;
  width: calc(var(--dw-tile-w) + var(--dw-gap));
  transform-style: preserve-3d;
}
.drift-wall__track {
  display: flex; flex-direction: column;
  will-change: transform; transform-style: preserve-3d;
}
.drift-wall__tile {
  position: relative; display: block; width: 100%;
  height: calc(var(--dw-tile-h) + var(--dw-gap));
  flex: 0 0 auto; outline: none; transform-style: preserve-3d;
}
.drift-wall__inner {
  position: absolute; inset: calc(var(--dw-gap) / 2);
  display: block; border-radius: var(--dw-radius); overflow: hidden;
  background: #0b0b12; opacity: var(--dw-dim);
  transform: translateZ(0); pointer-events: none;
  transition: transform .42s cubic-bezier(.22,1,.36,1),
              opacity .42s cubic-bezier(.22,1,.36,1),
              box-shadow .42s cubic-bezier(.22,1,.36,1);
}
.drift-wall__tile img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  filter: grayscale(var(--dw-gray)) saturate(.92);
  transition: filter .42s cubic-bezier(.22,1,.36,1);
  user-select: none; -webkit-user-drag: none;
}
.drift-wall__overlay {
  position: absolute; inset: 0;
  /* Ehsan: the blue wash over the cards was too heavy. .42 -> .22. */
  background: var(--dw-overlay); opacity: .22;
  pointer-events: none;
  transition: opacity .42s cubic-bezier(.22,1,.36,1);
}
.drift-wall__tile.is-active .drift-wall__inner,
.drift-wall__tile:focus-visible .drift-wall__inner {
  opacity: 1;
  transform: translateZ(var(--dw-lift));
  box-shadow: 0 24px 60px -18px rgba(0,0,0,.7);
}
.drift-wall__tile.is-active img,
.drift-wall__tile:focus-visible img { filter: grayscale(0) saturate(1.05); }
.drift-wall__tile.is-active .drift-wall__overlay,
.drift-wall__tile:focus-visible .drift-wall__overlay { opacity: 0; }
.drift-wall__tile:focus-visible .drift-wall__inner {
  box-shadow: 0 24px 60px -18px rgba(0,0,0,.7), 0 0 0 2px rgba(255,255,255,.9);
}
@media (prefers-reduced-motion: reduce) {
  .drift-wall__plane, .drift-wall__track { will-change: auto; }
}

/* CHARACTER headline — "INVEST IN ACTUATORS", drawn on over the revealed
   character in the last stretch of the hero runway. Centred both axes; the SVG
   sizes itself to its own bbox in ln-stroketext.js so it cannot clip. Above the
   character layers, below nothing. */
.ln-hero-stroketext {
  position: absolute;
  left: 50%;
  /* Sat at the vertical centre; moved DOWN to sit near the bottom of the
     viewport (Ehsan, 2026-08-05) so it reads under the character rather than
     across their face. */
  top: auto;
  bottom: 6%;
  transform: translateX(-50%);
  width: min(86vw, 1500px);
  height: auto;
  overflow: visible;
  pointer-events: none;
  opacity: 0;
  z-index: 5;
}

/* PERFECTION-LINE (see SECTION-MAP) — "Perfection is always on the move…" moved
   to the VHS pixel face (Ehsan, 2026-08-06).
   Scoped to its own containers rather than by text, and BOTH class prefixes are
   listed because Framer renames framer-* to studio-* at runtime.
   `!important` is required: the compiled bundle sets --framer-font-family and a
   font stack INLINE on this paragraph, which no plain rule can beat. */
.framer-a63klc .framer-text,
.studio-a63klc .studio-text,
.framer-1tai1ty .framer-text,
.studio-1tai1ty .studio-text {
  font-family: "Visitor TT1 BRK", "Courier New", monospace !important;
}

/* BIKE exit burn — RESTORED 2026-08-06 after a CSS slice deleted it.
   ⚠️ Without these rules the canvas has NO SIZE and the burn silently does not
   render — the JS keeps running and every hook reports healthy, so it looks
   fine from the console. Above .ln-bike-headline (z 0) and the canvas (z 1) so
   it consumes the bike AND the headline together. */
.ln-bike-burn {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  z-index: 3;
  pointer-events: none;
}

/* "AREAS OF EXPERTISE" — the Framer caption that floats over CARD-RISE.
   Ehsan asked for it gone on 2026-08-05; the rule was then DELETED BY ONE OF MY
   OWN CSS SLICES and the caption came back. Restored 2026-08-06.
   Confirmed live: exactly ONE node carries this text, at
   `DIV.studio-1tb9ckp > P.studio-text`. Both class prefixes are listed because
   Framer renames framer-* to studio-* at runtime.
   ⚠️ If this vanishes again, suspect a CSS deletion — not the bundle. */
.framer-1tb9ckp,
.studio-1tb9ckp {
  display: none !important;
}

/* NAV-HOME label -> the VHS pixel face, THIN (Ehsan, 2026-08-06).
   The @font-face ships visitor1 (regular) and visitor2 (bold); loading only the
   regular and pinning `font-weight: 400` stops the browser synthesising a fake
   bold. `!important` is required — the compiled bundle sets the font stack and
   weight INLINE on this label. */
#cikdf6 .studio-text,
#cikdf6 .framer-text,
#cikdf6 a,
#cikdf6 p {
  font-family: "Visitor TT1 BRK", "Courier New", monospace !important;
  font-weight: 400 !important;
  letter-spacing: .06em;
}

/* ============================================================================
   SLIDES — stacking scroll panels, cloned from thelinestudio.com (2026-08-06).
   Skeleton + motion only; no copy, imagery or fonts taken. Full derivation and
   the measured source values are in assets/scripts/ln-slides.js.

   ⚠️ ONE STICKY FOR THE WHOLE STACK; PANELS ARE LAYERS INSIDE IT.
   The previous build gave every panel its own sticky inside its own scroller.
   They unpinned on independent schedules, and the exit tween tore transparent
   wedges through an overflow:hidden window, so the previous panel could be seen
   sliding upward through the current one. The reference does not do that — its
   project panels are absolutely-positioned layers inside a SINGLE pinned window,
   moved by transform only (hence `yPercent:110 -> 0` in its source). One pin
   means no unpin race, and a layer at rest is a full-window opaque rectangle, so
   an entering layer can only ever reveal the layer directly beneath it.
   ========================================================================== */

.ln-slides-wrap {
  position: relative;
  /* Escape the Framer container the same way every other injected section does.
     Without this the panels are gutter-width, not full-bleed. */
  width: 100vw;
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
  /* BIKE's exit burn ends on a solid hold of exactly this blue and this section
     rises straight out of it. Must stay equal to EXIT_BLUE in ln-bike.js. */
  background: #094EBC;
}

.ln-slides-stage {
  position: sticky;
  top: 0;
  height: 100vh;
  overflow: hidden;
  /* Confines the acetate's mix-blend-mode:multiply to this stack, so it tints
     the footage and nothing else. Without it the blend reaches the page behind. */
  isolation: isolate;
}

.ln-slide {
  position: absolute;
  inset: 0;
  /* TOP LEFT — the reference's own origin for these panels. */
  transform-origin: 0% 0%;
  will-change: transform, filter;
  backface-visibility: hidden;
}


/* ---- video panel ---------------------------------------------------------- */

.ln-slide-video {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Only PARTLY desaturated. Pushing it to mono and then screening blue over it
     is what flattened the hero: the tint has to sit ON the picture, not replace
     it. Slight lift so the mid-tones survive the tint. */
  filter: saturate(.45) contrast(1.06) brightness(1.12);
}

/* A CHILD of the video panel now, not a sibling outside the pinned box — that is
   what makes it track the footage and actually read as an overlay. It holds over
   the clip, then tilts away to reveal it untinted before the next panel covers. */
.ln-slide-over {
  position: absolute;
  inset: 0;
  pointer-events: none;
  transform-origin: 0% 100%;   /* BOTTOM LEFT, the target's own exit origin */
  will-change: transform;
}

/* ⚠️ A REAL TINT — PARTIAL ALPHA, NORMAL BLEND. Not `multiply` (the target's
   value: correct for its bright flat animation, but it crushed these dark
   cinematic clips to 81% near-black) and not full-strength `screen` (which lifts
   every black pixel to exactly the tint colour, so a clip that is 63% black
   composites to a flat card — measured, and that is precisely what the hero
   looked like).
   A partial-alpha layer is literally "a blue tint on top of the video": the
   result is mix(footage, blue, alpha), so contrast is preserved at (1-alpha) of
   the original EVERYWHERE and the picture can never flatten out, whatever the
   clip. The exact burn-colour hand-off is handled by .ln-slide-seam instead. */
.ln-slide-acetate {
  position: absolute;
  inset: 0;
  opacity: .52;
}

/* Opaque burn colour at the instant the panel arrives, cleared over the first
   stretch so the footage fades up underneath the tint. This is what keeps the
   seam with BIKE's exit burn exact now that the tint is translucent. */
.ln-slide-seam {
  position: absolute;
  inset: 0;
  will-change: opacity;
}

.ln-slide-body {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  height: 100%;
  padding: 0 1.27315vw 1.27315vw;   /* the reference's exact gutter */
}

.ln-slide-mark {
  font-family: "Visitor TT1 BRK", "Courier New", monospace;
  font-size: clamp(46px, 11.2vw, 200px);
  font-weight: 400;
  line-height: .82;
  letter-spacing: .02em;
  color: #F4F7FD;
}

.ln-slide-sub {
  margin-top: .7vw;
  font-family: "Visitor TT1 BRK", "Courier New", monospace;
  font-size: clamp(9px, .78vw, 13px);
  letter-spacing: .14em;
  color: #F4F7FD;
  opacity: .85;
}

/* ---- work panel ----------------------------------------------------------- */

.ln-slide--work {
  display: flex;
  flex-direction: column;
}

.ln-slide-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  padding: 1.7vw 1.27315vw 1.1vw;
}

.ln-slide-note {
  font-family: "Visitor TT1 BRK", "Courier New", monospace;
  font-size: clamp(9px, .72vw, 12px);
  letter-spacing: .12em;
  text-transform: uppercase;
  color: #094EBC;
  white-space: nowrap;
}

.ln-slide-title {
  margin: 0;
  font-family: "Sora", "Inter", sans-serif;
  font-size: clamp(26px, 3.2vw, 58px);
  font-weight: 400;
  letter-spacing: -.02em;
  color: #0A0D12;
}


.ln-slide-fig {
  flex: 1 1 auto;
  margin: 0;
  overflow: hidden;
}

.ln-slide-fig img,
.ln-slide-fig video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* ---- text panel ----------------------------------------------------------- */

.ln-slide-pad {
  display: flex;
  flex-direction: column;
  justify-content: center;
  height: 100%;
  padding: 0 1.27315vw;
}

.ln-slide-label {
  margin-bottom: 1.3vw;
  font-family: "Visitor TT1 BRK", "Courier New", monospace;
  font-size: clamp(10px, .8vw, 14px);
  letter-spacing: .12em;
  text-transform: uppercase;
  color: #094EBC;
}

.ln-slide-big {
  margin: 0;
  font-family: "Sora", "Inter", sans-serif;
  font-size: clamp(40px, 7vw, 132px);
  font-weight: 400;
  line-height: .95;
  letter-spacing: -.03em;
  color: #0A0D12;
}

.ln-slide-copy {
  max-width: 37.26852vw;
  margin: 1.7vw 0 0;
  font-family: "Sora", "Inter", sans-serif;
  font-size: clamp(14px, 1.15vw, 21px);
  line-height: 1.45;
  color: #2A3340;
}



@media (max-width: 767px) {
  .ln-slide-copy { max-width: none; }
  .ln-slide-head { padding: 5vw 4vw 3vw; }
  .ln-slide-pad,
  .ln-slide-body { padding-left: 4vw; padding-right: 4vw; }
}

/* ---- SLIDES additions (2026-08-06, second pass) --------------------------- */

/* The transformed layers. `will-change` on both because each is rewritten every
   frame from scroll position. */
.ln-slide-tags {
  display: flex;
  flex-wrap: wrap;
  gap: .55vw 1.4vw;
  padding: .9vw 1.27315vw 1.4vw;
}

.ln-slide-tag {
  font-family: "Visitor TT1 BRK", "Courier New", monospace;
  font-size: clamp(9px, .72vw, 12px);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: #094EBC;
}

/* ⚠️ THE FIVE PROJECT SCROLL-STORIES ARE HIDDEN, NOT DELETED.
   (STORY-CRYPTO-UX `.studio-1y95s57` joined the set on 2026-08-06 night — it was
   rebuilt as a panel but its original was left visible, so the page carried both.)
   STORY-KAZANSEO / BRANDING-1 / BRANDING-2 / BUGATTI are rebuilt as SLIDES
   panels at the bottom of the page (#ln-story) using their own text and imagery.
   The originals are hidden rather than cut out of the compiled .mjs bundle for a
   specific reason: HANDOFF.md records that Framer's hydration RECONSTRUCTS DOM
   deleted from index.html, so a real removal means bracket-matched surgery on the
   bundle in four places. `display:none` cannot be undone by hydration, is one
   line, and is trivially reversible. Their images are reused by the new panels,
   so nothing extra is downloaded — the only cost is inert DOM. */
.studio-1y95s57,
.studio-14h4xrx,
.studio-1kmpgyt,
.studio-1750rdp,
.studio-14s8lhz { display: none !important; }

/* STORY-ANIMATION — the AR/VR phone ("Animation Section", 3692px, 22 images).
   DELETED 2026-08-06 on Ehsan's call: "DELETE the whole phone! no more phone!"
   Unlike the five above it was NEVER rebuilt as a panel, so it is the one
   hidden section whose images are used NOWHERE else — the usual "nothing extra
   downloads" argument does not apply here, so the wire cost was MEASURED rather
   than assumed. `display:none` does NOT stop an <img src> in the subtree from
   downloading, but only 9 of the 22 ever fetch (the rest are lazy/never-resolved
   srcset) and they total 68 KB against a 16.9 MB page — 0.4%. Not worth a strip
   script; if this section is ever cut from the bundle for real, that is where
   the remaining bytes go. Document height 65,721 -> 62,029px. */
.studio-1ahxq0q { display: none !important; }

/* Old Framer footer card — replaced wholesale by #ln-footer below. */
.studio-13ck8h9 { display: none !important; }

/* ============================================================================
   FOOTER — cloned from thelinestudio.com. Derivation in ln-footer.js.
   ========================================================================== */

.ln-footer {
  position: relative;
  overflow: clip;
  width: 100vw;
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
  background: #F4F7FD;
  /* Tall enough that the sticky panel has something to pin against. The
     reference is 1.5 viewports; less than that and the panel never pins. */
  min-height: 150vh;
}

.ln-footer-cta {
  position: relative;
  z-index: 4;
  background: #F4F7FD;
  padding: 0 1.27315vw;
}

.ln-footer-cta-link {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-top: 6vw;
  color: #0A0D12;
  text-decoration: none;
}

.ln-footer-cta-label {
  font-family: "Sora", "Inter", sans-serif;
  font-size: clamp(46px, 12vw, 208px);
  font-weight: 400;
  line-height: .9;
  letter-spacing: -.04em;
}

.ln-footer-cta-arrow {
  font-family: "Sora", "Inter", sans-serif;
  font-size: clamp(26px, 4vw, 64px);
}

.ln-footer-mark-wrap {
  position: relative;
  z-index: 3;
  overflow: hidden;
  background: #F4F7FD;
  padding-top: 17.36111vw;      /* the reference's own value */
  padding-bottom: .46296vw;
  /* ⚠️ NOT the 1.27vw gutter. The whole block slides left by 7% of the viewport
     (~120px at 1710) as it tilts, so a gutter-width indent puts the first glyph
     ~100px off-screen and the wordmark reads as "ELNIUM". This indent is sized
     so that AFTER the shift the mark lands back on the gutter. */
  padding-left: 8vw;
  padding-right: 1.27315vw;
  /* THIS is the element that rotates — its tilted BOTTOM EDGE is what reveals
     the coloured panel underneath on a diagonal. */
  transform-origin: 0% 0%;
  will-change: transform;
}

/* Paper bleed ABOVE the block. Rotating about the top-left lifts the top-RIGHT
   corner by width*sin(6deg) (~180px at 1710), which would open a wedge of blue
   between the CTA and this block. Same trick the reference uses under its
   coloured panel, pointed the other way. */
.ln-footer-mark-wrap::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: calc(100% - 1px);
  height: 40vh;
  background: #F4F7FD;
}

.ln-footer-mark {
  font-family: "Visitor TT1 BRK", "Courier New", monospace;
  font-size: clamp(56px, 17vw, 300px);
  font-weight: 400;
  line-height: .8;
  letter-spacing: .01em;
  white-space: nowrap;
  color: #0A0D12;
}

/* Starts half a viewport ABOVE the footer so the panel is already pinned when
   the footer arrives — without this it pops in instead of being revealed. */
.ln-footer-scroller {
  position: absolute;
  top: -50vh;
  right: 0;
  bottom: 0;
  left: 0;
}

.ln-footer-sticky {
  position: sticky;
  top: 0;
  z-index: 2;
  display: flex;
  height: 100vh;
}

.ln-footer-inner {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: flex-end;
}

/* A strip of the same colour ABOVE the panel. The wordmark crossing the panel's
   top edge tilts to -6deg; without this bleed a wedge of page shows through
   beside it. Pseudo-elements inherit from their originating element, so this
   picks up the inline colour set in JS. */
.ln-footer-inner::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: calc(100% - 1px);
  height: 10vh;
  background-color: inherit;
}

/* Holds the DAVID bar-halftone head. ⚠️ NO `mix-blend-mode: multiply` here —
   the bars ARE the artwork and are drawn light on the blue panel; multiplying
   would clamp them to the panel colour and erase them, which is exactly how the
   flat image this replaced managed to measure as present and be invisible. */
.ln-footer-fig {
  position: absolute;
  bottom: 0;
  /* The cropped head is PORTRAIT (2593x3541). Sized off height so it sits inside
     the panel instead of overrunning it. */
  left: 30%;
  width: 17vw;
  pointer-events: none;
  /* The source is a head on solid black; fading the frame stops the crop edge
     reading as a rectangle pasted onto the panel. */
  -webkit-mask-image: radial-gradient(ellipse 74% 78% at 50% 52%, #000 42%, transparent 82%);
  mask-image: radial-gradient(ellipse 74% 78% at 50% 52%, #000 42%, transparent 82%);
}

.ln-footer-david { display: block; width: 100%; height: auto; }

.ln-footer-content {
  position: relative;
  width: 100%;
  padding: 0 1.27315vw 1.27315vw;
  color: #F4F7FD;
}

.ln-footer-top {
  display: flex;
  justify-content: space-between;
  gap: 4vw;
  padding-bottom: 3.6vw;
}

.ln-footer-contact { display: flex; flex-direction: column; gap: .25vw; }

/* `.ln-footer-tel` was dropped from this rule 2026-08-06: the phone number was
   deleted from the footer, so nothing carries that class any more. */
.ln-footer-mail {
  color: #F4F7FD;
  text-decoration: none;
  font-family: "Sora", "Inter", sans-serif;
  font-size: clamp(14px, 1.15vw, 20px);
}

.ln-footer-cols { display: flex; gap: 3.2vw; }

.ln-footer-col-h {
  margin-bottom: .55vw;
  font-family: "Visitor TT1 BRK", "Courier New", monospace;
  font-size: clamp(9px, .72vw, 12px);
  letter-spacing: .14em;
  text-transform: uppercase;
  opacity: .7;
}

.ln-footer-col-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: .16vw;
  font-family: "Sora", "Inter", sans-serif;
  font-size: clamp(12px, .92vw, 16px);
}

/* Social links (2026-08-06). Inherit the list's size and face so a linked row
   is typographically identical to an inert one — only the hover marks it as
   clickable. `color: inherit` matters: an unstyled <a> inside the footer would
   otherwise pick up the browser's default blue on this pale panel. */
.ln-footer-link {
  color: inherit;
  text-decoration: none;
  transition: opacity 160ms ease-out;
}
.ln-footer-link:hover,
.ln-footer-link:focus-visible {
  opacity: .6;
  text-decoration: underline;
  text-underline-offset: 3px;
}

.ln-footer-news {
  display: flex;
  align-items: center;
  justify-content: space-between;
  border-top: 1px solid rgba(244, 247, 253, .28);
  padding-top: 1.1vw;
}

.ln-footer-news-label {
  font-family: "Sora", "Inter", sans-serif;
  font-size: clamp(20px, 2.6vw, 44px);
  letter-spacing: -.02em;
}

.ln-footer-news-arrow { font-family: "Sora", "Inter", sans-serif; font-size: clamp(16px, 1.6vw, 28px); }

.ln-footer-bottom {
  display: flex;
  align-items: center;
  gap: .6vw;
  padding-top: 1.1vw;
  font-family: "Visitor TT1 BRK", "Courier New", monospace;
  font-size: clamp(9px, .72vw, 12px);
  letter-spacing: .1em;
  opacity: .85;
}

.ln-footer-sep { opacity: .5; }

.ln-footer-up {
  margin-left: auto;
  padding: 0;
  background: none;
  border: 0;
  color: inherit;
  font: inherit;
  letter-spacing: inherit;
  cursor: pointer;
}

@media (max-width: 767px) {
  .ln-footer-top { flex-direction: column; gap: 2.4rem; }
  .ln-footer-cols { flex-wrap: wrap; gap: 2rem; }
  .ln-footer-cta,
  .ln-footer-content { padding-left: 4vw; padding-right: 4vw; }
}

/* ============================================================================
   COPY REWRITE + i18n SUPPORT (2026-08-06, Ehsan)
   Text content itself lives in assets/scripts/ln-i18n.js. This block handles
   only what text replacement cannot do: removing elements and restyling.
   ========================================================================== */

/* THE BLURRY BOX (EXPERTISE-CARD, "Areas of Expertise") — 2026-08-06, Ehsan:
   "delete all texts appearing inside that blurry box", and put the four lines
   there instead. See the screenshot ~/Desktop/TEXT PLACE.png.

   ⚠️ `visibility`, NOT `display`. The box carries THIRTY stacked skill blocks
   (Product Design, UI-UX Design, … Content Strategy), each a title plus bullets,
   and those blocks are what give this section its scroll LENGTH. The UFO story
   above is timed against that length, so `display:none` would collapse ~1700px
   and retime the entire sequence. `visibility:hidden` removes the ink and leaves
   every measurement identical — the same distinction that governs the intro
   gate, for the same reason.
   The injected line is exempted, since it is a child of the same box. */
#hero-section-1 .studio-text,
#hero-section-1 p,
#hero-section-1 h1,
#hero-section-1 h2,
#hero-section-1 h3 { visibility: hidden !important; }
#hero-section-1 .ln-exp-shuffle { visibility: visible !important; }

.ln-exp-host { position: relative; }

/* The four lines, centred in the box. Sora: these are SENTENCES, and the
   bitmap face is reserved for labels and single monumental words. */
.ln-exp-shuffle {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 5;
  width: min(86%, 60rem);
  text-align: center;
  pointer-events: none;
  font-family: "Sora", "Inter", sans-serif;
  font-weight: 600;
  font-size: clamp(1.05rem, 2.1vw, 2rem);
  line-height: 1.35;
  letter-spacing: -0.01em;
  color: #F4F7FD;
  opacity: 1;
  transition: opacity 260ms ease-out;
}
.ln-exp-shuffle.is-out { opacity: 0; }

/* THINGS I DECIDED — DELETED 2026-08-06 (screenshot ~/Desktop/DELETE IT.png).
   The title block and the quote block go; this also removes the stale
   "Franz Kafka" attribution, which was still sitting under a replacement line
   that is not Kafka's — an incoherence introduced by the copy pass.
   ⚠️ THE PARENT `.studio-nnt33i` IS NOT TOUCHED. SECTION-MAP records that it is
   `ln-hero.js`'s injection anchor, and removing it makes the whole CHARACTER
   section silently vanish. Only its two children are hidden. */
#works-start,
#meet-us-01 { display: none !important; }

/* PROFESSIONAL EXPERIENCE — DELETED 2026-08-06. Ehsan: "NO RESUME STUFF ON
   WEBSITE!" The whole block: heading plus every job entry. */
.studio-1x1rj0a { display: none !important; }

/* ART-OF-PORTFOLIO — one centred line that shuffles every 2s, no bullets, no
   sub-text. Visitor TT1 at Ehsan's instruction; the LINES ARE DELIBERATELY
   SHORT because a bitmap face stops being readable at sentence length, and
   German runs longer than English. Sized in vw so it never wraps.
   The fade is opacity only — no transform — so it cannot fight the UFO
   section's own scroll-driven transforms underneath it. */
/* ART-OF-PORTFOLIO — "THE ART OF PORTFOLIO", dead centre of the rotating UFO
   mandala, restored 2026-08-06 with a character-scramble on entry (ln-i18n.js).
   Visitor TT1 BRK at Ehsan's instruction, which is the right call here: it is a
   TITLE, not a sentence, so the bitmap face is doing exactly what it is for.
   ⚠️ `white-space: nowrap` is required — the scramble swaps every glyph each
   frame, and if the string can wrap it will re-wrap on its own animation and
   jitter the line. Sized in vw so it never needs to. */
.ln-artport {
  font-family: "Visitor TT1 BRK", "Courier New", monospace !important;
  color: #FFFFFF !important;
  text-align: center !important;
  font-size: clamp(1rem, 2.6vw, 2.6rem) !important;
  line-height: 1.25 !important;
  letter-spacing: .08em !important;
  white-space: nowrap !important;
}

/* THE INSTRUMENTS — Ehsan: "change the button to something that people don't
   think it is clickable". It was a filled rectangle reading as a button; now a
   plain letter-spaced label. No border, no fill, no cursor, no hover, and
   pointer-events off so it cannot even take a click. Visitor TT1 as asked. */
/* ⚠️ `!important` ON font-family IS REQUIRED HERE, and so is tagging the LEAF.
   The badge's text lives in a `<p class="studio-text">` that carries its own
   Framer font-family (DM Mono 16px). An inherited value can never beat a value
   the element sets on itself, which is why styling any ancestor left the badge
   in DM Mono through two attempts. */
.ln-not-a-button,
.ln-not-a-button * {
  font-family: "Visitor TT1 BRK", "Courier New", monospace !important;
  background: none !important;
  border: 0 !important;
  box-shadow: none !important;
  border-radius: 0 !important;
  cursor: default !important;
  pointer-events: none !important;
  letter-spacing: .22em !important;
  opacity: .72 !important;
}

/* HOW I WORK — four moves remain (Decompose, Route, Verify, Direct); items 5
   and 6 are deleted, numeral included.
   ⚠️ The removal is done in ln-i18n.js `hideApproachExtras()`, NOT here. A
   `#services > *:nth-child(n+5)` rule used to live at this spot: it assumes each
   item is a DIRECT child of #services, and one wrapper div in between makes it
   silently hide nothing at all. The JS finds each item by its heading text and
   walks up to the real block, which survives whatever the export nests. */

/* ...and the small grey body text in that section goes near-black navy, per
   Ehsan. The section sits on a light panel, so grey was reading as disabled. */
#services p, #services span, #services div {
  color: #0B1220 !important;
}

/* ⚠️ THE NUMERALS ARE THE EXCEPTION AND THE RULE ABOVE BROKE THEM.
   Each approach item leads with a big numeral sitting INSIDE A BLUE SQUARE, so
   the blanket near-black above turned dark-on-blue and the numbers stopped
   reading. Ehsan: "NUMBERS MUST BE in WHITE color."
   The numeral has no class of its own, and CSS cannot select on text content —
   so ln-i18n.js tags it by matching a lone 1-2 digit string inside #services
   and adds `.ln-approach-num`. The selector is ID-qualified deliberately:
   `#services p` above scores (1,0,1), so a bare `.ln-approach-num` (0,1,0)
   would LOSE even with !important, since !important only breaks ties between
   equally-weighted origins — specificity is still compared first. */
#services .ln-approach-num,
#services .ln-approach-num * {
  color: #FFFFFF !important;
  -webkit-text-fill-color: #FFFFFF !important;
}

/* Footer credit line + the "Get in touch" column item. Small, quiet, Sora —
   it is a sentence, so it must not be set in the bitmap face. */
.ln-footer-credit { opacity: .62; font-family: "Sora", "Inter", sans-serif; }

/* ============================================================================
   iPAD / TOUCH (2026-08-06)
   ⚠️ SCOPE NOTE: the layout at tablet width is handled by the Framer export's
   own `(max-width: 1199.98px)` breakpoint, which already exists. What follows is
   only the touch-specific hygiene that export does not cover.
   ========================================================================== */

/* iOS paints a grey flash over any tapped link/button. On a dark, deliberately
   art-directed page that reads as a rendering fault, not a control. */
a, button, [role="button"], .ln-footer-link, .ln-lang-toggle {
  -webkit-tap-highlight-color: transparent;
}

/* ⚠️⚠️ DO NOT PUT `overflow-x` OR `max-width` ON `body`. IT BREAKS THE UFO.
   A `body { max-width: 100vw; overflow-x: hidden }` rule was added here on
   2026-08-06 and immediately reverted: it moved the rotating saucer/mandala
   stack out of position so the UFO no longer sat low enough for its lower edge
   to disappear behind the bottom gradient.
   This is documented AT THE TOP OF THIS FILE and was written after three
   earlier attempts broke the same thing: clipping or constraining a composited,
   transform-animated subtree at `body` or `.studio-CYG9j` level breaks the hero
   backdrop layers. ONLY `html { overflow-x: hidden }` is safe, and it is
   already set at line ~48. It also already prevents sideways scrolling on a
   narrow screen, so nothing extra was ever needed here. */

/* Hover-only affordances must not leave dead visual states on touch, where the
   hover never ends. */
@media (hover: none) {
  .ln-footer-link:hover { opacity: 1; text-decoration: none; }
}

/* ============================================================================
   THE CURSOR (2026-08-07, Ehsan) — assets/scripts/ln-cursor.js
   Requirements: never disappear on a light section, say "drag me" over the bike,
   say "keep scrolling" in CHARACTER/BIKE/when idle — and be the ONLY pointer.
   ========================================================================== */

/* ⚠️ THE "TWO CURSORS" BUG WAS A TIMING WINDOW, AND IT TOOK THREE MEASUREMENTS
   TO SEE IT. FRAMER MOUNTS ITS CURSOR HOST LAZILY, ON FIRST POINTER INTERACTION.
   Read the DOM before you move the mouse and there is no host, no
   `studio-cursor-none` anywhere, and `cursor` computes to `auto`. Move the mouse
   once and BODY gains `studio-cursor-none`, `cursor` computes to `none`, and a
   10x10 `studio-yhezx2` box appears fixed at z-index 13. Both readings are real;
   they are just before and after.
   So on every fresh load there is a window where the native arrow is still
   visible AND our square is already drawn — which is exactly the two pointers
   Ehsan saw, and why it felt intermittent rather than constant.
   We now close that window ourselves instead of waiting for Framer, and the
   class is added FROM JS (see ln-cursor.js) so a failed script can never leave a
   visitor with no pointer at all. */
html.ln-cursor-live,
html.ln-cursor-live * { cursor: none !important; }

/* ---- the nav jump's cover (2026-08-07) — see dipTo() in ln-xelnium-nav.js ---
   The jump itself stays instant; this only hides the cut so a 50,000px move
   reads as navigation rather than as a fault.
   ⚠️ z-index sits UNDER the VHS intro (99999) so it can never cover the loading
   screen, and under the cursor (2147482000) so the pointer stays visible while
   the page changes — losing the cursor mid-jump is the thing that would make it
   feel broken again. `pointer-events: none` throughout: it must never eat a
   click even for the 400ms it is up.
   The base rule carries the SLOWER duration because that is the fade OUT; the
   `is-on` rule overrides it with the faster fade IN. */
.ln-nav-dip {
  position: fixed;
  inset: 0;
  z-index: 9500;
  background: #10141A;
  opacity: 0;
  pointer-events: none;
  transition: opacity .22s ease-out;
}
.ln-nav-dip.is-on { opacity: 1; transition-duration: .16s; }
@media (prefers-reduced-motion: reduce) {
  /* dipTo() already bails out before building this, but if one ever exists it
     must not sit on screen fading. */
  .ln-nav-dip { display: none !important; }
}

/* Framer's own cursor component. Currently an EMPTY 10x10 box at opacity 0, so
   it paints nothing today — but it is a real pointer-following element and any
   variant change upstream turns it into a genuine second cursor. Hidden rather
   than removed: hydration reconstructs deleted DOM but cannot undo a CSS rule.
   Verified unique (1 element, 0 children, pointer-events already none). */
.studio-CpIHg.studio-yhezx2 { display: none !important; }

.ln-cursor {
  position: fixed;
  top: 0; left: 0;
  width: 0; height: 0;
  z-index: 2147482000;     /* under the dev label layer, over everything else */
  pointer-events: none;    /* MUST stay none — it sits over every control */
  opacity: 0;
  transition: opacity .18s ease-out;
  will-change: transform;
}
.ln-cursor.is-on { opacity: 1; }

/* The square itself, centred on the pointer. Rotated 45° so it reads as a mark
   rather than as a UI chrome box. */
.ln-cursor__box {
  position: absolute;
  top: 0; left: 0;
  width: 13px; height: 13px;
  margin: -6.5px 0 0 -6.5px;
  background: #FFFFFF;
  transform: rotate(45deg);
  transition: width .22s cubic-bezier(.22,1,.36,1),
              height .22s cubic-bezier(.22,1,.36,1),
              margin .22s cubic-bezier(.22,1,.36,1),
              background-color .18s ease-out;
}

/* ⚠️ THE WHOLE POINT OF `is-light`. The mark was white and this page runs from
   #000307 to a #F4F7FD footer, so it vanished on every light section. The class
   is set from the REAL painted background sampled under the cursor, not from a
   scroll range — sections move on this page, colours do not follow them. */
.ln-cursor.is-light .ln-cursor__box { background: #0B1220; }

/* Press feedback. */
.ln-cursor.is-down .ln-cursor__box { width: 9px; height: 9px; margin: -4.5px 0 0 -4.5px; }

/* ---- over the 3D bike: big, blue, turning, with four arrows -------------- */
.ln-cursor.is-3d .ln-cursor__box {
  width: 58px; height: 58px;
  margin: -29px 0 0 -29px;
  background: rgba(9, 78, 188, .82);
  box-shadow: 0 0 0 1px rgba(255,255,255,.5), 0 10px 30px -10px rgba(9,78,188,.7);
  animation: ln-cursor-spin 3.4s linear infinite;
}
/* Blue stays blue on light ground — it is legible on both and changing it
   would break the "this is the interactive object" signal. */
.ln-cursor.is-light.is-3d .ln-cursor__box { background: rgba(9, 78, 188, .92); }

@keyframes ln-cursor-spin {
  from { transform: rotate(45deg); }
  to   { transform: rotate(405deg); }
}

/* The four arrows: chevrons pointing out along the axes you can orbit.
   Hidden unless `is-3d`. */
.ln-cursor__arw {
  position: absolute;
  top: 0; left: 0;
  width: 9px; height: 9px;
  margin: -4.5px 0 0 -4.5px;
  border-left: 2px solid #FFFFFF;
  border-top: 2px solid #FFFFFF;
  opacity: 0;
  transition: opacity .18s ease-out;
}
.ln-cursor.is-3d .ln-cursor__arw {
  opacity: .95;
  /* ⚠️ TWO FRAMES, NOT A SMOOTH GLIDE. Ehsan asked for "4 arrows animating in
     2 frames" — `steps(2)` snaps between an inner and an outer position, which
     reads as a deliberate hint. An eased tween reads as decoration and gets
     ignored, which is the opposite of the point. */
  animation-duration: .72s;
  animation-timing-function: steps(2, jump-none);
  animation-iteration-count: infinite;
  animation-direction: alternate;
}
.ln-cursor__arw--u { transform: translate(0, -30px) rotate(45deg); }
.ln-cursor__arw--d { transform: translate(0,  30px) rotate(225deg); }
.ln-cursor__arw--l { transform: translate(-30px, 0) rotate(-45deg); }
.ln-cursor__arw--r { transform: translate( 30px, 0) rotate(135deg); }

/* Per-arrow travel: each pushes further out along its own axis, in two steps.
   The keyframes carry the whole transform (translate AND rotate) because a
   keyframe replaces the property outright — animating translate alone would
   drop each arrow's rotation and point every chevron the same way. */
.ln-cursor.is-3d .ln-cursor__arw--u { animation-name: ln-arw-u; }
.ln-cursor.is-3d .ln-cursor__arw--d { animation-name: ln-arw-d; }
.ln-cursor.is-3d .ln-cursor__arw--l { animation-name: ln-arw-l; }
.ln-cursor.is-3d .ln-cursor__arw--r { animation-name: ln-arw-r; }

/* ---- the scroll hint: ONE down arrow ------------------------------------
   Ehsan: "showing arrow down when user needs to scroll (one animating arrow
   pointing down)" — shown inside CHARACTER, inside BIKE, and whenever the
   pointer has been still for a moment.
   ⚠️ IT REUSES THE `--d` CHEVRON RATHER THAN ADDING A FIFTH ELEMENT. The four
   arrows already exist and are already positioned on the axes; a separate node
   would be a second thing to keep in sync with the box's size and offsets.
   `is-3d` wins over `is-scroll` in the JS, so the two states can never stack
   into five arrows.
   Stepped, not eased, to match the 2-frame language Ehsan asked for on the
   orbit arrows and the bitmap face the rest of the site is set in. */
.ln-cursor.is-scroll .ln-cursor__arw--d {
  opacity: .95;
  animation: ln-arw-hint 1.05s steps(2, jump-none) infinite alternate;
}
@keyframes ln-arw-hint {
  from { transform: translate(0, 22px) rotate(225deg); }
  to   { transform: translate(0, 34px) rotate(225deg); }
}
/* On light ground the chevrons must flip with the box, or the hint vanishes on
   the footer and the light panels — the same failure the square itself had. */
.ln-cursor.is-light .ln-cursor__arw {
  border-left-color: #0B1220;
  border-top-color: #0B1220;
}
/* ...except over the bike, where the box stays brand blue and white chevrons
   are what read against it. */
.ln-cursor.is-light.is-3d .ln-cursor__arw {
  border-left-color: #FFFFFF;
  border-top-color: #FFFFFF;
}

@keyframes ln-arw-u { from { transform: translate(0,-26px) rotate(45deg);  } to { transform: translate(0,-38px) rotate(45deg);  } }
@keyframes ln-arw-d { from { transform: translate(0, 26px) rotate(225deg); } to { transform: translate(0, 38px) rotate(225deg); } }
@keyframes ln-arw-l { from { transform: translate(-26px,0) rotate(-45deg); } to { transform: translate(-38px,0) rotate(-45deg); } }
@keyframes ln-arw-r { from { transform: translate( 26px,0) rotate(135deg); } to { transform: translate( 38px,0) rotate(135deg); } }

/* Reduced motion: keep the affordance, drop the movement. The arrows and the
   colour still say "draggable"; only the spin and the stepping stop. */
@media (prefers-reduced-motion: reduce) {
  .ln-cursor.is-3d .ln-cursor__box,
  .ln-cursor.is-3d .ln-cursor__arw,
  .ln-cursor.is-scroll .ln-cursor__arw--d { animation: none !important; }
  .ln-cursor.is-3d .ln-cursor__box { transform: rotate(45deg); }
  /* The hint still POINTS, it just stops bobbing — the information survives. */
  .ln-cursor.is-scroll .ln-cursor__arw--d { transform: translate(0, 28px) rotate(225deg); }
}

/* Ehsan's signature in the footer (2026-08-07) — assets/scripts/ln-signature.js.
   Replaced the DAVID bar-halftone head.
   `currentColor` so the ink follows the footer panel's own text colour rather
   than being pinned to a hex that drifts if the panel is ever recoloured.
   `overflow: visible` matters: stroke-linecap round makes the drawn line sit
   slightly proud of the geometric viewBox, and the default SVG clip shaves the
   ends of the flourish. */
.ln-signature {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
  color: #CFE6FF;
}

/* The signature's own box. Overrides `.ln-footer-fig`, which is geometry tuned
   for the PORTRAIT DAVID head it replaced — pinned to bottom:0, 17vw wide,
   behind a radial mask for a rectangular crop. A 3.7:1 landscape mark in that
   box sat on the panel floor with the copyright line running through it.
   The mask is removed outright: it existed to fade a photographic crop edge,
   and stroked vector paths have no edge to fade. */
.ln-footer-fig--sig {
  /* ⚠️ Placed in the MEASURED gap between the two footer blocks, not by eye.
     The contact block ends at x214 and the link columns start at x976, leaving
     762px. At `left: 8%` the signature's long tail ran straight through the
     email address. 21% lands it inside that corridor with clearance on both
     sides, and the width is capped so it cannot grow into either. */
  left: 21%;
  right: auto;
  width: min(30vw, 480px);
  bottom: 7.5vw;
  -webkit-mask-image: none;
  mask-image: none;
}
