/* ==========================================================================
   VarBro — demo build
   Section 1: global foundation + hero
   ========================================================================== */

@layer reset, base, components, hero, about, stack, portfolio, responsive;

/* --------------------------------------------------------------------------
   Design tokens
   -------------------------------------------------------------------------- */
:root {
  --card-primary: linear-gradient(135deg, #6c63ff 0%, #c850c0 100%);
  --card-primary-a: #6c63ff;
  --card-primary-b: #c850c0;
  --card-secondary: #1a1f2e;
  --card-secondary-light: #f6f3fa;
  --card-accent-warm: linear-gradient(135deg, #7c1263 0%, #c850c0 100%);
  --card-accent-warm-a: #7c1263;
  --card-accent-warm-b: #c850c0;
  --card-accent-cool: linear-gradient(135deg, #0e4d5c 0%, #1a8fa3 100%);
  --card-accent-cool-a: #0e4d5c;
  --card-accent-cool-b: #1a8fa3;
  --anchor-purple: #4B1956;
  --ease-brand: cubic-bezier(0.16, 1, 0.3, 1);
  --dur: 0.3s;
}

/* Supplementary tokens — non-brand literals live here so that no raw hex
   appears anywhere else in this file. */
:root {
  --pure-white: #ffffff;
  --signal-green: #22c55e;
  --term-red: #ff5f57;
  --term-amber: #febc2e;
  --term-green: #28c840;
  --player-bg: #0d1018;   /* solid, deliberately not the glass-card look */

  --font-display: 'Syne', 'Syne Fallback', 'Trebuchet MS', sans-serif;
  --font-accent: 'Space Grotesk', 'Grotesk Fallback', 'Segoe UI', sans-serif;
  --font-body: 'Montserrat', 'Montserrat Fallback', 'Segoe UI', sans-serif;
  --font-ui: 'Inter', 'Inter Fallback', 'Segoe UI', sans-serif;
  --font-mono: 'JetBrains Mono', 'Mono Fallback', ui-monospace, monospace;

  /* Grows past 1280px on wide viewports so the hero's two-column grid can
     give the (now full-size) headline the room it needs without starving
     the card column — see .hero__inner. Only .hero__inner consumes this. */
  --shell-max: clamp(1280px, 99.3vw, 1680px);
  --shell-pad: 32px;

  /* z-index scale */
  --z-canvas: 0;
  --z-content: 2;
  --z-dots: 8000;
  --z-menu: 9000;
  --z-nav: 9500;
  --z-cursor: 9998;
  --z-preloader: 9999;
}

/* --------------------------------------------------------------------------
   Metric-matched fallback fonts

   Each local fallback is rescaled so its line box matches the web font it
   stands in for. Without this, the swap from fallback to web font resizes the
   hero text block and shifts the card grid — the whole of our measured CLS.
   Percentages were measured from the real rendered metrics, not estimated.
   -------------------------------------------------------------------------- */
@font-face {
  font-family: 'Syne Fallback';
  src: local('Trebuchet MS'), local('Helvetica Neue'), local('Arial');
  font-weight: 800;
  size-adjust: 165.97%;
  ascent-override: 56.03%;
  descent-override: 16.87%;
  line-gap-override: 0%;
}

@font-face {
  font-family: 'Syne Fallback';
  src: local('Trebuchet MS'), local('Helvetica Neue'), local('Arial');
  font-weight: 700;
  size-adjust: 114.30%;
  ascent-override: 81.37%;
  descent-override: 24.50%;
  line-gap-override: 0%;
}

@font-face {
  font-family: 'Grotesk Fallback';
  src: local('Segoe UI'), local('Helvetica Neue'), local('Arial');
  font-weight: 500;
  size-adjust: 105.48%;
  ascent-override: 92.91%;
  descent-override: 27.49%;
  line-gap-override: 0%;
}

@font-face {
  font-family: 'Grotesk Fallback';
  src: local('Segoe UI'), local('Helvetica Neue'), local('Arial');
  font-weight: 700;
  size-adjust: 101.46%;
  ascent-override: 96.59%;
  descent-override: 28.58%;
  line-gap-override: 0%;
}

@font-face {
  font-family: 'Montserrat Fallback';
  src: local('Segoe UI'), local('Helvetica Neue'), local('Arial');
  font-weight: 400;
  size-adjust: 111.66%;
  ascent-override: 86.87%;
  descent-override: 22.39%;
  line-gap-override: 0%;
}

@font-face {
  font-family: 'Montserrat Fallback';
  src: local('Segoe UI'), local('Helvetica Neue'), local('Arial');
  font-weight: 700;
  size-adjust: 110.42%;
  ascent-override: 87.85%;
  descent-override: 22.64%;
  line-gap-override: 0%;
}

@font-face {
  font-family: 'Inter Fallback';
  src: local('Segoe UI'), local('Helvetica Neue'), local('Arial');
  font-weight: 400;
  size-adjust: 107.94%;
  ascent-override: 89.86%;
  descent-override: 22.23%;
  line-gap-override: 0%;
}

@font-face {
  font-family: 'Mono Fallback';
  src: local('Consolas'), local('Menlo'), local('Courier New');
  font-weight: 400;
  size-adjust: 108.93%;
  ascent-override: 93.64%;
  descent-override: 27.54%;
  line-gap-override: 0%;
}

/* --------------------------------------------------------------------------
   Reset / base
   -------------------------------------------------------------------------- */
@layer reset {
  html {
    box-sizing: border-box;
    overflow-x: clip;            /* no horizontal overflow, keeps sticky working */
    -webkit-text-size-adjust: 100%;
  }

  html, body, h1, p, ul, li, figure, pre {
    margin: 0;
    padding: 0;
  }

  ul { list-style: none; }

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

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

  button {
    font: inherit;
    color: inherit;
    background: none;
    border: 0;
    cursor: pointer;
  }

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

@layer base {
  body {
    font-family: var(--font-body);
    font-weight: 400;
    background: var(--card-secondary);
    color: var(--pure-white);
    overflow-x: clip;
    /* Lenis drives scrolling; the native smooth behaviour must stay off. */
    scroll-behavior: auto;
  }

  /* Focus ring — single source of truth for the whole site. */
  :where(a, button, [tabindex]):focus-visible {
    outline: 2px solid var(--card-primary-a);
    outline-offset: 2px;
    border-radius: 4px;
  }

  /* High-contrast mode: never rely on the brand colour alone. */
  @media (forced-colors: active) {
    :where(a, button, [tabindex]):focus-visible { outline-color: Highlight; }
  }

  /* Lucide icon sizing — never set width/height inline on an icon. */
  .icon        { inline-size: 22px; block-size: 22px; stroke-width: 1.75; color: currentColor; }
  .icon--sm    { inline-size: 18px; block-size: 18px; }
  .icon--lg    { inline-size: 28px; block-size: 28px; }
  svg.icon     { display: inline-block; vertical-align: middle; }
}

/* --------------------------------------------------------------------------
   Components
   -------------------------------------------------------------------------- */
@layer components {

  /* ---------- Button ---------- */
  .btn {
    position: relative;
    display: inline-block;
    padding: 14px 32px;
    font-family: var(--font-accent);
    font-weight: 500;
    font-size: 15px;
    letter-spacing: 0.5px;
    line-height: 1.2;
    color: var(--card-primary-a);
    border: 1.5px solid var(--card-primary-a);
    border-radius: 999px;
    background: transparent;
    isolation: isolate;
    overflow: hidden;
    cursor: pointer;
    transition: all var(--dur) ease;
  }

  /* Gradients can't be interpolated, so the fill is a pseudo-element that
     fades in — visually identical to "background fills with the gradient". */
  .btn::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    background: var(--card-primary);
    opacity: 0;
    transition: opacity var(--dur) ease;
  }

  .btn:hover:not(:disabled),
  .btn:focus-visible {
    color: var(--pure-white);
    border-color: transparent;
  }

  .btn:hover:not(:disabled)::before,
  .btn:focus-visible::before { opacity: 1; }

  /* On dark sections the outline reads white until hovered. */
  [data-theme="dark"] .btn {
    color: rgb(255 255 255 / 0.9);
    border-color: rgb(255 255 255 / 0.9);
  }

  [data-theme="dark"] .btn:hover:not(:disabled),
  [data-theme="dark"] .btn:focus-visible { color: var(--pure-white); border-color: transparent; }

  .btn--quiet {
    color: rgb(255 255 255 / 0.55);
    border-color: rgb(255 255 255 / 0.22);
  }

  /* ---------- Preloader ---------- */
  #preloader {
    position: fixed;
    inset: 0;
    z-index: var(--z-preloader);
    display: grid;
    place-items: center;
    background: var(--card-secondary);
  }

  /* Taken out of flow and centred: swapping the text re-centres it without
     ever relaying out the overlay, so the cycle contributes nothing to CLS. */
  .preloader__line {
    position: absolute;
    inset-block-start: 50%;
    inset-inline: 0;
    translate: 0 -50%;
    block-size: 1.5em;
    /* The element sizes as if empty, so retyping the line cannot register
       as a layout shift at all. */
    contain: strict;
    font-family: var(--font-accent);
    font-weight: 500;
    font-size: 16px;
    line-height: 1.5;
    letter-spacing: 0.5px;
    color: rgb(255 255 255 / 0.75);
    text-align: center;
    padding-inline: 24px;
  }

  .preloader__line.is-final { color: rgb(255 255 255 / 0.95); }

  /* Page is not scrollable until the preloader is gone. */
  body:not(.is-loaded) { overflow: hidden; height: 100%; }

  /* ---------- Nav bar ---------- */
  .nav {
    position: fixed;
    inset-block-start: 0;
    inset-inline: 0;
    z-index: var(--z-nav);
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 24px 32px;
    background: transparent;
    pointer-events: none;               /* only the controls are clickable */
  }

  .nav > * { pointer-events: auto; }

  .nav__logo {
    position: relative;
    display: block;
    inline-size: 44px;
    block-size: 44px;
    border-radius: 12px;
    overflow: hidden;
    /* No background of its own — it sits directly on the transparent nav.
       Only .is-missing (the PNGs failed to load) gets a fill, so the "Vb"
       text fallback still has something to read against. */
  }

  .nav__logo.is-missing { background: var(--card-primary); }

  .nav__logo-img {
    position: absolute;
    inset: 0;
    inline-size: 100%;
    block-size: 100%;
    object-fit: cover;
    transition: opacity 0.2s ease;
  }

  .nav__logo-img--light { opacity: 0; }

  .nav--on-light .nav__logo-img--dark  { opacity: 0; }
  .nav--on-light .nav__logo-img--light { opacity: 1; }

  /* Visible only while the logo files are absent (see main.js). */
  .nav__logo-fallback {
    position: absolute;
    inset: 0;
    display: none;
    place-items: center;
    font-family: var(--font-display);
    font-weight: 800;
    font-size: 19px;
    color: var(--pure-white);
  }

  .nav__logo.is-missing .nav__logo-fallback { display: grid; }
  .nav__logo.is-missing .nav__logo-img { display: none; }

  /* ---------- Hamburger ---------- */
  .burger {
    display: grid;
    gap: 6px;
    inline-size: 24px;
    padding: 10px 0;                    /* 44px tall touch target */
    background: none;
  }

  .burger__line {
    display: block;
    inline-size: 24px;
    block-size: 2px;
    border-radius: 2px;
    background: var(--pure-white);
    transition: background-color 0.2s ease, transform 0.35s var(--ease-brand), opacity 0.2s ease;
  }

  .nav--on-light .burger__line { background: var(--card-secondary); }

  /* X state — the menu is open. */
  .burger.is-open .burger__line { background: var(--pure-white); }
  .burger.is-open .burger__line:nth-child(1) { transform: translateY(8px) rotate(45deg); }
  .burger.is-open .burger__line:nth-child(2) { opacity: 0; }
  .burger.is-open .burger__line:nth-child(3) { transform: translateY(-8px) rotate(-45deg); }

  /* ---------- Fullscreen menu ---------- */
  .menu {
    position: fixed;
    inset: 0;
    z-index: var(--z-menu);
    background: var(--card-secondary);
    clip-path: inset(0 0 100% 0);       /* wiped in from the top */
    visibility: hidden;
  }

  .menu.is-open { visibility: visible; }

  .menu__inner {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 40px;
    block-size: 100%;
    padding: 24px;
  }

  .menu__list { text-align: center; }

  .menu__link {
    display: inline-block;
    padding: 6px 12px;
    font-family: var(--font-display);
    font-weight: 800;
    font-size: clamp(32px, 6vw, 64px);
    line-height: 1.35;
    color: var(--pure-white);
    transition: color var(--dur) ease;
  }

  .menu__link:hover { color: var(--card-primary-b); }

  .menu__social-link {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 8px 12px;
    font-family: var(--font-accent);
    font-weight: 500;
    font-size: 14px;
    letter-spacing: 0.5px;
    color: rgb(255 255 255 / 0.6);
    transition: color var(--dur) ease;
  }

  .menu__social-link:hover { color: var(--pure-white); }

  /* ---------- Side dots ---------- */
  .dots {
    position: fixed;
    inset-inline-end: 22px;
    inset-block-start: 50%;
    translate: 0 -50%;
    z-index: var(--z-dots);
    display: grid;
    justify-items: center;
    gap: 16px;
    opacity: 0;                          /* hidden during hero */
    pointer-events: none;
  }

  .dots.is-visible { pointer-events: auto; }

  .dot {
    inline-size: 8px;
    block-size: 8px;
    padding: 0;
    border-radius: 999px;
    background: rgb(255 255 255 / 0.28);
    transition: background-color var(--dur) ease;
  }

  .dot.is-active { background: var(--card-primary-a); }

  .dots--on-light .dot { background: rgb(26 31 46 / 0.3); }
  .dots--on-light .dot.is-active { background: var(--card-primary-a); }

  /* The 24px WCAG target is provided by a transparent hit area. */
  .dot::after {
    content: "";
    position: absolute;
    inline-size: 24px;
    block-size: 24px;
    translate: -50% -50%;
    inset-block-start: 50%;
    inset-inline-start: 50%;
  }

  .dot { position: relative; }

  /* ---------- Custom cursor ---------- */
  .cursor-dot,
  .cursor-ring {
    position: fixed;
    inset-block-start: 0;
    inset-inline-start: 0;
    z-index: var(--z-cursor);
    border-radius: 999px;
    pointer-events: none;
    mix-blend-mode: difference;
    opacity: 0;
    will-change: transform;
  }

  .cursor-dot {
    inline-size: 6px;
    block-size: 6px;
    background: var(--pure-white);
  }

  .cursor-ring {
    inline-size: 32px;
    block-size: 32px;
    border: 1.5px solid rgb(255 255 255 / 0.4);
  }

  body.has-cursor { cursor: none; }
  body.has-cursor .cursor-dot,
  body.has-cursor .cursor-ring { opacity: 1; }
}

/* --------------------------------------------------------------------------
   Hero
   -------------------------------------------------------------------------- */
@layer hero {

  #hero {
    position: relative;
    min-block-size: 100svh;
    display: flex;
    align-items: center;
    overflow: hidden;
    background: var(--card-secondary);
    /* Deliberately asymmetric (less top than bottom), so the vertically-
       centered .hero__inner reads slightly higher than dead-center under
       the nav rather than visually sinking toward the middle. */
    padding-block: 76px 64px;
  }

  .hero__canvas {
    position: absolute;
    inset: 0;
    z-index: var(--z-canvas);
    inline-size: 100%;
    block-size: 100%;
  }

  /* CSS fallback background — applied by shader.js when WebGL is skipped. */
  #hero.hero--css-bg {
    /* Deliberately low-key: the brand colours are currents in a near-black
       field, matching the shader, so white hero text stays legible. */
    background:
      radial-gradient(60% 80% at 18% 22%, color-mix(in srgb, var(--card-primary-a) 28%, transparent) 0%, transparent 62%),
      radial-gradient(55% 70% at 82% 28%, color-mix(in srgb, var(--card-primary-b) 20%, transparent) 0%, transparent 64%),
      radial-gradient(70% 90% at 50% 88%, color-mix(in srgb, var(--card-accent-cool-a) 42%, transparent) 0%, transparent 66%),
      linear-gradient(135deg,
        var(--card-secondary) 0%,
        color-mix(in srgb, var(--card-accent-cool-a) 42%, var(--card-secondary)) 50%,
        var(--card-secondary) 100%);
    background-size: 180% 180%, 180% 180%, 180% 180%, 300% 300%;
    background-position: 0% 50%, 100% 50%, 50% 100%, 0% 50%;
    animation: heroDrift 20s linear infinite;
  }

  #hero.hero--css-bg .hero__canvas { display: none; }

  @keyframes heroDrift {
    0%   { background-position:   0% 50%, 100% 50%,  50% 100%,   0% 50%; }
    50%  { background-position: 100% 50%,   0% 50%,  50%   0%, 100% 50%; }
    100% { background-position:   0% 50%, 100% 50%,  50% 100%,   0% 50%; }
  }

  .hero__inner {
    position: relative;
    z-index: var(--z-content);
    inline-size: 100%;
    max-inline-size: var(--shell-max);
    margin-inline: auto;
    padding-inline: var(--shell-pad);
    display: grid;
    /* Sized to exactly track .hero-headline's own clamp() below: required
       width = 11.40 * headline-font-size + 30 (30px clearance), measured
       from real canvas text metrics of line 3 ("who " + "animates", the
       binding case — always wider than "Developer" alone). Since the
       headline is clamp(41.8px, calc(8.25vw - 36.3px), 92.4px) (the whole
       clamp scaled 10% up from clamp(38px, calc(7.5vw - 33px), 84px)),
       this is that same clamp linearly scaled: min = 11.40*41.8+30, mult =
       11.40*8.25 (as a vw coefficient) with the matching flat term
       (11.40*-36.3+30), max = 11.40*92.4+30 — expressed via calc() because
       the preferred value has both a vw term and a flat additive constant,
       which clamp()'s middle argument can't express as a bare `Nvw`. The
       card column gets whatever's left via minmax(0, 1fr).

       Re-derived after the card column's own restructure (stamp moved
       above the player instead of beside it — see .hero__row--top,
       .stamp, .player): that dropped the column's real minimum width
       requirement, which used to be set by fitting stamp+player side by
       side and is now set by the terminal+building row instead (row 1
       only needs the player's own 186px floor now). Re-measured via real
       DOM min-content widths (a hidden probe span using the terminal's
       own computed font, injected per checkpoint — not eyeballed) at
       each of 960/1097/1152/1280/1366/1440/1536/1600/1920px: the
       binding minimum is building's fixed 185px + 10px gap + the
       terminal's actual min-content width for its longest line ("$ git
       push origin main"), which itself shrinks with the terminal's own
       responsive font-size clamp down to 152px at 960px and plateaus at
       192px from ~1280px on (font hits its 13px ceiling there). This
       clamp is fitted to leave ~15px of slack above that measured floor
       at every checkpoint — tightest at 960px (~3-5px real margin, since
       960 remains a genuine floor per the comment below), with much more
       slack opening up from ~1280px on as the terminal's minimum plateaus
       while the shell keeps growing. */
    grid-template-columns: clamp(506.5px, calc(94.05vw - 383.8px), 1083.4px) minmax(0, 1fr);
    align-items: center;
    gap: 56px;
  }

  /* ---------- Left column ---------- */
  .hero-eyebrow {
    font-family: var(--font-accent);
    font-weight: 500;
    font-size: 13px;
    letter-spacing: 2px;
    text-transform: uppercase;
    color: rgb(255 255 255 / 0.6);
    margin-block-end: 20px;
  }

  .hero-headline {
    font-family: var(--font-display);
    font-weight: 800;
    /* Uniform size on all 3 lines. Floor kept at 960px logical width — a
       large share of real desktop visitors run Windows display scaling
       (125-200%) on 1920x1080 monitors, which produces logical widths as
       low as ~1097px, not just genuinely narrow windows.

       Enlarged from clamp(36px, 3.96vw, 76px) now that the card column
       (see .hero__inner) needs much less width than before the stamp
       moved above the player: the 960px floor only grows 36px -> 38px
       (960 stays the genuine tight checkpoint, ~3-5px of real margin
       left there), but the ceiling grows 76px -> 84px and the whole
       1097-1600px range in between gets noticeably larger, since that's
       where the restructure freed up the most real slack. See
       .hero__inner's own comment for the exact re-measurement this
       clamp (and its matching column-width clamp) is derived from.

       Scaled up another 10% across the board (all three clamp() terms —
       floor, vw coefficient, and flat additive constant — multiplied by
       1.1 together, so the curve's shape is unchanged, just 10% larger at
       every viewport width): 38->41.8, 7.5->8.25, -33->-36.3, 84->92.4. */
    font-size: clamp(41.8px, calc(8.25vw - 36.3px), 92.4px);
    line-height: 1.05;
    color: var(--pure-white);
    margin-block-end: 24px;
    text-wrap: balance;
  }

  /* Each of the 3 forced lines is its own block; "who" and the morph word
     share the third line's normal inline flow. */
  .hero-headline__line { display: block; }

  .hero-headline__morph {
    position: relative;
    display: inline-block;
    vertical-align: top;
    /* min-width is measured in JS so the longest word never reflows the line */
  }

  /* Zero-width space keeps the line box height while .word is absolute. */
  .hero-headline__morph::before { content: "\200b"; }

  .hero-headline__morph .word {
    position: absolute;
    inset-block-start: 0;
    inset-inline-start: 0;
    white-space: nowrap;
    /* The cycling word reads as one continuous brand gradient rather than
       plain white. */
    background: var(--card-primary);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
  }

  /* Per-character gradient (FIX 2): background-clip:text does NOT reach
     through nested elements — a descendant paints its own box independently
     of an ancestor's clipped background — so each spinning-in/out letter
     needs the same gradient declaration again. hero.js sets
     background-size/-position per character (to the full word's width,
     offset by that char's own resting position) so the slices line up into
     one continuous gradient rather than one per glyph, regardless of where
     the letter is mid-flight. No overflow-hidden mask wraps these — a
     rotated letter would get its corners clipped by a box sized to its
     unrotated footprint. */
  .hero-headline__morph .wm-char {
    display: inline-block;
    vertical-align: top;
    will-change: transform, opacity;
    background: var(--card-primary);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
  }

  .hero-sub {
    font-family: var(--font-body);
    font-weight: 400;
    font-size: clamp(15px, 1.6vw, 18px);
    line-height: 1.7;
    color: rgb(255 255 255 / 0.72);
    max-inline-size: 480px;
    margin-block-end: 32px;
    text-wrap: pretty;
  }

  /* ---------- Right column: two rows, four elements ---------- */
  .hero__cards {
    display: grid;
    /* Without an explicit column track, a single "auto" column grows to
       fit its children's min-content needs and silently overflows this
       element's own assigned width instead of respecting it — the same
       reason .hero__inner's own columns are minmax(0, …) rather than
       plain 1fr. Caught via real measurement: rows were rendering wider
       than .hero__cards itself reported. */
    grid-template-columns: minmax(0, 1fr);
    /* Matches .hero__row--bottom's own 10px gap (between terminal and
       building) so the player-to-row-2 gap reads the same as the
       terminal-to-building gap — one uniform rhythm across all 3 cards. */
    gap: 10px;
    /* Nudges all 4 cards down relative to the left column's headline,
       independent of .hero__inner's own align-items: center. */
    margin-block-start: 60px;
    /* Kept for the scroll-out scatter's rotationY/rotationZ (hero.js) —
       not for hover-tilt, which no longer exists on any hero element. */
    perspective: 900px;
  }

  .hero__row {
    display: flex;
    gap: 16px;
  }

  /* Row 1 — the player, with the stamp overlaid on its top-left corner
     (see .card-scatter--stamp below) instead of sitting in its own row
     above it. Right-aligned (flex-end) so the player's own right edge
     lines up with row 2's right edge (building card, see
     .hero__row--bottom) instead of leaving a gap at the section's edge —
     the player is narrower than the column (see .card-scatter--player)
     so left-alignment (the flex default before this) stranded it short
     of the column's right edge. Now row-direction (stamp sits beside the
     player instead of overlaid on its corner): justify-content: flex-end
     keeps that same group-right-alignment, gap: 10px matches the uniform
     spacing used everywhere else in the card column (see .hero__cards,
     .hero__row--bottom). align-items: flex-end (not center) bottom-aligns
     the shorter stamp with the taller player, so the stamp's own gap down
     to row 2 matches the player's — center would leave the stamp's bottom
     edge higher than the player's, and it's row 1's shared bottom edge
     (not either child's own height) that .hero__cards' gap measures from. */
  .hero__row--top {
    flex-direction: row;
    align-items: flex-end;
    justify-content: flex-end;
    gap: 10px;
  }

  /* Sits beside the player as its own element now, not overlaid on its
     corner — flex: 0 0 auto so it keeps its own clamp()'d size instead of
     stretching/shrinking with the row. */
  .card-scatter--stamp {
    flex: 0 0 auto;
  }

  .card-scatter--player {
    flex: 0 0 auto;
    /* Narrower than the column on purpose (see .player) — width is now
       independent of the stamp, so it's set directly as a share of the
       card column rather than "fills what the stamp left over".

       clamp() floor, not a flat 54%: a blind percentage of the column
       shrinks along with it as the headline clamp below grows (see
       .hero__inner), and by ~260px wide the title/artist text (fixed
       15px/11px, not responsive) starts ellipsis-truncating hard —
       measured via each string's real text width, not eyeballed:
       "Lo-fi Coding Mix" needs ~119px, comfortably inside a ~250px
       total row (art + gap + text + gap + eq + padding). 60% only
       actually applies once the column is wide enough for it to exceed
       that floor (from roughly the 1440px+ checkpoints on); at every
       narrower one the floor wins and the title stays fully readable. */
    inline-size: clamp(260px, 60%, 340px);
    /* The real structural floor, after trimming the controls row's own
       gap/button sizes and the card's horizontal padding (see .player,
       .player__controls, .player__btn--*): 4 buttons (32+42+32+32) + 3
       gaps (8px) = 162px, + 12px padding each side = 186px. Kept as a
       secondary safety net — the clamp's own 260px floor above already
       clears it with room to spare. */
    min-inline-size: 186px;
    display: grid;
  }

  /* Row 2 — terminal + building card. Building's content (a fixed
     uppercase label + a 20px spinner, stacked — see .building__header)
     never changes with viewport width, so rather than a percentage split
     (which has to be re-tuned every time the overall card column's width
     changes — as it now does a lot, across 960-1920px), building gets a
     flat width sized to its own real minimum (the label's one-line width,
     160px — tested down to 158px before it wraps to two lines) and
     terminal absorbs whatever's left via minmax(0, 1fr). */
  .hero__row--bottom {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 160px;
    gap: 10px;
    /* Widens the row 48px into the headline gap (see .hero__inner's own
       56px column gap — capped 8px short of it so this never touches the
       headline's own box) instead of past the section's right edge — the
       terminal (this row's 1fr column) absorbs it, building stays put in
       both size and position since justify-self: end anchors the row's
       right edge exactly where it already sits. */
    inline-size: calc(100% + 48px);
    justify-self: end;
  }

  /* Same overflow trap as .hero__cards above: an explicit column keeps
     this from growing past whatever width its flex/grid parent assigns it
     to fit its child's min-content instead. */
  .card-scatter { display: grid; grid-template-columns: minmax(0, 1fr); }

  .card {
    position: relative;
    padding: 20px;
    border-radius: 20px;
    background: rgb(255 255 255 / 0.04);
    border: 1px solid rgb(255 255 255 / 0.09);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
  }

  /* Trimmed horizontal padding: part of fitting the terminal's own
     content (plus the building card, its row-mate) into the width left
     over after the full-size headline — see .hero__inner / .hero__row--bottom.

     Vertical padding enlarged +20px each side (+40px total card height,
     across two +20px passes): building matches it automatically via
     .card-scatter's grid stretch + its own justify-content: center (see
     .card--building), so both cards grow together without touching
     building's own rule. */
  .card--terminal {
    padding-inline: 10px;
    padding-block: 40px;
  }

  /* ---------- Stamp: standalone circular link-button ---------- */
  .stamp {
    position: relative;
    display: grid;
    place-items: center;
    /* Shrunk again now that it sits in its own row above the (narrower)
       player instead of beside it — reads as a small badge, clearly
       narrower than the player beneath it, not comparable in width.
       Trimmed a little further (~10%) per feedback, alongside
       .stamp__inner shrinking below. */
    inline-size: clamp(58px, 5.8vw, 79px);
    aspect-ratio: 1;
    border-radius: 999px;
    background: var(--card-accent-warm);
    transition: background var(--dur) ease, transform var(--dur) ease;
  }

  .stamp:hover {
    background: var(--card-primary);
    transform: scale(1.05);
  }

  /* No mousemove tilt on this (or any) hero element any more — its own
     spin + icon-swap on hover is enough interaction. */
  .stamp__ring {
    position: absolute;
    inset: 0;
    inline-size: 100%;
    block-size: 100%;
    animation: stampSpin 14s linear infinite;
  }

  @keyframes stampSpin {
    to { transform: rotate(360deg); }
  }

  .stamp__text {
    font-family: 'Antonio', var(--font-accent), sans-serif;
    font-weight: 500;
    /* Local units of the 200-unit viewBox. Spacing is not set here:
       textLength on the textPath (index.html) fits the string to the full
       circumference, so the size is chosen so that the natural length is
       close to it and the spacingAndGlyphs stretch stays small. */
    font-size: 24px;
    fill: rgb(255 255 255 / 0.9);
  }

  .stamp__inner {
    position: relative;
    z-index: 1;
    /* 52% of the stamp's diameter (was 60%, before that 70%) — narrower
       white circle per feedback, leaving a visibly wider gradient band for
       the rotating text than before. */
    inline-size: 52%;
    aspect-ratio: 1;
    border-radius: 999px;
    background: var(--pure-white);
    color: var(--card-primary-a);
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
  }

  /* Base transition (not just on :hover) so the swap reverses smoothly on
     mouse-leave too, instead of snapping back. */
  .stamp__icon { position: absolute; transition: translate 0.3s ease-in-out; }

  /* translate % is relative to the icon's own (fixed, 22px) box, not the
     white circle. 260% clears the white circle's radius at every size
     since .stamp__inner shrank to 60% — kept as-is; a smaller circle only
     ever needs less clearance, never more. */
  .stamp__icon--copy { translate: -260% 260%; }

  .stamp:hover .stamp__icon:not(.stamp__icon--copy) { translate: 260% -260%; }

  .stamp:hover .stamp__icon--copy {
    translate: 0 0;
    transition-delay: 0.1s;
  }

  /* ---------- Music player ---------- */
  .player {
    /* Solid, not glass: the deliberately distinct "interactive object" of
       the row. Overrides the shared .card glass background above. */
    background: var(--player-bg);
    border-radius: 28px;
    /* Vertical padding trimmed further (18px -> 13px) and the row gap
       tightened (14px -> 11px) — together with .player__art shrinking
       below, this is most of the ~13% height reduction now that the card
       is narrower and no longer needs to fill the row beside the stamp. */
    padding: 13px 12px;
    display: grid;
    /* Same overflow trap as .hero__cards/.card-scatter elsewhere in this
       file: without an explicit column, the implicit "auto" track sizes
       to a child's own max-content contribution rather than the
       container's own width — .player__controls (a centered flex row)
       was quietly ~30px wider than its own buttons, which the old,
       wider player always had room to absorb invisibly. Now that the
       player is deliberately narrower (see .card-scatter--player), that
       invisible slack pushed .player__controls past the card's own
       right edge. Caught via real getBoundingClientRect measurement,
       not visually — nothing was clipped or misaligned on screen. */
    grid-template-columns: minmax(0, 1fr);
    gap: 11px;
    align-content: center;
  }

  .player__top { display: flex; align-items: center; gap: 12px; }

  .player__art {
    flex: none;
    /* Shrunk with the player's own height reduction. */
    inline-size: 50px;
    block-size: 50px;
    border-radius: 13px;
    background: var(--card-primary);
    color: var(--pure-white);
    display: grid;
    place-items: center;
  }

  .player__meta { min-inline-size: 0; flex: 1 1 auto; }

  .player__title {
    /* Space Grotesk only loads 500/700 (see the fonts <link>) — 700 stands
       in for the spec's 600, visually close and avoids a 3rd web-font weight. */
    font-family: var(--font-accent);
    font-weight: 700;
    font-size: 15px;
    color: var(--pure-white);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .player__artist {
    font-family: var(--font-ui);
    font-weight: 400;
    font-size: 11px;
    color: rgb(255 255 255 / 0.55);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .eq {
    flex: none;
    display: flex;
    align-items: flex-end;               /* bars grow from a shared baseline */
    gap: 2px;
    block-size: 32px;
  }

  .eq__bar {
    inline-size: 3px;
    block-size: 4px;
    border-radius: 2px;
    background: var(--card-primary);
  }

  .player__progress { display: flex; align-items: center; gap: 10px; }

  .player__time {
    flex: none;
    font-family: var(--font-ui);
    font-weight: 400;
    font-size: 11px;
    color: rgb(255 255 255 / 0.45);
    font-variant-numeric: tabular-nums;
  }

  .player__bar {
    position: relative;
    flex: 1 1 auto;
    block-size: 24px;              /* comfortable hit area, WCAG 2.5.8 */
    display: flex;
    align-items: center;
    cursor: pointer;
    touch-action: pan-y;           /* horizontal drag only, page still scrolls */
  }

  .player__bar::before {
    content: "";
    position: absolute;
    inset-inline: 0;
    block-size: 3px;
    border-radius: 999px;
    background: rgb(255 255 255 / 0.15);
  }

  .player__bar-fill {
    position: absolute;
    inset-inline-start: 0;
    inline-size: 0%;
    block-size: 3px;
    border-radius: 999px;
    background: var(--card-primary);
  }

  .player__bar-handle {
    position: absolute;
    inset-inline-start: 0%;
    inline-size: 10px;
    block-size: 10px;
    border-radius: 999px;
    background: var(--pure-white);
    translate: -50% 0;
    box-shadow: 0 0 0 4px color-mix(in srgb, var(--card-primary-a) 35%, transparent);
  }

  .player__controls { display: flex; align-items: center; justify-content: center; gap: 8px; }

  .player__btn {
    flex: none;
    display: grid;
    place-items: center;
    border-radius: 999px;
    border: 1.5px solid rgb(255 255 255 / 0.25);
    color: var(--pure-white);
    transition: border-color var(--dur) ease, background-color var(--dur) ease;
  }

  .player__btn--skip,
  .player__btn--mute {
    inline-size: 32px;
    block-size: 32px;
    border-color: transparent;
  }

  .player__btn--skip:hover:not(:disabled),
  .player__btn--mute:hover:not(:disabled) { background: rgb(255 255 255 / 0.08); }

  .player__btn--play {
    inline-size: 42px;
    block-size: 42px;
  }

  .player__btn--play:hover:not(:disabled) {
    background: rgb(255 255 255 / 0.08);
    border-color: var(--card-primary-a);
  }

  .player__icon--pause { display: none; }
  .player__btn--play[aria-pressed="true"] .player__icon--play  { display: none; }
  .player__btn--play[aria-pressed="true"] .player__icon--pause { display: inline-block; }

  .player__icon--muted { display: none; }
  .player__btn--mute[aria-pressed="true"] .player__icon--unmuted { display: none; }
  .player__btn--mute[aria-pressed="true"] .player__icon--muted   { display: inline-block; }

  /* Missing audio file / no AudioContext — see cards.js */
  .player.is-disabled { opacity: 0.55; }
  .player.is-disabled .player__btn,
  .player.is-disabled .player__bar { pointer-events: none; }

  /* ---------- Terminal ---------- */
  .term {
    position: relative;
    overflow: hidden;
    border-radius: 20px;
    background: rgb(0 0 0 / 0.35);
    /* Horizontal trimmed further (12px -> 8px) — part of fitting the
       longest terminal line into the width available at 960px, where the
       building card next to it (fixed-width, already at its own bare
       minimum) can't give up any more room. */
    padding: 16px 8px;
  }

  /* Gradient top border: a plain CSS border can't take a gradient, so a
     1px strip does the job instead — clipped to the rounded corners by
     .term's own overflow:hidden. */
  .term::before {
    content: "";
    position: absolute;
    inset-block-start: 0;
    inset-inline: 0;
    block-size: 1px;
    background: var(--card-primary);
  }

  /* Mirror of the top strip, on the bottom edge. */
  .term::after {
    content: "";
    position: absolute;
    inset-block-end: 0;
    inset-inline: 0;
    block-size: 1px;
    background: var(--card-primary);
  }

  .term__bar { display: flex; gap: 6px; margin-block-end: 12px; }

  .term__dot {
    inline-size: 12px;
    block-size: 12px;
    border-radius: 999px;
    opacity: 0.6;
  }

  .term__dot--r { background: var(--term-red); }
  .term__dot--a { background: var(--term-amber); }
  .term__dot--g { background: var(--term-green); }

  .term__body {
    font-family: var(--font-mono);
    font-weight: 400;
    /* Flattened to a flat 10px (was a responsive clamp up to 13px):
       measured that the 10% headline enlargement elsewhere in
       .hero__inner shrank the cards column at every desktop width, and
       shrinks it hardest at the ~1600-1920px plateau where .hero__inner's
       own shell-max and the headline's clamp both saturate together —
       even after widening this row 48px (see .hero__row--bottom), "$ git
       push origin main" (the longest line) only fits there, without
       clipping, at the same size the narrowest checkpoints already used. */
    font-size: 10px;
    line-height: 1.6;
    /* Exactly four lines, fixed — not min-height. Combined with `pre`
       (never wrap), the box cannot change size as the text types, clears
       and restarts, so the card grid never reflows. */
    block-size: calc(4 * 1.6em);
    white-space: pre;
    overflow: hidden;
  }

  .term__line { display: block; }
  .term__line--cmd { color: rgb(255 255 255 / 0.85); }
  .term__line--ok  { color: var(--signal-green); }

  .term__caret { animation: caretBlink 1.06s steps(1, end) infinite; }

  @keyframes caretBlink {
    0%, 50%   { opacity: 1; }
    50.01%, 100% { opacity: 0; }
  }

  /* ---------- "Currently building" card ---------- */
  .card--building {
    /* Same 20px as every other hero card — .card already sets this via
       inheritance, made explicit here per spec. */
    padding: 20px;
    display: flex;
    flex-direction: column;
    /* Now deliberately matches the terminal's height instead of its own
       content height: .card-scatter's grid stretches this by default
       (no more align-self:start override), and justify-content:center
       keeps the label+spinner/title pair centered in the extra vertical
       space that stretch creates rather than left top-aligned with dead
       space below. */
    justify-content: center;
  }

  /* Stacked (spinner under the label) rather than side by side — frees the
     card to be as narrow as the label's own one-line width instead of
     needing room for label+spinner together, which is what let the
     terminal card next to it (see .hero__row--bottom) grow wider. */
  .building__header {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 6px;
    inline-size: 100%;
  }

  .building__label {
    /* var(--font-accent) = 'Space Grotesk' + the metric-matched fallback +
       generic — same typeface the spec calls for, without losing the
       CLS-safety that fallback provides. */
    font-family: var(--font-accent);
    font-weight: 500;
    font-size: 12px;
    text-transform: uppercase;
    letter-spacing: 1px;
    color: rgb(255 255 255 / 0.55);
  }

  /* Single dashed arc — a standard loading-spinner shape, not a full ring
     and not the earlier triangle mark. */
  .building__spinner {
    inline-size: 20px;
    block-size: 20px;
    color: var(--card-primary-a);
    animation: building-spin 1.4s linear infinite;
    flex-shrink: 0;
  }

  @keyframes building-spin {
    to { transform: rotate(360deg); }
  }

  .building__project {
    /* var(--font-display) = 'Syne' + fallback + generic, matching the spec. */
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 18px;
    line-height: 1.3;
    /* Exact 12px gap between the header row and the title, per spec. */
    margin-block-start: 12px;
    /* Shimmer kept exactly as implemented in the earlier pass — mostly
       white, with a soft brand-tinted highlight band (30% of the text's
       own width, since the tile is 3x that width) gliding through.
       background-size is an exact multiple of the shift distance below,
       so the loop is seamless. This means color ends up transparent (the
       gradient shows through the clipped text), not the spec's literal
       #fff — that #fff is exactly what reduced-motion below falls back to
       when the shimmer is off. */
    background: linear-gradient(
      100deg,
      var(--pure-white) 0%,
      var(--pure-white) 40%,
      color-mix(in srgb, var(--card-primary-a) 55%, var(--pure-white)) 48%,
      color-mix(in srgb, var(--card-primary-b) 55%, var(--pure-white)) 52%,
      var(--pure-white) 60%,
      var(--pure-white) 100%
    );
    background-size: 300% 100%;
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    animation: buildingShimmer 4.5s linear infinite;
  }

  @keyframes buildingShimmer {
    from { background-position: 0% 0; }
    to   { background-position: -300% 0; }
  }

  /* ---------- Placeholder sections ---------- */
  .placeholder { min-block-size: 100vh; }
  .placeholder--light { background: var(--card-secondary-light); }
  .placeholder--dark  { background: var(--card-secondary); }
  .placeholder--white { background: var(--pure-white); }

  /* ---------- Standalone placeholder page (e.g. rolam-bovebben.html) ---------- */
  .page-placeholder {
    min-block-size: 70svh;
    display: grid;
    place-items: center;
    font-family: var(--font-body);
    font-size: clamp(16px, 1.6vw, 20px);
    color: rgb(255 255 255 / 0.55);
  }

  /* ---------- Site footer (standalone pages, e.g. rolam-bovebben.html) ---------- */
  .site-footer {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    padding: 32px;
    border-block-start: 1px solid rgb(255 255 255 / 0.1);
  }

  .site-footer__text {
    font-family: var(--font-ui);
    font-size: 13px;
    color: rgb(255 255 255 / 0.5);
  }

  .site-footer__link {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    font-family: var(--font-ui);
    font-weight: 500;
    font-size: 14px;
    color: rgb(255 255 255 / 0.75);
    transition: color var(--dur) ease;
  }

  .site-footer__link:hover,
  .site-footer__link:focus-visible { color: var(--card-primary-a); }
}

/* --------------------------------------------------------------------------
   Responsive
   -------------------------------------------------------------------------- */
@layer responsive {

  /* Tablet/mobile — single column, cards below the text. */
  /* Lowered from 1439px to 959px: a large share of real desktop visitors
     run Windows display scaling (125-200%) on 1920x1080 monitors, which
     produces logical CSS widths as low as ~1097px — the OLD 1440px
     breakpoint pushed that whole common, ordinary desktop setup into the
     stacked/mobile-style layout. Getting side-by-side to genuinely work
     down to 960px took three changes together: the headline's own clamp
     floor dropped (36px vs 34px, scaling down further at narrow widths —
     see .hero-headline), row 2 changed from a percentage split to a flat
     185px for the building card (its content never scales with viewport)
     + minmax(0, 1fr) for the terminal, and the music player's buttons/
     gaps/padding were trimmed further (see .player__btn--*,
     .player__controls, .player). 960px is a genuine floor, not an
     arbitrary cut-off: below it there is no combination of these levers
     left that keeps every card usable — verified across 9 checkpoints
     from 960px to 1920px, see the report. */
  @media (max-width: 959px) {
    .hero__inner {
      grid-template-columns: 1fr;
      gap: 48px;
    }

    .hero-sub { max-inline-size: 620px; }

    /* The stamp, player, and the terminal+building row stack in one
       column, full width, in reading order: stamp -> player -> terminal
       -> building. Previously this only applied below 767px, so
       768-1439px rendered as two 2-up rows (stamp+player side by side,
       terminal+building side by side) instead of a genuine single column
       — confirmed via real DOM measurement at 1024px. `display: contents`
       on the row wrappers lifts their children (now including the stamp,
       a `.hero__row--top` sibling of the player — see .card-scatter--stamp
       above) to be direct flex items of .hero__cards, so the existing DOM
       order becomes the stacking order with no markup change. */
    .hero__cards {
      display: flex;
      flex-direction: column;
      gap: 16px;
    }

    .hero__row { display: contents; }

    /* Desktop's 54% (see .card-scatter--player) was sized for the stamp
       sitting directly above the player in its own row — irrelevant here,
       where every card stacks full-width in one column. */
    .card-scatter--player { inline-size: auto; min-inline-size: 0; }
  }

  /* Dots clutter narrow viewports. */
  @media (max-width: 899px) {
    .dots { display: none; }
  }

  @media (max-width: 767px) {
    :root { --shell-pad: 20px; }

    #hero { padding-block: 104px 56px; }

    .nav { padding: 16px 20px; }
  }

  /* --------------------------------------------------------------------
     Reduced motion — case by case, never a blanket animation kill.
     -------------------------------------------------------------------- */
  @media (prefers-reduced-motion: reduce) {
    .stamp__ring,
    .building__spinner,
    .term__caret {
      animation: none;
    }

    /* Fully static plain white text — not just the animation stopped, since
       the shimmer's gradient-clip would otherwise freeze mid-sweep with
       part of the text a permanently different (if static) tint. */
    .building__project {
      animation: none;
      background: none;
      color: var(--pure-white);
    }

    /* The fallback gradient must be completely static. */
    #hero.hero--css-bg { animation: none; }

    .burger__line { transition: background-color 0.2s ease; }

    .nav__logo-img { transition: none; }

    .stamp,
    .stamp__icon,
    .stamp__icon--copy { transition: none; }
  }
}

/* ==========================================================================
   === About section ===
   #about ("Rólam"): photo with a scroll-scrubbed four-frame sequence
   (js/about.js) beside the bio. Light theme — the nav and dot rail flip to
   their light variants via main.js's generic section[data-theme] triggers.
   ========================================================================== */
@layer about {

  #about {
    min-height: 100svh;
    padding-block: clamp(80px, 12vh, 140px);
    display: flex;
    align-items: center;
    background: var(--card-secondary-light);
  }

  .about__inner {
    width: 100%;
    max-width: 1280px;
    margin-inline: auto;
    padding-inline: 32px;
    display: grid;
    grid-template-columns: minmax(0, 5fr) minmax(0, 6fr);
    column-gap: clamp(40px, 6vw, 96px);
    align-items: center;
  }

  .about__photo {
    position: relative;
    aspect-ratio: 4 / 5;
    /* Same size as `height: min(72svh, 640px)` whenever the column has
       room for it, but expressed as a width so the 4:5 ratio survives
       when the column is narrower than that height implies (measured:
       5fr of the grid is ~381px at a 960px viewport vs the 512px a
       640px-tall 4:5 box needs). A fixed height + max-width: 100% would
       squash the box to ~381x640 there instead of shrinking the photo. */
    width: min(100%, calc(min(72svh, 640px) * 4 / 5));
    justify-self: center;
    border-radius: 28px;
    overflow: hidden;
    box-shadow: 0 30px 60px -30px color-mix(in srgb, var(--card-primary-a) 45%, transparent);
  }

  .about__img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
  }

  /* Photo 1 is the always-opaque base layer; 2-4 stack above it in order
     and are revealed 1:1 with scroll by about.js. Without JS motion (reduced
     motion, <768px, low CPU) they stay hidden and photo 1 shows alone. */
  .about__img--2,
  .about__img--3,
  .about__img--4 { opacity: 0; }

  .about__eyebrow {
    font-family: var(--font-accent);
    font-weight: 500;
    font-style: normal;
    font-size: 13px;
    letter-spacing: 2px;
    color: color-mix(in srgb, var(--card-secondary) 60%, transparent);
  }

  .about__title {
    font-family: var(--font-display);
    font-weight: 800;
    font-size: clamp(30px, 3.4vw, 52px);
    line-height: 1.1;
    color: var(--card-secondary);
    margin: 14px 0 24px;
  }

  .about__bio {
    font-family: var(--font-body);
    font-weight: 400;
    font-size: clamp(15px, 1.25vw, 18px);
    line-height: 1.75;
    color: color-mix(in srgb, var(--card-secondary) 78%, transparent);
    max-width: 540px;
  }

  .about__bio + .about__bio { margin-top: 16px; }

  /* .btn is the site's standard button (see @layer components) — #about's
     data-theme="light" already gives it the right resting look (coloured
     outline, same as how [data-theme="dark"] gives the hero's identical
     "Kapcsolat" button a white one), so no section-specific override is
     needed beyond spacing. */
  .about__more { margin-top: 32px; }

  /* All photos failed to load (about.js): drop the photo column and let
     the text span the full width. */
  #about.about--no-photo .about__inner { grid-template-columns: minmax(0, 1fr); }
  #about.about--no-photo .about__photo { display: none; }

  /* Same breakpoint as the hero's single-column switch. */
  @media (max-width: 959px) {
    .about__inner {
      grid-template-columns: minmax(0, 1fr);
      row-gap: 36px;
    }

    .about__photo {
      height: auto;
      width: min(100%, 420px);
    }
  }
}

