/* Michal Kašpar — shared styles */
@font-face {
  font-family: "Maax";
  /* woff2 first (~36KB, ~¼ the size); .otf fallback for old browsers. */
  src: url("fonts/Maax-Regular.woff2") format("woff2"),
       url("fonts/Maax-Regular.otf") format("opentype");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Maax";
  src: url("fonts/Maax-Medium.woff2") format("woff2"),
       url("fonts/Maax-Medium.otf") format("opentype");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}
@font-face {
  /* Italic at weight 400 — Maax-Italic.otf is the shipped italic cut. */
  font-family: "Maax";
  src: url("fonts/Maax-Italic.otf") format("opentype");
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}
@font-face {
  /* Italic at weight 500 — Medium-Italic. Until a proper
     Maax-MediumItalic.otf file lands in /fonts, this falls back to
     Maax-Italic.otf at weight 400 (browsers will lean on
     `font-synthesis` to thicken the strokes). Swap the src to
     "fonts/Maax-MediumItalic.otf" once the file is uploaded. */
  font-family: "Maax";
  src: url("fonts/Maax-Italic.otf") format("opentype");
  font-weight: 500;
  font-style: italic;
  font-display: swap;
}

:root {
  /* ─── Brand palette ──────────────────────────────────────
     The single source of truth for every colour in the project.
     Just TWO backgrounds (primary + secondary accent), three text
     roles, one border. Everything else is composed via color-mix
     so a brand swap here cascades through the whole site.
     `dist/` build artefacts are NOT touched — regenerate after
     editing tokens. */

  /* Backgrounds */
  --color-bg-primary:   #16151E;   /* base dark canvas — everything sits on this */
  --color-bg-secondary: #D7FF00;   /* lime accent — CTAs, focus, hover fills */

  /* Text */
  --color-text-primary:   #F6F6F6; /* high-contrast on dark */
  --color-text-secondary: #16151E; /* dark text on accent backgrounds */
  --color-text-muted:     #636268; /* labels, captions, mono kickers */

  /* Lines & borders */
  --color-border:         #49484F; /* hairlines, dividers, sidebar edge */

  /* ─── Extended (composed from the palette above) ───── */
  /* Soft body copy — between primary text and muted */
  --color-text-soft:    #cfcfcf;
  /* Status */
  --color-status-error: #ff3b30; /* REC dot */
  /* Neutral primitives — required for blend-mode / overlay math */
  --color-pure-white:   #ffffff;
  --color-pure-black:   #000000;

  /* ─── Layout ─────────────────────────────────────────────
     Width of the fixed vertical navigation bar (Version B).
     Used by `.side-nav` itself AND by every container that
     needs to clear it on the left edge. */
  --navbar-width: clamp(48px, 4.6vw, 64px);

  /* ─── Legacy aliases ─────────────────────────────────────
     Older variable names kept so existing var(--bg) usage in
     this file + sibling stylesheets keeps working. New code
     should reach for --color-* directly. */
  --bg:          var(--color-bg-primary);
  --fg:          var(--color-text-primary);
  --muted:       var(--color-text-muted);
  --accent:      var(--color-bg-secondary);
  --line:        var(--color-border);
  --line-strong: color-mix(in srgb, var(--color-text-primary) 32%, transparent);

  --grain-opacity: 0;
  --font-sans: "Maax", "Helvetica Neue", Helvetica, Arial, sans-serif;
  --font-mono: "Maax", "Helvetica Neue", Helvetica, Arial, sans-serif;

  /* ── Responsive layout scale ───────────────────────────────
     - --frame-*  : outer corner marks (crosshairs) → very close to edges
     - --edge-*   : content padding from viewport edges (nav, footer)
     - --inset    : inner offset for kicker / timecode / strip-bottom */
  --frame-x: clamp(14px, 1.4vw, 22px);
  --frame-y: clamp(14px, 1.4vh, 22px);
  --edge-x:  clamp(28px, 3vw, 56px);
  --edge-y:  clamp(20px, 2.4vh, 36px);
  --inset:   clamp(64px, 8vh, 112px);

  /* ── Type scale ─────────────────────────────────────────────
     5 sizes, all fluid via clamp(min, vw, max):

       xs   → captions, kickers, mono labels, footer        ~14px
       base → body, paragraphs, "STARTING AT", value rows   ~20px
       lg   → lede paragraphs, FAQ questions, modal H2      ~40px
       xl   → service names, section headlines (H2)         ~60px
       2xl  → big claims, "Selected work" headline (H1)    ~120px

     Old --text-sm / --text-md are intentionally dropped — every
     usage was remapped onto the 5 above to keep the type system
     tight. */
  --text-xs:   clamp(13px, 0.95vw, 14px);
  --text-base: clamp(16px, 1.55vw, 20px);
  --text-lg:   clamp(26px, 3.2vw,  40px);
  --text-xl:   clamp(36px, 4.8vw,  60px);
  --text-2xl:  clamp(60px, 9vw,   120px);

  /* Micro tier — UI chrome below the content scale (nav labels,
     mono nums, modal fine print, cookie/preloader copy). The 5-size
     scale above is for CONTENT; these two cover sub-14px furniture
     so chrome stops drifting across 10/11/12/12.5/13/13.5px. */
  --text-2xs:   13px;   /* fine print: privacy body, cookie body, mono labels */
  --text-micro: 11px;   /* nums, tags, dense footer/menu chrome */

  /* ── Motion ─────────────────────────────────────────────────
     Single source of truth for animation timing + easing. Names
     describe the curve's feel, not where it's used, so any motion
     can reach for the right one.
       ease-out      — general smooth deceleration
       ease-out-soft — gentle overshoot-free settle (arrows, menus)
       ease-out-expo — long, dramatic reveal tail (text/line reveals)
       ease-in-out   — symmetric dramatic sweep (page wipe, burger) */
  --ease-out:      cubic-bezier(0.2, 0.8, 0.2, 1);
  --ease-out-soft: cubic-bezier(0.2, 0.85, 0.2, 1);
  --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-out-quint:cubic-bezier(0.22, 1, 0.36, 1);
  --ease-in-out:   cubic-bezier(0.7, 0, 0.3, 1);

  --dur-1: 220ms;   /* micro feedback: colour/opacity, cursor morph */
  --dur-2: 320ms;   /* small UI: arrows, icons, hovers */
  --dur-3: 540ms;   /* panels, menu items, larger moves */
  --dur-4: 720ms;   /* expressive: card line reveal */
  --dur-5: 1100ms;  /* hero line-reveal rise */
}

