
  :root{
    --bg: #0e1215;
    --ink: #f6f7f8;
    --line: rgba(246,247,248,0.14);
    --pill-bg: rgba(246,247,248,0.045);
    --pill-bg-strong: rgba(246,247,248,0.08);
    --accent: #bfe9ef;
    --accent-2: #73dee8;
    --ease: cubic-bezier(.16,1,.3,1);
  }

  *{ box-sizing: border-box; }

  html{
    height: 100%;
    scroll-behavior: smooth;
    scrollbar-width: thin;
    scrollbar-color: rgba(246,247,248,0.22) transparent;
  }

  body{
    margin: 0;
    background: var(--bg);
    overscroll-behavior: none;
    /* belt-and-braces: mobile Safari can still let the page scroll
       horizontally when some descendant's intrinsic min-width blows out
       past the viewport, even when that element (or an ancestor) has its
       own overflow:hidden -- clipping here is the standard fix for that
       class of bug, independent of tracking down which nested flex/grid
       item WebKit is mis-sizing. Deliberately NOT on html too -- setting
       overflow-x without overflow-y forces the *other* axis to auto per
       spec, and pairing that with html's own height:100% above turned
       body into its own competing internal scroll container instead of
       the page scrolling normally. Body has no height set, so it never
       actually overflows vertically within its own box, and that forced
       auto is inert here */
    overflow-x: hidden;
    font-family: 'Barlow Semi Condensed', 'Arial Narrow', Arial, sans-serif;
    font-weight: 500;
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;
  }

  ::-webkit-scrollbar{
    width: 10px;
  }

  ::-webkit-scrollbar-track{
    background: transparent;
  }

  ::-webkit-scrollbar-thumb{
    background: rgba(246,247,248,0.18);
    border-radius: 999px;
    border: 2px solid var(--bg);
  }

  ::-webkit-scrollbar-thumb:hover{
    background: rgba(246,247,248,0.3);
  }

  /* faint, static grain over the whole page -- keeps the flat dark
     background from looking like banded color rather than a material */
  .grain{
    position: fixed;
    inset: 0;
    z-index: 999;
    pointer-events: none;
    opacity: .05;
    mix-blend-mode: overlay;
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='120' height='120'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
  }

  .deck{
    position: relative;
    width: 100%;
    /* fallback only, in case JS hasn't run yet -- syncDeckHeight() in
       app.js immediately overrides this with a value computed from the
       actual reveal thresholds (headline -> callouts -> about), so the
       two can't silently drift apart the way a hand-tuned guess here
       once did (the about paragraph started fading via heroFade before
       its own reveal had finished) */
    min-height: 260vh;
    background: var(--bg);
  }

  .slide{
    position: sticky;
    top: 0;
    height: 100vh;
    min-height: 500px;
    overflow: hidden;
  }

  /* ---------- hero media ---------- */

  .slide--hero{
    --hero-fade: 0;
    background: var(--bg);
  }

  /* fades the photo and the "about" text out over the last stretch of
     scroll before the hero unpins, so the handoff into the design-process
     section is a dissolve instead of a hard cut. No transition -- this is
     driven every animation frame while scrolling (see the rAF loop in
     app.js), so a transition here would just be permanently chasing a
     constantly-moving target, reading as laggy/jittery instead of smooth,
     especially on touch where a fling can move --hero-fade a lot between
     frames */
  .hero-media,
  .about{
    opacity: calc(1 - var(--hero-fade));
  }

  .hero-media{
    position: absolute;
    inset: 0;
    overflow: hidden;
  }

  .hero-media__frame{
    position: absolute;
    inset: 0;
    will-change: transform;
    transition: opacity 1.1s var(--ease), filter 1.1s var(--ease);
  }

  .slide--hero.is-about .hero-media__frame{
    opacity: .5;
    filter: blur(2px);
  }

  .hero-media__img{
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: 56% 52%;
    display: block;
    opacity: 0;
    transform: scale(1.055);
    filter: blur(10px);
    animation: mediaIn 1.5s var(--ease) .15s forwards;
  }

  .hero-media__vignette{
    position: absolute;
    inset: 0;
    pointer-events: none;
    background:
      radial-gradient(ellipse 90% 80% at 50% 50%, transparent 45%, var(--bg) 120%),
      linear-gradient(180deg, rgba(14,18,21,0.5) 0%, transparent 16%, transparent 78%, rgba(14,18,21,0.65) 100%);
  }

  @keyframes mediaIn{
    to{ opacity: 1; transform: scale(1); filter: blur(0); }
  }

  /* ---------- brand mark ---------- */

  .brand{
    position: fixed;
    top: 2.9rem;
    left: 3rem;
    z-index: 4;
    display: inline-block;
    opacity: 0;
    transform: translateY(-6px);
    animation: riseIn .7s var(--ease) .95s forwards;
    text-decoration: none;
    cursor: pointer;
    border-radius: 6px;
  }

  .brand:focus-visible{
    outline: 2px solid var(--accent);
    outline-offset: 8px;
  }

  .brand__mark{
    display: block;
    width: 122px;
    height: auto;
    transition: opacity .3s var(--ease);
  }

  .brand:hover .brand__mark{ opacity: .8; }
  .brand:active .brand__mark{ opacity: .6; }

  .brand__mark path{
    stroke-linecap: round;
  }

  .brand__mark .wordmark{
    opacity: 0;
    transition: opacity .6s var(--ease);
  }

  .brand__mark.is-drawn .wordmark{
    opacity: 1;
  }

  @keyframes riseIn{
    to{ opacity: 1; transform: translateY(0); }
  }

  /* ---------- joystick configurator CTA ----------
     fixed top-right, mirroring .brand's fixed top-left placement and
     entrance timing so the two read as a matched pair */

  .joystick-cta{
    /* it's a <button> (opens a popup, not a link) -- reset the browser's
       default button chrome so only the rules below paint it */
    appearance: none;
    -webkit-appearance: none;
    margin: 0;
    position: fixed;
    top: 2.9rem;
    right: 3rem;
    z-index: 4;
    display: inline-flex;
    align-items: center;
    padding: .55rem 1.1rem;
    border-radius: 10px;
    background: var(--pill-bg);
    border: 1px solid var(--line);
    backdrop-filter: blur(18px) saturate(140%);
    -webkit-backdrop-filter: blur(18px) saturate(140%);
    color: var(--ink);
    font-family: 'Barlow Semi Condensed', 'Arial Narrow', Arial, sans-serif;
    font-weight: 500;
    font-size: .85rem;
    text-transform: uppercase;
    letter-spacing: .04em;
    text-decoration: none;
    cursor: pointer;
    opacity: 0;
    transform: translateY(-6px);
    animation: riseIn .7s var(--ease) .95s forwards;
    transition: background .3s var(--ease), border-color .3s var(--ease);
  }

  .joystick-cta:hover{
    background: var(--pill-bg-strong);
  }

  .joystick-cta:focus-visible{
    outline: 2px solid var(--accent);
    outline-offset: 4px;
  }

  /* ---------- headline (revealed on scroll) ---------- */

  .headline{
    position: absolute;
    left: 3rem;
    right: 3rem;
    top: 47%;
    transform: translateY(-50%);
    z-index: 2;
    margin: 0;
    font-family: 'Barlow Semi Condensed', 'Arial Narrow', Arial, sans-serif;
    font-weight: 500;
    font-size: clamp(4.95rem, 13.7vw, 13.95rem);
    line-height: .88;
    letter-spacing: 0;
    color: var(--ink);
    text-shadow: 0 4px 28px rgba(0,0,0,0.22);
    pointer-events: none;
    user-select: none;
  }

  .headline__inner{
    --reveal: 0;
    --headline-y: 56px;
    opacity: var(--reveal);
    transform: translateY(var(--headline-y));
    /* no transition -- driven every animation frame while scrolling (see
       the rAF loop in app.js); see the .hero-media/.about comment above
       for why that means no transition here either */
  }

  .headline__line{
    display: block;
  }

  /* ---------- feature callouts (revealed after the headline, on further scroll) ---------- */

  .callouts{
    --callout-in: 0;
    --callout-out: 0;
    counter-reset: callout;
    position: absolute;
    inset: 0;
    z-index: 2;
    pointer-events: none;
  }

  .callouts__lines{
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    opacity: calc(var(--callout-in) * clamp(0, 1 - var(--callout-out) * 4, 1));
    /* no transition -- see .headline__inner's comment */
  }

  .callout-line{
    stroke: rgba(246,247,248,0.55);
    stroke-width: 1.5;
    stroke-dasharray: 1.5 7;
    stroke-linecap: round;
  }

  .callout-dot{
    fill: #fff;
  }

  .callout{
    --stagger: 0;
    --dx: 0;
    --dy: 0;
    --in: clamp(0, calc((var(--callout-in) - var(--stagger)) * 3), 1);
    --out: var(--callout-out);
    counter-increment: callout;
    position: absolute;
    max-width: 15.5rem;
    font-family: 'Barlow Semi Condensed', 'Arial Narrow', Arial, sans-serif;
    font-weight: 500;
    font-size: 1.05rem;
    line-height: 1.3;
    letter-spacing: .03em;
    text-transform: uppercase;
    color: rgba(246,247,248,0.94);
    opacity: calc(var(--in) * (1 - var(--out)));
    transform:
      translate(calc(var(--dx) * var(--out) * 160px), calc(var(--dy) * var(--out) * 160px))
      translateY(calc((1 - var(--in)) * 20px))
      scale(calc(1 + var(--out) * 1.1));
    /* no transition -- see .headline__inner's comment */
  }

  .callout::before{
    content: counter(callout, decimal-leading-zero);
    display: block;
    font-size: .68rem;
    letter-spacing: .16em;
    color: var(--accent);
    margin-bottom: .35rem;
  }

  .callout:nth-of-type(1){ --stagger: 0; }
  .callout:nth-of-type(2){ --stagger: .07; }
  .callout:nth-of-type(3){ --stagger: .14; }
  .callout:nth-of-type(4){ --stagger: .21; }
  .callout:nth-of-type(5){ --stagger: .28; }

  /* ---------- about ---------- */

  .about{
    position: absolute;
    left: 3rem;
    width: min(64rem, 82vw);
    top: 7.5rem;
    bottom: 9rem;
    z-index: 2;
    pointer-events: none;
  }

  .about__viewport{
    position: relative;
    height: 100%;
  }

  .about__p{
    --reveal: 0;
    --scroll-y: 0px;
    position: absolute;
    left: 0;
    right: 0;
    top: 50%;
    margin: 0;
    font-family: 'Barlow Semi Condensed', 'Arial Narrow', Arial, sans-serif;
    font-weight: 400;
    font-size: clamp(3.4rem, 5.6vw, 5rem);
    line-height: .95;
    color: var(--ink);
    text-shadow: 0 4px 28px rgba(0,0,0,0.22);
    opacity: var(--reveal);
    /* --scroll-y (set in app.js) carries it on scrolling away for real
       once fully revealed, instead of holding position at top:50% for
       the rest of the hero's pin duration -- see .headline__inner's
       comment for why there's no transition here either */
    transform: translateY(calc(-50% + (1 - var(--reveal)) * 18px + var(--scroll-y)));
  }

  /* ---------- dock nav ---------- */

  .dock{
    position: fixed;
    left: 50%;
    bottom: calc(2.9rem + 30px);
    z-index: 3;
    display: flex;
    align-items: center;
    gap: 2.1rem;
    height: 60px;
    /* left padding is sized so the hand icon's own circle lands concentric
       with the pill's rounded left cap; right padding gives the last
       link's hover pill (which balloons past its own text via the
       highlight's padX) that same concentric clearance before the rounded
       right cap -- 1.5rem measured out to matching gaps on both ends,
       2rem overshot it, leaving the hover pill visibly off-center */
    padding: 0 1.5rem 0 2px;
    box-sizing: border-box;
    background: var(--pill-bg);
    border: 1px solid var(--line);
    border-radius: 999px;
    backdrop-filter: blur(18px) saturate(140%);
    -webkit-backdrop-filter: blur(18px) saturate(140%);
    box-shadow: 0 1px 0 rgba(255,255,255,0.03) inset, 0 20px 40px -20px rgba(0,0,0,0.6);
    opacity: 0;
    transform: translate(-50%, 26px);
    transition: opacity .8s var(--ease), transform .8s var(--ease);
    list-style: none;
    margin: 0;
  }

  .dock.is-ready{
    opacity: 1;
    transform: translate(-50%, 0);
  }

  /* a single shared blob that slides/stretches/reshapes between whichever
     icon or link is hovered or active, instead of each item fading its
     own highlight in place -- the mismatched transform/width/height
     durations plus an overshooting easing is what sells the "liquid"
     feel: position leads, size stretches to catch up, then settles */
  .dock__highlight{
    position: absolute;
    top: 0;
    left: 0;
    width: 0;
    height: 0;
    background: var(--pill-bg);
    border-radius: 999px;
    opacity: 0;
    pointer-events: none;
    z-index: -1;
    transition:
      transform .5s cubic-bezier(.4, 1.3, .3, 1),
      width .55s cubic-bezier(.4, 1.3, .3, 1) .03s,
      height .55s cubic-bezier(.4, 1.3, .3, 1) .03s,
      opacity .25s var(--ease),
      background .25s var(--ease);
    will-change: transform, width, height;
  }

  .dock__icon{
    display: grid;
    place-items: center;
    width: 56px;
    height: 56px;
    border: none;
    padding: 0;
    border-radius: 50%;
    background: transparent;
    flex: none;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: transform .2s var(--ease);
    /* the hand glyph sits centered in this circular hit-area with ~17px of
       transparent padding on each side, so the flex gap after it reads as
       much bigger than the gap between two text links -- pull the next
       item in to cancel that out and match the visual spacing */
    margin-right: -17px;
  }

  .dock__icon svg{
    width: 22px;
    height: auto;
  }

  .dock__icon:active{
    transform: scale(0.92);
  }

  .dock__icon:focus-visible{
    outline: 2px solid var(--accent);
    outline-offset: 3px;
  }

  /* hamburger toggle for the mobile full-screen nav sheet -- hidden until
     the mobile breakpoint replaces the inline links with it */
  .dock__menu-toggle{
    display: none;
    position: relative;
    width: 44px;
    height: 44px;
    border: none;
    border-radius: 50%;
    background: transparent;
    flex: none;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: background .3s var(--ease);
  }

  .dock__menu-toggle:active{
    background: var(--pill-bg);
  }

  .dock__menu-toggle:focus-visible{
    outline: 2px solid var(--accent);
    outline-offset: 3px;
  }

  .dock__menu-toggle .bar{
    position: absolute;
    left: 50%;
    top: 50%;
    width: 18px;
    height: 2px;
    background: var(--ink);
    border-radius: 2px;
    transform: translate(-50%, -50%);
    transition: transform .3s var(--ease), opacity .3s var(--ease);
  }

  .dock__menu-toggle .bar:nth-child(1){ transform: translate(-50%, -50%) translateY(-4px); }
  .dock__menu-toggle .bar:nth-child(2){ transform: translate(-50%, -50%) translateY(4px); }

  .dock__menu-toggle.is-open .bar:nth-child(1){ transform: translate(-50%, -50%) rotate(45deg); }
  .dock__menu-toggle.is-open .bar:nth-child(2){ transform: translate(-50%, -50%) rotate(-45deg); }

  .dock__link{
    /* the joystick configurator entry is a <button> (opens a popup, not a
       link) sharing this class for identical styling -- these resets are
       no-ops for the actual <a> links, which have no default chrome */
    appearance: none;
    -webkit-appearance: none;
    background: none;
    border: none;
    margin: 0;
    font-family: inherit;
    color: var(--ink);
    font-weight: 400;
    text-decoration: none;
    font-size: 1.05rem;
    text-transform: uppercase;
    letter-spacing: .01em;
    white-space: nowrap;
    position: relative;
    padding: .35rem 0;
    transition: color .3s var(--ease);
  }

  /* the joystick configurator link only exists for the mobile hamburger
     sheet (desktop/tablet get the fixed .joystick-cta button instead) --
     hidden above the mobile breakpoint; the <=767px rules below already
     handle showing/hiding every .dock__link (this one included) between
     the closed and open states of that sheet */
  @media (min-width: 768px){
    .dock__link--mobile-only{ display: none; }
  }

  .dock__link:hover,
  .dock__link:focus-visible{
    color: var(--ink);
  }

  .dock__link.is-active{
    color: var(--ink);
  }

  .dock__link:focus-visible{
    outline: 2px solid var(--accent);
    outline-offset: 6px;
    border-radius: 999px;
  }

  @media (prefers-reduced-motion: reduce){
    html{ scroll-behavior: auto; }

    .hero-media__img, .brand, .dock, .dock__icon, .dock__link{
      animation: none !important;
      opacity: 1 !important;
      transform: none !important;
      filter: none !important;
    }
    .brand__mark .wordmark{ opacity: 1 !important; }
    .headline__inner{
      transition: opacity .3s linear !important;
      transform: none !important;
    }
    .callout, .callouts__lines{
      transition: opacity .3s linear !important;
      transform: none !important;
    }
    .dock__highlight{
      transition: opacity .2s linear !important;
    }
    .community__intro, .cc-card, .faq__intro, .faq-item,
    .hex-process__intro{
      transition: opacity .3s linear !important;
      transform: none !important;
    }
    /* the hex cells' reveal is scroll-bound (see updateReveal() in
       app.js), which is exactly the kind of motion this preference asks
       to skip -- pin them fully visible regardless of what JS sets inline */
    .hex{
      opacity: 1 !important;
      transform: none !important;
    }
  }

  /* ---------- shared section heading (design process / community / faq,
     plus the "about" statement borrows .process__title for its size) ---------- */

  .process__title{
    font-weight: 500;
    font-size: clamp(2.6rem, 5.5vw, 4.4rem);
    line-height: 1.02;
    margin: 0 0 1.4rem;
  }

  .process__lede{
    font-size: 1.15rem;
    line-height: 1.55;
    color: rgba(246,247,248,0.68);
    margin: 0 auto;
    max-width: 38rem;
  }

  /* ---------- design process: hexagon-grid concept ----------
     the whole shape is a single normalized (0-1, objectBoundingBox) SVG
     clip-path shared by every cell -- see the hidden <svg><clipPath> near
     the top of <body>. .hex-grid is built at runtime by app.js: one cell
     per real process photo, centered in the section. Sizing lives
     entirely in --hex-w so the honeycomb math (row pitch = 75% of hex
     height, alternate rows offset by 50% of hex width) stays correct at
     any size, including the breakpoint overrides below. Each cell's
     opacity/transform are driven directly off scroll position in JS
     (updateReveal()), not a CSS class toggle -- see .hex's transition
     comment below for why. */

  .hex-process{
    position: relative;
    background: var(--bg);
    color: var(--ink);
    padding: 9rem 0 10rem;
    overflow: hidden;
  }

  .hex-process__intro{
    max-width: 46rem;
    margin: 0 auto 5rem;
    padding: 0 3rem;
    text-align: center;
    opacity: 0;
    transform: translateY(40px);
    transition: opacity .8s var(--ease), transform .8s var(--ease);
  }

  .hex-process__intro.is-visible{
    opacity: 1;
    transform: none;
  }

  /* no max-width -- app.js centers the grid itself within the section,
     which still bleeds past both edges on most viewports since the cells
     are sized generously; the section's own overflow:hidden clips it */
  .hex-grid{
    --hex-w: 336px;
    position: relative;
    width: 100%;
  }

  .hex-slot{
    position: absolute;
  }

  .hex{
    /* shrunk to less than the slot's full pitch and centered within it --
       a regular hexagon shrunk concentrically like this pulls back the
       same distance on all 6 sides, which is what makes the gap between
       neighboring cells read as equal in every direction rather than
       tighter along one axis than another */
    position: absolute;
    inset: 0;
    margin: auto;
    width: 88%;
    height: 88%;
    text-align: center;
    /* opacity/transform are set inline by updateReveal() every animation
       frame while scrolling (see the rAF loop in app.js), scrubbing
       directly with scroll position -- no transition, for the same
       reason as .headline__inner above: a transition here would just be
       permanently chasing a moving target instead of tracking it exactly */
  }

  /* the clip-path (and everything it masks) lives on this inner wrapper
     rather than .hex itself, because .hex's own opacity/transform are
     overwritten inline by updateReveal() every animation frame */
  .hex__mask{
    position: absolute;
    inset: 0;
    clip-path: url(#hexClip);
    background: var(--pill-bg);
  }

  .hex--photo .hex__mask{
    background: var(--pill-bg-strong);
  }

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

  /* dark gradient over the bottom of the photo -- kept even without the
     old label text since it also just reads as a nice bit of depth/
     grounding on the photo itself */
  .hex__scrim{
    position: absolute;
    inset: 0;
    background: linear-gradient(to top, rgba(10,12,14,0.85) 0%, rgba(10,12,14,0.35) 40%, rgba(10,12,14,0) 68%);
    pointer-events: none;
  }

  /* ---------- community ----------
     ported from a high-fidelity design handoff (dark #0b0b0c card grid,
     Barlow Semi Condensed + JetBrains Mono) -- pixel values below are the
     handoff's literal px specs converted to rem at the site's 16px root */

  .community{
    position: relative;
    background: var(--bg);
    color: #f2f1ee;
    padding: 8.125rem 0 8.75rem; /* 130px 0 140px */
    overflow: hidden;
  }

  /* faint 80px-pitch grid backdrop behind the whole section */
  .community::before{
    content: '';
    position: absolute;
    inset: 0;
    background-image:
      linear-gradient(rgba(255,255,255,0.03) 1px, transparent 1px),
      linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px);
    background-size: 80px 80px;
    pointer-events: none;
  }

  .community__inner{
    position: relative;
    max-width: 93.75rem; /* 1500px */
    margin: 0 auto;
    padding: 0 5rem; /* 0 80px */
  }

  .community__intro{
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 1.5rem; /* 24px */
    margin-bottom: 4rem; /* 64px */
    padding-bottom: 2.5rem; /* 40px */
    opacity: 0;
    transform: translateY(40px);
    transition: opacity .8s var(--ease), transform .8s var(--ease);
  }

  .community__intro.is-visible{
    opacity: 1;
    transform: none;
  }

  /* shares .process__title with the design-process/faq headers instead of
     its own custom size -- .community__intro already spaces title/lede
     apart with its own flex gap, so that class's built-in margin-bottom
     just needs zeroing here to avoid doubling up the gap */
  .community__intro .process__title{
    margin: 0;
  }

  .community__lede{
    max-width: 23.75rem; /* 380px */
    font-size: 1.25rem; /* 20px */
    line-height: 1.55;
    font-weight: 400;
    color: rgba(255,255,255,0.8);
    margin: 0 auto;
  }

  .cc-grid{
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 1.5rem; /* 24px */
  }

  .cc-card{
    position: relative;
    display: flex;
    flex-direction: column;
    /* same translucent-ink-on-bg token the dock nav pill uses, rather than
       a flat hardcoded gray -- keeps the card surface visibly tied to
       whatever --bg is instead of reading as a second, disconnected color */
    background: var(--pill-bg);
    border: 1px solid rgba(255,255,255,0.09);
    border-radius: 10px;
    color: inherit;
    text-decoration: none;
    opacity: 0;
    transform: translateY(40px);
    transition: opacity .8s var(--ease), transform .8s var(--ease), border-color .25s ease;
    /* grid items default to min-width:auto (their content's un-shrunk
       size) -- without this, a long nowrap label (see .cc-idx) forces the
       whole single-column mobile grid wider than the viewport, which then
       gets silently clipped by .community's overflow:hidden instead of
       wrapping, cropping every card's right edge */
    min-width: 0;
    overflow: hidden;
  }

  .cc-card.is-visible{
    opacity: 1;
    transform: none;
  }

  /* shared by both community cards now that they're styled identically --
     youtube and discord are visual twins, same padding/type scale/corner
     treatment, just different content. justify-content is deliberately
     left at its flex-start default (not space-between): the avatar/title
     block needs to sit at a fixed position right under the idx pill on
     both cards regardless of copy length, so it's the CTA below the copy
     that moves with differing description lengths, not the avatar. */
  .cc-card--feature{
    padding: 3rem; /* 48px */
  }

  /* corner-bracket decorations: hidden by default, fade in on hover */
  .cc-corner{
    position: absolute;
    width: 14px;
    height: 14px;
    border-color: rgba(242,241,238,0.35);
    opacity: 0;
    transition: opacity .25s ease;
  }

  .cc-card:hover .cc-corner{
    opacity: 1;
  }

  .cc-corner--tl{ top: 14px; left: 14px; border-top: 1px solid; border-left: 1px solid; }
  .cc-corner--tr{ top: 14px; right: 14px; border-top: 1px solid; border-right: 1px solid; }
  .cc-corner--bl{ bottom: 14px; left: 14px; border-bottom: 1px solid; border-left: 1px solid; }
  .cc-corner--br{ bottom: 14px; right: 14px; border-bottom: 1px solid; border-right: 1px solid; }

  .cc-card__top{
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    gap: .5rem;
    min-width: 0;
    /* now that .cc-card--feature no longer uses space-between to push the
       channel block to the bottom, this margin is what creates the gap
       between the idx pill and the avatar row instead */
    margin-bottom: 1.75rem; /* 28px */
  }

  .cc-idx{
    font-family: 'JetBrains Mono', monospace;
    font-size: .8125rem; /* 13px */
    letter-spacing: .1em;
    color: #8f8d88;
    border: 1px solid rgba(255,255,255,0.15);
    border-radius: 999px;
    padding: .375rem .875rem; /* 6px 14px */
    /* nowrap on desktop reads fine (plenty of room), but the hero card's
       longer "01 · PROCESS VIDEOS" label forced this pill wider than a
       mobile card has room for, blowing out the single-column grid --
       letting it wrap to a second line under its own pill shape is a much
       smaller visual cost than the whole grid overflowing the viewport */
    white-space: normal;
    transition: color .25s ease, border-color .25s ease;
  }

  .cc-card:hover .cc-idx{
    color: var(--accent-2);
    border-color: rgba(115,222,232,0.5);
  }

  .cc-channel{
    display: flex;
    align-items: center;
    gap: 1.09375rem; /* 17.5px, 14px x1.25 */
    margin-bottom: 1.75rem; /* 28px */
  }

  .cc-channel__avatar{
    width: 5.46875rem; /* 87.5px, 70px x1.25 */
    height: 5.46875rem;
    border-radius: 50%;
    object-fit: cover;
    display: block;
  }

  .cc-channel__meta{
    display: flex;
    flex-direction: column;
    gap: .15625rem; /* 2.5px, 2px x1.25 */
    min-width: 0;
  }

  .cc-channel__name{
    font-size: 1.328125rem; /* 21.25px, 17px x1.25 */
    font-weight: 600;
    color: #f2f1ee;
  }

  .cc-channel__handle{
    font-family: 'JetBrains Mono', monospace;
    font-size: .9375rem; /* 15px, 12px x1.25 */
    color: #6f6d68;
    /* one unbroken string (no spaces) -- without this it can't wrap at
       all, so on narrow phones it becomes a second hard-coded minimum
       width fighting the same mobile grid the idx pill above was forcing
       wider than the viewport */
    overflow-wrap: anywhere;
  }

  .cc-card__heading-row{
    display: flex;
    align-items: center;
    gap: .75rem; /* 12px */
  }

  .cc-card__icon{
    display: block;
    padding-top: 4px;
  }

  /* both icons sized to the same height-to-title-font-size ratio (~0.87x
     the shared title size), width following each SVG's own aspect ratio
     so neither is stretched: youtube.svg is 84:60, discord.svg is 83:64 */
  .cc-card__icon--discord{ width: 2.6rem; height: 2rem; margin-top: 4px; } /* ~42x32 */
  .cc-card__icon--youtube{ width: 2.8rem; height: 2rem; margin-top: 4px; } /* ~45x32 */

  .cc-card__title{
    font-weight: 500;
    font-size: 2.25rem; /* 36px, was 42px */
    letter-spacing: -.01em;
    text-transform: lowercase;
    color: #fff;
    margin: 0;
  }

  /* the title's own margin-bottom used to create the gap before the copy
     paragraph, but now that it's wrapped alongside the icon in a flex
     heading-row, a child's margin no longer collapses out through a flex
     container -- move the gap to the row itself */
  .cc-card--feature .cc-card__heading-row{
    margin-bottom: .875rem; /* 14px */
  }

  .cc-card__copy{
    font-size: 1rem; /* 16px */
    line-height: 1.6;
    font-weight: 400;
    color: #9c9a95;
    margin: 0;
    max-width: 27.5rem; /* 440px */
    margin-bottom: 1.875rem; /* 30px */
  }

  .cc-cta{
    display: inline-flex;
    align-items: center;
    gap: .625rem; /* 10px */
    font-size: .9375rem; /* 15px */
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .05em;
    color: var(--accent);
    transition: color .25s ease;
  }

  .cc-card:hover .cc-cta{
    color: var(--accent-2);
  }

  .cc-cta__arrow{ flex-shrink: 0; }

  @media (max-width: 1100px){
    .community{ padding: 6rem 0 7rem; }
    .community__inner{ padding: 0 2rem; } /* 0 32px */
    .cc-grid{ grid-template-columns: 1fr; }
    .cc-card{ min-height: auto; }
    .cc-card__title{ font-size: 1.5rem; } /* 24px, was 28px */
    /* scaled down with the shared title's own 36px -> 24px drop, same ratio */
    .cc-card__icon--youtube{ width: 1.8375rem; height: 1.3125rem; } /* ~29x21 */
    .cc-card__icon--discord{ width: 1.7rem; height: 1.3125rem; } /* ~27x21 */

    /* desktop's 48px card padding was eating deep into a mobile card's
       much narrower width, which is exactly what made the idx pill +
       decorative number stop fitting side by side */
    .cc-card--feature{ padding: 1.75rem; } /* 28px */

    /* channel row reverts to its original (pre-1.25x) size on mobile --
       the enlarged version is desktop-only. Top margin tightened along
       with the desktop rule now that the idx row is just the pill */
    .cc-channel{
      gap: .875rem; /* 14px */
      margin-top: .5rem; /* 8px */
      margin-bottom: 1.75rem; /* 28px */
    }
    .cc-channel__avatar{
      width: 4.375rem; /* 70px */
      height: 4.375rem;
    }
    .cc-channel__meta{ gap: .125rem; } /* 2px */
    .cc-channel__name{ font-size: 1.0625rem; } /* 17px */
    .cc-channel__handle{ font-size: .75rem; } /* 12px */
  }

  /* ---------- faq ---------- */

  .faq{
    position: relative;
    background: var(--bg);
    color: var(--ink);
    padding: 9rem 3rem 10rem;
  }

  .faq__intro{
    max-width: 46rem;
    margin: 0 auto 4rem;
    text-align: center;
    opacity: 0;
    transform: translateY(40px);
    transition: opacity .8s var(--ease), transform .8s var(--ease);
  }

  .faq__intro.is-visible{
    opacity: 1;
    transform: none;
  }

  .faq__list{
    max-width: 46rem;
    margin: 0 auto;
    display: flex;
    flex-direction: column;
  }

  .faq-item{
    border-bottom: 1px solid var(--line);
    opacity: 0;
    transform: translateY(30px);
    transition: opacity .7s var(--ease), transform .7s var(--ease);
  }

  .faq-item.is-visible{
    opacity: 1;
    transform: none;
  }

  .faq-item__q{
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1.5rem;
    background: none;
    border: none;
    padding: 1.5rem 0;
    color: var(--ink);
    font-family: inherit;
    font-weight: 500;
    font-size: 1.15rem;
    text-align: left;
    cursor: pointer;
  }

  .faq-item__q:focus-visible{
    outline: 2px solid var(--accent);
    outline-offset: 4px;
  }

  .faq-item__icon{
    position: relative;
    width: 20px;
    height: 20px;
    flex: none;
  }

  .faq-item__icon .bar{
    position: absolute;
    left: 50%;
    top: 50%;
    width: 14px;
    height: 2px;
    background: var(--ink);
    border-radius: 2px;
    transform: translate(-50%, -50%);
    transition: transform .3s var(--ease), opacity .3s var(--ease);
  }

  .faq-item__icon .bar:nth-child(2){
    transform: translate(-50%, -50%) rotate(90deg);
  }

  .faq-item.is-open .faq-item__icon .bar:nth-child(2){
    transform: translate(-50%, -50%) rotate(0deg);
  }

  .faq-item__a{
    display: grid;
    grid-template-rows: 0fr;
    overflow: hidden;
    transition: grid-template-rows .4s var(--ease);
  }

  .faq-item.is-open .faq-item__a{
    grid-template-rows: 1fr;
  }

  .faq-item__a-inner{
    overflow: hidden;
    min-height: 0;
  }

  .faq-item__a-inner p{
    margin: 0 0 1.6rem;
    padding-right: 2rem;
    font-size: 1rem;
    line-height: 1.6;
    color: rgba(246,247,248,0.72);
  }

  @media (max-width: 1023px){
    .faq{ padding: 7rem 1.8rem 8rem; }
  }

  @media (max-width: 767px){
    .faq{ padding: 5.5rem 1.4rem 6.5rem; }
    .faq-item__q{ font-size: 1.05rem; }
  }

  /* ---------- footer ---------- */

  .site-footer{
    border-top: 1px solid var(--line);
    /* extra top/bottom room (was 2.4rem/10rem) so the fixed dock pill
       overlapping the bottom of the footer has more breathing space
       around it, rather than sitting almost flush against the border */
    padding: 3.4rem 3rem 12rem;
    /* a 1fr/auto/1fr grid (not flex's space-between) so the middle
       column is centered on the footer itself -- flex centered the text
       within whatever space was left after the brand and links, which
       skewed it sideways the moment the links group (now 3 links, not 1)
       got wider than the brand text */
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: center;
    gap: 1.5rem;
    color: rgba(246,247,248,0.55);
    font-size: .95rem;
  }

  .site-footer__brand{
    font-weight: 500;
    letter-spacing: .06em;
    color: var(--ink);
    justify-self: start;
  }

  .site-footer__text{
    margin: 0;
    text-align: center;
    justify-self: center;
  }

  .site-footer__links{
    display: flex;
    align-items: center;
    gap: 1.5rem;
    justify-self: end;
  }

  .site-footer__link{
    color: rgba(246,247,248,0.55);
    text-decoration: none;
    border-bottom: 1px solid var(--line);
    transition: color .3s var(--ease), border-color .3s var(--ease);
  }

  .site-footer__link:hover,
  .site-footer__link:focus-visible{
    color: var(--ink);
    border-color: var(--ink);
  }

  .site-footer__link:focus-visible{
    outline: 2px solid var(--accent);
    outline-offset: 4px;
  }

  @media (max-width: 1023px){
    .site-footer{ padding: 3.2rem 1.8rem 11.5rem; }
    .hex-process{ padding: 7rem 0 8rem; }
    .hex-process__intro{ padding: 0 1.8rem; }
    .hex-grid{ --hex-w: 220px; }
  }

  @media (max-width: 767px){
    .process__title{ font-size: clamp(2.1rem, 8vw, 2.8rem); }
    .process__lede{ font-size: 1.02rem; }
    .hex-process{ padding: 5.5rem 0 6.5rem; }
    .hex-process__intro{ padding: 0 1.2rem; }
    .hex-grid{ --hex-w: 232px; }
    .site-footer{
      grid-template-columns: 1fr;
      text-align: center;
      padding: 3rem 1.4rem 10.5rem;
    }
    .site-footer__brand{ order: 1; justify-self: center; }
    /* copyright line was sitting above the links -- moved to the bottom,
       after the links, the more usual footer convention */
    .site-footer__links{ order: 2; justify-self: center; flex-wrap: wrap; justify-content: center; }
    .site-footer__text{ order: 3; justify-self: center; }
  }

  /* ==========================================================
     breakpoint 1 -- iPad portrait (and other mid-size tablets)
     the desktop layout and interactions still apply here, just
     scaled down; the dock nav still fits as a pill of text links.
     The callouts are the one exception -- they switch to the same
     simple centered stack the phone breakpoint uses (rather than
     keeping the leader-line layout, just scaled), because the
     scroll-driven hero image pan (see heroPanActive() in app.js,
     also gated at 1023px) invalidates the leader lines' fixed
     percentage anchor points the moment the crop starts moving
     ========================================================== */
  @media (max-width: 1023px){
    .brand{ top: 1.8rem; left: 1.8rem; }
    .brand__mark{ width: 104px; }

    .joystick-cta{ top: 1.8rem; right: 1.8rem; padding: .5rem .9rem; font-size: .78rem; }

    .headline{
      left: 1.8rem;
      right: 1.8rem;
      font-size: clamp(3.8rem, 11vw, 8.5rem);
    }

    .hero-media__img{ object-position: 60% 50%; }

    .about{ left: 1.8rem; width: 88vw; top: 6.5rem; bottom: 8rem; }
    .about__p{ font-size: clamp(2.6rem, 6.6vw, 3.8rem); }

    /* -- callouts: the dashed leader lines only make sense pointing at
       a large, stationary photo -- once the image itself is panning
       (heroPanActive(), <=1023px) they'd be pointing at stale
       coordinates, so replace the whole layer with a simple centered
       stack instead, same as phone */
    .callouts__lines{ display: none; }

    .callouts{
      display: flex;
      flex-direction: column;
      align-items: center;
      /* bottom-anchored (not centered) so the stack enters lower on
         screen, clear of the headline's own territory (top:60%, see
         .headline above) instead of overlapping it -- --callouts-y then
         carries it upward through the screen as the person keeps
         scrolling, same as any normal in-flow content would */
      justify-content: flex-end;
      gap: 1.15rem;
      padding: 0 1.8rem 9rem;
    }

    .callout{
      position: static !important;
      max-width: 100%;
      text-align: center;
      font-size: 1.05rem;
      line-height: 1.2;
      /* heavier than desktop's 500 -- the centered stack has less visual
         support (no leader line pointing at the feature) to read as
         intentional, so a bolder weight carries it on its own */
      font-weight: 700;
      /* --callouts-y (set in app.js, inherited from #callouts) carries the
         stack on scrolling away for real once fully faded in, instead of
         relying only on --callout-out's fade while holding position --
         same "let it scroll normally" treatment as the headline */
      transform: translateY(calc((1 - var(--in)) * 14px + var(--callouts-y, 0px))) !important;
    }

    .dock{
      bottom: 1.6rem;
      gap: 1.9rem;
      /* right padding measured out the same way as the desktop rule's --
         1.3rem left the "questions" hover pill only ~3px from the cap,
         well short of the ~6.5px a pill this height needs to read as
         concentric with the dock's own rounded end */
      padding: .5rem 1.5rem .5rem .5rem;
      max-width: 92vw;
      overflow-x: auto;
      scrollbar-width: none;
    }
    .dock::-webkit-scrollbar{ display: none; }
    .dock__icon{ width: 46px; height: 46px; }
    .dock__link{ font-size: 1rem; }
  }

  /* ==========================================================
     breakpoint 2 -- mobile
     the dock nav collapses to an icon + hamburger that opens a
     full-screen sheet, and the photo-anchored callouts (which
     rely on leader lines pointing at a large image) collapse
     into a simple centered stack instead
     ========================================================== */
  @media (max-width: 767px){
    /* fallback only -- see the desktop rule's comment. Shorter than
       desktop's because syncDeckHeight() also scales with MOBILE_SCALE */
    .deck{ min-height: 200vh; }

    .brand{ top: 1.4rem; left: 1.4rem; z-index: 4; }
    .brand__mark{ width: 84px; }

    /* lives inside the hamburger sheet instead (see .dock__link--mobile-only) */
    .joystick-cta{ display: none; }

    .headline{
      left: 1.4rem;
      right: 1.4rem;
      top: 60%;
      font-size: clamp(2.5rem, 15vw, 4.6rem);
      line-height: .94;
    }

    /* narrow portrait viewports can't afford to crop-to-fill like desktop/
       tablet do -- stretching the frame nearly full-height made it so much
       taller/narrower than the photo's own ~16:9 shape that cover had to
       zoom way in just to fill it, cropping most of the width away.
       Shrinking the *frame itself* (rather than shrinking the image inside
       a full-size frame, which just left a bordered rectangle of visible
       background around it) fixes the amount of crop directly -- the image
       still fills its frame edge to edge with no visible seam, the frame
       is just a shorter, less extreme shape now */
    .hero-media{
      top: 0;
      bottom: auto;
      /* stops shrinking below 520px on narrower phones -- the frame just
         crops a bit more off the left/right at that point instead of the
         photo continuing to shrink into a tiny strip */
      height: clamp(520px, 132vw, 600px);
    }

    /* no separate scale/object-position override here -- phones inherit
       the same crop as tablet (see the 1023px breakpoint's object-position
       and the base frame's un-scaled default) so there's no visual jump
       crossing into this breakpoint. Only the scroll-driven pan (see
       heroPanActive() in the reveal IIFE) differs below 1023px -- it takes
       over from that same shared resting position instead of snapping to
       a phone-specific one */

    .about{ left: 1.4rem; width: 86vw; top: 6rem; bottom: 7rem; }
    /* font-weight explicitly set here (not just relying on the base
       .about__p{font-weight:400} rule) because this element also carries
       .process__title for its heading-style sizing elsewhere, and that
       class's own font-weight:500 -- defined later in the file, same
       specificity -- was winning the cascade, silently rendering this
       500 instead of the 400 its own rule asked for. Sizes are the old
       ones scaled up 30% (1.9/7.6/2.6 -> 2.5/9.9/3.4) */
    .about__p{ font-size: clamp(2.5rem, 9.9vw, 3.4rem); font-weight: 400; line-height: 1.2; }

    /* callouts already switched to the centered stack at the 1023px
       breakpoint above (see heroPanActive()) -- nothing further to do
       here, just a smaller font to match the rest of this breakpoint */
    .callout{ font-size: 1rem; }

    /* -- dock nav: icon + hamburger only; the hamburger expands the
       whole dock into a fixed full-screen link sheet -- */
    .dock{
      gap: .5rem;
      padding: .4rem;
      max-width: none;
      overflow: visible;
      /* only opacity/transform ever animate here -- the pill-to-fullscreen
         resize (width/height/position/background/border-radius) always
         happens in a single instant frame while opacity is still 0 (see
         is-menu-open/is-menu-visible below and the setOpen() sequencing
         in app.js), so the jump is never actually seen */
      transition: opacity .35s var(--ease), transform .35s var(--ease);
    }

    .dock__menu-toggle{ display: grid; place-items: center; }

    /* closed state: hand icon + hamburger bars are replaced by a plain
       "MENU" label pill -- the icon still returns for the open sheet's
       link list (see .dock.is-menu-open .dock__icon below), and the bars
       still exist for the open/close morph, just hidden while closed */
    .dock:not(.is-menu-open) .dock__icon{ display: none; }

    .dock:not(.is-menu-open) .dock__menu-toggle{
      width: auto;
      height: 46px;
      padding: 0 1.3rem;
      border-radius: 999px;
    }

    .dock:not(.is-menu-open) .dock__menu-toggle .bar{ display: none; }

    .dock__menu-toggle::after{
      content: "MENU";
      display: none;
      font-size: .95rem;
      letter-spacing: .09em;
      color: var(--ink);
      white-space: nowrap;
    }

    .dock:not(.is-menu-open) .dock__menu-toggle::after{ display: inline-block; }

    .dock__link{ display: none; }

    .dock.is-menu-open{
      position: fixed;
      top: 0;
      left: 50%;
      bottom: 0;
      width: 100vw;
      height: 100vh;
      border-radius: 0;
      border: none;
      background: rgba(14,18,21,0.94);
      backdrop-filter: blur(24px) saturate(140%);
      -webkit-backdrop-filter: blur(24px) saturate(140%);
      flex-direction: column;
      justify-content: center;
      align-items: center;
      gap: 2.3rem;
      padding: 0;
      z-index: 50;
      /* hidden and slid down a little until is-menu-visible lands a frame
         later -- see the setOpen() sequencing note on .dock's transition */
      opacity: 0;
      pointer-events: none;
      transform: translate(-50%, 28px);
    }

    .dock.is-menu-open.is-menu-visible{
      opacity: 1;
      pointer-events: auto;
      transform: translate(-50%, 0);
    }

    /* the shared hover/active blob is a desktop-hover affordance -- the
       full-screen sheet has its own big-target list, no chasing pill */
    .dock.is-menu-open .dock__highlight{ display: none; }

    /* the hand icon becomes the first item in the open sheet's link list
       (it's already first in DOM order, right before the toggle) */
    .dock.is-menu-open .dock__icon{
      display: grid;
      opacity: 1;
      animation: none;
      width: 72px;
      height: 72px;
      /* the desktop dock cancels out the icon's internal glyph padding with
         a negative margin so it sits snug against the first nav link --
         irrelevant here (it's centered alone in a column), and left in it
         would just shove the icon off-center */
      margin-right: 0;
    }

    .dock.is-menu-open .dock__icon svg{
      width: 30px;
    }

    /* the toggle button itself stays put at the same spot the closed
       pill occupied, instead of getting swept into the centered list --
       it's taken out of the flex flow so opening/closing the sheet
       doesn't move the button the person just tapped */
    .dock.is-menu-open .dock__menu-toggle{
      position: absolute;
      left: 50%;
      bottom: 34px;
      transform: translateX(-50%);
    }

    .dock.is-menu-open .dock__link{
      display: block;
      font-size: 1.7rem;
      opacity: 1;
      transform: none;
      animation: none;
    }
  }

  html.no-scroll{ overflow: hidden; }
