:root{--grain-url:url("assets/grain.png");
  /* Handed to FrostPanel's `pad` prop, which lands inline. Declaring it as
     a token is what lets the phone breakpoint reach it. */
  --panel-pad:var(--space-9)}
*{box-sizing:border-box}
html{scroll-behavior:smooth}
body{margin:0;background:var(--abyss-900);color:var(--text-primary);font:var(--type-body);-webkit-font-smoothing:antialiased}
a{color:var(--frost-300);text-decoration:none}
a:hover{color:var(--whiteout)}
img{display:block;max-width:100%}
.gx-sec{max-width:var(--maxw-page);margin:0 auto;padding:var(--space-13) var(--gutter-page)}
.gx-eyebrow{display:flex;align-items:baseline;justify-content:space-between;gap:var(--space-7);flex-wrap:wrap;margin-bottom:var(--space-9)}
.gx-grid3{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(260px,100%),1fr));gap:var(--space-9)}
.gx-grid2{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(320px,100%),1fr));gap:var(--space-8)}
.gx-faq{border-top:1px solid var(--line-hairline)}
.gx-faq details{border-bottom:1px solid var(--line-hairline)}
.gx-faq summary{list-style:none;cursor:pointer;padding:var(--space-7) 0;display:flex;align-items:center;justify-content:space-between;gap:var(--space-6);font:var(--type-subheading);letter-spacing:var(--tracking-tight);color:var(--text-primary);transition:var(--transition-hover)}
.gx-faq summary::-webkit-details-marker{display:none}
.gx-faq summary:hover{color:var(--frost-200)}
.gx-faq summary .gx-plus{font:var(--type-meta);color:var(--text-faint);flex:none}
.gx-faq details[open] summary .gx-plus{color:var(--frost-300)}
.gx-faq p{font:var(--type-body);color:var(--text-body);margin:0 0 var(--space-8);max-width:var(--measure-prose);text-wrap:pretty}
/* Phase 16: was --type-display-2 (64px desktop), louder than any project
   title on the site. It is an address, not a statement. */
.gx-addr{display:flex;flex-direction:column;align-items:flex-start;gap:var(--space-3);
  margin:var(--space-7) 0 var(--space-8);position:relative}
.gx-mail{display:inline-flex;align-items:center;gap:var(--space-5);
  min-height:44px;padding:0;border:0;background:none;cursor:pointer;
  font:var(--type-meta);font-size:clamp(15px,1.4vw,20px);letter-spacing:0;
  color:var(--text-primary);text-align:left;
  transition:var(--transition-hover);-webkit-tap-highlight-color:transparent}
.gx-mail:hover{color:var(--frost-200)}
.gx-mail:focus-visible{outline:none;box-shadow:0 0 0 3px rgba(126,180,214,.28);border-radius:var(--radius-sm)}
.gx-mail-text{word-break:break-all}
.gx-mail-act{flex:none;font:var(--type-label);letter-spacing:var(--tracking-label);
  text-transform:uppercase;color:var(--text-faint);
  border:var(--border-hair) solid var(--line-hairline);border-radius:var(--radius-pill);
  padding:5px 10px;transition:var(--transition-hover)}
.gx-mail:hover .gx-mail-act{color:var(--text-primary);border-color:var(--line-strong)}
.gx-mail-alt{font:var(--type-caption);color:var(--text-faint);
  display:inline-flex;align-items:center;min-height:32px}
.gx-mail-alt:hover{color:var(--text-primary)}

/* Toast. Anchored to the address rather than the viewport so it cannot
   cover anything else, and it never takes focus. */
.gx-toast{position:absolute;left:0;bottom:100%;margin-bottom:var(--space-4);
  padding:8px 14px;border-radius:var(--radius-pill);
  background:var(--frost-100,#F9FCFD);color:var(--abyss-900);
  font:var(--type-label);letter-spacing:var(--tracking-label);text-transform:uppercase;
  white-space:nowrap;pointer-events:none;
  opacity:0;transform:translateY(6px);
  transition:opacity 200ms var(--ease-glide,cubic-bezier(.22,1,.36,1)),
             transform 200ms var(--ease-glide,cubic-bezier(.22,1,.36,1));
  box-shadow:var(--shadow-md)}
.gx-toast.is-up{opacity:1;transform:none}
.gx-ftlink{font:var(--type-label);letter-spacing:var(--tracking-label);text-transform:uppercase;color:var(--text-muted);transition:var(--transition-hover)}
.gx-ftlink:hover{color:var(--text-primary)}
.gx-post{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--space-7);text-decoration:none;color:inherit;padding:var(--space-8) 0;border-top:1px solid var(--line-hairline);transition:var(--transition-surface)}
.gx-post:hover{color:inherit}
@media(min-width:860px){.gx-post{grid-template-columns:56px 300px minmax(0,1fr) auto;align-items:center;gap:var(--space-8)}}
.gx-post .gx-thumb{border-radius:var(--radius-md);overflow:hidden}
.gx-post:hover .gx-arrow{opacity:1;transform:none}
.gx-arrow{opacity:.35;transform:translateX(-6px);transition:var(--transition-hover);font:var(--type-meta);color:var(--frost-300)}
.gx-stat{font:var(--type-display-3);letter-spacing:var(--tracking-display);color:var(--text-primary);margin:0}

/* ── Clients: 360° rotating brand ring ─────────────────────────────── */
.gx-ring-stage{position:relative;height:288px;max-width:920px;margin:0 auto;perspective:1500px;perspective-origin:50% 50%;overflow:hidden;cursor:grab;touch-action:pan-y;-webkit-mask-image:linear-gradient(90deg,transparent 0,#000 9%,#000 91%,transparent 100%);mask-image:linear-gradient(90deg,transparent 0,#000 9%,#000 91%,transparent 100%)}
.gx-ring-stage:active{cursor:grabbing}
.gx-ring-stage:focus-visible{outline:none;border-radius:var(--radius-lg);box-shadow:0 0 0 3px rgba(126,180,214,.14);border-color:var(--focus-ring)}
.gx-ring{position:absolute;top:50%;left:50%;width:var(--ring-card-w,252px);height:158px;margin:-79px 0 0 calc(var(--ring-card-w,252px) / -2);transform-style:preserve-3d;will-change:transform}
.gx-ring-card{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--space-4);padding:var(--space-5) var(--space-5);border-radius:var(--radius-lg);backface-visibility:hidden;background:linear-gradient(180deg,rgba(255,255,255,.14),rgba(127,180,214,.05));border:1px solid var(--line-strong);box-shadow:inset 0 1px 0 rgba(255,255,255,.16),var(--shadow-card,0 18px 48px rgba(3,6,12,.45));-webkit-backdrop-filter:blur(18px) saturate(140%);backdrop-filter:blur(18px) saturate(140%);transition:border-color var(--duration-hover,180ms) var(--ease-glide,cubic-bezier(.22,1,.36,1))}
.gx-ring-mark{font-family:var(--font-display);font-weight:600;font-size:clamp(16px,1.5vw,21px);letter-spacing:var(--tracking-display);line-height:1.05;color:var(--text-primary);text-align:center;text-wrap:balance}
.gx-ring-meta{font:var(--type-meta);letter-spacing:var(--tracking-label);text-transform:uppercase;color:var(--text-muted);text-align:center}
.gx-ring-reflect{position:absolute;left:0;right:0;bottom:0;height:48px;background:linear-gradient(180deg,transparent,var(--abyss-900));pointer-events:none}
.gx-ring-ctrls{display:flex;align-items:center;gap:var(--space-5);flex-wrap:wrap}
.gx-sr{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}
@media(max-width:640px){.gx-ring-stage{height:228px;perspective:1000px}.gx-ring{--ring-card-w:208px}}
@media(prefers-reduced-motion:reduce){.gx-ring-stage{cursor:default}}

/* ── Accessibility: text tokens raised to clear WCAG AA on every ground ──── */
:root{
  --text-muted:rgba(198,220,233,0.88);
  --text-faint:rgba(166,201,222,0.78);
  --text-muted-on-light:rgba(6,16,31,0.62);
  /* Deeper protection gradient — overlay text must clear AA over any plate. */
  --grad-protect:linear-gradient(0deg,rgba(3,6,12,0.94) 0%,rgba(3,6,12,0.72) 42%,rgba(3,6,12,0.18) 78%,transparent 100%);
}
/* Ghost controls placed on a rime (light) section */
.gx-onlight button,.gx-onlight a{color:var(--text-on-light) !important;border-color:rgba(6,16,31,.28) !important}
.gx-onlight button:hover,.gx-onlight a:hover{color:var(--abyss-900) !important;border-color:rgba(6,16,31,.5) !important}
/* Footer links: give the 11px mono type a real hit area */
.gx-ftlink{display:inline-flex;align-items:center;min-height:44px}
.gx-skip{position:absolute;left:var(--space-5);top:-100px;z-index:999;padding:var(--space-4) var(--space-6);border-radius:var(--radius-pill,999px);background:var(--frost-100,#F9FCFD);color:var(--abyss-900);font:var(--type-label);letter-spacing:var(--tracking-label);text-transform:uppercase;transition:top var(--duration-hover,180ms) var(--ease-glide,cubic-bezier(.22,1,.36,1))}
.gx-skip:focus{top:var(--space-5);color:var(--abyss-900)}

/* Nav: keep the capsule usable once four items have to fit */
nav:not(.gx-mnav) button,nav:not(.gx-mnav) a{min-height:44px}
/* The 768-820px band used to squeeze the capsule's pills to 10px type and
   12px padding, so "Home" measured 53.2px there against 67.45px on a
   desktop — a button changing width between breakpoints. It is not needed:
   the capsule's natural width is ~625px and it has 736px to sit in at
   768px, because below 768px the capsule is replaced by .gx-mnav outright
   rather than being crushed to fit. Rule removed; the pills now measure
   the same at every width that renders them. */

/* ── Brand ring: real client logos on frosted plates ────────────────────── */
.gx-ring-tile{width:100%;height:74px;flex:none;border-radius:var(--radius-md);background:var(--frost-100,#F9FCFD);display:flex;align-items:center;justify-content:center;padding:10px 16px;overflow:hidden;box-shadow:inset 0 0 0 1px rgba(6,16,31,.06)}
.gx-ring-logo{max-width:100%;max-height:54px;width:auto;height:auto;object-fit:contain;filter:saturate(.88) contrast(1.02);transition:filter var(--duration-hover,180ms) var(--ease-glide,cubic-bezier(.22,1,.36,1))}
.gx-ring-card:hover .gx-ring-logo{filter:none}

/* ── Archive rows ───────────────────────────────────────────────────────── */
.gx-row{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--space-6);align-items:center;padding:var(--space-7) 0;border-top:1px solid var(--line-hairline);text-decoration:none;color:inherit;transition:var(--transition-surface)}
@media(min-width:900px){.gx-row{grid-template-columns:56px 200px minmax(0,1fr) 150px 92px;gap:var(--space-8)}}
.gx-row:hover{color:inherit}
.gx-row .gx-rowthumb{border-radius:var(--radius-md);overflow:hidden}
.gx-row:hover .gx-arrow{opacity:1;transform:none}

/* ── Project page ───────────────────────────────────────────────────────── */
.gx-specs{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(150px,100%),1fr));gap:var(--space-8);border-top:1px solid var(--line-hairline);padding-top:var(--space-8)}
.gx-plates{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(300px,100%),1fr));gap:var(--space-7)}
.gx-prose{font:var(--type-body);color:var(--text-body);max-width:var(--measure-prose);text-wrap:pretty}
.gx-pagenav{display:flex;justify-content:space-between;gap:var(--space-7);flex-wrap:wrap;border-top:1px solid var(--line-hairline);padding-top:var(--space-8)}