/* ══════════════════════════════════════════════════════════════
   HIGH-RES / XL  (≥ 1921px)
   The layout keys off 100vw math with a fixed right navbar and no
   max-width cap — but every size token is a CAPPED clamp(), so past
   ~1333px the furniture freezes at its ceiling and content strands
   in empty space on 2.5K/4K displays. Rather than cap+centre the
   whole layout (which would fight the 100vw grid math), we re-open
   the clamp ceilings here so type, spacing, navbar and insets keep
   scaling proportionally. Only token VALUES change — the whole
   system already consumes them, so this cascades everywhere.
   Tuned conservatively (~+30–45% at the top, reached near ~2560px+);
   captions/chrome grow least so fine print stays calm.
   ══════════════════════════════════════════════════════════════ */
@media (min-width: 1921px) {
  :root {
    /* Layout chrome */
    --navbar-width: clamp(64px, 3.6vw, 92px);
    --frame-x: clamp(22px, 1.4vw, 30px);
    --frame-y: clamp(22px, 1.4vh, 30px);
    --edge-x:  clamp(56px, 3.4vw, 96px);
    --edge-y:  clamp(36px, 2.6vh, 60px);
    --inset:   clamp(112px, 8vh, 168px);

    /* Type scale — captions barely grow, display scales up hard */
    --text-xs:   clamp(14px, 0.82vw, 17px);
    --text-base: clamp(20px, 1.3vw,  27px);
    --text-lg:   clamp(40px, 2.7vw,  56px);
    --text-xl:   clamp(60px, 4.1vw,  92px);
    --text-2xl:  clamp(120px, 7.6vw, 184px);

    /* Micro chrome — nudge up just slightly so it doesn't vanish */
    --text-2xs:   14px;
    --text-micro: 12px;
  }
}

* { box-sizing: border-box; margin: 0; padding: 0; }

html, body {
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font-sans);
  font-weight: 500;
  -webkit-font-smoothing: antialiased;
  text-rendering: geometricPrecision;
  letter-spacing: -0.005em;
  /* Maax stylistic-set switch. Edit `--font-features` below to
     toggle the variants embedded in this build of the Maax OTFs:
        normal   — Standard (default Maax)
        "ss01"   — Geometric (Bauhaus-elementary: round n, square m,
                   single-storey a, straight r/e/g)
        "ss02"   — subtle alt (case-sensitive forms / minor swaps)
        "ss03"   — subtle alt
     NB: the printed 205TF spec PDF lists ss01=Arrows, ss02=Geometric,
     ss03=Modern, ss04=Grotesk — but the OTF files in /fonts ship
     these features remapped (verified by parsing the GSUB table +
     side-by-side rendering). Use `font-test.html` to A/B if you
     ever swap the OTFs for a different cut. */
  font-feature-settings: var(--font-features, normal);
}

/* Display sizes — anything text-lg and up gets the Geometric stylistic
   set so the Bauhaus-elementary glyphs (single-storey a, square m,
   straight n, simplified g/r/e) carry headlines, while running body
   copy keeps Maax Standard for legibility at small sizes.
   NOTE: in this build of the Maax OTFs the Geometric variant lives
   under `ss01`, not `ss02` (the printed 205TF spec PDF documents
   ss02 = Geometric, but the actual OTF features are shifted — verified
   by parsing the GSUB table + visual diff). Override per-element by
   setting `font-feature-settings` back to `normal` further down. */