/* ==========================================================================
   === Stack section ===
   #stack ("A stackem"): a pinned, scroll-driven 3D carousel of 7 technology
   badges (js/stack.js) over an ambient Three.js particle field.

   The ring is real DOM in a preserve-3d context — not WebGL — so the logos
   stay crisp at any DPI and the names/descriptions stay real, selectable
   text. js/stack.js writes only --zf (0..1 along the radius), --s, opacity
   and filter per badge; the transform ORDER (rotateY then translateZ) has
   to come from CSS, because GSAP composes translate before rotate, which
   would stack every badge on the same point instead of spreading them
   around the ring.
   ========================================================================== */
@layer stack {

  #stack {
    position: relative;
    display: flex;
    align-items: center;
    min-block-size: 100svh;
    padding-block: clamp(48px, 7svh, 96px);
    background: var(--card-secondary);
    /* No overflow: hidden here. The canvas is inset:0 inside this (relative)
       box so it cannot bleed out anyway, and clipping the pinned section
       made unpinning register as a full-viewport layout shift (measured:
       0.96 of the section's 2.0 CLS came from it alone). */
  }

  #stack-bg {
    position: absolute;
    inset: 0;
    z-index: 0;
    inline-size: 100%;
    block-size: 100%;
    pointer-events: none;
    /* The canvas is a decorative backdrop: nothing inside it can affect
       layout. Saying so matters — without it, the moment the section pins
       (position: static -> fixed) Chrome re-resolves this absolutely
       positioned replaced element and books it as a full-viewport layout
       shift, which measured as CLS 1.0 on a normal wheel scroll even though
       nothing visibly moves. With containment it is 0.004. */
    contain: layout paint;
  }

  .stack__inner {
    position: relative;
    z-index: 1;
    inline-size: 100%;
    max-inline-size: 1280px;
    margin-inline: auto;
    padding-inline: var(--shell-pad);
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
  }

  .stack__eyebrow {
    font-family: var(--font-accent);
    font-weight: 500;
    font-size: 13px;
    letter-spacing: 2px;
    color: rgb(255 255 255 / 0.55);
  }

  .stack__title {
    font-family: var(--font-display);
    font-weight: 800;
    font-size: clamp(30px, 3.4vw, 52px);
    line-height: 1.1;
    color: var(--pure-white);
    margin: 14px 0 0;
  }

  /* ---------- the ring ---------- */

  .stack__stage {
    position: relative;
    inline-size: 100%;
    /* An svh term as well as vw: on a short viewport the ring has to shrink
       or it collides with the title and the caption (checked at 700px). */
    block-size: clamp(250px, 40svh, 420px);
    margin-block-start: clamp(16px, 3svh, 40px);
    perspective: 1200px;
    perspective-origin: 50% 42%;
  }

  .stack__ring {
    /* A zero-size box at the stage centre: every badge is positioned against
       this single point, so the ring axis is exactly the centre. */
    position: absolute;
    inset-block-start: 50%;
    inset-inline-start: 50%;
    inline-size: 0;
    block-size: 0;
    transform-style: preserve-3d;
    /* Negative X tilt looks DOWN onto the ring (near edge drops, far edge
       rises), which is what makes it read as an ellipse rather than a line.

       The order here is load-bearing and must stay in CSS: rotateX THEN
       rotateY spins the ring about its own tilted axis (a lazy susan).
       gsap.set(ring, {rotationX, rotationY}) produces the matrix the other
       way round — decoded from a live frame, it was Ry·Rx, i.e. the ring
       was tilted first and then spun about the WORLD vertical, so the tilt
       swept around with the rotation and the front badge picked up a roll
       that grew to ~15deg by the end of the pin. stack.js only writes
       --ring-y. */
    transform: rotateX(-12deg) rotateY(var(--ring-y, 0deg));
    --ring-r: clamp(230px, 26vw, 380px);
    --badge-w: clamp(150px, 15vw, 200px);
    --badge-h: clamp(128px, 13vw, 168px);
  }

  .stack__badge {
    position: absolute;
    inline-size: var(--badge-w);
    block-size: var(--badge-h);
    /* Centre the badge box on the ring origin so rotateY turns it about the
       ring axis rather than about its own corner. */
    inset-inline-start: calc(var(--badge-w) / -2);
    inset-block-start: calc(var(--badge-h) / -2);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: clamp(10px, 1.2vw, 16px);
    /* --a is fixed per badge (set in the markup); --zf and --s are driven by
       stack.js — --zf 0 -> 1 is the entrance flying out from the centre. */
    transform:
      rotateY(var(--a, 0deg))
      translateZ(calc(var(--ring-r) * var(--zf, 1)))
      scale(var(--s, 1));
    will-change: transform, opacity, filter;
  }

  .stack__icon {
    inline-size: clamp(44px, 4vw, 56px);
    block-size: clamp(44px, 4vw, 56px);
    /* A gradient cannot be applied to an SVG fill, so the logo is a mask over
       a painted box: white normally, brand gradient when focused. */
    background: var(--pure-white);
    -webkit-mask-repeat: no-repeat;
    -webkit-mask-position: center;
    -webkit-mask-size: contain;
    mask-repeat: no-repeat;
    mask-position: center;
    mask-size: contain;
    transition: background 0.3s ease;
  }

  /* The mask URLs live here, not in the markup: a url() inside a custom
     property is resolved against the stylesheet that consumes it, so an
     inline --icon written in index.html resolved to /css/assets/icons/*.svg
     and 404'd, leaving every logo invisible. Declared in this file the
     relative path is correct, and it stays correct under the /demovarbro/
     base path on Pages. */
  .stack__badge[data-icon="html5"] .stack__icon {
    -webkit-mask-image: url('../assets/icons/html5.svg');
            mask-image: url('../assets/icons/html5.svg');
  }
  .stack__badge[data-icon="css3"] .stack__icon {
    -webkit-mask-image: url('../assets/icons/css3.svg');
            mask-image: url('../assets/icons/css3.svg');
  }
  .stack__badge[data-icon="javascript"] .stack__icon {
    -webkit-mask-image: url('../assets/icons/javascript.svg');
            mask-image: url('../assets/icons/javascript.svg');
  }
  .stack__badge[data-icon="vscode"] .stack__icon {
    -webkit-mask-image: url('../assets/icons/vscode.svg');
            mask-image: url('../assets/icons/vscode.svg');
  }
  .stack__badge[data-icon="gsap"] .stack__icon {
    -webkit-mask-image: url('../assets/icons/gsap.svg');
            mask-image: url('../assets/icons/gsap.svg');
  }
  .stack__badge[data-icon="threejs"] .stack__icon {
    -webkit-mask-image: url('../assets/icons/threejs.svg');
            mask-image: url('../assets/icons/threejs.svg');
  }
  .stack__badge[data-icon="claude"] .stack__icon {
    -webkit-mask-image: url('../assets/icons/claude.svg');
            mask-image: url('../assets/icons/claude.svg');
  }

  .stack__badge.is-focused .stack__icon { background: var(--card-primary); }

  .stack__name {
    font-family: var(--font-accent);
    font-weight: 500;
    font-size: clamp(14px, 1.2vw, 17px);
    letter-spacing: 0.5px;
    color: rgb(255 255 255 / 0.8);
    transition: color 0.3s ease;
  }

  .stack__badge.is-focused .stack__name { color: var(--pure-white); }

  /* Always in the DOM as real text for screen readers and crawlers; clipped
     rather than display:none / opacity:0 so it is still announced. The flat
     fallback below un-clips it. */
  .stack__desc {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }

  /* ---------- caption: a visual projection of the focused badge ---------- */

  .stack__caption {
    margin-block-start: clamp(12px, 2.5svh, 32px);
    /* Reserved height: swapping the text must not move the layout. */
    min-block-size: clamp(74px, 11svh, 104px);
    max-inline-size: 560px;
  }

  .stack__caption-name {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: clamp(18px, 1.8vw, 24px);
    line-height: 1.2;
    color: var(--pure-white);
    margin: 0 0 8px;
  }

  .stack__caption-desc {
    font-family: var(--font-body);
    font-weight: 400;
    font-size: clamp(14px, 1.1vw, 16px);
    line-height: 1.6;
    color: rgb(255 255 255 / 0.62);
    margin: 0;
  }

  /* ======================================================================
     Flat fallback — viewport < 768px, hardwareConcurrency < 4 (the same
     low-CPU test shader.js uses) or prefers-reduced-motion. No pin, no 3D,
     no WebGL: a card grid with every description visible at rest.
     ====================================================================== */

  #stack.stack--flat {
    min-block-size: 0;
    padding-block: clamp(72px, 10svh, 120px);
    background:
      radial-gradient(60% 55% at 18% 18%, color-mix(in srgb, var(--card-primary-a) 26%, transparent) 0%, transparent 62%),
      radial-gradient(55% 50% at 85% 30%, color-mix(in srgb, var(--card-primary-b) 20%, transparent) 0%, transparent 64%),
      radial-gradient(70% 60% at 50% 95%, color-mix(in srgb, var(--card-accent-cool-a) 30%, transparent) 0%, transparent 66%),
      var(--card-secondary);
  }

  .stack--flat #stack-bg,
  .stack--flat .stack__caption { display: none; }

  .stack--flat .stack__stage {
    block-size: auto;
    perspective: none;
    margin-block-start: clamp(28px, 5svh, 48px);
  }

  .stack--flat .stack__ring {
    position: static;
    inline-size: 100%;
    block-size: auto;
    transform: none;
    transform-style: flat;
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: clamp(12px, 1.6vw, 20px);
  }

  .stack--flat .stack__badge {
    position: static;
    inline-size: auto;
    block-size: auto;
    transform: none;
    filter: none;
    padding: clamp(20px, 2.4vw, 28px) clamp(14px, 1.8vw, 22px);
    border-radius: 20px;
    background: rgb(255 255 255 / 0.04);
    border: 1px solid rgb(255 255 255 / 0.09);
    gap: 12px;
  }

  /* Un-clipped here: in the flat grid the description is the content, not an
     accessibility-only copy. */
  .stack--flat .stack__desc {
    position: static;
    inline-size: auto;
    block-size: auto;
    margin: 0;
    overflow: visible;
    clip-path: none;
    white-space: normal;
    font-family: var(--font-body);
    font-size: clamp(12px, 1vw, 13.5px);
    line-height: 1.6;
    color: rgb(255 255 255 / 0.55);
    text-wrap: pretty;
  }

  /* 7 cards read as 4 + 3 once there is room for four columns. */
  @media (min-width: 768px) {
    .stack--flat .stack__ring { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  }
}