/* ── Studio ─────────────────────────────────────────────────────────────── */
.gx-cvrow{display:grid;grid-template-columns:minmax(120px,160px) minmax(0,1fr);gap:var(--space-8);padding-top:var(--space-7);border-top:1px solid var(--line-light)}
@media(max-width:767px){.gx-cvrow{grid-template-columns:minmax(0,1fr);gap:var(--space-4)}}

@media(prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  *,*::before,*::after{animation-duration:.001ms !important;animation-iteration-count:1 !important;transition-duration:.001ms !important}
}

/* ── Downloadable documents (CV / résumé) ───────────────────────────────── */
.gx-docs{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:var(--space-4);max-width:var(--measure-prose)}
.gx-doc{display:flex;align-items:center;gap:var(--space-5);min-height:64px;padding:var(--space-5) var(--space-6);border-radius:var(--radius-lg);border:1px solid rgba(6,16,31,.16);background:rgba(255,255,255,.82);color:var(--text-on-light);text-decoration:none;transition:var(--transition-surface,all 320ms cubic-bezier(.22,1,.36,1))}
.gx-doc:hover{color:var(--abyss-900);border-color:rgba(6,16,31,.34);background:#fff;transform:translateY(-2px)}
.gx-doc:active{transform:scale(.985)}
.gx-doc:focus-visible{outline:none;border-color:var(--focus-ring);box-shadow:0 0 0 3px rgba(126,180,214,.28)}
.gx-doc-primary{background:var(--abyss-900);border-color:var(--abyss-900);color:var(--frost-100,#F9FCFD)}
.gx-doc-primary:hover{background:var(--abyss-800,#0A1526);border-color:var(--abyss-800,#0A1526);color:var(--whiteout,#fff)}
.gx-doc-ext{flex:none;min-width:46px;padding:4px 8px;border-radius:var(--radius-sm,8px);font:var(--type-meta);letter-spacing:var(--tracking-label);text-align:center;background:rgba(6,16,31,.10);color:var(--text-on-light)}
.gx-doc-primary .gx-doc-ext{background:rgba(255,255,255,.16);color:var(--frost-100,#F9FCFD)}
.gx-doc-text{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1}
.gx-doc-label{font:var(--type-subheading);letter-spacing:var(--tracking-tight);color:inherit}
.gx-doc-note{font:var(--type-body-sm);color:var(--text-body-on-light);text-wrap:pretty}
.gx-doc-primary .gx-doc-note{color:rgba(249,252,253,.80)}
.gx-doc-size{flex:none;font:var(--type-meta);letter-spacing:var(--tracking-label);color:var(--text-muted-on-light)}
.gx-doc-primary .gx-doc-size{color:rgba(249,252,253,.72)}
@media(max-width:767px){
  /* Narrow: badge + size + arrow on one line, label and note full width below. */
  .gx-doc{flex-wrap:wrap;row-gap:var(--space-4);padding:var(--space-5)}
  .gx-doc-size{margin-left:auto}
  .gx-doc-text{order:4;flex:1 0 100%}
}

/* ── Highlight (Stiled / Konnect) ──────────────────────────────────────── */
.gx-highlight{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--space-10);align-items:center}
@media(min-width:960px){.gx-highlight{grid-template-columns:minmax(0,1fr) minmax(0,1.05fr);gap:var(--space-12)}}
.gx-hl-mark{font-family:var(--font-display);font-weight:600;font-size:clamp(34px,4vw,52px);letter-spacing:var(--tracking-display);line-height:1;color:var(--text-primary)}
.gx-hl-stat{border-top:1px solid var(--line-hairline);border-bottom:1px solid var(--line-hairline);padding:var(--space-7) 0}

/* ── Device frame for the embedded build ────────────────────────────────── */
.gx-stage{display:flex;justify-content:center;padding:var(--space-8) 0}
.gx-device{position:relative;border-radius:44px;padding:0;overflow:hidden;background:var(--abyss-900);border:1px solid var(--line-strong);box-shadow:inset 0 1px 0 rgba(255,255,255,.16),0 40px 90px rgba(3,6,12,.55)}
.gx-device-screen{border:0;display:block;transform-origin:top left;background:#fff;border-radius:44px}
.gx-device-loading{position:absolute;inset:0;display:grid;place-items:center;font:var(--type-label);letter-spacing:var(--tracking-label-wide);text-transform:uppercase;color:var(--text-muted);background:var(--abyss-900);pointer-events:none}
@media(max-width:767px){.gx-device,.gx-device-screen{border-radius:32px}}

/* Document row placed on a dark frosted panel */
.gx-doc-onDark{background:rgba(255,255,255,.06);border-color:var(--line-strong);color:var(--text-primary)}
.gx-doc-onDark:hover{background:rgba(255,255,255,.12);border-color:var(--focus-ring);color:var(--whiteout)}
.gx-doc-onDark .gx-doc-ext{background:rgba(255,255,255,.14);color:var(--text-primary)}
.gx-doc-onDark .gx-doc-note{color:var(--text-body)}
.gx-doc-onDark .gx-doc-size{color:var(--text-muted)}
.gx-device-still{width:100%;height:100%;object-fit:cover;object-position:top center;display:block;border-radius:44px}
@media(max-width:767px){.gx-device-still{border-radius:32px}}

/* ── Konnect case study ─────────────────────────────────────────────────── */
.gx-figrow{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(160px,100%),1fr));gap:var(--space-9)}
.gx-fig{font-family:var(--font-display);font-weight:600;font-size:clamp(28px,3.2vw,44px);letter-spacing:var(--tracking-display);line-height:1;color:var(--text-primary);margin:0}
.gx-chapters{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(280px,100%),1fr));gap:var(--space-10)}

.gx-bands{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:var(--space-3)}
.gx-bands li{display:grid;grid-template-columns:64px minmax(0,1fr) auto;gap:var(--space-5);align-items:baseline;padding:var(--space-5) var(--space-6);border-radius:var(--radius-md);background:rgba(255,255,255,.82);border:1px solid rgba(6,16,31,.12)}
.gx-band-mult{font:var(--type-meta);letter-spacing:var(--tracking-label);color:var(--text-on-light)}
.gx-band-body{display:flex;flex-direction:column;gap:2px;min-width:0}
.gx-band-name{font:var(--type-subheading);letter-spacing:var(--tracking-tight);color:var(--text-on-light)}
.gx-band-desc{font:var(--type-body-sm);color:var(--text-body-on-light);text-wrap:pretty}
.gx-band-count{font:var(--type-meta);letter-spacing:var(--tracking-label);color:var(--text-body-on-light);white-space:nowrap}
@media(max-width:640px){.gx-bands li{grid-template-columns:minmax(0,1fr);row-gap:var(--space-2,4px);margin-left:0 !important}}

.gx-tablewrap{overflow-x:auto;border-radius:var(--radius-lg);border:1px solid var(--line-hairline)}
.gx-table{width:100%;border-collapse:collapse;min-width:460px}
.gx-table caption{text-align:left}
.gx-table th,.gx-table td{text-align:left;padding:var(--space-6) var(--space-7);border-bottom:1px solid var(--line-hairline);font:var(--type-body-sm);color:var(--text-body)}
.gx-table thead th{font:var(--type-label);letter-spacing:var(--tracking-label);text-transform:uppercase;color:var(--text-muted)}
.gx-table tbody th{font:var(--type-body-sm);color:var(--text-primary);font-weight:400}
.gx-table tbody tr:last-child th,.gx-table tbody tr:last-child td{border-bottom:0}
.gx-was{color:var(--text-muted);text-decoration:line-through;text-decoration-thickness:1px}
.gx-now{color:var(--signal-live,#4FD1C5)}

.gx-checks{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(auto-fit,minmax(min(300px,100%),1fr));gap:var(--space-6)}
.gx-checks li{display:grid;grid-template-columns:26px minmax(0,1fr);gap:var(--space-5);align-items:start;font:var(--type-body-sm);color:var(--text-body);text-wrap:pretty}
.gx-check-mark{display:grid;place-items:center;width:26px;height:26px;border-radius:50%;font:var(--type-meta)}
.gx-check-ok .gx-check-mark{background:rgba(79,209,197,.16);color:var(--signal-live,#4FD1C5)}
.gx-check-warn .gx-check-mark{background:rgba(232,184,122,.18);color:var(--flare-300,#E8B87A)}

.gx-screens{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(220px,100%),1fr));gap:var(--space-8)}

/* Embedded document */
.gx-embed{border-radius:var(--radius-lg);overflow:hidden;border:1px solid var(--line-hairline);background:#fff}
.gx-embed-frame{width:100%;height:min(78vh,900px);border:0;display:block}
.gx-embed-cta{display:flex;align-items:center;gap:var(--space-5);width:100%;min-height:72px;padding:var(--space-6);border-radius:var(--radius-lg);border:1px solid var(--line-strong);background:rgba(255,255,255,.06);color:var(--text-primary);text-align:left;cursor:pointer;transition:var(--transition-surface,all 320ms cubic-bezier(.22,1,.36,1))}
.gx-embed-cta:hover{background:rgba(255,255,255,.12);border-color:var(--focus-ring)}
.gx-embed-cta:focus-visible{outline:none;border-color:var(--focus-ring);box-shadow:0 0 0 3px rgba(126,180,214,.28)}
.gx-embed-cta .gx-doc-ext{background:rgba(255,255,255,.14);color:var(--text-primary)}
.gx-embed-cta .gx-doc-note{color:var(--text-body)}
/* Second highlight mirrors the first so the two don't read as a repeat */
@media(min-width:960px){
  .gx-highlight-flip{grid-template-columns:minmax(0,1.05fr) minmax(0,1fr)}
  .gx-highlight-flip > :first-child{order:2}
  .gx-highlight-flip > :last-child{order:1}
}

/* ── Scroll reveal ──────────────────────────────────────────────────────
   One controlled entrance per section, not motion on everything. Driven by
   reveal.js (IntersectionObserver, reveal-once). `.gx-reveal` fades the
   block itself; `.gx-reveal-stagger` fades its direct children with a short
   capped stagger. Both collapse to an instant opacity swap under
   prefers-reduced-motion (see the query at the top of this file). */
.gx-reveal{opacity:0;transform:translateY(16px);transition:opacity var(--dur-base) var(--ease-glide),transform var(--dur-base) var(--ease-glide)}
.gx-reveal.is-in{opacity:1;transform:none}
.gx-reveal-stagger > *{opacity:0;transform:translateY(16px);transition:opacity var(--dur-base) var(--ease-glide),transform var(--dur-base) var(--ease-glide)}
.gx-reveal-stagger.is-in > *{opacity:1;transform:none}
.gx-reveal-stagger.is-in > *:nth-child(1){transition-delay:0ms}
.gx-reveal-stagger.is-in > *:nth-child(2){transition-delay:50ms}
.gx-reveal-stagger.is-in > *:nth-child(3){transition-delay:100ms}
.gx-reveal-stagger.is-in > *:nth-child(4){transition-delay:150ms}
.gx-reveal-stagger.is-in > *:nth-child(5){transition-delay:200ms}
.gx-reveal-stagger.is-in > *:nth-child(n+6){transition-delay:240ms}
.gx-reveal-right{opacity:0;transform:translateX(48px);transition:opacity var(--dur-base) var(--ease-glide),transform var(--dur-base) var(--ease-glide)}
.gx-reveal-right.is-in{opacity:1;transform:none}
@media(prefers-reduced-motion:reduce){
  .gx-reveal,.gx-reveal-stagger > *,.gx-reveal-right{opacity:1 !important;transform:none !important;transition:none !important}
}

/* ── Vinyl (hero centerpiece) ─────────────────────────────────────────────
   Spins only while actually playing — a functional loop, not decoration.
   Ships fully inert if no audio file exists (Vinyl's own JS hides the play
   control on <audio onError>), so the disc alone is a safe default state. */
.gx-vinyl-stage{display:flex;flex-direction:column;align-items:center;text-align:center;width:100%;padding:0 clamp(50px,8vw,100px) var(--space-13);gap:var(--space-9)}
.gx-vinyl{position:relative;display:flex;flex-direction:column;align-items:center;gap:var(--space-6);width:100%}
.gx-vinyl-disc{position:relative;width:100%;aspect-ratio:1;border-radius:50%;background:
    repeating-radial-gradient(circle at 50% 50%,var(--abyss-700) 0 2px,var(--abyss-800) 2px 4px);
  border:1px solid var(--line-strong);box-shadow:0 28px 64px rgba(3,6,12,.55),inset 0 1px 0 rgba(255,255,255,.1)}
.gx-vinyl-disc.is-spinning{animation:gx-vinyl-spin 7s linear infinite}
.gx-vinyl-ring{position:absolute;inset:0;width:100%;height:100%}
.gx-vinyl-ring-text{font-family:var(--font-mono);font-size:9px;letter-spacing:.14em;fill:rgba(249,252,253,.68)}
.gx-vinyl-label{position:absolute;top:50%;left:50%;width:38%;aspect-ratio:1;border-radius:50%;transform:translate(-50%,-50%);
  background-size:220% auto;background-position:50% 18%;border:1px solid rgba(255,255,255,.25);box-shadow:0 0 0 1px rgba(3,6,12,.4)}
.gx-vinyl-spindle{position:absolute;top:50%;left:50%;width:6px;height:6px;border-radius:50%;background:var(--frost-050);transform:translate(-50%,-50%)}
.gx-vinyl-btn{transform:scale(1)}
.gx-vinyl-caption{display:flex;flex-direction:column;align-items:center}
@keyframes gx-vinyl-spin{to{transform:rotate(360deg)}}
/* ── Orbit hero (2026-10-01) ─────────────────────────────────────────────
   Order: badge + big claim → the artwork's slot → line, actions, facts →
   signature. The artwork itself is .gx-orbit-layer: one square canvas,
   ~2× the slot, centred on it by home.jsx, laid over the hero AND the top
   of the next section (.gx-orbit-span). It ignores the pointer — the page
   listens — so hero text (.gx-fore, above it) stays selectable/clickable. */
.gx-orbit-span{position:relative;overflow-x:clip}
/* z-index 1: under the hero's content (GrainField lifts its children to 2)
   but over the next section, which paints at auto. */
.gx-orbit-layer{position:absolute;z-index:1;pointer-events:none;
  -webkit-mask-image:radial-gradient(closest-side,#000 80%,transparent 100%);mask-image:radial-gradient(closest-side,#000 80%,transparent 100%)}
.gx-orbit-layer .orbit-canvas{position:absolute;inset:0;width:100%;height:100%;display:block}
.gx-orbit-labels{position:absolute;inset:0;pointer-events:none}
.gx-orbit-label{position:absolute;left:0;top:0;white-space:nowrap;will-change:transform;text-align:center;
  font:500 12px/1.2 var(--font-mono);letter-spacing:.14em;text-transform:uppercase;color:var(--text-primary);
  text-shadow:0 0 2px rgba(255,255,255,.95),0 0 8px rgba(255,255,255,.9),0 0 16px rgba(255,255,255,.8)}
html.gx-diver-hover,html.gx-diver-hover *{cursor:grab}
html.gx-diver-grabbing,html.gx-diver-grabbing *{cursor:grabbing;user-select:none}

.gx-fore{position:relative;z-index:4;pointer-events:none}
.gx-fore a,.gx-fore button,.gx-fore input,.gx-fore [role=button],.gx-fore h1,.gx-fore h2,.gx-fore p{pointer-events:auto}
/* Hero (white, 2026-10-01). Air first: a broad gap above the claim and
   below the whole hero, and room between every element — nothing sits on
   the artwork. The claim leads, big; the sphere follows; then the line and
   actions on the LEFT and the facts and signature on the RIGHT:
     ≥1180px  three columns, the two blocks beside the sphere;
     760–1179 the sphere, then the two blocks side by side under it;
     <760     stacked, centred, still with room around each. */
.gx-hero{display:flex;flex-direction:column;align-items:center;text-align:center;
  padding-top:clamp(96px,12vw,184px);padding-bottom:clamp(112px,13vw,200px)}
.gx-hero-head{display:flex;flex-direction:column;align-items:center}
.gx-hero .gx-hero-h1{font:var(--type-display-1);font-size:clamp(44px,7.2vw,112px);line-height:.98;letter-spacing:var(--tracking-display);
  color:var(--text-primary);margin:var(--space-8) 0 0;max-width:15ch;text-wrap:balance}
.gx-hero-stage{position:relative;width:100%;display:grid;grid-template-columns:1fr;justify-items:center;
  row-gap:clamp(48px,6vw,88px);margin-top:clamp(56px,7vw,112px)}
.gx-hero-slot{position:relative;grid-area:1/1;width:min(100%,820px);aspect-ratio:1}
.gx-hero-slot .gx-vinyl{position:absolute;inset:6%}
.gx-hero-foot{display:flex;flex-direction:column;align-items:center;gap:var(--space-8)}
.gx-hero-sub{font:var(--type-body-lg);color:var(--text-body);margin:0;max-width:34ch;text-wrap:pretty}
.gx-hero-foot .gx-ctarow{display:flex;gap:var(--space-5);flex-wrap:wrap;justify-content:center}
.gx-hero-sign{display:flex;flex-direction:column;align-items:center;gap:var(--space-5)}
.gx-hero-sign .gx-hero-person{margin:var(--space-2) 0 0}
.gx-hero-person{font:var(--type-display-3);letter-spacing:var(--tracking-display);color:var(--text-primary)}

@media (min-width:760px) and (max-width:1179px){
  .gx-hero-stage{grid-template-columns:1fr 1fr;column-gap:var(--space-11);align-items:start}
  .gx-hero-slot{grid-area:1/1/2/3;justify-self:center}
  .gx-hero-foot{grid-area:2/1;align-items:flex-start;text-align:left;justify-self:start}
  .gx-hero-foot .gx-ctarow{justify-content:flex-start}
  .gx-hero-sign{grid-area:2/2;align-items:flex-end;text-align:right;justify-self:end}
  .gx-hero-sign > div{justify-content:flex-end}
}
@media (min-width:1180px){
  .gx-hero-stage{--orb:min(50vw,820px);grid-template-columns:minmax(0,1fr) var(--orb) minmax(0,1fr);align-items:center;column-gap:clamp(40px,4vw,80px)}
  .gx-hero-slot{grid-area:1/2;width:100%}
  .gx-hero-foot{grid-area:1/1;align-items:flex-start;text-align:left;justify-self:start}
  .gx-hero-foot .gx-ctarow{justify-content:flex-start}
  .gx-hero-sign{grid-area:1/3;align-items:flex-end;text-align:right;justify-self:end}
  .gx-hero-sign > div{justify-content:flex-end}
}

/* Work list on home. */
.gx-work-sec{padding-top:var(--space-12);padding-bottom:var(--space-13)}
.gx-work-foot{display:flex;justify-content:space-between;align-items:center;gap:var(--space-7);flex-wrap:wrap;
  margin-top:var(--space-9);border-top:1px solid var(--line-hairline);padding-top:var(--space-7)}

/* Sound orb: the music control on every page, fixed bottom-right. A glass
   sphere like the ones riding the orbit — the record turns inside it. */
.gx-orb{position:fixed;right:20px;bottom:20px;z-index:60;width:64px;height:64px;padding:0;border:0;border-radius:50%;cursor:pointer;
  /* The glass is drawn in WebGL; the button only blurs what's behind it
     (true frosted refraction of the page) and casts a soft shadow. */
  background:transparent;-webkit-backdrop-filter:blur(6px) saturate(1.4);backdrop-filter:blur(6px) saturate(1.4);
  box-shadow:0 12px 26px rgba(14,14,15,.14),0 2px 6px rgba(14,14,15,.06)}
.gx-orb-disc{position:absolute;inset:17%;border-radius:50%;
  background:radial-gradient(circle,transparent 0 31%,rgba(0,0,0,0) 31%),repeating-radial-gradient(circle at 50% 50%,var(--abyss-900) 0 1.2px,var(--abyss-700) 1.2px 2.4px);
  box-shadow:0 0 0 1px rgba(255,255,255,.08),0 4px 10px rgba(3,6,12,.6)}
.gx-orb-disc::before{content:"";position:absolute;inset:31%;border-radius:50%;background:url(assets/work/portrait.png) 50% 18%/220% auto;box-shadow:0 0 0 1px rgba(3,6,12,.5)}
.gx-orb-disc::after{content:"";position:absolute;left:50%;top:50%;width:4px;height:4px;margin:-2px 0 0 -2px;border-radius:50%;background:var(--frost-050)}
.gx-orb.is-playing .gx-orb-disc{animation:gx-vinyl-spin 3.2s linear infinite}
/* The glass itself is the WebGL canvas (shared.jsx GlassOrb). Without
   WebGL 2 the orb gets .is-static and the CSS record shows instead. */
.gx-orb-art{position:absolute;inset:0;width:100%;height:100%;border-radius:50%;display:block}
.gx-orb .gx-orb-disc{display:none}
.gx-orb.is-static .gx-orb-disc{display:block}
.gx-orb.is-asking{animation:gx-orb-pulse 1.6s ease-in-out infinite}
.gx-orb:focus-visible{outline:none;box-shadow:0 0 0 3px rgba(14,14,15,.35),inset 0 0 0 1px rgba(255,255,255,.9),0 14px 30px rgba(14,14,15,.16)}
.gx-orb-tip{position:fixed;right:96px;bottom:40px;z-index:60;font:500 11px/1 var(--font-mono);letter-spacing:.14em;text-transform:uppercase;
  color:var(--text-primary);background:rgba(255,255,255,.9);border:1px solid var(--line-strong);padding:8px 12px;border-radius:999px;pointer-events:none;
  opacity:0;transform:translateX(6px);transition:opacity .3s,transform .3s}
.gx-orb-tip.is-shown{opacity:1;transform:none}
@keyframes gx-orb-pulse{50%{box-shadow:inset 0 0 0 1px rgba(255,255,255,.9),0 14px 30px rgba(14,14,15,.16),0 0 0 10px rgba(14,14,15,.07)}}
@media (max-width:640px){
  .gx-orb{right:14px;bottom:14px;width:54px;height:54px}
  .gx-orb-tip{right:80px;bottom:30px}
  .gx-orbit-label{font-size:10px}
  .gx-hero-foot .gx-ctarow{width:100%}
}

@media(prefers-reduced-motion:reduce){
  .gx-vinyl-disc.is-spinning{animation:none}
  .gx-orb.is-playing .gx-orb-disc,.gx-orb.is-asking{animation:none}
}

/* ── Highlight big header ("large header, small subtext") ───────────────
   Used only where content.js supplies `bigHeadline` (Stiled, on the home
   page). Sits on the existing --type-display scale, just larger — no new
   font size invented, this is the same clamp() shape as --type-display-1. */
.gx-hl-big{font-family:var(--font-display);font-weight:600;font-size:clamp(2.5rem,6.6vw,5.25rem);line-height:.96;letter-spacing:var(--tracking-display);margin:0;text-wrap:balance;max-width:14ch}

/* ── Feature composites (Stiled hands+phones, Konnect hand+phone) ───────
   Single flattened photographic images — real composition, real content,
   no scroll-driven choreography. Just reveal once like everything else. */
.gx-feature-composite{display:block;width:100%;height:auto;border-radius:var(--radius-2xl);box-shadow:var(--shadow-lg)}

/* ── Full-bleed photography (Stiled banner, hero, gallery) ───────────────
   These render as SIBLINGS of <Sec> inside <GrainField>. `.gx-sec` is what
   carries --maxw-page and the gutter, so anything outside it already spans
   the field edge to edge — no width:100vw + negative-margin hack, which
   breaks the moment a classic scrollbar takes up layout width. */
/* The ratio lives on the IMAGE, never on the wrapper. Put it on the wrapper
   and the wrapper's height is fixed — so on mobile, where the caption stops
   being absolute and rejoins the flow, the words land past the wrapper's
   bottom edge and overflow:hidden eats them. Asked for a hero title on a
   phone and got a silent, empty photograph. */
.gx-bleed{position:relative;display:block;width:100%;overflow:hidden}
.gx-bleed img{display:block;width:100%;height:auto;aspect-ratio:4/5;object-fit:cover;object-position:center}
.gx-bleed-gallery img{aspect-ratio:1/1}

/* Below 768px the overlay un-floats and sits under the frame: type over a
   phone-width crop is unreadable. The photograph still runs edge to edge —
   only the words move.

   Two independent tone knobs, because the photograph and the section it sits
   on do not have to agree. Konnect is exactly that case: a light studio shot
   dropped into a dark (abyss) section. Tie both states to one class and the
   stacked mobile text renders dark-on-dark — invisible.
     `field*` → colour of the STACKED text, follows the section underneath.
     `on*`    → colour of the FLOATING text, follows the photograph. */
.gx-bleed-text{padding:var(--space-9) var(--gutter-page) var(--space-10)}
.gx-bleed-fieldLight .gx-bleed-text{color:var(--text-on-light)}
.gx-bleed-fieldDark .gx-bleed-text{color:var(--whiteout)}
/* Overlaid type is sized to the photograph's empty zone, not to the page's
   standalone display scale — the trio shot only clears about a third of its
   width at the bottom, and type that runs under a phone is type nobody can
   read. `ch` is deliberately avoided for the box width: it resolves against
   this element's 16px font, not the 50px headline sitting inside it. */
.gx-bleed-text .gx-hl-big{font-size:clamp(2rem,3.6vw,3.4rem);max-width:none}

@media(min-width:768px){
  .gx-bleed img{aspect-ratio:16/9;max-height:88vh}
  .gx-bleed-gallery img{aspect-ratio:16/9}
  /* z-index:2 gives this its own stacking context, so the scrim below can sit
     at -1 — behind the words, still above the photograph. */
  .gx-bleed-text{position:absolute;z-index:2;inset:auto auto 0 0;max-width:min(620px,44%);padding:var(--space-12) var(--gutter-page)}
  .gx-bleed-center .gx-bleed-text{inset:0 auto 0 0;display:flex;flex-direction:column;justify-content:center}
  /* Floating over the photograph now, so the photograph decides the ink. */
  .gx-bleed-onLight .gx-bleed-text{color:var(--text-on-light)}
  .gx-bleed-onDark .gx-bleed-text{color:var(--whiteout)}
  /* Invisible against the empty part of the field it sits on; quietly buys
     contrast back if a crop slides the subject under the type at an odd
     viewport ratio. */
  .gx-bleed-text::before{content:"";position:absolute;inset:-28% -34% -30% -45%;z-index:-1;pointer-events:none}
  .gx-bleed-onLight .gx-bleed-text::before{background:radial-gradient(ellipse at 32% 66%,rgba(255,255,255,.94) 0%,rgba(255,255,255,.74) 46%,rgba(255,255,255,0) 72%)}
  .gx-bleed-onDark .gx-bleed-text::before{background:radial-gradient(ellipse at 32% 56%,rgba(20,9,2,.66) 0%,rgba(20,9,2,.36) 46%,rgba(20,9,2,0) 72%)}
}

/* ── Re-Soil ripple wall ─────────────────────────────────────────────────
   Ported from the live Re-Soil hero (.fhero / .wall / .wall-tile). Same
   structure: an absolutely-positioned tile grid, a scrim over it, and the
   copy on top, bottom-left. resoil-wall.js owns .gx-rewall-grid entirely —
   it writes every tile's left/top/size and an inline transform each frame,
   so nothing here may set position or transform on a tile. */
.gx-rewall{position:relative;overflow:hidden;display:flex;flex-direction:column;justify-content:flex-end;
  min-height:min(100svh,760px);background:var(--abyss-900);color:var(--whiteout);user-select:none;
  padding:var(--space-12) var(--gutter-page) var(--space-11)}
.gx-rewall-grid{position:absolute;inset:0;overflow:hidden;z-index:0;touch-action:pan-y}
.gx-wall-tile{position:absolute;background-image:var(--wall-sheet);background-repeat:no-repeat;
  background-size:600% 100%;will-change:transform}
/* Holds the type legible over a wall that is constantly changing value. */
.gx-rewall-scrim{position:absolute;inset:0;z-index:1;pointer-events:none;
  background:linear-gradient(to top,rgba(3,6,12,.92) 0%,rgba(3,6,12,.78) 34%,rgba(3,6,12,.5) 62%,rgba(3,6,12,.34) 100%)}
.gx-rewall-copy{position:relative;z-index:2;max-width:min(640px,92%)}
.gx-rewall-copy > *{pointer-events:auto}
/* (min-height for phones now lives in the phase 4 block below) */


/* ══════════════════════════════════════════════════════════════════════
   MOBILE — phase 0: foundation
   One breakpoint, 767px. Everything below it reads off a 3-column grid
   (16px margins, 12px gutters) and its own type ramp. The desktop scale
   has hard rem floors — clamp() never fires on a phone — so the ramp is
   redeclared here rather than widened, which would drag the desktop
   composition down with it.
   ═════════════════════════════════════════════════════════════════════ */
@media(max-width:767px){
  :root{
    /* 3-column grid: 375px → 106.33px columns */
    --grid-margin:16px;
    --grid-gutter:12px;
    --grid-col:calc((100vw - 2 * var(--grid-margin) - 2 * var(--grid-gutter)) / 3);
    --grid-2col:calc(var(--grid-col) * 2 + var(--grid-gutter));
    --gutter-page:var(--grid-margin);

    /* ── Type ramp ────────────────────────────────────────────────────
       The desktop scale is clamp()ed with rem FLOORS — 3.5rem on
       display-1 — so on a phone the clamp never fires and a 56px
       headline ships at 375px. Redefining the tokens here (rather than
       lowering the floors, which would drag the desktop composition
       down with it) also reaches every INLINE `font:var(--type-…)` in
       the JSX, because a var() resolves at use time. No !important,
       no component edits. Shorthand shape must stay intact. */
    --type-display-1:600 clamp(34px,9.6vw,48px)/1.0 var(--font-display);
    --type-display-2:600 clamp(26px,7.4vw,36px)/1.04 var(--font-display);
    --type-display-3:600 clamp(23px,6.4vw,30px)/1.08 var(--font-display);
    --type-heading:600 19px/1.2 var(--font-display);
    --type-subheading:500 15px/1.32 var(--font-display);
    --type-body-lg:400 16px/1.58 var(--font-body);
    --type-body:400 15px/1.6 var(--font-body);
    --type-body-sm:400 13px/1.54 var(--font-body);
    --type-label:500 10px/1 var(--font-mono);
    --type-meta:400 11px/1.45 var(--font-mono);
    --type-caption:400 11px/1.45 var(--font-mono);

    /* ── Rhythm ───────────────────────────────────────────────────────
       Only the steps above 40px move. space-1…9 are component-internal
       (gaps inside a card, a button's icon gap) and are already phone-
       sized; the big steps are page rhythm, and 128px of section
       padding is a third of a phone screen spent on nothing. */
    --space-10:36px;
    --space-11:44px;
    --space-12:56px;
    --space-13:64px;
    --space-14:88px;

    --measure-prose:100%;
    --measure-narrow:100%;
  }
}

/* ══ MOBILE — phase 1: navigation ══════════════════════════════════════
   .gx-mnav renders only below 768px (shared.jsx swaps components, so the
   bar and the design-system capsule are never in the DOM together). The
   sheet stays mounted for its transition and is `inert` while closed. */
.gx-mnav,.gx-mnav-spacer{display:none}
@media(max-width:767px){
  :root{--mnav-h:48px;--mnav-top:10px}
  /* Fixed, not sticky: sticky is bounded by the GrainField it lives in, so
     the bar vanished for the remaining ~15,000px of the page. The spacer
     holds the seat it no longer occupies in flow. */
  .gx-mnav-spacer{display:block;height:calc(var(--mnav-h) + var(--mnav-top))}
  /* Width comes from the viewport, never from `right`. A fixed box resolved
     with left+right takes the initial containing block — which Chrome grows
     to the document's scrollWidth — so one overflowing element anywhere on
     the page silently stretched the bar past the screen edge. */
  .gx-mnav{
    position:fixed;top:var(--mnav-top);left:var(--grid-margin);right:auto;
    width:calc(100vw - 2 * var(--grid-margin));z-index:60;
    display:flex;align-items:center;justify-content:space-between;gap:var(--space-4);
    height:var(--mnav-h);padding:0 7px 0 16px;
    border-radius:var(--radius-pill);
    background:var(--glass-card);
    border:var(--border-hair) solid var(--line-hairline);
    -webkit-backdrop-filter:var(--blur-heavy);backdrop-filter:var(--blur-heavy);
    box-shadow:var(--shadow-md),var(--inset-rim);
    transition:transform 320ms var(--ease-glide,cubic-bezier(.22,1,.36,1)),
               opacity 220ms var(--ease-glide,cubic-bezier(.22,1,.36,1));
  }
  .gx-mnav.is-tucked{transform:translateY(calc(-100% - var(--mnav-top) - 6px));opacity:0}
  .gx-mnav-brand{
    font-family:var(--font-display);font-weight:600;font-size:15px;line-height:1;
    letter-spacing:var(--tracking-tight);color:var(--text-primary);
    display:inline-flex;align-items:center;height:100%;
  }
  .gx-mnav-brand:hover{color:var(--text-primary)}
  .gx-mnav-toggle{
    position:relative;flex:none;width:34px;height:34px;padding:0;
    display:grid;place-items:center;border-radius:var(--radius-pill);
    border:var(--border-hair) solid var(--line-hairline);
    background:rgba(255,255,255,.08);cursor:pointer;
    -webkit-tap-highlight-color:transparent;
  }
  /* 34px reads better in a 48px bar than a 44px circle does; the target
     is brought back up to 44 with an invisible skirt. */
  .gx-mnav-toggle::after{content:"";position:absolute;inset:-5px}
  .gx-mnav-toggle:focus-visible{outline:none;border-color:var(--focus-ring);box-shadow:0 0 0 3px rgba(126,180,214,.28)}
  .gx-mnav-bars{position:relative;display:block;width:15px;height:9px}
  .gx-mnav-bars i{
    position:absolute;left:0;right:0;height:1.5px;border-radius:2px;
    background:var(--text-primary);
    transition:top var(--duration-hover,180ms) var(--ease-glide,cubic-bezier(.22,1,.36,1)),
               transform var(--duration-hover,180ms) var(--ease-glide,cubic-bezier(.22,1,.36,1));
  }
  .gx-mnav-bars i:nth-child(1){top:0}
  .gx-mnav-bars i:nth-child(2){top:7.5px}
  .gx-mnav-toggle[aria-expanded="true"] .gx-mnav-bars i:nth-child(1){top:3.75px;transform:rotate(45deg)}
  .gx-mnav-toggle[aria-expanded="true"] .gx-mnav-bars i:nth-child(2){top:3.75px;transform:rotate(-45deg)}

  .gx-mnav-sheet{
    position:fixed;top:0;left:0;right:auto;bottom:auto;
    width:100vw;height:100dvh;z-index:55;
    display:flex;flex-direction:column;
    /* clears the 10px offset + 48px bar */
    padding:calc(var(--mnav-h) + var(--mnav-top) + var(--space-9)) var(--grid-margin)
            calc(var(--space-8) + env(safe-area-inset-bottom,0px));
    background:rgba(3,6,12,.90);
    -webkit-backdrop-filter:blur(26px) saturate(140%);backdrop-filter:blur(26px) saturate(140%);
    opacity:0;visibility:hidden;pointer-events:none;
    overflow-y:auto;overscroll-behavior:contain;
    transition:opacity 260ms var(--ease-glide,cubic-bezier(.22,1,.36,1)),visibility 260ms;
  }
  .gx-mnav-sheet.is-open{opacity:1;visibility:visible;pointer-events:auto}
  .gx-mnav-list{list-style:none;margin:auto 0;padding:0;border-top:1px solid var(--line-hairline)}
  .gx-mnav-item{
    display:flex;align-items:center;gap:var(--space-5);
    width:100%;min-height:64px;padding:var(--space-5) 0;
    border-bottom:1px solid var(--line-hairline);
    font-family:var(--font-display);font-weight:600;font-size:26px;line-height:1;
    letter-spacing:var(--tracking-display);color:var(--text-muted);
    text-decoration:none;
    opacity:0;transform:translateY(10px);
    transition:opacity 300ms var(--ease-glide,cubic-bezier(.22,1,.36,1)),
               transform 300ms var(--ease-glide,cubic-bezier(.22,1,.36,1)),
               color var(--duration-hover,180ms) linear;
  }
  .gx-mnav-item.is-active{color:var(--text-primary)}
  .gx-mnav-item:hover{color:var(--text-primary)}
  .gx-mnav-sheet.is-open .gx-mnav-item{opacity:1;transform:none}
  .gx-mnav-sheet.is-open li:nth-child(1) .gx-mnav-item{transition-delay:70ms}
  .gx-mnav-sheet.is-open li:nth-child(2) .gx-mnav-item{transition-delay:110ms}
  .gx-mnav-sheet.is-open li:nth-child(3) .gx-mnav-item{transition-delay:150ms}
  .gx-mnav-sheet.is-open li:nth-child(n+4) .gx-mnav-item{transition-delay:190ms}
  .gx-mnav-idx{flex:none;width:22px;font:var(--type-label);letter-spacing:var(--tracking-label);color:var(--text-faint)}
  .gx-mnav-go{margin-left:auto;font:var(--type-meta);color:var(--text-faint)}
  .gx-mnav-item.is-active .gx-mnav-go{color:var(--frost-300)}

  .gx-mnav-foot{display:flex;flex-direction:column;gap:var(--space-6);padding-top:var(--space-9)}
  /* This is the same "Let's chat" the desktop capsule carries, so it takes
     that button's geometry exactly — the design system's `sm` primary,
     8/16 padding at 10px — and lands on the same 110px. min-height keeps
     the 44px target that the padding alone would not reach. */
  .gx-mnav-cta{
    display:inline-flex;align-items:center;justify-content:center;
    align-self:flex-start;padding:8px 16px;min-height:44px;border-radius:var(--radius-pill);
    background:var(--action-fill);color:var(--action-ink);
    font:var(--type-label);font-size:10px;letter-spacing:var(--tracking-label);
    text-transform:uppercase;box-shadow:var(--shadow-sm);
  }
  .gx-mnav-cta:hover{background:var(--action-fill-hover);color:var(--action-ink)}
  .gx-mnav-meta{display:flex;flex-direction:column;gap:4px;font:var(--type-caption);color:var(--text-faint);word-break:break-word}
}

/* ══ MOBILE — TabSwitch (year filter) ═══════════════════════════════════
   A design-system control with inline styles and no class of its own. Five
   pills at 18px side padding measure 381px, which overflowed the screen by
   6px — and a fixed sheet sized to that scrollWidth, so the whole nav went
   with it. Below 768px the strip scrolls instead of pushing the page. */
@media(max-width:767px){
  .gx-eyebrow > div{max-width:100%;overflow-x:auto;overscroll-behavior-x:contain;scrollbar-width:none}
  .gx-eyebrow > div::-webkit-scrollbar{display:none}
}

/* ══ MOBILE — phase 2: hero ════════════════════════════════════════════
   The token ramp above already resized the headline, lede and meta. What
   is left is composition: the desktop hero is a two-up row (lede left,
   buttons right, baseline-aligned) that wraps into a column on a phone
   and inherits a 72px row gap meant to separate COLUMNS, not stacked
   blocks. And the vinyl needs a size it was never given — it was sitting
   on a clamp() with a 50px side padding, which is not a grid position. */
@media(max-width:767px){
  /* 56px of section padding on top of the 58px nav spacer is a screenful
     of nothing before the first word. */
  #gx-hero{padding-top:var(--space-8);padding-bottom:var(--space-9)}
  .gx-hero-h1{max-width:none}

  /* Once wrapped these are stacked blocks, not adjacent columns. */
  .gx-hero-lede{gap:var(--space-8);align-items:stretch}

  /* A button is the same object at every size: it keeps the intrinsic
     width its own label gives it on desktop, and that width never changes
     between breakpoints. The design system already fixes the padding and
     font in px, so the rule here is simply to impose nothing — no
     stretching to the column, no per-breakpoint min-height.

     What the row owns instead is placement: buttons sit side by side while
     they fit and wrap onto the next line when they do not, left-aligned to
     the gutter like every other block on the page. */
  .gx-ctarow{width:100%;flex-direction:row;flex-wrap:wrap;gap:var(--space-4);align-items:center}

  /* Disc spans two grid columns plus a gutter (≈236px at 375), centred —
     a deliberate object against a 36px headline rather than a 275px
     medallion that outweighs it. The ring text is SVG in a 0–200 viewBox,
     so it scales with the disc and stays crisp. */
  .gx-vinyl-stage{padding-inline:var(--grid-margin);gap:var(--space-8)}
  .gx-vinyl{max-width:calc(var(--grid-2col) + var(--grid-gutter));margin-inline:auto}
}

/* ══ Banner body — padding out of inline styles ════════════════════════
   BannerFeature used to carry its section padding inline, which a media
   query cannot reach. Both grounds now live here; .gx-sec sets the same
   specificity, so source order (this, later in the file) decides. */
.gx-banner-body{padding-top:var(--space-12);padding-bottom:var(--space-14)}

/* ══ MOBILE — phase 3: feature banners ═════════════════════════════════ */
@media(max-width:767px){
  /* The stacked overlay inherited padding tuned for the DESKTOP floating
     version, where the words sit inside the photograph and need room to
     clear its edge. Stacked under the frame they need none of it: 36px
     below the headline plus 56px of section padding left ~90px of dead
     field between a statement and the sentence answering it. */
  .gx-bleed-text{padding:var(--space-7) var(--gutter-page) var(--space-7)}
  .gx-banner-body{padding-top:var(--space-8);padding-bottom:var(--space-12)}

  /* 14ch resolves against THIS element's font-size, so at 28px it caps the
     headline near 196px — half the column — and shatters it into stubs.
     The grid already constrains the measure; the ch cap has to go. */
  .gx-hl-big{font-size:clamp(26px,7.6vw,34px);max-width:none}
  .gx-bleed-text .gx-hl-big{font-size:clamp(26px,7.6vw,34px)}

  /* The headline stat is the hook of the module — at display-3 (23px) it
     read as a caption. Sits below the hero's 36px, above body copy. */
  .gx-stat{font-size:30px;line-height:1.04}
  /* Figures snap to the 3-column grid instead of a 160px minimum that
     could only ever produce two columns and an orphan. */
  .gx-figrow{grid-template-columns:repeat(auto-fit,minmax(var(--grid-col),1fr));gap:var(--space-7)}
  .gx-fig{font-size:26px}
  .gx-highlight{gap:var(--space-9)}
  .gx-hl-stat{padding:var(--space-6) 0}
}

/* ══ Brand grid (phone replacement for the 360° cylinder) ══════════════
   BrandsGrid only renders below 768px, so these need no media query. It
   borrows .gx-ring-tile / .gx-ring-logo / .gx-ring-meta from the cylinder
   — same frosted plate, same mono caption — so only the arrangement
   changes between breakpoints, not the material.

   Two columns, not three: the page grid is 3-column, but a 106px track
   leaves a 74px inner box once the plate's padding is taken, and
   "Australian Avocados" is not legible at 74px. Logos get 1.5 columns
   each; the margins and gutters are still the 3-column system's. */
.gx-brands-sec{padding-top:var(--space-12);padding-bottom:var(--space-11)}
.gx-brandgrid{list-style:none;margin:0;padding:0;display:grid;
  grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--space-7) var(--grid-gutter)}
.gx-brandcell{display:flex;flex-direction:column;align-items:center;gap:var(--space-4);min-width:0}
.gx-brandcell .gx-ring-meta{text-align:center}

/* ══ MOBILE — phase 4: Re-Soil wall ════════════════════════════════════ */
@media(max-width:767px){
  /* 92% of the content box put the copy — and therefore its CTA — at
     316px, 27px narrower than every other button on the page and aligned
     to nothing. The gutter already does this job. */
  .gx-rewall-copy{max-width:none}
  /* The ≤560px rule left 561–767px on the desktop min-height of
     min(100svh,760px): a full-screen wall with the copy pinned to the
     floor, so a tablet-width phone opened on an empty field. */
  .gx-rewall{min-height:min(78svh,600px);padding:var(--space-11) var(--gutter-page) var(--space-10)}
}

/* ══ Touch: reveal what only hover revealed ════════════════════════════
   .gx-arrow carries the year on an archive row and "READ" on a post —
   content, not decoration — but it sat at 0.35 opacity and only lifted
   on :hover. A touch device never fires hover, so on every phone and
   tablet the year was permanently greyed out. Keyed to the pointer, not
   the viewport, so a touchscreen laptop is covered too. */
@media(hover:none){
  .gx-arrow{opacity:1;transform:none}
}

/* ══ MOBILE — phase 5: index rows and lists ════════════════════════════ */
@media(max-width:767px){
  /* An archive row stacked into five full-width blocks — a lone "01", a
     343x214 plate, title, discipline, year — measured 436px. Fourteen of
     them is 6,100px of scrolling to read a *project index*. The large
     imagery already lives on the home page's ProjectCards; this is the
     index, so it is built to be scanned: plate in column 1, everything
     else in columns 2-3. */
  .gx-row{
    grid-template-columns:var(--grid-col) minmax(0,1fr) auto;
    grid-template-areas:
      "thumb head year"
      "thumb body body"
      "thumb meta meta";
    column-gap:var(--grid-gutter);
    row-gap:var(--space-2);
    align-items:start;
    padding:var(--space-6) 0;
  }
  .gx-row-idx{grid-area:head}
  .gx-rowthumb{grid-area:thumb;align-self:start}
  .gx-row-body{grid-area:body}
  .gx-row-meta{grid-area:meta;margin-top:var(--space-2)}
  .gx-row-year{grid-area:year;justify-self:end}
  /* Uniform row heights are what make an index scannable, so the scope
     line is held to two. */
  .gx-row-body p{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
  .gx-row-body h3{font:var(--type-subheading);letter-spacing:var(--tracking-tight)}

  /* Same shape for a blog post: plate left, kicker/title/standfirst right. */
  .gx-post{
    grid-template-columns:var(--grid-col) minmax(0,1fr) auto;
    grid-template-areas:
      "thumb head read"
      "thumb body body";
    column-gap:var(--grid-gutter);
    row-gap:var(--space-3);
    align-items:start;
    padding:var(--space-6) 0;
  }
  .gx-post-idx{grid-area:head}
  .gx-post .gx-thumb{grid-area:thumb;align-self:start}
  .gx-post-body{grid-area:body}
  .gx-post-read{grid-area:read;justify-self:end}
  .gx-post-body h3{font:var(--type-subheading);letter-spacing:var(--tracking-tight)}
  .gx-post-body > p{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}

  /* Card grids are single-column here; 40px between stacked cards is a
     column gap doing a row gap's job. */
  .gx-grid3{gap:var(--space-8)}
  .gx-grid2{gap:var(--space-7)}
}

/* Both of these carried their layout in inline styles until phase 6. They
   sit ABOVE the phone breakpoint on purpose: same specificity as the rules
   below, so source order is what decides which one wins. */
.gx-statrow{display:flex;flex-wrap:wrap;gap:var(--space-9);margin-top:var(--space-11)}
.gx-svc{display:flex;flex-direction:column;gap:var(--space-6)}

/* ══ MOBILE — phase 6: services, FAQ, recognition ══════════════════════
   The plan also listed tables, bands and checks. Those classes exist in
   this stylesheet but are rendered by nothing — see the note at the end
   of this file — so there was no mobile layout to give them. */
@media(max-width:767px){
  /* Three stats on a 40px flex gap wrapped 2 + 1, leaving an orphan
     aligned to nothing. One stat per grid column instead, so the trio
     reads as a set and the labels wrap inside their own column. */
  .gx-statrow{
    display:grid;
    grid-template-columns:repeat(3,minmax(0,1fr));
    gap:var(--grid-gutter);
    margin-top:var(--space-9);
  }
  .gx-statrow .gx-stat{font-size:26px}

  /* 40px of inset on a 343px card leaves 263px of measure. */
  :root{--panel-pad:var(--space-7)}

  /* 68px per summary is more target than it needs; 60px still clears the
     44px minimum comfortably and buys back ~100px over eight questions. */
  .gx-faq summary{padding:var(--space-6) 0}
  .gx-faq p{margin-bottom:var(--space-7)}

  /* Stacked service cards: 20px between a plate and its title is right,
     20px between one card's button and the next card's plate is not —
     that gap belongs to .gx-grid2, which phase 5 already set. */
  .gx-svc{gap:var(--space-5)}
}

/* ══ Dead rules ════════════════════════════════════════════════════════
   .gx-table / .gx-tablewrap / .gx-bands / .gx-checks / .gx-check-mark /
   .gx-screens / .gx-chapters / .gx-embed are not rendered by any page in
   this project — they look like leftovers from a case study that was
   rewritten. Left in place rather than deleted on a guess; they cost
   nothing at runtime and nobody asked for them to go. */

/* Contact split and footer columns — layout moved out of inline styles so
   the phone breakpoint below can reach it. Declared above that block on
   purpose: equal specificity, so source order is what decides. */
.gx-contact{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(280px,100%),1fr));gap:var(--space-11);align-items:center}
.gx-footgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(180px,100%),1fr));gap:var(--space-9)}

/* ══ MOBILE — phase 7: contact and footer ══════════════════════════════
   The email itself needed nothing: --type-display-2 had a 44px floor that
   would have run off a 375px screen, and the phase 0 ramp already brought
   it to 27.75px across two lines inside the gutter. */
@media(max-width:767px){
  /* "Book a call" and "Instagram" were two 186px pills sized by their own
     labels — the same raggedness the hero had. Fifth and final home for
     .gx-ctarow, so every primary action on the site is now one control. */
  .gx-contact{gap:var(--space-8)}

  /* Four stacked blocks — brand, Site, Elsewhere, Status — ran to ~850px
     of footer on a 343px column, because auto-fit's 180px minimum can
     only ever resolve to one track here. Two explicit columns: the two
     link lists pair up, the blocks that carry prose span the full width. */
  .gx-footgrid{
    grid-template-columns:repeat(2,minmax(0,1fr));
    gap:var(--space-7) var(--grid-gutter);
  }
  .gx-foot-brand,.gx-foot-status{grid-column:1 / -1}
}

/* ══ MOBILE — phase 8: secondary pages ═════════════════════════════════
   work / studio / cv / project / stiled / konnect. The structural work
   was already done: phase 5 rebuilt the archive rows and blog posts,
   phase 0's ramp and gutters carry the type and margins, and DeviceDemo
   measures its own stage so the phone frame scales itself (319x691 at
   375px). No page overflows. What remained were dead bands — .gx-cvrow,
   .gx-doc and the device radii were written to 560/640px breakpoints, so
   561-767px still got the desktop layout. Those now end at 767 with
   everything else; see the rules above.

   .gx-specs is deliberately left at two columns rather than snapped to
   the 3-column grid: its values are phrases ("One app, eight views,
   seven overlays"), and a 106px track turns each into a five-line stack. */
@media(max-width:767px){
  /* Metadata and controls under the device frame: on one line they were
     a wrapped MetaRow squeezed against three 44px buttons. Stacked, each
     gets the full column and the buttons keep their own size. */
  .gx-ring-ctrls{flex-direction:column;align-items:flex-start;gap:var(--space-5)}
  .gx-specs{gap:var(--space-7);padding-top:var(--space-7)}
  .gx-plates{gap:var(--space-6)}
  .gx-pagenav{gap:var(--space-5);padding-top:var(--space-7)}
  .gx-stage{padding:var(--space-6) 0}
}

/* ══ Image treatment — de-tint ═════════════════════════════════════════
   ImagePlate applied a colour grade as an INLINE filter on every <img>:
     cool  saturate(.72) contrast(1.06) brightness(.94) hue-rotate(-8deg)
     deep  saturate(.55) contrast(1.14) brightness(.72) hue-rotate(-12deg)
   The hue-rotate is the blue cast, the brightness is the darkening. Every
   explicit call site now passes grade="none" instead, so those are gone
   without a fight.

   ProjectCard is the exception: the design system hard-codes
   `grade: hover ? "cool" : "deep"` inside the bundle and writes it inline
   from React state, so there is no prop to pass and no source to edit.
   Hence the one !important below — and note what that grade did on a
   phone, where hover never fires: every work card sat permanently at
   brightness .72. The captions keep their contrast either way, because
   that comes from the --grad-protect scrim, not from the filter. */
.gx-grid3 figure img{filter:none !important}

/* The plate's backing shows through transparent art and while the photo
   loads; it was a saturated blue gradient
   (#7FB4D6 -> #1F5C8C -> #06101F), written inline on the <figure> by
   ImagePlate — so this needs !important for the same reason the grade
   did. Neutral now, keyed to the field it sits on. */
.gx-grid3 figure,.gx-rowthumb figure,.gx-post .gx-thumb figure,.gx-svc > figure,
.gx-sec figure,.gx-plates figure,.gx-screens figure{
  background-image:none !important;
  background-color:var(--abyss-800,#0A1526) !important;
}
/* Client marks were held at saturate(.88) and only released on hover — so
   on a phone they never reached full colour at all. */
.gx-ring-logo{filter:none}
.gx-ring-card:hover .gx-ring-logo{filter:none}

/* ══ Magnetic tilt ═════════════════════════════════════════════════════
   tilt.js feeds --tilt-* on the plate; the transform is composed here so
   nothing has to be written inline. `scale` is used as its own property
   rather than folded into the transform: that way it can carry a
   transition while the transform is being rewritten every frame, and it
   composes with the anchor's React-driven translateY instead of
   overwriting it. */
.gx-grid3 figure,.gx-rowthumb figure,.gx-post .gx-thumb figure,.gx-svc > figure{
  transform:perspective(760px) rotateX(var(--tilt-rx,0deg)) rotateY(var(--tilt-ry,0deg));
  transform-style:preserve-3d;
}
.gx-grid3 figure.is-tilting,.gx-rowthumb figure.is-tilting,
.gx-post .gx-thumb figure.is-tilting,.gx-svc > figure.is-tilting{will-change:transform}
.gx-grid3 figure img,.gx-rowthumb figure img,.gx-post .gx-thumb figure img,.gx-svc > figure img{
  transform:translate3d(var(--tilt-px,0px),var(--tilt-py,0px),0);
  scale:1;transition:scale 560ms var(--ease-glide,cubic-bezier(.22,1,.36,1));
}
/* Slightly over-scaled while tilting so the parallax drift never exposes
   an edge inside the rounded frame. */
.gx-grid3 figure.is-tilting img,.gx-rowthumb figure.is-tilting img,
.gx-post .gx-thumb figure.is-tilting img,.gx-svc > figure.is-tilting img{scale:1.06}

/* Press. `scale` again, so it stacks with whatever transform the design
   system is already writing on the anchor. Fires on touch too, where it
   is the only feedback there is. */
.gx-grid3 > a:active,.gx-row:active,.gx-post:active,.gx-doc:active{
  scale:.985;transition:scale 120ms ease-out
}
.gx-grid3 > a,.gx-row,.gx-post{transition:scale 260ms var(--ease-glide,cubic-bezier(.22,1,.36,1))}

@media(prefers-reduced-motion:reduce){
  .gx-grid3 figure,.gx-rowthumb figure,.gx-post .gx-thumb figure,.gx-svc > figure{transform:none}
  .gx-grid3 figure img,.gx-rowthumb figure img,.gx-post .gx-thumb figure img,.gx-svc > figure img{
    transform:none;scale:1 !important;transition:none
  }
}

/* ══ Phase 9 — QA fixes ════════════════════════════════════════════════
   The vinyl's play/pause control is a 32px IconButton, which is the right
   optical size against the disc but eight pixels short of a usable touch
   target. Same trick as the nav toggle: keep the circle, extend the hit
   area with an invisible skirt. */
/* (.gx-vinyl-btn retired in phase 18 — the 32px icon button it padded out
   to a 44px hit area was replaced by .gx-audio-btn, which is 44px for
   real and carries a visible label.) */

/* ══ Phase 18 — audio control ══════════════════════════════════════════
   Replaces a 32px icon-only button. Sound is opt-in, so this control is
   the ONLY way it ever starts — it has to be findable and it has to say
   what it does. Pill, label, 44px minimum, sitting under the disc. */
.gx-audio-btn{
  display:inline-flex;align-items:center;gap:var(--space-4);
  min-height:44px;padding:0 var(--space-6) 0 var(--space-5);
  border-radius:var(--radius-pill);
  border:var(--border-hair) solid var(--line-strong);
  background:var(--glass-card);color:var(--text-primary);
  -webkit-backdrop-filter:var(--blur-frost);backdrop-filter:var(--blur-frost);
  font:var(--type-label);font-size:10px;letter-spacing:var(--tracking-label);
  text-transform:uppercase;cursor:pointer;
  box-shadow:var(--inset-rim);
  transition:var(--transition-hover);
  -webkit-tap-highlight-color:transparent;
}
.gx-audio-btn:hover{background:rgba(255,255,255,.14);border-color:var(--focus-ring)}
.gx-audio-btn:focus-visible{outline:none;border-color:var(--focus-ring);box-shadow:0 0 0 3px rgba(126,180,214,.28)}
.gx-audio-btn:active{scale:.985}
.gx-audio-glyph{display:inline-block;min-width:11px;font-size:9px;line-height:1;color:var(--frost-300)}
.gx-audio-btn[aria-pressed="true"] .gx-audio-glyph{color:var(--signal-live,#4FD1C5)}
.gx-audio-text{line-height:1}

/* ══ Phase 17 — footer: a heading must not look like a control ═════════
   `<Label>` and `.gx-ftlink` were byte-identical: same font token, same
   tracking, same uppercase, same --text-muted. So SITE and ELSEWHERE read
   as two more buttons that happened not to work. Headings recede and
   widen; links come forward and gain a hover underline. */
.gx-foot-head{
  font-size:9px !important;
  letter-spacing:var(--tracking-label-wide) !important;
  color:var(--text-faint) !important;
}
.gx-ftlink{color:var(--text-primary);text-decoration-color:transparent}
.gx-ftlink:hover{
  color:var(--whiteout);
  text-decoration:underline;text-underline-offset:4px;
  text-decoration-thickness:1px;text-decoration-color:currentColor;
}
.gx-ftlink:focus-visible{outline:none;text-decoration:underline;text-underline-offset:4px}

/* ══ Phase 14 — one project, one object ════════════════════════════════
   The photograph and the body panel were on the same GrainField but shared
   no visible identity and met at a hard horizontal edge — so Konnect's
   light studio shot above a dark section read as two separate things.

   The seam is dissolved with a mask rather than a colour-matched gradient:
   masking fades the image to transparent so WHATEVER field sits behind it
   shows through, which survives rime, abyss and flat without three rules
   that have to be kept in sync with the design system's gradients. */
.gx-bleed img{
  -webkit-mask-image:linear-gradient(to bottom,#000 82%,transparent 100%);
  mask-image:linear-gradient(to bottom,#000 82%,transparent 100%);
}

/* The body then opens with the same name the photograph is labelled with. */
.gx-case-tag{
  display:flex;align-items:center;gap:var(--space-5);
  margin:0 0 var(--space-8);
}
.gx-case-name{
  font-family:var(--font-display);font-weight:600;
  font-size:clamp(15px,1.3vw,18px);letter-spacing:var(--tracking-tight);
  white-space:nowrap;
}
.gx-case-rule{flex:1 1 auto;height:1px;background:currentColor;opacity:.22;min-width:16px}
.gx-case-meta{
  font:var(--type-label);letter-spacing:var(--tracking-label);
  text-transform:uppercase;white-space:nowrap;
}

/* ══ Phase 13 — outcome ════════════════════════════════════════════════
   Renders only when content.js supplies one, so the page is never shown
   half-built while the figures are pending. Deliberately heavier than the
   problem stat above it: the result is the point of the case study. */
.gx-outcome{
  border-top:1px solid var(--line-hairline);
  margin-top:var(--space-9);padding-top:var(--space-7);
}
.gx-outcome-value{
  font-family:var(--font-display);font-weight:600;
  font-size:clamp(34px,4.4vw,56px);line-height:1;
  letter-spacing:var(--tracking-display);
  margin:var(--space-5) 0 0;
}
.gx-outcome-label{
  font:var(--type-body-sm);margin:var(--space-4) 0 0;
  max-width:34ch;text-wrap:pretty;
}
@media(max-width:767px){
  /* Wraps rather than overflows: "Stahl · Industrial design · 2024" is too
     long for one 288px line at 320. The meta drops to its own line and the
     rule stretches across the first. */
  .gx-case-tag{gap:var(--space-2) var(--space-4);margin-bottom:var(--space-7);flex-wrap:wrap}
  .gx-case-meta{white-space:normal}
  .gx-outcome{margin-top:var(--space-8);padding-top:var(--space-6)}
  .gx-outcome-value{font-size:34px}
  /* A shallower fade on a phone: the crop is already tall and a deep one
     eats too much of the photograph. */
  .gx-bleed img{
    -webkit-mask-image:linear-gradient(to bottom,#000 90%,transparent 100%);
    mask-image:linear-gradient(to bottom,#000 90%,transparent 100%);
  }
}

/* ══ Phase 17 — in-page jump links (cv.html) ═══════════════════════════
   The CV sat ~4,000px below a heading that did not mention it. Both halves
   are now one tap from the top. scroll-margin keeps the fixed mobile bar
   from covering the target it just jumped to. */
.gx-jump{display:flex;flex-wrap:wrap;gap:var(--space-4);margin-bottom:var(--space-8)}
.gx-jump a{
  display:inline-flex;align-items:center;min-height:44px;
  padding:0 var(--space-6);border-radius:var(--radius-pill);
  border:var(--border-hair) solid var(--line-hairline);
  font:var(--type-label);letter-spacing:var(--tracking-label);
  text-transform:uppercase;color:var(--text-body);
  transition:var(--transition-hover);
}
.gx-jump a:hover{color:var(--text-primary);border-color:var(--line-strong);background:rgba(255,255,255,.06)}
.gx-jump a:focus-visible{outline:none;border-color:var(--focus-ring);box-shadow:0 0 0 3px rgba(126,180,214,.28)}
#cv,#writing,#contact{scroll-margin-top:var(--space-9)}
@media(max-width:767px){#cv,#writing,#contact{scroll-margin-top:calc(var(--mnav-h) + var(--mnav-top) + var(--space-5))}}

/* ══ Refillable CAN — spotlight stage (home) ═══════════════════════════
   The whole stage is one <a> to bottle.html. Your seamless blue field sits
   behind; a radial vignette closes in on the centre so everything outside
   the spotlight falls to dark, and bottle-scene.js lights the bottle itself
   with a narrow spot from directly above. */
.gx-bottle-stage{
  position:relative;display:block;isolation:isolate;overflow:hidden;
  height:min(88vh,820px);min-height:560px;
  background-color:#0a1526;background-size:cover;background-position:50% 100%;
  color:var(--whiteout);text-decoration:none;cursor:grab;
  touch-action:pan-y;-webkit-tap-highlight-color:transparent;
}
.gx-bottle-stage:hover{color:var(--whiteout)}
.gx-bottle-stage:active{cursor:grabbing}
.gx-bottle-stage:focus-visible{outline:none;box-shadow:inset 0 0 0 3px var(--focus-ring)}
.gx-bottle-stage::after{
  content:"";position:absolute;inset:0;z-index:1;pointer-events:none;
  /* Light touch: the blue field is Stang's own image and should read as
     given — this only settles the far edges. */
  background:radial-gradient(ellipse 70% 80% at 50% 60%,transparent 0%,rgba(3,6,12,.12) 60%,rgba(3,6,12,.55) 100%);
}
/* The pool the spot throws on the floor beneath the bottle. */
.gx-bottle-pool{
  position:absolute;left:50%;bottom:9%;z-index:0;width:min(520px,70%);height:90px;
  transform:translateX(-50%);filter:blur(6px);pointer-events:none;
  background:radial-gradient(ellipse at center,rgba(190,220,255,.30) 0%,rgba(120,170,230,.10) 45%,transparent 70%);
}
/* Two classes on purpose: the shared `.gx-3d{position:relative}` below is
   declared later and would win at equal specificity, collapsing the stage
   to 0px tall — the source-order trap PIXEL-SPEC warns about. */
.gx-3d.gx-bottle-3d{position:absolute;inset:0;z-index:2}

/* Shared by both scenes: poster until the first real frame, then cross-fade. */
.gx-3d{position:relative}
.gx-3d-canvas{position:absolute;inset:0;width:100%;height:100%;display:block;opacity:0;
  transition:opacity 600ms var(--ease-glide,cubic-bezier(.22,1,.36,1))}
.gx-3d.is-live .gx-3d-canvas{opacity:1}
.gx-3d-poster{
  position:absolute;left:50%;top:48%;height:74%;width:auto;transform:translate(-50%,-50%);
  object-fit:contain;pointer-events:none;
  /* The still is cropped from a slide with its own dark ground; feathering
     the edges stops it reading as a rectangle on the blue field. */
  -webkit-mask-image:radial-gradient(ellipse 48% 50% at 50% 50%,#000 62%,transparent 100%);
  mask-image:radial-gradient(ellipse 48% 50% at 50% 50%,#000 62%,transparent 100%);
  transition:opacity 600ms var(--ease-glide,cubic-bezier(.22,1,.36,1));
}
.gx-3d.is-live .gx-3d-poster{opacity:0}

.gx-bottle-copy{
  position:absolute;z-index:3;left:0;bottom:0;max-width:min(440px,34%);
  padding:var(--space-12) var(--gutter-page);
  display:flex;flex-direction:column;gap:var(--space-6);pointer-events:none;
}
.gx-bottle-copy .gx-hl-big{font-size:clamp(2rem,3.6vw,3.4rem);color:var(--whiteout);max-width:none}
.gx-bottle-hint{font:var(--type-label);letter-spacing:var(--tracking-label);text-transform:uppercase;color:var(--text-faint)}
.gx-hint-touch{display:none}
@media(hover:none){.gx-hint-pointer{display:none}.gx-hint-touch{display:inline}}

@media(max-width:767px){
  /* Stacked like every other banner on a phone: the bottle keeps the stage,
     the words sit beneath it on the same blue field — one object. */
  .gx-bottle-stage{height:auto;min-height:0;display:flex;flex-direction:column;background-position:50% 30%}
  .gx-3d.gx-bottle-3d{position:relative;inset:auto;height:min(64svh,520px)}
  .gx-bottle-pool{bottom:auto;top:calc(min(64svh,520px) - 76px);width:80%}
  .gx-bottle-stage::after{background:radial-gradient(ellipse 90% 60% at 50% 40%,transparent 0%,rgba(3,6,12,.12) 60%,rgba(3,6,12,.55) 100%)}
  .gx-bottle-copy{position:relative;max-width:none;padding:var(--space-2) var(--gutter-page) var(--space-10)}
  .gx-bottle-copy .gx-hl-big{font-size:clamp(26px,7.6vw,34px)}
}

/* ══ bottle.html ═══════════════════════════════════════════════════════ */
.gx-can-crumb{
  position:relative;z-index:2;max-width:var(--maxw-page);margin:0 auto;
  padding:var(--space-6) var(--gutter-page) var(--space-7);
  display:flex;gap:var(--space-4);align-items:center;
  font:var(--type-label);letter-spacing:var(--tracking-label);text-transform:uppercase;color:var(--text-faint);
}
.gx-can-crumb a{color:var(--text-muted);display:inline-flex;align-items:center;min-height:44px}
.gx-can-crumb a:hover{color:var(--text-primary)}

/* "Can of…" — pinned scene on white. ~4.5 screens of scroll for 5 words. */
.gx-canof{position:relative;height:calc(100svh * 4.5);background:#fff;color:#06101F}
.gx-canof-pin{position:sticky;top:0;height:100svh;overflow:hidden}
/* Full-scene canvas: bottle-scene.js sizes the bottle to 22% of the width
   (34% on a phone) and puts the cap on the vertical centre line. The words
   hang either side of it on that same line — "Can of…" left, the word
   column right — as on slide 60. */
.gx-canof-bottle{position:absolute;inset:0}
.gx-canof-bottle .gx-3d{position:absolute;inset:0}
.gx-canof-lead,.gx-canof-slot{
  position:absolute;top:50%;z-index:2;margin:0;
  font-family:var(--font-display);font-weight:600;
  font-size:clamp(40px,6.2vw,104px);line-height:1.04;letter-spacing:var(--tracking-display);
  --canof-gap:calc(11vw + 2.5vw);   /* half the bottle's width + breathing room */
}
.gx-canof-lead{right:calc(50% + var(--canof-gap));transform:translateY(-50%);white-space:nowrap}
.gx-canof-slot{
  left:calc(50% + var(--canof-gap));height:calc(1.04em * 5);transform:translateY(-50%);
  overflow:hidden;
  -webkit-mask-image:linear-gradient(to bottom,transparent 0%,#000 22%,#000 78%,transparent 100%);
  mask-image:linear-gradient(to bottom,transparent 0%,#000 22%,#000 78%,transparent 100%);
}
.gx-canof-slot ul{list-style:none;margin:0;padding:0;will-change:transform}
.gx-canof-slot li{height:1.04em;white-space:nowrap;will-change:filter,opacity}
/* Bottom-left, not bottom-centre: centred, it sat on the bottle's dark body
   as dark text on dark and could not be read. */
.gx-canof-cue{
  position:absolute;left:var(--gutter-page);bottom:var(--space-8);margin:0;
  font:var(--type-label);letter-spacing:var(--tracking-label-wide);text-transform:uppercase;color:rgba(6,16,31,.5);
}
.gx-canof.is-static{height:auto}
.gx-canof.is-static .gx-canof-pin{position:relative;height:min(100svh,900px)}

/* The glass bar is tuned for dark fields; over the white scene its white
   wordmark vanished. Darker glass only on pages that have the scene. */
body:has(.gx-canof) .gx-mnav{background:rgba(6,16,31,.78)}

@media(max-width:767px){
  /* Same row as desktop, scaled: the bottle takes a third of the width
     here, so each side gets ~30vw for its words. */
  .gx-canof-cue{display:none}
  .gx-canof-lead,.gx-canof-slot{font-size:clamp(19px,5.8vw,40px);--canof-gap:calc(17vw + 3vw)}
}

/* The deck — one continuous scroll. */
.gx-deck-head{display:flex;flex-wrap:wrap;align-items:flex-end;justify-content:space-between;gap:var(--space-6);margin-bottom:var(--space-10)}
.gx-deck-wrap{position:relative;max-width:1200px;margin:0 auto}
.gx-deck-count{
  position:sticky;top:var(--space-6);z-index:3;float:right;margin:0 0 calc(-1 * var(--space-9)) 0;
  padding:6px 12px;border-radius:var(--radius-pill);
  background:rgba(3,6,12,.72);border:var(--border-hair) solid var(--line-hairline);
  -webkit-backdrop-filter:var(--blur-frost);backdrop-filter:var(--blur-frost);
  font:var(--type-meta);color:var(--text-muted);font-variant-numeric:tabular-nums;pointer-events:none;
}
.gx-deck-count span{color:var(--text-primary)}
.gx-deck{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:var(--space-8);clear:both}
.gx-deck li{border-radius:var(--radius-md);overflow:hidden;border:var(--border-hair) solid var(--line-hairline);background:#fff;box-shadow:var(--shadow-md)}
.gx-deck img{display:block;width:100%;height:auto;aspect-ratio:16/9}
/* Extra full-size renders: portrait, uncropped, two to a row. */
.gx-deck{display:grid;grid-template-columns:1fr 1fr}
.gx-deck li{grid-column:1 / -1}
.gx-deck li.gx-deck-extra{grid-column:auto}
.gx-deck li.gx-deck-extra img{aspect-ratio:auto;height:100%;object-fit:cover}
@media(max-width:767px){
  .gx-deck{gap:var(--space-5)}
  .gx-deck li{border-radius:var(--radius-sm)}
  .gx-deck-count{top:calc(var(--mnav-h) + var(--mnav-top) + var(--space-3))}
}

/* ══ Problem | Outcome — one row ═══════════════════════════════════════
   Two equal columns under a single rule. Both figures share one size so
   neither reads as the headline over the other; the pair is the point. */
.gx-proof{
  display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--space-7);
  border-top:1px solid var(--line-hairline);border-bottom:1px solid var(--line-hairline);
  padding:var(--space-7) 0;
}
.gx-proof-value{
  font-family:var(--font-display);font-weight:600;
  font-size:clamp(26px,3.2vw,44px);line-height:1;letter-spacing:var(--tracking-display);
  margin:var(--space-4) 0 0;white-space:nowrap;
}
.gx-proof-label{font:var(--type-body-sm);margin:var(--space-3) 0 0;text-wrap:pretty}
@media(max-width:767px){
  .gx-proof{gap:var(--grid-gutter);padding:var(--space-6) 0}
  /* Sized to fit "258 / 1,874" in half of a 288px column at 320. */
  .gx-proof-value{font-size:clamp(20px,6.4vw,30px)}
}

/* Liquid chrome CTA (shared.jsx ChromeCTA): the contact section's
   object. A chrome icon that melts mail → message → phone; a press acts on
   what it shows. The label under it follows the icon. */
.gx-chrome-cta{position:relative;display:flex;flex-direction:column;align-items:center;gap:var(--space-6);
  width:100%;max-width:420px;justify-self:center;text-decoration:none;color:var(--text-primary);border-radius:var(--radius-lg)}
.gx-chrome-cta-art{display:block;width:100%;aspect-ratio:1;max-width:360px}
.gx-chrome-cta-label{position:relative;display:grid;place-items:center;height:1.2em;
  font:500 12px/1.2 var(--font-mono);letter-spacing:.16em;text-transform:uppercase;color:var(--text-muted)}
.gx-chrome-cta-label > span{grid-area:1/1;opacity:0;transform:translateY(6px);transition:opacity .45s,transform .45s}
.gx-chrome-cta-label > span.is-on{opacity:1;transform:none}
.gx-chrome-cta:hover .gx-chrome-cta-label{color:var(--text-primary)}
.gx-chrome-cta:focus-visible{outline:2px solid var(--focus-ring);outline-offset:6px}
@media(max-width:767px){
  .gx-contact{grid-template-columns:minmax(0,1fr)}
  .gx-chrome-cta{max-width:300px}
}

/* Liquid CTA (shared.jsx LiquidCTA): the liquid icon is itself the link —
   a clean press opens what it shows (email / Instagram / book a call), a
   drag only splashes. The stage is bigger than the icon so splashes have
   room. pan-y keeps the page scrollable on phones. */
.gx-liquid-cta{display:flex;justify-content:center;width:100%;justify-self:center;text-decoration:none;color:inherit;border-radius:var(--radius-lg);cursor:pointer}
.gx-liquid-cta-stage{position:relative;width:min(100%,480px);aspect-ratio:1;touch-action:pan-y;
  margin:calc(-1 * var(--space-9)) 0}
.gx-liquid-cta-stage .liquid-icon-canvas{position:absolute;inset:0;width:100%;height:100%;display:block}
.gx-liquid-cta:focus-visible{outline:2px solid var(--focus-ring);outline-offset:-40px}
@media(max-width:767px){.gx-liquid-cta-stage{width:min(100%,360px);margin:calc(-1 * var(--space-7)) 0}}

/* Detailed benefits belong to the Konnect case study. */
.gx-benefits{margin:var(--space-7) 0 0;padding-left:1.2em;max-width:var(--measure-prose);color:var(--text-body);font:var(--type-body-sm)}
.gx-benefits li{padding-left:var(--space-2);text-wrap:pretty}
.gx-benefits li+li{margin-top:var(--space-5)}
.gx-benefits li::marker{color:var(--frost-300)}
.gx-benefits strong{color:var(--text-primary);font-weight:500}
.gx-rowthumb--contain img{object-fit:contain!important;background:var(--pure)}

/* Konnect homepage teaser — one image, one pitch, one route to the case study. */
.gx-konnect-feature{display:grid;grid-template-columns:minmax(0,1.1fr) minmax(0,1fr);gap:var(--space-11);align-items:center;padding-top:var(--space-11);padding-bottom:var(--space-11)}
.gx-konnect-feature-image{margin:0;overflow:hidden;border-radius:var(--radius-md);aspect-ratio:6/5;background:var(--frost-100)}
.gx-konnect-feature-image img{width:100%;height:100%;object-fit:cover;object-position:52% center;display:block}
.gx-konnect-feature-copy{min-width:0}
.gx-konnect-feature-copy h2{font:var(--type-display-3);letter-spacing:var(--tracking-display);color:var(--text-primary);max-width:17ch;margin:var(--space-6) 0;text-wrap:balance;overflow-wrap:anywhere}
.gx-konnect-feature-copy p{font:var(--type-body-lg);color:var(--text-body);max-width:34ch;margin:0 0 var(--space-8);text-wrap:pretty}
@media(max-width:767px){
  .gx-konnect-feature{grid-template-columns:minmax(0,1fr);gap:var(--space-7);padding-top:var(--space-9);padding-bottom:var(--space-9)}
  .gx-konnect-feature-image{aspect-ratio:4/3}
  .gx-konnect-feature-copy h2{margin-top:var(--space-5)}
}