.reel .strip .title,
.vision-layer .closer-claim,
.hs-headline,
.service-name,
.modal h2,
.privacy-body h2 {
  font-feature-settings: "ss01";
}

body {
  background-color: var(--bg);
}

body {
  min-height: 100vh;
  /* clip (not hidden) so we never accidentally create a horizontal
     scroll container — kills the sideways "drift into empty space"
     that off-page sliders/headlines caused, on every page. */
  overflow-x: clip;
}
html { overflow-x: clip; }

a { color: inherit; text-decoration: none; }
button { background: none; border: none; color: inherit; font: inherit; cursor: pointer; padding: 0; }

::selection { background: var(--fg); color: var(--bg); }

/* film grain overlay — disabled per user request; element is hidden
   via display:none. Keeping the rule commented in case it returns. */
.grain { display: none !important; }

/* Navbar */
.nav {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 50;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: var(--edge-y) var(--edge-x);
  mix-blend-mode: difference;
}
.nav .home {
  font-size: 14px;
  letter-spacing: 0.02em;
  font-weight: 500;
  text-transform: uppercase;
}
.nav .home .dot {
  display: inline-block;
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--accent);
  margin-right: 10px;
  vertical-align: middle;
  transform: translateY(-1px);
}
.nav-links {
  display: flex;
  gap: 28px;
  font-size: var(--text-xs);
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.nav-links button,
.nav-links a {
  position: relative;
  padding: 4px 0;
  transition: color 220ms ease;
}
.nav-links button::after,
.nav-links a::after {
  content: "";
  position: absolute;
  left: 0; bottom: 0;
  width: 0; height: 1px;
  background: currentColor;
  transition: width 240ms ease, background 220ms ease;
}
.nav-links button:hover,
.nav-links a:hover { color: var(--accent); }
.nav-links button:hover::after,
.nav-links a:hover::after { width: 100%; }

/* Modal */
.modal-backdrop {
  position: fixed;
  inset: 0;
  background: color-mix(in srgb, var(--color-pure-black) 85%, transparent);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 420ms ease, visibility 0s linear 420ms, backdrop-filter 420ms ease;
}
.modal-backdrop.open {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transition: opacity 420ms ease, visibility 0s linear 0s, backdrop-filter 420ms ease;
}
.modal {
  background: var(--color-bg-primary);
  border: 1px solid var(--line);
  border-radius: 20px;
  max-width: 720px;
  width: 100%;
  padding: 56px 64px;
  position: relative;
  transform: translateY(20px) scale(0.985);
  opacity: 0.6;
  transition: transform 460ms var(--ease-out),
              opacity 380ms ease;
}
.modal-backdrop.open .modal {
  transform: translateY(0) scale(1);
  opacity: 1;
}
.modal .close {
  position: absolute;
  top: 18px;
  right: 22px;
  font-size: 39px;
  line-height: 1;
  letter-spacing: 0;
  text-transform: none;
  color: var(--muted);
  background: transparent;
  border: 0;
  padding: 4px 8px;
  cursor: pointer;
  transition: color 200ms ease, transform 240ms ease;
}
.modal .close:hover { color: var(--fg); transform: rotate(90deg); }
.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
.modal .label {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  /* Drop tracking — small mono eyebrow labels ("About Me", "Contact", "Privacy") read cleaner tight. */
  letter-spacing: 0;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 28px;
}
.modal h2 {
  font-size: var(--text-lg);
  font-weight: 500;
  letter-spacing: -0.02em;
  margin-bottom: 28px;
  line-height: 1.05;
}
.modal p {
  font-size: 15px;
  font-weight: 400;
  line-height: 1.6;
  color: var(--color-text-soft);
  margin-bottom: 16px;
  max-width: 60ch;
}
.modal .contact-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  padding: 18px 0;
  border-top: 1px solid var(--line);
  font-size: var(--text-base);
}
.modal .contact-row:last-child { border-bottom: 1px solid var(--line); }
.modal .contact-row .k { color: var(--muted); font-family: var(--font-mono); font-size: var(--text-xs); letter-spacing: 0; text-transform: uppercase; }
.modal .contact-row .v { font-size: var(--text-base); }
.modal .contact-row a { border-bottom: 1px solid transparent; transition: border-color 180ms ease, color 180ms ease; }
.modal .contact-row a:hover { border-color: var(--accent); color: var(--accent); }
.modal .contact-row .sep { color: var(--muted); margin-right: 2px; }