/* ==========================================================================
   === Portfolio section ===
   #portfolio ("Munkáim"): "stacking cases". Every project is one dark,
   viewport-tall card floating on the light section. The cards are
   position: sticky children of ONE shared container (#pf-deck) — the next
   one scrolls up and covers the previous, which scales down and darkens
   (scrubbed in js/portfolio.js; no pin anywhere in this section). Card
   content is rendered from js/portfolio-data.js.

   Stacking is on for >= 900px wide (width only — a short window keeps the
   desktop layout, see the compact tiers) and no reduced-motion.
   Everywhere else the same cards are ordinary stacked blocks.

   Tweak knobs (on .pf-deck):  --dwell  scroll held on a settled card before
                                        the next one starts to enter
                               --hold   the same for the Instagram card

   Spacing inside a card uses ONE scale: --s1..--s5 = 8 / 16 / 24 / 32 / 48
   (multiples 1,2,3,4,6 of --u). On short screens --u shrinks, so all the
   steps shrink in proportion.
   ========================================================================== */
@layer portfolio {

  #portfolio {
    padding-block: clamp(80px, 12vh, 140px) 24px;
    background: var(--card-secondary-light);
  }

  .pf__inner {
    max-width: 1280px;
    margin-inline: auto;
    padding-inline: 32px;
  }

  .pf__head { margin-block-end: clamp(32px, 5vh, 56px); max-width: 720px; }

  .pf__eyebrow {
    font-family: var(--font-accent);
    font-weight: 500;
    font-size: 13px;
    letter-spacing: 2px;
    color: color-mix(in srgb, var(--card-secondary) 60%, transparent);
  }

  .pf__title {
    font-family: var(--font-display);
    font-weight: 800;
    font-size: clamp(30px, 3.4vw, 52px);
    line-height: 1.1;
    color: var(--card-secondary);
    margin: 14px 0 20px;
  }

  .pf__sub {
    font-family: var(--font-body);
    font-weight: 400;
    font-size: clamp(15px, 1.25vw, 18px);
    line-height: 1.7;
    color: color-mix(in srgb, var(--card-secondary) 62%, transparent);
  }

  /* ---------- Deck ---------- */
  .pf-deck {
    --dwell: 60svh;
    --hold: 80svh;
    display: flex;
    flex-direction: column;
    gap: 24px;
    padding-inline: 16px;
  }

  .pf-hold { display: none; }

  /* ---------- Shared bits ---------- */
  .pf-tag {
    display: inline-flex;
    align-items: center;
    block-size: 28px;
    padding: 0 12px;
    font-family: var(--font-accent);
    font-weight: 500;
    font-size: 11px;
    letter-spacing: 0.8px;
    text-transform: uppercase;
    line-height: 1;
    color: var(--pure-white);
    background: rgb(255 255 255 / 0.1);
    border: 1px solid rgb(255 255 255 / 0.18);
    border-radius: 999px;
    white-space: nowrap;
  }
  .pf-tag--ghost {
    background: transparent;
    color: rgb(255 255 255 / 0.55);
    border-color: rgb(255 255 255 / 0.3);
  }

  /* Browser-window mockup (cards and dialog). */
  .pf-win {
    display: block;
    inline-size: 100%;
    margin-inline: auto;
    padding: 0;
    border: 1px solid rgb(255 255 255 / 0.08);
    border-radius: 14px;
    overflow: hidden;
    background: #0b0c12;
    color: inherit;
    text-align: start;
    cursor: pointer;
    box-shadow:
      0 40px 80px -30px color-mix(in srgb, var(--accent, #6c63ff) 45%, transparent),
      0 20px 40px -20px rgb(0 0 0 / 0.5);
  }
  .pf-win--static { cursor: inherit; }
  .pf-win:focus-visible { outline: 3px solid var(--accent, var(--card-primary-a)); outline-offset: 4px; }

  .pf-win__bar {
    display: flex;
    align-items: center;
    gap: 12px;
    block-size: 36px;
    padding-inline: 14px;
    background: #1d1f2b;
  }

  .pf-win__dots { display: flex; gap: 7px; flex: none; }
  .pf-win__dots i { display: block; inline-size: 10px; block-size: 10px; border-radius: 999px; }
  .pf-win__dots i:nth-child(1) { background: #ff5f57; }
  .pf-win__dots i:nth-child(2) { background: #febc2e; }
  .pf-win__dots i:nth-child(3) { background: #28c840; }

  .pf-win__addr {
    flex: 0 1 auto;
    min-inline-size: 0;
    max-inline-size: 66%;
    margin-inline: auto;
    padding: 4px 12px;
    display: flex;
    align-items: center;
    gap: 6px;
    border-radius: 999px;
    background: rgb(255 255 255 / 0.06);
    font-family: var(--font-ui);
    font-size: 12px;
    line-height: 1.2;
    color: rgb(255 255 255 / 0.55);
  }
  .pf-win__addr .pf-win__lock { flex: none; inline-size: 12px; block-size: 12px; }
  .pf-addr-text { min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* link variant — only in the dialog, only when a project has addressLabel + addressHref */
  .pf-win__addr--link { text-decoration: none; cursor: pointer; transition: color 0.2s ease; }
  .pf-win__addr--link:hover,
  .pf-win__addr--link:focus-visible { color: rgb(255 255 255 / 0.9); }

  .pf-win__view {
    display: block;
    position: relative;
    aspect-ratio: 16 / 9;
    background: #0b0c12;
  }
  .pf-win__view--contain { aspect-ratio: 16 / 10; }

  .pf-win__view img,
  .pf-win__video {
    position: absolute;
    inset: 0;
    inline-size: 100%;
    block-size: 100%;
    object-fit: cover;
    object-position: top center;
    display: block;
  }
  .pf-win__view--contain img { object-fit: contain; }

  .pf-win__video { opacity: 0; transition: opacity 0.4s ease; }
  .pf-win.has-video .pf-win__video { opacity: 1; }

  /* ---------- Case card — one-column layout (mobile / flow) ----------
     Order: meta row, name, window, task, solution, chips, button. The
     wrappers dissolve (display: contents) so every piece is a flex item of
     .pf-case__inner and can be ordered. */
  .pf-case {
    --u: 8px;
    --s1: var(--u);
    --s2: calc(var(--u) * 2);
    --s3: calc(var(--u) * 3);
    --s4: calc(var(--u) * 4);
    --s5: calc(var(--u) * 6);
    position: relative;
    isolation: isolate;
    overflow: hidden;
    border-radius: 24px;
    color: var(--pure-white);
    background:
      radial-gradient(ellipse 70% 80% at 75% 40%, color-mix(in srgb, var(--accent) 22%, transparent), transparent 70%),
      #12131c;
    transform-origin: 50% 0;
  }

  .pf-case__bgname {
    position: absolute;
    inset-inline: 0;
    inset-block-start: 50%;
    translate: 0 -50%;
    z-index: 0;
    overflow: hidden;
    pointer-events: none;
    white-space: nowrap;
    font-family: var(--font-display);
    font-weight: 800;
    font-size: clamp(80px, 13vw, 240px);
    line-height: 1;
    text-transform: uppercase;
    color: rgb(255 255 255 / 0.055);
  }
  .pf-case__bgname span { display: inline-block; margin-inline-start: -1vw; }

  .pf-case__inner {
    position: relative;
    z-index: 1;
    display: flex;
    flex-direction: column;
    gap: var(--s2);
    padding: var(--s3);
    box-sizing: border-box;
  }

  .pf-case__content,
  .pf-case__stage,
  .pf-case__text,
  .pf-case__top,
  .pf-case__mid,
  .pf-case__bottom { display: contents; }

  .pf-case__meta   { order: 1; }
  .pf-case__name   { order: 2; }
  .pf-case__media  { order: 3; min-inline-size: 0; }
  .pf-case__block  { order: 4; }
  .pf-case__chips  { order: 5; }
  .pf-case__btn    { order: 6; }

  .pf-case__meta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s1) 10px;
    min-block-size: 36px;
  }

  .pf-case__num {
    font-family: var(--font-accent);
    font-weight: 500;
    font-size: 13px;
    letter-spacing: 1.5px;
    line-height: 1;
    color: rgb(255 255 255 / 0.6);
    margin-inline-end: 6px;
  }

  .pf-case__name {
    font-family: var(--font-display);
    font-weight: 800;
    font-size: clamp(28px, 7vw, 40px);
    line-height: 1.05;
    margin: 0;
    color: var(--pure-white);
  }

  .pf-case__label {
    font-family: var(--font-accent);
    font-weight: 600;
    font-size: 12px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    line-height: 1;
    color: var(--accent);
    margin-block-end: var(--s1);
  }

  .pf-case__block p {
    font-family: var(--font-body);
    font-weight: 400;
    font-size: 15px;
    line-height: 1.5;
    color: rgb(255 255 255 / 0.75);
  }

  .pf-case__chips { display: flex; flex-wrap: wrap; gap: var(--s1); list-style: none; padding: 0; margin: 0; }
  .pf-case__chips li {
    padding: 5px 13px;
    font-family: var(--font-ui);
    font-size: 13px;
    line-height: 1.3;
    color: rgb(255 255 255 / 0.85);
    border: 1px solid rgb(255 255 255 / 0.2);
    border-radius: 999px;
  }

  .pf-case__btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    inline-size: 100%;
    min-block-size: 48px;
    padding-block: 0;
  }

  /* ---------- Instagram card ---------- */
  .pf-ig {
    position: relative;
    isolation: isolate;
    overflow: hidden;
    border-radius: 24px;
    min-block-size: 420px;
    padding: 40px 24px;
    display: grid;
    place-items: center;
    text-align: center;
    color: var(--pure-white);
    background: var(--card-primary);
    transform-origin: 50% 0;
  }

  /* Slowly drifting soft glow. */
  .pf-ig::before,
  .pf-ig::after {
    content: "";
    position: absolute;
    z-index: -1;
    inline-size: 70%;
    aspect-ratio: 1;
    border-radius: 50%;
    filter: blur(60px);
    opacity: 0.55;
    pointer-events: none;
  }
  .pf-ig::before {
    inset-block-start: -25%;
    inset-inline-start: -10%;
    background: radial-gradient(circle, rgb(255 255 255 / 0.55), transparent 65%);
    animation: pfGlowA 16s ease-in-out infinite alternate;
  }
  .pf-ig::after {
    inset-block-end: -35%;
    inset-inline-end: -10%;
    background: radial-gradient(circle, #4B1956, transparent 65%);
    animation: pfGlowB 20s ease-in-out infinite alternate;
  }
  @keyframes pfGlowA { to { transform: translate(30%, 22%) scale(1.15); } }
  @keyframes pfGlowB { to { transform: translate(-28%, -20%) scale(1.2); } }

  .pf-ig__inner { display: flex; flex-direction: column; align-items: center; gap: clamp(8px, 1.6svh, 14px); max-inline-size: 1100px; }

  /* The wrapper floats (6px travel); the svg inside is free for the entrance tween. */
  .pf-ig__float { display: block; animation: pfFloat 5s ease-in-out infinite alternate; }
  @keyframes pfFloat { from { transform: translateY(-3px); } to { transform: translateY(3px); } }
  .pf-ig__glyph { display: block; inline-size: 56px; block-size: 56px; color: rgb(255 255 255 / 0.9); }

  .pf-ig__kicker {
    font-family: var(--font-body);
    font-size: clamp(14px, 1.2vw, 17px);
    color: rgb(255 255 255 / 0.7);
  }

  .pf-ig__title {
    font-family: var(--font-display);
    font-weight: 800;
    font-size: clamp(32px, min(4.6vw, 7.2svh), 76px);
    line-height: 1.05;
    text-wrap: balance;
    color: var(--pure-white);
  }

  .pf-ig__handle {
    font-family: var(--font-accent);
    font-weight: 500;
    font-size: clamp(15px, 1.4vw, 20px);
    letter-spacing: 0.5px;
    color: rgb(255 255 255 / 0.9);
  }

  .pf-ig__btn {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    margin-block-start: 8px;
    padding: 14px 32px;
    font-family: var(--font-accent);
    font-weight: 500;
    font-size: 15px;
    letter-spacing: 0.5px;
    line-height: 1.2;
    color: var(--pure-white);
    text-decoration: none;
    border: 1.5px solid var(--pure-white);
    border-radius: 999px;
    background: transparent;
    cursor: pointer;
    transition: background-color var(--dur) ease, color var(--dur) ease, border-color var(--dur) ease;
  }
  .pf-ig__btn:hover,
  .pf-ig__btn:focus-visible { background: var(--pure-white); color: #4B1956; }
  .pf-ig__btn:focus-visible { outline: 3px solid var(--pure-white); outline-offset: 3px; }

  /* ---------- Desktop: meta bar + two-column stage ----------
     Row 1 (36px): "01 / 06" + chips. Row 2: the stage — text column left
     (~36%), browser window right (~64%). The window is the height
     reference: the stage is as tall as the window, and the text column
     stretches to it, so name/window share a top edge and button/window a
     bottom edge. Text uses the same left edge as the meta bar. */
  @media (min-width: 900px) {
    .pf-case {
      --u: clamp(5px, 0.9svh, 8px);
      --padx: 48px;
      border-radius: 32px;
    }

    .pf-case__inner {
      display: grid;
      grid-template-rows: 36px minmax(0, 1fr);
      row-gap: var(--s3);
      /* top meta row 32px from the card top; the left padding also clears the fixed nav logo */
      padding: var(--s4) var(--padx) var(--s4) max(var(--padx), 84px);
    }

    .pf-case__content {
      display: grid;
      align-content: center;
      min-block-size: 0;
      min-inline-size: 0;
    }

    .pf-case__stage {
      display: grid;
      grid-template-columns: minmax(0, 36fr) minmax(0, 64fr);
      column-gap: var(--s5);
      align-items: stretch;
      min-inline-size: 0;
    }

    .pf-case__text {
      display: flex;
      flex-direction: column;
      justify-content: space-between;
      gap: var(--s2);
      min-inline-size: 0;
    }
    .pf-case__top,
    .pf-case__mid,
    .pf-case__bottom { display: flex; flex-direction: column; min-inline-size: 0; }
    .pf-case__mid    { gap: var(--s3); }
    .pf-case__bottom { gap: var(--s3); }

    .pf-case__meta, .pf-case__name, .pf-case__media, .pf-case__block, .pf-case__chips, .pf-case__btn { order: 0; }

    .pf-case__meta { flex-wrap: nowrap; min-block-size: 0; block-size: 36px; gap: var(--s1); }
    .pf-case__num { margin-inline-end: var(--s1); }

    .pf-case__name {
      font-size: clamp(26px, 2.7vw, 52px);
      line-height: 1.02;
      /* optical: pulls the cap-height up to the window's top edge */
      margin: -0.03em 0 0;
    }

    .pf-case__block p { font-size: clamp(12.5px, 1.05vw, 16px); }
    .pf-case__chips li { padding: 4px 12px; font-size: clamp(11.5px, 0.95vw, 13px); }

    .pf-case__btn {
      inline-size: auto;
      align-self: flex-start;
      min-block-size: 0;
      padding: clamp(10px, 1.3svh, 14px) 28px;
      font-size: clamp(13px, 1vw, 15px);
    }

    .pf-case__media { display: flex; align-items: flex-start; }
    .pf-case .pf-win { margin-inline: auto 0; }

    .pf-ig { border-radius: 32px; }
  }

  @media (min-width: 900px) and (max-width: 1279px) {
    .pf-case { --padx: 32px; }
  }

  /* ---------- Compact tiers: SHORT desktop windows ----------
     Width alone decides the layout (>= 900px = desktop + stacking). A short
     window (e.g. a non-fullscreen browser, ~1097x480) only shrinks or hides
     things — the alignment grid itself never changes. */
  @media (min-width: 900px) and (max-height: 699px) {
    .pf-case {
      --u: clamp(3.5px, 0.85svh, 6px);
      --padx: 32px;
    }
    .pf-case__inner {
      grid-template-rows: 28px minmax(0, 1fr);
      padding: 24px var(--padx) 24px max(var(--padx), 84px);
    }
    .pf-case__meta { block-size: 28px; }
    .pf-case__name { font-size: clamp(24px, calc(3.2svh + 1vw), 40px); }
    .pf-case__block p { font-size: 13px; line-height: 1.45; }
    .pf-case__chips li { padding: 3px 10px; font-size: 11.5px; }
    .pf-case__btn { padding: 9px 22px; font-size: 13px; }
  }

  @media (min-width: 900px) and (max-height: 559px) {
    /* the three feature chips stay in the case-study dialog */
    .pf-case__chips { display: none; }
    .pf-case__text { gap: var(--s1); }
    .pf-case__mid, .pf-case__bottom { gap: var(--s2); }
  }

  @media (min-width: 900px) and (max-height: 439px) {
    /* keep "A feladat" + the button */
    .pf-case__mid .pf-case__block:nth-child(2) { display: none; }
    .pf-case__inner { padding: 16px var(--padx) 16px max(var(--padx), 84px); }
  }

  /* ---------- Stacking: sticky cards, dwell, hold ---------- */
  @media (min-width: 900px) and (prefers-reduced-motion: no-preference) {
    .pf-deck { gap: 0; }

    .pf-case {
      position: sticky;
      inset-block-start: 16px;
      block-size: calc(100svh - 32px);
      /* the dwell: space after a settled card before the next one enters
         (+16px because the settled card sits 16px below the viewport top) */
      margin-block-end: calc(var(--dwell) + 16px);
    }
    .pf-case__inner { block-size: 100%; }

    /* the window can never grow taller than the content row */
    .pf-case__content { container-type: size; }
    .pf-case .pf-win { max-inline-size: calc((100cqh - 36px) * 16 / 9); }

    .pf-ig {
      position: sticky;
      inset-block-start: 16px;
      block-size: 70svh;
      min-block-size: 0;
    }

    /* the hold: the Instagram card stays stuck for this much scroll */
    .pf-hold { display: block; block-size: var(--hold); }
  }

  /* ---------- Case-study dialog (one screen) ---------- */
  html.pf-lock { overflow: hidden; }

  .pf-modal {
    position: fixed;
    inset: 0;
    z-index: 9700;           /* above the nav (9500), below the cursor (9998) */
    perspective: 1400px;
    --accent: #6c63ff;
  }
  .pf-modal[hidden] { display: none; }

  .pf-modal__scrim { position: absolute; inset: 0; background: rgb(10 10 20 / 0.92); }

  .pf-modal__panel {
    position: absolute;
    inset: 24px;
    max-inline-size: 1600px;
    margin-inline: auto;
    border-radius: 28px;
    overflow: hidden;
    border: 1px solid rgb(255 255 255 / 0.1);
    background:
      radial-gradient(ellipse 70% 70% at 75% 30%, color-mix(in srgb, var(--accent) 22%, transparent), transparent 70%),
      #12131c;
  }

  /* header (auto) + body (the rest) */
  .pf-modal__inner {
    block-size: 100%;
    display: grid;
    grid-template-rows: auto minmax(0, 1fr);
    box-sizing: border-box;
    transform-origin: 50% 0;
  }

  .pf-modal__head {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 16px;
    min-block-size: 72px;
    padding: 14px 32px 14px;
    box-sizing: border-box;
  }
  .pf-modal__titles { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 16px; min-inline-size: 0; }

  .pf-modal__title {
    font-family: var(--font-display);
    font-weight: 800;
    font-size: clamp(24px, 2.6vw, 40px);
    line-height: 1.05;
    margin: 0;
    color: var(--pure-white);
  }

  /* The site's one outline button, squared up to a 44px hit area. */
  .pf-modal__close,
  .pf-gallery__nav {
    flex: none;
    display: grid;
    place-items: center;
    inline-size: 44px;
    block-size: 44px;
    padding: 0;
  }

  /* Mobile / narrow: one column, the body scrolls inside the panel. */
  .pf-modal__body {
    min-block-size: 0;
    padding: 0 16px 24px;
    display: flex;
    flex-direction: column;
    gap: 24px;
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: none;
  }
  .pf-modal__body::-webkit-scrollbar { display: none; }

  .pf-modal__gal {
    flex: none;
    block-size: 45dvh;
    min-block-size: 220px;
    min-inline-size: 0;
    display: grid;
    grid-template-rows: minmax(0, 1fr) auto;
    gap: 8px;
  }

  .pf-gallery {
    min-block-size: 0;
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: minmax(0, 1fr);
    align-items: stretch;
    gap: 16px;
  }

  .pf-gallery__track {
    min-inline-size: 0;
    block-size: 100%;
    display: flex;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
    scrollbar-width: none;
    cursor: grab;
    container-type: size;
  }
  .pf-gallery__track::-webkit-scrollbar { display: none; }
  .pf-gallery__track.is-dragging { scroll-snap-type: none; cursor: grabbing; user-select: none; }
  .pf-gallery__track:focus-visible { outline-offset: -3px; }

  .pf-slide {
    flex: 0 0 100%;
    min-inline-size: 0;
    block-size: 100%;
    scroll-snap-align: center;
    scroll-snap-stop: always;
    display: grid;
    place-items: center;
  }

  /* The window scales to the track: never wider than it, never taller than it
     (36px title bar + a 16:10 viewport). */
  .pf-slide .pf-win {
    margin: 0;
    inline-size: min(100cqw, (100cqh - 36px) * 1.6);
    /* a tight shadow: the track clips anything that spreads past its edges */
    box-shadow: 0 8px 20px -10px color-mix(in srgb, var(--accent, #6c63ff) 55%, transparent);
  }

  /* Arrows sit outside the image edges (desktop); hidden on phones — swipe + dots remain. */
  .pf-gallery__nav { display: none; align-self: center; }
  .pf-gallery__nav:disabled { opacity: 0.3; cursor: default; }

  .pf-modal__foot { display: flex; justify-content: center; align-items: center; gap: 16px; min-block-size: 24px; }

  .pf-counter {
    font-family: var(--font-accent);
    font-weight: 500;
    font-size: 14px;
    letter-spacing: 1px;
    color: rgb(255 255 255 / 0.8);
    min-inline-size: 3.5em;
    text-align: center;
  }

  .pf-dots { display: flex; gap: 0; }
  .pf-dot {
    inline-size: 24px;
    block-size: 24px;
    padding: 0;
    border: 0;
    background: none;
    display: grid;
    place-items: center;
    cursor: pointer;
  }
  .pf-dot::before {
    content: "";
    inline-size: 8px;
    block-size: 8px;
    border-radius: 999px;
    background: rgb(255 255 255 / 0.35);
    transition: inline-size 0.3s ease, background 0.3s ease;
  }
  .pf-dot.is-active::before { inline-size: 22px; background: var(--accent); }

  .pf-modal__story { display: grid; gap: 24px; align-content: start; min-inline-size: 0; }

  .pf-story__block h4 {
    font-family: var(--font-accent);
    font-weight: 600;
    font-size: 12px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    line-height: 1;
    color: var(--accent);
    margin-block-end: 8px;
  }
  .pf-story__block p {
    font-family: var(--font-body);
    font-size: 14px;
    line-height: 1.5;
    color: rgb(255 255 255 / 0.78);
  }

  /* Wide: two columns — gallery | text — and nothing scrolls. 1024px, not 1100,
     so a 1097px laptop gets the two-column dialog too. */
  @media (min-width: 1024px) {
    .pf-modal__head { padding-inline: 32px; }

    .pf-modal__body {
      display: grid;
      grid-template-columns: minmax(0, 1fr) clamp(340px, 26vw, 400px);
      gap: 32px;
      padding: 0 32px 32px;
      overflow: hidden;
    }

    .pf-modal__gal { block-size: auto; min-block-size: 0; }

    .pf-gallery { grid-template-columns: 44px minmax(0, 1fr) 44px; }
    .pf-gallery__nav { display: grid; }

    /* text column: fits by construction, vertically centred */
    .pf-modal__story {
      align-content: center;
      overflow: hidden;
      min-block-size: 0;
    }
    .pf-story__block p { font-size: clamp(13px, 1.7svh, 16px); }
  }

  /* Short windows: smaller margins and header, tighter text — still one screen. */
  @media (max-height: 599px) {
    .pf-modal__panel { inset: 12px; }
    .pf-modal__head { min-block-size: 56px; padding-block: 6px; }
    .pf-modal__title { font-size: clamp(20px, 2.2vw, 28px); }
  }

  @media (min-width: 1024px) and (max-height: 599px) {
    .pf-modal__body { padding: 0 24px 16px; gap: 24px; }
    .pf-modal__story { gap: 12px; }
    .pf-story__block h4 { margin-block-end: 4px; }
    .pf-story__block p { font-size: clamp(12px, 2.2svh, 14px); line-height: 1.45; }
  }

  /* No Flip (reduced motion / GSAP missing): plain fade. */
  .pf-modal.pf-modal--fade { opacity: 0; transition: opacity 0.3s ease; }
  .pf-modal.pf-modal--fade.is-open { opacity: 1; }

  @media (max-width: 639px) {
    .pf__inner { padding-inline: 16px; }
    .pf-deck { padding-inline: 16px; }
    .pf-modal__panel { inset: 12px; border-radius: 20px; }
    .pf-modal__head { padding-inline: 16px; }
    .pf-win__addr { max-inline-size: 78%; }
  }

  @media (prefers-reduced-motion: reduce) {
    .pf-ig::before, .pf-ig::after, .pf-ig__float { animation: none; }
    .pf-win__video { display: none; }
  }
}