/* ── Custom cursor ──────────────────────────────────────────── */
@media (hover: hover) and (pointer: fine) {
  html, body { cursor: none; }
  a, button, [role="button"], .work-row, .dot, .bar, .social,
  .nav-links button, [data-modal-open], [data-modal-close],
  .indicator .dot, .footer-link {
    cursor: none;
  }
  .cursor {
    position: fixed;
    top: 0; left: 0;
    width: 0; height: 0;
    pointer-events: none;
    z-index: 9999;
    mix-blend-mode: difference;
    will-change: transform;
  }
  .cursor-dot {
    position: absolute;
    left: -25px; top: -25px;
    width: 50px; height: 50px;
    border-radius: 50%;
    background: var(--accent);
    transform: scale(0.2);
    transform-origin: center;
    will-change: transform;
    transition: width var(--dur-1) var(--ease-out),
                height var(--dur-1) var(--ease-out),
                left var(--dur-1) var(--ease-out),
                top var(--dur-1) var(--ease-out),
                background var(--dur-1) ease,
                border var(--dur-1) ease,
                opacity 180ms ease;
  }
  .cursor.hover .cursor-dot {
    width: 44px; height: 44px;
    left: -22px; top: -22px;
    background: transparent;
    border: 1px solid var(--accent);
    transform: scale(1);
  }
  .cursor.play .cursor-dot {
    width: 44px; height: 44px;
    left: -22px; top: -22px;
    background: transparent;
    border: none;
    border-radius: 0;
    background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 44 44"><polygon points="15,9 36,22 15,35" fill="none" stroke="%23D7FF00" stroke-width="1.4" stroke-linejoin="round"/></svg>');
    background-size: 100% 100%;
    background-repeat: no-repeat;
    transform: scale(1);
  }
  /* Arrow state — fired over the end-card "Let's talk" anchor and
     the Vision big-claim. Cursor grows into a filled yellow disc
     with an UP arrow glyph inside, replacing the small ring. Drops
     the difference blend so the yellow reads as true accent on
     top of any background, and the arrow sits crisp in black. */
  .cursor.arrow {
    mix-blend-mode: normal;
  }
  .cursor.arrow .cursor-dot {
    width: 96px; height: 96px;
    left: -48px; top: -48px;
    border-radius: 50%;
    background: var(--accent);
    border: 1px solid var(--accent);
    background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80" fill="none"><line x1="40" y1="60" x2="40" y2="22" stroke="black" stroke-width="4" stroke-linecap="round"/><path d="M24 38 L40 22 L56 38" stroke="black" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/></svg>');
    background-size: 50% 50%;
    background-repeat: no-repeat;
    background-position: center;
    transform: scale(1);
  }
  .cursor.hidden { opacity: 0; }
}

/* ── Inline highlight on hover ──────────────────────────────── */
.title.reveal-words .word > span {
  padding: 0 0.04em;
}

.modal .contact-footer {
  margin-top: 56px;
  padding-top: 0;
  display: flex;
  justify-content: space-between;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: 0;
  text-transform: uppercase;
  color: var(--muted);
}
.modal .contact-footer a {
  color: var(--muted);
  transition: color 200ms ease;
}
.modal .contact-footer a:hover { color: var(--accent); }

/* Reusable: meta/mono label */
.mono {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--muted);
}

/* Placeholder media (subtle stripes, like an empty reel slot) */
.media-ph {
  position: relative;
  overflow: hidden;
  background: var(--color-bg-primary);
  background-image:
    repeating-linear-gradient(
      45deg,
      color-mix(in srgb, var(--color-text-primary) 1.8%, transparent) 0px,
      color-mix(in srgb, var(--color-text-primary) 1.8%, transparent) 8px,
      transparent 8px,
      transparent 16px
    );
}
.media-ph::after {
  content: attr(data-label);
  position: absolute;
  bottom: 14px;
  left: 16px;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--color-text-primary) 35%, transparent);
}

/* corner crosshairs (cinema/registration marks) */
.crosshair {
  position: absolute;
  width: 18px; height: 18px;
  border: 1px solid color-mix(in srgb, var(--color-text-primary) 18%, transparent);
}
.crosshair.tl { top: 16px; left: 16px; border-right: none; border-bottom: none; }
.crosshair.tr { top: 16px; right: 16px; border-left: none; border-bottom: none; }
.crosshair.bl { bottom: 16px; left: 16px; border-right: none; border-top: none; }
.crosshair.br { bottom: 16px; right: 16px; border-left: none; border-top: none; }

/* ── Line reveal (mask-up) ────────────────────────────────────
   Typographic "rise from the bottom" effect for any element
   tagged [data-line-reveal]. The companion line-reveal.js script
   splits the text into visual lines (Range API), wraps each in
   a mask span, then flips .is-in via IntersectionObserver to
   animate the inner spans up from inside the mask. The element
   is held at visibility:hidden until the script finishes its
   first split so there's no flash of unsplit text. */
[data-line-reveal] {
  visibility: hidden;
}
[data-line-reveal].lr-ready { visibility: visible; }
[data-line-reveal] .lr-mask {
  /* No clipping — the line is NOT masked. It slides up AND fades in
     from 0 opacity, so nothing is hidden by an overflow edge. */
  display: block;
}
[data-line-reveal] .lr-line {
  display: block;
  transform: translateY(0.6em);
  opacity: 0;
  /* Line-reveal runs slower than the shared --dur-5 token (its own
     2.2s rise + fade) for a calmer, more deliberate entrance. */
  transition: transform 2200ms var(--ease-out-quint),
              opacity 2200ms var(--ease-out-quint);
  transition-delay: var(--lr-delay, 0ms);
  will-change: transform, opacity;
}
[data-line-reveal].is-in .lr-line {
  transform: translateY(0);
  opacity: 1;
}

/* ── Reveal (opacity + scale) ──────────────────────────────────
   [data-reveal] fades and gently scales up when it scrolls into
   view (wired by decode-reveal.js, which holds the reveal until any
   inner <img> has decoded — so late uploads still animate in cleanly
   instead of popping). Parallax (if also [data-parallax]) lives on
   the inner <img>, so the two transforms never collide. */
[data-reveal] {
  opacity: 0;
  transform: scale(0.965);
  transition: opacity 780ms cubic-bezier(0.22, 0.61, 0.36, 1),
              transform 780ms cubic-bezier(0.22, 0.61, 0.36, 1);
  will-change: opacity, transform;
}
[data-reveal].is-revealed {
  opacity: 1;
  transform: none;
}
@media (prefers-reduced-motion: reduce) {
  [data-reveal] { transition-duration: 1ms; transform: none; }
}
/* After a resize re-split the lines are already revealed — land
   them instantly so they don't slide up again on every tick. */
[data-line-reveal].lr-no-anim .lr-line {
  transition: none;
}
@media (prefers-reduced-motion: reduce) {
  [data-line-reveal] .lr-line {
    transform: none;
    opacity: 1;
    transition: none;
  }
}

/* ── reveal-words: lock after first play ──────────────────────
   .title.reveal-words runs a per-word slide-up tied to the
   .in-view class on its reel. On a horizontal-scroll layout
   .in-view gets removed when the reel leaves and re-added when
   the user scrolls back, which re-triggers the animation and
   produces a flicker on fast scroll. Once the first play has
   finished the line-reveal script tags the title with .played,
   pinning the words in their final position regardless of
   subsequent .in-view toggles. */
.title.reveal-words.played .word > span {
  animation: none !important;
  transform: translateY(0) !important;
}

/* ── Page transition overlay ───────────────────────────────────
   Yellow plane that rides up from below the viewport to cover
   the page when the user clicks an internal link, then on the
   next page rides further up off-screen to reveal the new
   content. JS owns the state transitions; CSS just defines the
   three rest positions and the easing curve.
   The overlay sits at z-index 9500 so it lands above the cookie
   banner and side-nav but below the first-load preloader
   (which is 9000, intentionally — the preloader only ever runs
   on a cold visit, so the two animations can't collide). */
.page-transition {
  position: fixed;
  inset: 0;
  z-index: 9500;
  background: var(--accent);
  transform: translateY(100%);
  pointer-events: none;
  transition: transform 500ms var(--ease-in-out);
}
.page-transition.is-cover { transform: translateY(0); }
.page-transition.is-exit  { transform: translateY(-100%); }
/* Pre-paint phase on the NEW page: the inline head script sets
   <html class="is-entering"> if we arrived from an internal nav.
   In that window we hold the overlay at translateY(0) WITHOUT a
   transition, so the new page never flashes through before the
   exit animation kicks in. */
html.is-entering .page-transition {
  transform: translateY(0);
  transition: none;
}

/* ── Decode reveal ─────────────────────────────────────────────
   Entrance effect (wired by decode-reveal.js): each character
   reveals as a left-to-right wavefront reaches it, flickering 2–3
   random glyphs before settling. Chars are hidden until the JS
   builds + plays them; the .dr-armed guard avoids a flash of the
   full string before the wave starts. */
.dr-char { display: inline-block; position: relative; text-align: center; }
/* Words stay whole — inline-block chars would otherwise break
   mid-word and ragged-align multi-line titles. */
.dr-word { display: inline-block; white-space: nowrap; }
[data-decode-reveal].dr-armed .dr-char { opacity: 0; }
/* The char currently at the wavefront sits inside a solid block —
   the old-terminal "decoder cursor" (Alien MU/TH/UR green-screen
   feel): a filled rectangle with the random glyph knocked out of it
   in the background colour. */
.dr-char.dr-lit {
  background: var(--dr-rect, #f6f6f6);
  color: var(--dr-ink, var(--color-bg-primary));
}

/* ══════════════════════════════════════════════════════════════
   SITE FOOTER  (shared across every page)
   Self-contained: brings its own central vertical hairline and
   normalizes the lab-edge gutter token so it renders identically
   on pages that don't define it. Behaviour (scramble-decode) is
   wired by shared.js.
   ══════════════════════════════════════════════════════════════ */
.site-footer {
  position: relative;
  z-index: 2;
  color: var(--fg);
  /* Clear the fixed right-side navbar. */
  padding-right: var(--navbar-width);
  /* Normalize the gutter token (lab/about/contact define it in
     :root; index/lab may not). */
  --lab-edge-l: 20px;
  --lab-edge-r: 20px;
}
/* Central vertical hairline — spans the footer's full height at
   the column boundary. Same viewport-based formula the rows use,
   so everything crosses exactly. Sits behind the footer content. */
.site-footer::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--grid-center, calc((100vw - var(--navbar-width)) / 2));
  width: 1px;
  background: var(--color-border);
  z-index: -1;
  pointer-events: none;
}

/* ── Bio band ──────────────────────────────────────────────── */
.site-footer .sf-bio {
  position: relative;
  min-height: 56vh;
  padding: clamp(40px, 6vh, 80px) var(--lab-edge-l) var(--lab-edge-l);
  display: flex;
  flex-direction: column;
  border-top: 1px solid var(--color-border);
}
.site-footer .sf-bio-grid {
  display: grid;
  grid-template-columns: calc((100vw - var(--navbar-width)) / 2 - var(--lab-edge-l)) 1fr;
  column-gap: var(--lab-edge-l);
  flex: 1;
  align-items: end;
}
.site-footer .c-blurb {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  line-height: 1.6;
  color: var(--muted);
  font-weight: 400;
  letter-spacing: 0;
  text-transform: uppercase;
  max-width: 52ch;
  margin: 0;
}

/* ── Menu rows ─────────────────────────────────────────────── */
.site-footer .sf-main { display: grid; }
.site-footer .sf-row {
  display: grid;
  grid-template-columns: var(--grid-center, calc((100vw - var(--navbar-width)) / 2)) 1fr;
  border-top: 1px solid var(--color-border);
}
.site-footer .sf-cell {
  padding: 0;
  min-width: 0;
}
.site-footer .sf-cell a {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: clamp(12px, 1vw, 18px);
  padding: clamp(14px, 1.8vh, 22px) var(--lab-edge-l);
  font-size: var(--text-base);
  font-weight: 500;
  letter-spacing: -0.005em;
  color: var(--fg);
  transition: background-color 220ms ease, color 220ms ease;
}
.site-footer .sf-cell a .sf-arrow {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: clamp(22px, 2vw, 30px);
  height: clamp(22px, 2vw, 30px);
  color: currentColor;
  opacity: 0;
  transform: translateX(-10px);
  transition: opacity var(--dur-1) ease,
              transform var(--dur-2) var(--ease-out-soft);
}
.site-footer .sf-cell a .sf-arrow svg { width: 100%; height: 100%; display: block; }
.site-footer .sf-cell a:hover {
  background: var(--accent);
  color: var(--color-bg-primary);
}
.site-footer .sf-cell a:hover .sf-arrow {
  opacity: 1;
  transform: translateX(0);
}

/* ── Bottom block ──────────────────────────────────────────── */
.site-footer .sf-bottom {
  position: relative;
  display: grid;
  grid-template-columns: var(--grid-center, calc((100vw - var(--navbar-width)) / 2)) 1fr;
}
.site-footer .sf-bottom::before {
  content: "";
  position: absolute;
  top: 0;
  left: var(--grid-center, calc((100vw - var(--navbar-width)) / 2));
  right: 0;
  height: 1px;
  background: var(--color-border);
  pointer-events: none;
}
.site-footer .sf-bottom-right {
  display: grid;
  grid-template-columns: 1fr 1fr;
  align-items: end;
  row-gap: clamp(80px, 14vh, 200px);
  padding: clamp(14px, 1.8vh, 22px) var(--lab-edge-l) var(--edge-y);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: 400;
  letter-spacing: 0;
  color: var(--color-text-soft);
}
.site-footer .sf-bottom .sf-bl-tl,
.site-footer .sf-bottom .sf-bl-tr {
  color: var(--muted);
  text-transform: uppercase;
  --sweep-rect: var(--fg);
  transition: color 240ms ease;
}
.site-footer .sf-bottom .sf-bl-tl:hover,
.site-footer .sf-bottom .sf-bl-tr:hover { color: var(--fg); }
.site-footer .sf-bottom .sf-bl-tr { justify-self: end; }
.site-footer .sf-bottom .sf-bl-socials {
  grid-column: 1 / span 2;
  display: flex;
  justify-content: center;
  gap: clamp(24px, 3vw, 48px);
  color: var(--color-text-soft);
}
.site-footer .sf-bottom .sf-bl-socials a {
  color: inherit;
  transition: color 220ms ease;
}
.site-footer .sf-bottom .sf-bl-socials a:hover { color: var(--accent); }
.site-footer .sf-bl-socials a .ext-arrow {
  display: inline-block;
  transition: transform 260ms var(--ease-out-soft);
}
.site-footer .sf-bl-socials a:hover .ext-arrow { transform: translateY(-4px); }
.site-footer .sf-bottom .sf-bl-mark {
  color: var(--muted);
  font-weight: 400;
  font-size: calc(var(--text-base) * 2);
  line-height: 1;
  --sweep-rect: var(--fg);
  transition: color 240ms ease;
}
.site-footer .sf-bottom .sf-bl-mark:hover { color: var(--fg); }
.site-footer .sf-bl-mark,
.site-footer .sf-bl-copy { align-self: baseline; }
.site-footer .sf-bottom .sf-bl-copy {
  justify-self: end;
  text-transform: uppercase;
  color: var(--muted);
}

/* ── Scramble-decode highlight ─────────────────────────────── */
.sweep-link {
  position: relative;
  display: inline-block;
  white-space: nowrap;
  --sweep-rect: var(--muted);
  --sweep-ink: var(--color-bg-primary);
}
.sweep-link .sweep-char {
  position: relative;
  display: inline-block;
  text-align: center;
}
/* The travelling block is a DIRECT background on the lit char (not a
   z-index:-1 pseudo — that hid behind any opaque section background,
   so the block was invisible on the clients reel + footer). Glyph
   flips to the ink shade so it reads knocked-out of the block. */
.sweep-link .sweep-char.lit {
  background: var(--sweep-rect);
  color: var(--sweep-ink);
}
.sweep-link.is-accent { --sweep-rect: var(--accent); }
.sweep-link.is-stay .sweep-char.done { color: var(--accent); }

.site-footer .sf-cell a.sweep-link:hover {
  background: transparent;
  color: var(--fg);
  padding-left: var(--lab-edge-l);
}
.site-footer .sf-cell a.sweep-link .sf-arrow { color: var(--accent); }
.site-footer .sf-cell a.sweep-link .sf-arrow svg path,
.site-footer .sf-cell a.sweep-link .sf-arrow svg line {
  stroke-width: 2px;
  vector-effect: non-scaling-stroke;
}

/* ── Shop: work-in-progress, no real destination yet ──────────
   Suppress the hover arrow entirely (it implies "go somewhere")
   and strike the label in BOTH rest and hover states so it reads
   as a not-yet-live item. Keep the decode-sweep hover so it still
   feels alive, just visibly parked. */
.site-footer .sf-cell a.sweep-link.is-wip { cursor: default; }
.site-footer .sf-cell a.sweep-link.is-wip .sf-arrow { display: none; }
/* Shop reads MUTED in both rest and hover — the decode sweep still
   runs on hover, but stays inside the muted palette (no accent flip,
   no off-white). The lit decode block + settled glyphs are remapped
   to the muted shade; the strike uses currentColor so it follows. */
.site-footer .sf-cell a.sweep-link.is-wip,
.site-footer .sf-cell a.sweep-link.is-wip:hover,
.site-footer .sf-cell a.sweep-link.is-wip .sweep-label,
.site-footer .sf-cell a.sweep-link.is-wip:hover .sweep-label {
  color: var(--muted);
}
.site-footer .sf-cell a.sweep-link.is-wip { --sweep-rect: var(--muted); }
.site-footer .sf-cell a.sweep-link.is-wip.is-stay .sweep-char.done { color: var(--muted); }
/* The sweep effect splits the label into inline-block .sweep-char
   spans, and text-decoration does NOT cross into inline-block
   descendants — so a plain line-through on .sweep-label never
   rendered. Draw the strike as a pseudo-element line across the
   whole word instead (inline-block label so it's exactly word-wide). */
.site-footer .sf-cell a.sweep-link.is-wip .sweep-label {
  position: relative;
  display: inline-block;
}
.site-footer .sf-cell a.sweep-link.is-wip .sweep-label::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 2px;
  transform: translateY(-50%);
  background: currentColor;
  pointer-events: none;
}

/* On the homepage the footer lives INSIDE the .reels scroll
   container, which already clears the navbar via its own
   padding-right — so the footer must not add it again (that would
   double-inset the right edge and break the centre-line math). */
.reels > .site-footer { padding-right: 0; }

@media (max-width: 760px), (max-width: 1024px) and (orientation: portrait) {
  .site-footer::before { display: none; }
  .site-footer {
    padding-right: 0;
    /* Clear the fixed bottom navbar, then leave only an --edge-x gap
       beneath the copyright row — matching the container's own side
       inset, instead of the previous oversized dead space. */
    padding-bottom: calc(var(--navbar-height-mobile, 64px) + var(--edge-x));
  }
  /* Menu: flatten the two-column rows into ONE column, ordered
     column-major — Shop, Lab, Work, About, Contact — with a hairline
     above every item (rows lose their box via display:contents, so
     the divider moves onto each cell). */
  .site-footer .sf-main { display: flex; flex-direction: column; }
  .site-footer .sf-row { display: contents; }
  .site-footer .sf-cell { border-top: 1px solid var(--color-border); }
  .site-footer .sf-row:nth-child(1) > .sf-cell:nth-child(1) { order: 1; } /* Shop */
  .site-footer .sf-row:nth-child(2) > .sf-cell:nth-child(1) { order: 2; } /* Lab */
  .site-footer .sf-row:nth-child(1) > .sf-cell:nth-child(2) { order: 3; } /* Work */
  .site-footer .sf-row:nth-child(2) > .sf-cell:nth-child(2) { order: 4; } /* About */
  .site-footer .sf-row:nth-child(3) > .sf-cell:nth-child(2) { order: 5; } /* Contact */
  .site-footer .sf-row > .sf-cell:empty { display: none; }

  /* Bottom block: Privacy / Cookies, then socials, then mchlkspr +
     copyright — stacked compactly. No tall min-height / space-between
     (that pushed the copyright row up and left a big void below it);
     the block now sits flush after the menu, and the footer's own
     padding-bottom leaves just an --edge-x gap before the navbar. */
  .site-footer .sf-bottom { grid-template-columns: 1fr; }
  .site-footer .sf-bottom::before { left: 0; }
  .site-footer .sf-bottom-left { display: none; }
  .site-footer .sf-bottom-right {
    grid-template-columns: 1fr 1fr;
    align-content: space-between;
    /* Taller, more "square" block on mobile — the rows were squashed
       into a short band at the bottom. Give it real height and spread
       the three rows (privacy/cookies · socials · mark/copyright)
       evenly across it so each gets room to breathe. */
    min-height: 62vw;
    row-gap: clamp(48px, 11vh, 120px);
    padding-top: clamp(40px, 8vh, 88px);
    padding-bottom: 0;
  }
  .site-footer .sf-bottom .sf-bl-socials { gap: 18px; }
  /* Bio band: cap the height so it doesn't read as a tall, mostly-empty
     plate above the menu (was 56vh with the paragraph bottom-anchored). */
  .site-footer .sf-bio { min-height: 40vh; }
  .site-footer .sf-bio-grid { grid-template-columns: 1fr; }
}

/* ════════════════════════════════════════════════════════════
   Cursor-follow label  (built + wired in shared.js)
   A lime rectangle that unrolls left→right beside the cursor over
   any [data-cursor-label] element; the text fades in once the box
   has opened. Sits OUTSIDE the difference-blended #cursor so the
   lime reads true. The page #cursor's dot is dimmed (.cursor.dim)
   while a label shows, so there's a single dot. Pointer only.
   ════════════════════════════════════════════════════════════ */
@media (hover: hover) and (pointer: fine) {
  .cursor-label {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 9998;
    pointer-events: none;
    opacity: 0;
    will-change: transform;
  }
  .cursor-label .cl-box {
    position: absolute;
    top: 0;
    left: -15px;
    transform: translateY(-50%);
    display: inline-flex;
    align-items: center;
    height: 26px;
    padding: 0 16px 0 26px;
    border-radius: 999px;
    background: var(--cl-bg, var(--accent));
    white-space: nowrap;
    clip-path: inset(0 100% 0 0 round 999px);
    transition: clip-path 380ms var(--ease-out-expo),
                background-color 220ms ease;
  }
  .cursor-label .cl-text {
    font-family: var(--font-mono);
    font-size: 12px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    white-space: nowrap;
    color: var(--cl-fg, var(--color-pure-black));
    transform: translateY(1px);
    opacity: 0;
    transition: opacity 160ms ease;
  }
  /* Random/colour mode (about hero) keeps the words' own casing
     instead of the uppercase used for fixed action labels. */
  .cursor-label.set-mode .cl-text {
    text-transform: none;
    letter-spacing: 0.02em;
  }
  /* Visible state is INSTANT (no opacity transition) so the entrance
     reads off the clip unroll alone. The exit is a separate eased
     fade (.hiding) that leaves the clip OPEN — so the box dissolves
     in place instead of closing back in reverse. */
  .cursor-label.show { opacity: 1; }
  .cursor-label.hiding {
    opacity: 0;
    transition: opacity 260ms var(--ease-out-soft);
  }
  .cursor-label.open .cl-box { clip-path: inset(0 0 0 0 round 999px); }
  .cursor-label.show .cl-text {
    opacity: 1;
    transition: opacity 200ms ease calc(380ms * 0.6);
  }

  /* ── Cursor-label icon (data-cursor-icon on a zone) ───────
     Inline SVG slot rendered after the text. Inherits cl-fg via
     currentColor. Hidden by default; .with-icon reveals it. */
  .cursor-label .cl-icon {
    display: none;
    flex: 0 0 auto;
    width: 14px;
    height: 14px;
    margin-left: 8px;
    color: var(--cl-fg, var(--color-pure-black));
    opacity: 0;
    transition: opacity 160ms ease;
  }
  .cursor-label .cl-icon svg {
    display: block;
    width: 100%;
    height: 100%;
  }
  .cursor-label.with-icon .cl-icon { display: inline-flex; }
  .cursor-label.show .cl-icon {
    opacity: 1;
    transition: opacity 200ms ease calc(380ms * 0.6);
  }

  /* ── Large pill variant (data-cursor-size="lg") ──────────── */
  .cursor-label.cl-lg .cl-box {
    height: 40px;
    padding: 0 22px 0 32px;
  }
  .cursor-label.cl-lg .cl-text {
    font-size: 13px;
    letter-spacing: 0.08em;
  }
  .cursor-label.cl-lg .cl-icon {
    width: 18px;
    height: 18px;
    margin-left: 10px;
  }
}
