/* ============================================================================
   Qollab - letsqollab.com

   Design read: agency and studio landing plus portfolio, for prospective
   clients and recruiters. Restrained editorial-technical language.
   Dials: DESIGN_VARIANCE 7, MOTION_INTENSITY 5, VISUAL_DENSITY 3.

   NO GRADIENTS. Every colour is a flat value. check_static.py fails the build
   if a gradient function appears anywhere in this file.

   Palette is the real brand palette from qollab_logo_animation/src/brand.tsx:
       INK #0F172A  BLUE #2563EB  CORAL #F43F5E  GREY #475569  OFFWHITE #F8FAFC

   Accent discipline: BLUE is the single interactive accent, used identically in
   every section. CORAL is the brand signature, reserved for the wordmark and
   exactly one highlighted word per page. Buttons are ink-on-paper, which keeps
   maximum contrast in both themes and reads as studio rather than startup.
   ========================================================================= */

@import url('fonts.css?v=1c3d5502e9');

/* =============================== tokens ================================== */
:root {
  color-scheme: light;

  --void:      #FFFFFF;
  --surface:   #F2F5FC;   /* the tint band, a pale tint of the brand blue */
  --surface-2: #E6ECF8;
  /* The colour block IS the brand ink: the same value as the letterforms in
     the wordmark, so the darkest thing on the page belongs to the logo. */
  --band-bg:   #0F172A;
  --band-text: #F8FAFC;
  --band-muted:#A9B6D1;
  --band-line: rgba(255, 255, 255, .16);
  --line:      #DEE5F2;
  --line-2:    #C3CDE2;
  --text:      #0F172A;   /* the brand ink. 17.85:1 on --void */
  --muted:     #4A5675;   /*  7.29:1 on --void, 6.68 on the tint */
  --blue:      #2563EB;   /*  5.17:1 on --void */
  --coral:     #C81E3F;   /*  5.66:1 on --void */
  --nav-bg:    rgba(255, 255, 255, .88);
  --shadow:    0 1px 2px rgba(15, 23, 42, .04), 0 12px 32px -12px rgba(15, 23, 42, .16);
  /* the lifted variant, for a card that should read as detached from the page */
  --shadow-float: 0 2px 4px rgba(15, 23, 42, .05), 0 14px 28px -10px rgba(15, 23, 42, .14), 0 40px 72px -28px rgba(15, 23, 42, .26);

  /* ---- spacing scale. One scale. Nothing invents its own value. ---- */
  --s-1: .25rem;  --s-2: .5rem;   --s-3: .75rem;  --s-4: 1rem;
  --s-5: 1.5rem;  --s-6: 2rem;    --s-7: 3rem;    --s-8: 4rem;
  --s-9: 6rem;    --s-10: 8rem;

  /* One inset for the whole site, matching the carousel exactly. The
     max-width has to go with it: capping content at 1200px would centre it
     and give a 360px margin on a wide display, which is not the 10px the
     gallery runs at. Prose keeps its own cap below, so line length is still
     protected even though sections now run edge to edge. */
  /* Container and rhythm measured from the reference: a 1680px cap with a
     16px gutter, and up to 160px of air above and below each section. Below
     1680 this is effectively full bleed; above it the content centres rather
     than stretching to a 1900px measure. The carousel takes the same cap, so
     every section and the gallery still share one inset. */
  --gutter:    16px;
  /* The ONE vertical rhythm, and it is the gutter. The frame is the same
     distance from the content on all four edges. Separation between
     sections is carried by the colour bands and by a hairline where two
     unbanded sections meet, not by stacked whitespace. */
  --section-y: var(--gutter);
  --maxw:      1680px;
  --prose:     68ch;

  --nav-h:  72px;
  --radius: 10px;         /* one locked scale: 10px surfaces, 16px list, 24px sheet, 999px pills */
  --radius-ios:   16px;   /* the list group */
  --radius-sheet: 24px;   /* the sheet beside it */
  --ease:   cubic-bezier(.22, .61, .36, 1);
}

:root[data-theme='dark'] {
  color-scheme: dark;

  --void:      #0F172A;   /* brand ink, the logo's own backdrop */
  --surface:   #161F38;
  --surface-2: #1B2544;
  /* In dark mode the block LIFTS rather than sinking. Darker than an already
     dark canvas is indistinguishable: measured, it sat 8 points of luminance
     from the page where the light theme has 215. A raised panel reads. */
  --band-bg:   #1D2744;
  --band-text: #F8FAFC;
  --band-muted:#A9B6D1;
  --band-line: rgba(255, 255, 255, .12);
  --line:      #273253;
  --line-2:    #3B4A70;
  --text:      #F8FAFC;   /* 17.06:1 on --void */
  --muted:     #A3B2CC;   /*  8.33:1 on --void */
  --blue:      #60A5FA;   /*  7.02:1 on --void */
  --coral:     #FB7185;   /*  6.63:1 on --void */
  --nav-bg:    rgba(15, 23, 42, .88);
  --shadow:    0 1px 2px rgba(0, 0, 0, .3), 0 12px 32px -12px rgba(0, 0, 0, .6);
  --shadow-float: 0 2px 4px rgba(0, 0, 0, .45), 0 14px 28px -10px rgba(0, 0, 0, .55), 0 40px 72px -28px rgba(0, 0, 0, .7);
}

/* =============================== base ==================================== */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; padding: 0; }

html {
  scroll-behavior: smooth;
  -webkit-text-size-adjust: 100%;
}

body {
  font-family: 'Outfit', system-ui, -apple-system, 'Segoe UI', sans-serif;
  font-size: clamp(1rem, .97rem + .14vw, 1.0625rem);
  line-height: 1.6;
  color: var(--text);
  background: var(--void);
  -webkit-font-smoothing: antialiased;
  overflow-x: clip;        /* clip, not hidden: hidden promotes body to the
                              scroll container and breaks scroll-margin + sticky */
}

img, svg, video { max-width: 100%; height: auto; display: block; }
a { color: inherit; text-decoration: none; }
ul, ol { list-style: none; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }

::selection { background: var(--text); color: var(--void); }

:focus-visible { outline: 2px solid var(--blue); outline-offset: 3px; border-radius: 4px; }

[id] { scroll-margin-top: calc(var(--nav-h) + var(--s-5)); }

.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}

.skip {
  position: absolute; left: -9999px; top: 0; z-index: 200;
  padding: var(--s-3) var(--s-5);
  background: var(--text); color: var(--void); font-weight: 600;
}
.skip:focus { left: var(--s-4); top: var(--s-4); }

/* The gutter lives here and ONLY here. Sections set padding-block only, never
   the padding shorthand, which is how the hero once ended up flush to the edge. */
.wrap {
  width: 100%;
  max-width: var(--maxw);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.section { padding-block: var(--section-y); }
/* Only where one unbanded section follows another: everywhere else the
   band edge already does this job. */
.section:not(.section--tint):not(.section--dark)
  + .section:not(.section--tint):not(.section--dark) {
  border-top: 1px solid var(--line);
}

/* Grid and flex children default to min-width:auto, which means they refuse to
   shrink below their content's min-content width. A nowrap chip rail or a card
   track then pushes the whole page wider than the viewport even though it has
   overflow-x:auto. Every layout child and every scroll track gets min-width:0. */
.hero__grid > *, .feature > *, .work-grid > *, .profile > *,
.band__grid > *, .service > *, .steps > *, .stats > * { min-width: 0; }
.rail, .gallery, .railwrap { min-width: 0; max-width: 100%; }
.section--tint { background: var(--surface); }

/* The colour-block section. Tokens are redefined on the section itself, so
   every child inherits the right value and a contrast audit still resolves a
   real painted background rather than guessing at one.

   The house rule is normally that sections do not invert mid-page. This is the
   documented exception: a deliberate colour block used as the rhythm of the
   whole page, which is exactly what the reference does. */
.section--dark {
  --void: var(--band-bg);
  --surface: #1C2743;
  --surface-2: #243052;
  --text: var(--band-text);
  --muted: var(--band-muted);
  --line: var(--band-line);
  --line-2: rgba(255, 255, 255, .28);
  background: var(--band-bg);
  color: var(--band-text);
}
:root[data-theme='dark'] .section--dark { --surface: #27335A; --surface-2: #2E3A64; }

/* .band paints --surface, and .section--dark redefines --surface, so without
   this the two colour blocks on the page came out as two different colours. */
.band.section--dark { background: var(--band-bg); border-top-color: var(--band-line); }

.icon {
  width: 1.15em; height: 1.15em; flex: none;
  /* Tabler outline icons carry no paint of their own. Without these
     the paths fill solid black and stroke-width applies to nothing. */
  fill: none;
  stroke: currentColor;
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* ============================ typography ================================= */
h1, h2, h3, h4 { font-weight: 700; line-height: 1.1; letter-spacing: -.022em; }

h1 { font-size: clamp(1.95rem, 1.2rem + 2.9vw, 3.4rem); font-weight: 800; }
h2 { font-size: clamp(1.75rem, 1.2rem + 2.3vw, 2.75rem); }
h3 { font-size: clamp(1.125rem, 1rem + .5vw, 1.375rem); }

.lead  { font-size: clamp(1.05rem, 1rem + .35vw, 1.25rem); color: var(--muted); max-width: var(--prose); }
.muted { color: var(--muted); }
.script { font-family: 'Caveat', cursive; font-weight: 700; color: var(--coral); }
.hl     { color: var(--coral); }     /* flat, never a clipped gradient */

/* Eyebrows are rationed: max ceil(sections / 3) on a page, never two in a row. */
.eyebrow {
  font-size: .75rem; font-weight: 600;
  letter-spacing: .16em; text-transform: uppercase;
  color: var(--muted);
  margin-bottom: var(--s-4);
}

.section__head { margin-bottom: clamp(2rem, 5vw, var(--s-8)); max-width: var(--prose); }
.section__head h2 + p { margin-top: var(--s-4); }

.rule { height: 1px; border: 0; background: var(--line); }

/* ============================= buttons =================================== */
.btn {
  display: inline-flex; align-items: center; gap: var(--s-2);
  padding: .8rem 1.4rem;
  border-radius: 999px;
  font-weight: 600; font-size: .95rem; white-space: nowrap;
  border: 1px solid transparent;
  transition: transform .18s var(--ease), background-color .18s var(--ease),
              border-color .18s var(--ease), color .18s var(--ease);
}
/* Ink on paper. Inverts with the theme, always maximum contrast. */
.btn--primary { background: var(--text); color: var(--void); }
.btn--ghost   { border-color: var(--line-2); color: var(--text); }

@media (hover: hover) and (pointer: fine) {
  .btn:hover { transform: translateY(-2px); }
  .btn--ghost:hover { border-color: var(--text); }
}
.btn:active { transform: translateY(0); }

.link-arrow {
  display: inline-flex; align-items: center; gap: var(--s-2);
  font-weight: 600; color: var(--blue);
}
@media (hover: hover) and (pointer: fine) {
  .link-arrow:hover { text-decoration: underline; text-underline-offset: 4px; }
}

/* =============================== nav ===================================== */
.nav {
  position: sticky; top: 0; z-index: 60;
  height: var(--nav-h);
  display: flex; align-items: center;
  background: var(--nav-bg);
  border-bottom: 1px solid var(--line);
}
@supports ((backdrop-filter: blur(10px)) or (-webkit-backdrop-filter: blur(10px))) {
  .nav { -webkit-backdrop-filter: blur(10px) saturate(1.4); backdrop-filter: blur(10px) saturate(1.4); }
}
.nav__inner {
  width: 100%; max-width: var(--maxw); margin-inline: auto;
  padding-inline: var(--gutter);
  display: flex; align-items: center; gap: var(--s-5);
}
.nav__brand { flex: none; }
.nav__brand img { height: 34px; width: auto; }

/* Two lockups ship in the markup because the letterforms are pixels, not text:
   the light lockup is brand ink and vanishes on the dark canvas, which is also
   brand ink. Exactly one is displayed, so only one reaches the a11y tree. */
.logo__dark { display: none; }
:root[data-theme='dark'] .logo__light { display: none; }
:root[data-theme='dark'] .logo__dark  { display: block; }

.nav__links { display: flex; align-items: center; gap: clamp(.9rem, 1.8vw, 1.6rem); margin-left: auto; }
.nav__links a {
  font-size: .925rem; font-weight: 500; color: var(--muted);
  padding-block: var(--s-2);
  border-bottom: 1px solid transparent;
  transition: color .18s var(--ease), border-color .18s var(--ease);
}
@media (hover: hover) and (pointer: fine) {
  .nav__links a:not(.btn):hover { color: var(--text); border-bottom-color: var(--text); }
}
.nav__links a.is-active:not(.btn) { color: var(--text); border-bottom-color: var(--text); }
.nav__links a.btn--primary { color: var(--void); }

.nav__tools { display: flex; align-items: center; gap: var(--s-2); margin-left: var(--s-2); }

.iconbtn {
  display: inline-grid; place-items: center;
  width: 40px; height: 40px;
  border: 1px solid var(--line-2); border-radius: var(--radius);
  color: var(--text);
  transition: border-color .18s var(--ease), color .18s var(--ease);
}
@media (hover: hover) and (pointer: fine) {
  .iconbtn:hover { border-color: var(--text); }
}
.iconbtn .i-sun { display: none; }
:root[data-theme='dark'] .iconbtn .i-sun  { display: block; }
:root[data-theme='dark'] .iconbtn .i-moon { display: none; }

.nav__toggle { display: none; }

.menu {
  position: fixed; inset: var(--nav-h) 0 0 0; z-index: 55;
  display: none; flex-direction: column;
  padding: var(--s-5) var(--gutter) var(--s-8);
  background: var(--void);
  border-top: 1px solid var(--line);
  overflow-y: auto;
}
.menu.is-open { display: flex; }
.no-scroll { overflow: hidden; }
.menu a {
  padding: var(--s-4) 0;
  font-size: 1.25rem; font-weight: 600;
  border-bottom: 1px solid var(--line);
}
.menu .btn { margin-top: var(--s-6); align-self: flex-start; }

@media (max-width: 900px) {
  .nav__links { display: none; }
  .nav__tools { margin-left: auto; }
  .nav__toggle { display: inline-grid; }
}

/* ===================== 1. HERO - asymmetric split ======================== */
.hero { padding-block: var(--section-y); }
.hero__grid {
  display: grid;
  gap: clamp(2rem, 5vw, var(--s-8));
  align-items: center;
}
.hero__logo { width: clamp(200px, 24vw, 330px); margin-bottom: var(--s-4); }
.hero h1 { max-width: 17ch; margin-bottom: var(--s-4); }
.hero .lead { max-width: 46ch; }
.hero__cta { display: flex; flex-wrap: wrap; gap: var(--s-3); margin-top: var(--s-6); }

.hero__shot {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--surface);
  box-shadow: var(--shadow);
}
.hero__shot img { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; object-position: top center; }

@media (min-width: 1000px) {
  .hero__grid { grid-template-columns: 1.05fr .95fr; }   /* asymmetric, not 50/50 */
}

/* ================= 2. WORK - featured row + 2-up grid ==================== */
.feature {
  display: grid; gap: clamp(1.5rem, 4vw, var(--s-7));
  align-items: center;
  padding-bottom: var(--s-8);
  margin-bottom: var(--s-8);
  border-bottom: 1px solid var(--line);
}
@media (min-width: 900px) { .feature { grid-template-columns: .9fr 1.1fr; } }
.feature__shot {
  border: 1px solid var(--line); border-radius: var(--radius);
  overflow: hidden; background: var(--surface-2);
}
.feature__shot img { width: 100%; aspect-ratio: 16 / 10; object-fit: cover; object-position: top center; }
.feature h3 { font-size: clamp(1.35rem, 1.1rem + 1vw, 1.875rem); margin-bottom: var(--s-3); }
.feature p  { color: var(--muted); max-width: 54ch; }
/* No block-level cap. A max-width here would leave dead space on the right of
   a wide display, which is the opposite of sharing the gallery's inset. Line
   length is already protected where it matters: --prose on .lead and the
   section heads, 62ch on service copy, 72ch on a post. */
.feature .quote {
  font-family: 'Caveat', cursive; font-size: 1.45rem; line-height: 1.3;
  color: var(--text); margin-bottom: var(--s-4);
}

.work-grid { display: grid; gap: clamp(1.5rem, 3.5vw, var(--s-7)); }
@media (min-width: 720px)  { .work-grid { grid-template-columns: repeat(2, 1fr); } }
/* Sections run the full width now, so a two-up grid would hand each card most
   of a 1900px display. More columns as the viewport earns them. */
@media (min-width: 1500px) { .work-grid { grid-template-columns: repeat(3, 1fr); } }

.work { display: flex; flex-direction: column; gap: var(--s-4); }
.work__shot {
  border: 1px solid var(--line); border-radius: var(--radius);
  overflow: hidden; background: var(--surface-2);
  transition: border-color .22s var(--ease);
}
.work__shot img {
  width: 100%; aspect-ratio: 16 / 10; object-fit: cover; object-position: top center;
  transition: transform .5s var(--ease);
}
@media (hover: hover) and (pointer: fine) {
  .work:hover .work__shot { border-color: var(--line-2); }
  .work:hover .work__shot img { transform: scale(1.03); }
}
.work p { color: var(--muted); font-size: .95rem; }
.work .quote { font-family: 'Caveat', cursive; font-size: 1.2rem; color: var(--text); }

/* ================= 3. STUDIO - one wide profile ========================== */
.profile { display: grid; gap: clamp(1.75rem, 5vw, var(--s-8)); align-items: start; }
@media (min-width: 860px) { .profile { grid-template-columns: 300px 1fr; } }
.profile__shot {
  border: 1px solid var(--line); border-radius: var(--radius);
  overflow: hidden; max-width: 300px;
}
.profile__shot img { width: 100%; aspect-ratio: 1; object-fit: cover; }
.profile__role { color: var(--blue); font-weight: 600; margin-top: var(--s-2); }
.profile__body > p + p { margin-top: var(--s-4); }
.profile__body p { color: var(--muted); max-width: var(--prose); }

.stats {
  display: grid; gap: var(--s-5) var(--s-6);
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  margin-top: var(--s-8); padding-top: var(--s-7);
  border-top: 1px solid var(--line);
}
.stat__n {
  font-size: clamp(1.5rem, 1.2rem + 1.4vw, 2.125rem);
  font-weight: 800; letter-spacing: -.025em;
  font-variant-numeric: tabular-nums;
}
.stat__l { font-size: .825rem; color: var(--muted); margin-top: var(--s-1); }

/* ============== 4. STACK - a slow, continuous marquee ===================
   The one marquee on the page. It is the justified use: a breadth of tools
   where no single item needs attention, so motion carries the list instead of
   a wrapped cloud of pills.

   The track holds the same row twice and travels exactly -50%, which is one
   row, so the loop is seamless. The second row is aria-hidden: a screen reader
   must not read the stack out twice.

   It pauses on hover and on focus. Content that moves for longer than five
   seconds needs a way to stop it (WCAG 2.2.2), and under reduced motion it
   stops entirely and becomes a normal scrollable strip so every chip is still
   reachable.
   ===================================================================== */
.marquee { overflow: hidden; }
.marquee__track {
  display: flex;
  width: max-content;
  animation: marquee 72s linear infinite;
}
.marquee__row {
  display: flex;
  gap: var(--s-3);
  padding-right: var(--s-3);
  list-style: none;
  margin: 0;
}
@keyframes marquee {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}
.marquee:hover .marquee__track,
.marquee:focus-within .marquee__track { animation-play-state: paused; }

@media (prefers-reduced-motion: reduce) {
  .marquee__track { animation: none; }
  .marquee { overflow-x: auto; }
}

/* ================= 4b. STACK - manual scroll-snap strip ================= */
.railwrap { position: relative; }
.rail {
  display: flex; gap: var(--s-3);
  overflow-x: auto; overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
  scrollbar-width: none;
  padding-bottom: var(--s-2);
}
.rail::-webkit-scrollbar { display: none; }
.rail > * { scroll-snap-align: start; flex: none; }

.chip {
  border: 1px solid var(--line-2); border-radius: 999px;
  padding: .45rem .95rem; font-size: .875rem; color: var(--muted);
  white-space: nowrap;
}
.chip--tag { font-size: .75rem; letter-spacing: .04em; }

/* ============== 5. SERVICES - 2/7/2 cards, scroll driven ================= */
/* Layout, stated as twelve columns because that is how it was specified: the
   two service columns are two columns wide each, the panel is seven, and the
   single column left over is split evenly between the two gaps so the panel
   sits dead centre. Written in 24ths, which is the only way to say half a
   column exactly:  4 | 1 | 14 | 1 | 4  ==  2 | .5 | 7 | .5 | 2 . */
.svgrid {
  display: grid;
  grid-template-columns: 4fr 1fr 14fr 1fr 4fr;
  align-items: start;
}
.svgrid > * { min-width: 0; }      /* or a long title refuses to shrink */
.svlist--left  { grid-column: 1; }
.svpanel       { grid-column: 3; }
.svlist--right { grid-column: 5; }

/* ---- the scroll track, same mechanism as the carousel ------------------- */
.svtrack { display: grid; grid-template-columns: 1fr; }
.svtrack > * { grid-area: 1 / 1; min-width: 0; }
/* A lead-in, so sentinel one reaches the viewport centre only after the
   sticky layer has actually pinned. Shorter steps than the carousel: there
   are ten of them, and this is the second pinned section on the page. */
/* Lead-in and run-out, both load-bearing.
   A sticky layer stops sticking once the track has scrolled past it, which is
   after (track height - layer height) of travel. The layer here is a full
   viewport tall, so without a run-out the last two sentinels are reached only
   after the layer has already let go, and the final services scroll past
   unpinned. The run-out has to satisfy
       trail >= layer height + nav - half a step - half a viewport
   which for this layer, around 70vh tall, is 70 + 8 - 13 - 50 = 15vh. 24vh
   leaves slack without leaving a screen of dead scroll after the last one. */
.svtrack__steps { display: grid; padding-top: 30vh; padding-bottom: 24vh; }
.svstep { height: 26vh; }
/* Natural height, pinned below the nav. It was briefly a viewport-tall box
   that centred its own contents, which balanced the pinned view but put half
   an empty viewport above the heading for as long as the layer was still in
   normal flow. The top of a section is seen more often than the bottom. */
.svtrack__sticky {
  position: sticky;
  top: calc(var(--nav-h) + var(--s-4));
  align-self: start;
  /* no vertical padding of its own: the section frame already sets it */
}

.services__head { margin-bottom: var(--s-5); max-width: 76ch; }
.services__head .lead { font-size: clamp(1rem, .95rem + .3vw, 1.125rem); }

/* ---- the service rows: an iOS inset grouped list ------------------------ */
/* The group owns the radius and clips; the rows inside are square and parted
   by hairlines. That is what makes it read as a phone list instead of a
   stack of loose cards. */
.svlist {
  list-style: none;
  margin: 0;
  padding: 0;
  background: var(--void);
  border: 1px solid var(--line);
  border-radius: var(--radius-ios);
  box-shadow: var(--shadow);
  overflow: hidden;
}
.svlist__item + .svlist__item .svrow { border-top: 1px solid var(--line); }

.svrow {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: var(--s-2);
  width: 100%;
  padding: var(--s-4) var(--s-3) var(--s-4) var(--s-4);
  background: transparent;
  border: 0;
  text-align: left;
  font: inherit;
  color: var(--text);
  cursor: pointer;
  transition: background-color .25s var(--ease), color .25s var(--ease);
}
.svrow__num {
  font-size: .6875rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  letter-spacing: .08em;
  color: var(--muted);
  transition: color .25s var(--ease);
}
.svrow__title { font-weight: 600; font-size: .9375rem; line-height: 1.3; letter-spacing: -.01em; }
.svrow__go { color: var(--line-2); transition: color .25s var(--ease), transform .25s var(--ease); }

.svrow:hover { background: var(--surface-2); }
/* The selected row inverts. --text and --void swap per theme on their own,
   so one rule stays 17:1 in light and in dark. */
.svrow.is-active { background: var(--text); color: var(--void); }
.svrow.is-active .svrow__num { color: var(--void); opacity: .72; }
.svrow.is-active .svrow__go { color: var(--void); transform: translateX(3px); }
.svrow:focus-visible { outline: 2px solid var(--blue); outline-offset: -3px; }

/* ---- the description panel: an iOS sheet -------------------------------- */
.svpanel {
  background: var(--void);
  border: 1px solid var(--line);
  border-radius: var(--radius-sheet);
  box-shadow: var(--shadow);
  padding: var(--s-3) var(--s-6) var(--s-6);
  display: grid;
  justify-items: center;
  align-content: start;
  /* The panel carries its OWN view-transition-name, because the root
     transition is switched off further down this file: with no name of its
     own it would swap instantly while the carousel kept animating. */
  view-transition-name: sv-panel;
}
.svpanel__grab {
  width: 36px; height: 5px;
  border-radius: 999px;
  background: var(--line-2);
  margin-bottom: var(--s-6);
}
/* A floor under the box, so stepping from a short service to a long one
   morphs the text rather than visibly resizing the sheet. */
.svpanel__body { width: 100%; max-width: 48ch; min-height: 15rem; }

.svpane { display: none; }
.svpane.is-on { display: block; }
.svpane__kick {
  font-size: .6875rem; font-weight: 600;
  letter-spacing: .14em; text-transform: uppercase;
  color: var(--muted);
  margin-bottom: var(--s-3);
}
.svpane__title {
  font-size: clamp(1.375rem, 1.05rem + 1.2vw, 2.125rem);
  line-height: 1.1; letter-spacing: -.022em;
  margin-bottom: var(--s-4);
}
.svpane__des { color: var(--muted); font-size: 1.0625rem; line-height: 1.62; }
.svpane__note {
  margin-top: var(--s-5);
  padding-top: var(--s-4);
  border-top: 1px solid var(--line);
  font-size: .875rem; font-weight: 500;
  color: var(--text);
}

/* ---- collapsed: no pinning, one column, the sheet pinned to the top ----- */
/* Three columns need real width. Below that the sheet moves to the first row
   and sticks, the two lists stack beneath it, and a tap drives the sheet. */
@media (max-width: 1040px), (max-height: 820px) {
  .svtrack__steps { padding-top: 0; padding-bottom: 0; }
  .svstep { height: 0; }
  .svtrack__sticky {
    position: static; padding-block: 0;
    min-height: 0; display: block;
  }
  .svgrid { grid-template-columns: 1fr; gap: var(--s-4); }
  .svpanel {
    grid-column: 1; grid-row: 1;
    position: sticky; top: calc(var(--nav-h) + var(--s-2));
    z-index: 1;
    padding: var(--s-3) var(--s-5) var(--s-5);
  }
  .svpanel__body { min-height: 0; max-width: none; }
  .svpanel__grab { margin-bottom: var(--s-4); }
  .svlist--left  { grid-column: 1; grid-row: 2; }
  .svlist--right { grid-column: 1; grid-row: 3; }
}

/* ---- no script: every description is simply present -------------------- */
html:not(.js) .svtrack__sticky { position: static; min-height: 0; display: block; }
html:not(.js) .svtrack__steps { padding-top: 0; padding-bottom: 0; }
html:not(.js) .svstep { height: 0; }
html:not(.js) .svpanel { position: static; }
html:not(.js) .svpanel__body { min-height: 0; }
html:not(.js) .svpane { display: block; }
html:not(.js) .svpane + .svpane {
  margin-top: var(--s-6); padding-top: var(--s-6);
  border-top: 1px solid var(--line);
}
html:not(.js) .svrow__go { display: none; }

@media (prefers-reduced-motion: reduce) {
  .svrow, .svrow__go { transition: none !important; }
}

/* ================= 6. JOURNEY - vertical timeline ======================== */
.role {
  position: relative;
  padding-left: var(--s-6);
  padding-bottom: var(--s-8);
  border-left: 1px solid var(--line);
}
.role:last-child { padding-bottom: 0; }
.role::before {
  content: ''; position: absolute; left: -4px; top: .5rem;
  width: 7px; height: 7px; border-radius: 50%; background: var(--text);
}
.role__top { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s-3); }
.role__where { color: var(--muted); font-size: .95rem; margin: var(--s-1) 0 var(--s-4); }
.badge {
  font-size: .7rem; letter-spacing: .1em; text-transform: uppercase; font-weight: 700;
  color: var(--void); background: var(--text);
  border-radius: 999px; padding: .2rem .6rem;
}
.role ul { display: grid; gap: var(--s-2); margin-bottom: var(--s-4); }
.role li { color: var(--muted); font-size: .95rem; max-width: 66ch; }

/* ============ QOREX - scroll-driven carousel ============================
   Two layers over one backdrop:

     carousel__bg     the active member's own image, blurred and dimmed
     carousel__cards  one full-bleed card visible at a time, carrying the copy
     qstrip           a window of three thumbnails STARTING at the active
                      member, so whoever fills the background is also the
                      first thumbnail

   The strip is separate from the cards precisely because the active member
   has to appear in both places at once, and one element cannot do that.
   It repeats what the card already says, so it is aria-hidden with empty alt
   text: otherwise every member is announced twice.
   ===================================================================== */
/* The carousel shares the site container exactly: same cap, same gutter.
   Without the cap it would start at 16px on a 1920 display while every
   section started at 136px, and the two would visibly disagree. */
.carousel {
  --carousel-inset: var(--gutter);
  max-width: var(--maxw);
  margin-inline: auto;
  padding-inline: var(--carousel-inset);
}

.carousel__stage {
  position: relative;
  /* 54vh, not 58: the Built with strip now pins under the stage and the
     two together have to fit the viewport. */
  --th-stage: min(clamp(340px, 42vw, 640px), 54vh);
  height: var(--th-stage);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--surface-2);
  border: 1px solid var(--line);

  --th-w: clamp(92px, 15vw, 168px);
  --th-h: clamp(136px, 21vw, 232px);
  --th-gap: clamp(10px, 1.4vw, 18px);
  --th-step: calc(var(--th-w) + var(--th-gap));
  --th-vstep: calc(var(--th-h) + var(--th-gap));
}

.carousel__bg { position: absolute; inset: 0; z-index: 0; overflow: hidden; }
.carousel__bg img {
  width: 100%; height: 100%;
  object-fit: cover;
  transform: scale(1.14);
  filter: blur(26px) brightness(.45) saturate(1.1);
}

/* ---- the cards ---------------------------------------------------------- */
.carousel__cards { position: absolute; inset: 0; z-index: 2; }

.qcard {
  position: absolute; inset: 0;
  opacity: 0; visibility: hidden;
  transition: opacity .45s var(--ease), visibility .45s var(--ease);
}
.qcard.is-active { opacity: 1; visibility: visible; }

.qcard__img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; object-position: center 32%;
}

/* A real photograph is not 16:9 concept art. Cover-cropping a portrait into a
   3:1 card removes the face, so a photo is contained and placed left of
   centre: the thumbnail row always begins at 50% of the stage, and anything
   to the right of that gets covered by it. */
.qcard--photo .qcard__img { object-fit: contain; object-position: 16% center; }
@media (max-width: 760px) {
  .qcard--photo .qcard__img { object-fit: cover; object-position: center 28%; }
}

.qcard__sigil {
  position: absolute; inset: 0; z-index: 1;
  display: grid; place-items: center; align-content: center;
  gap: var(--s-3);
  color: rgba(255, 255, 255, .78);
  pointer-events: none;
}
.qcard__sigil svg { width: min(42%, 220px); height: auto; }
.qcard.is-active .qcard__sigil {
  inset: clamp(1.25rem, 4vw, 3rem) auto auto clamp(1.25rem, 4vw, 3rem);
  justify-items: start; gap: var(--s-2);
}
.qcard.is-active .qcard__sigil svg { width: 84px; }
.qcard__sigil span {
  font-size: .64rem; letter-spacing: .18em; text-transform: uppercase;
  color: #FFFFFF;
  background: rgba(10, 15, 28, .82);
  border-radius: 999px;
  padding: .3rem .75rem;
}

/* Flat scrim, never a gradient. Dark enough that white copy clears AA even
   over a pure white photograph: worst case measures 9.14:1. It WRAPS the
   content so a contrast audit resolves the right background. */
.qcard__scrim {
  position: absolute; inset: 0;
  background: rgba(10, 15, 28, .76);
  pointer-events: none;
}
.qcard.is-active .qcard__scrim { pointer-events: auto; }

.qcard__content {
  position: absolute;
  left: clamp(1.25rem, 4vw, 3rem);
  right: clamp(1.25rem, 4vw, 3rem);
  bottom: clamp(1.5rem, 4vw, 2.75rem);
  max-width: 46ch;
  color: #FFFFFF;
}

/* Inline, NOT flex. A flex container discards whitespace-only text nodes, so
   the spaces around the pipe disappear from the rendered text and the line
   reads "GrowthHuman" to anything that copies or reads it out. */
.qcard__role {
  display: block;
  font-size: .72rem; letter-spacing: .16em; text-transform: uppercase;
  font-weight: 600; color: #FFFFFF;
}
.qsep { opacity: .45; font-weight: 400; }
.qtag { font-weight: 700; letter-spacing: .14em; }

.qcard__content h3 { font-size: clamp(1.6rem, 1.2rem + 1.8vw, 2.5rem); color: #FFFFFF; margin-top: var(--s-2); }
.qcard__quote { font-family: 'Caveat', cursive; font-size: 1.35rem; color: #FFFFFF; margin-top: var(--s-2); }
.qcard__des { font-size: .95rem; color: #FFFFFF; opacity: .9; margin-top: var(--s-3); }

.js .qcard.is-active .qcard__role  { animation: qrise .6s var(--ease) both; }
.js .qcard.is-active h3            { animation: qrise .6s var(--ease) .08s both; }
.js .qcard.is-active .qcard__quote { animation: qrise .6s var(--ease) .16s both; }
.js .qcard.is-active .qcard__des   { animation: qrise .6s var(--ease) .24s both; }

@keyframes qrise {
  from { opacity: 0; transform: translateY(22px); }
  to   { opacity: 1; transform: none; }
}

/* ---- the thumbnail strip ------------------------------------------------ */
.qstrip {
  position: absolute; inset: 0; z-index: 4;
  list-style: none; margin: 0; padding: 0;
  pointer-events: none;
}
.qthumb {
  position: absolute; top: 50%;
  width: var(--th-w); height: var(--th-h);
  transform: translateY(-50%);
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: 0 18px 40px -18px rgba(0, 0, 0, .55);
  left: calc(50% + (var(--th-step) * 3));
  opacity: 0;
  transition: left .55s var(--ease), opacity .45s var(--ease);
}
.qthumb img { width: 100%; height: 100%; object-fit: cover; object-position: center 32%; }

/* slot 0 is the member currently filling the background */
.qthumb[data-slot='0'] {
  left: 50%; opacity: 1;
  outline: 2px solid rgba(255, 255, 255, .85);
  outline-offset: -2px;
}
.qthumb[data-slot='1'] { left: calc(50% + var(--th-step)); opacity: 1; }
.qthumb[data-slot='2'] { left: calc(50% + (var(--th-step) * 2)); opacity: 1; }

/* ---- the 5 + 3 thumbnail grid ------------------------------------------ */
/* Five photos on the first row, three on the second. Eight visible of nine,
   so the ninth rotates in as the active member advances.

   Two things here are not stylistic choices.

   WIDTH drives the size now, not height. With three rows the stage height was
   the binding constraint; with five across it is the width, so the thumbnail
   is solved from the half-stage it has to fit into and the height follows
   from aspect-ratio. Deriving the height arithmetically was the obvious move
   and it is wrong: --th-w contains a percentage, and a percentage resolves
   against the containing block's WIDTH for `width` and its HEIGHT for
   `height`, so the same token would mean two different lengths.

   The rows are anchored to the MIDLINE rather than to the top, row one by its
   bottom edge and row two by its top. That centres the pair without anyone
   needing to know the computed height, which is the same percentage problem
   seen from the other side.

   Absolute positioning rather than CSS grid, as before: the slide between
   positions is a transition, and a real grid snaps between cells. */
@media (min-width: 1280px) {
  .carousel__stage {
    --th-pad: var(--s-6);
    --th-gap: clamp(8px, .85vw, 14px);
    /* the half stage, less the padding and the four gaps between five */
    --th-w: calc((50% - var(--th-pad) - (var(--th-gap) * 4)) / 5);
  }
  .qthumb {
    top: auto;
    transform: none;
    height: auto;
    aspect-ratio: 168 / 232;      /* the portrait shape the strip always had */
    transition: left .55s var(--ease), top .55s var(--ease),
                bottom .55s var(--ease), opacity .45s var(--ease);
  }

  /* row 1: five, sitting on the midline */
  .qthumb[data-slot='0'] { left: 50%; opacity: 1; }
  .qthumb[data-slot='1'] { left: calc(50% + var(--th-step)); opacity: 1; }
  .qthumb[data-slot='2'] { left: calc(50% + (var(--th-step) * 2)); opacity: 1; }
  .qthumb[data-slot='3'] { left: calc(50% + (var(--th-step) * 3)); opacity: 1; }
  .qthumb[data-slot='4'] { left: calc(50% + (var(--th-step) * 4)); opacity: 1; }
  .qthumb[data-slot='0'],
  .qthumb[data-slot='1'],
  .qthumb[data-slot='2'],
  .qthumb[data-slot='3'],
  .qthumb[data-slot='4'] { bottom: calc(50% + (var(--th-gap) / 2)); }

  /* row 2: four, hanging from it.
     This was three, which showed eight of nine and left one member parked
     off the right edge at opacity 0 on every step. That was tolerable while
     the ninth was a placeholder; once Vex had a real portrait it meant a
     member of the team was permanently invisible. Four here uses the fourth
     of the five columns --th-w already solves for, so nothing re-measures. */
  .qthumb[data-slot='5'] { left: 50%; opacity: 1; }
  .qthumb[data-slot='6'] { left: calc(50% + var(--th-step)); opacity: 1; }
  .qthumb[data-slot='7'] { left: calc(50% + (var(--th-step) * 2)); opacity: 1; }
  .qthumb[data-slot='8'] { left: calc(50% + (var(--th-step) * 3)); opacity: 1; }
  .qthumb[data-slot='5'],
  .qthumb[data-slot='6'],
  .qthumb[data-slot='7'],
  .qthumb[data-slot='8'] { top: calc(50% + (var(--th-gap) / 2)); bottom: auto; }

  /* anything past the ninth waits off the right edge, as before */
}

/* ---- short viewports --------------------------------------------------- */
/* The pinned layer now carries three things: the heading, the gallery and the
   Built with strip. Measured at 1366x768, which is an ordinary laptop, they
   came to 21px more than the viewport, the layer stopped sticking and the
   strip fell off screen on eight of the nine steps. The gallery gives up the
   difference, because it is the only one of the three that can.

   Expressed against height, not width: the constraint is vertical, and 1366
   is a wide viewport with a short one. */
@media (max-height: 880px) {
  .carousel__stage { --th-stage: min(clamp(300px, 40vw, 560px), 46vh); }
  .qorex__head { margin-bottom: var(--s-4); }
}

/* ---- Built with, at the foot of the QoreX section ---------------------- */
/* It used to be a tinted section of its own, which gave it the section frame
   for free. Inside QoreX it has to carry its own separation, or it sits flush
   against the carousel above it. The chips take their colour from the section
   tokens, which .section--dark already redefines, so no second rule. */
/* Compact on purpose. This block renders inside the pinned layer so it stays
   on screen for the whole scroll-driven sequence, and the pinned layer has to
   fit the viewport or it stops sticking. At full size it overflowed by 90px
   at 1280x820, so the label and the strapline share one line. */
/* keeps .wrap's gutter: zeroing it put the heading hard against the edge */
.qstack { margin-top: var(--s-4); }
.qstack__row {
  display: flex; flex-wrap: wrap; align-items: baseline;
  gap: var(--s-2) var(--s-3);
  margin-bottom: var(--s-3);
}
.qstack h3 {
  font-size: .8125rem; font-weight: 700;
  letter-spacing: .12em; text-transform: uppercase;
  margin: 0;
}
.qstack__note { font-size: .8125rem; color: var(--muted); margin: 0; }
.qstack .marquee { margin: 0; }
.qstack .chip { padding-block: .34rem; font-size: .8125rem; }

@media (max-width: 900px) {
  /* the pinned layer collapses at this width anyway, so it can breathe */
  .qstack { margin-top: var(--s-5); }
}

/* ---- controls ----------------------------------------------------------- */
/* Top-right, not bottom-right: the fixed back-to-top control lives in the
   bottom-right corner of the viewport and the two were overlapping. */
.carousel__nav {
  display: flex; gap: var(--s-2);
  position: absolute; right: var(--s-5); top: var(--s-5); z-index: 5;
}
.carousel__nav button:disabled { opacity: .35; cursor: default; }
.carousel__nav .iconbtn {
  background: rgba(255, 255, 255, .92);
  border-color: rgba(10, 15, 28, .25);
  color: #0F172A;
}
@media (hover: hover) and (pointer: fine) {
  .carousel__nav .iconbtn:hover { background: #FFFFFF; border-color: #0F172A; }
}

@media (max-width: 640px) {
  .carousel { --carousel-inset: var(--gutter); }
  .carousel__nav { right: var(--s-4); top: var(--s-4); }
  .qcard__des { display: none; }     /* the quote carries it at phone size */
}

/* ---- scroll-driven track ------------------------------------------------ */
.qtrack { display: grid; grid-template-columns: 1fr; }
/* min-width: 0 is load-bearing. A grid item defaults to min-width: auto and
   refuses to shrink below its content's min-content width. The marquee
   track is width: max-content, so once Built with moved inside this layer
   the whole page measured 1680px wide at every viewport, down to 320. */
.qtrack > * { grid-area: 1 / 1; min-width: 0; }

/* A lead-in before the first sentinel. Without it, sentinel one reaches the
   viewport centre while the sticky layer is still in normal flow, so the first
   card is clipped at the bottom of the screen. */
.qtrack__steps { display: grid; padding-top: 34vh; }
.qstep { height: 46vh; }

.qtrack__sticky {
  position: sticky;
  top: calc(var(--nav-h) + var(--s-4));
  align-self: start;
  /* no vertical padding of its own: the section frame already sets it */
}

/* The heading pins with the stage, so its spacing is tightened: the whole
   block has to clear the nav and still leave the stage fully on screen. */
.qorex__head { margin-bottom: var(--s-5); max-width: 76ch; }
.qorex__head .lead { font-size: clamp(1rem, .95rem + .3vw, 1.125rem); }

/* VT:start - generated by tools/build_vt.py, do not hand-edit */
/* One name per element. A duplicate makes the browser abandon the
   whole transition; a missing one makes that element snap while the
   rest morph. Both are silent, so the count is generated. */
.carousel__cards .qcard:nth-child(1) { view-transition-name: qc-1; }
.carousel__cards .qcard:nth-child(2) { view-transition-name: qc-2; }
.carousel__cards .qcard:nth-child(3) { view-transition-name: qc-3; }
.carousel__cards .qcard:nth-child(4) { view-transition-name: qc-4; }
.carousel__cards .qcard:nth-child(5) { view-transition-name: qc-5; }
.carousel__cards .qcard:nth-child(6) { view-transition-name: qc-6; }
.carousel__cards .qcard:nth-child(7) { view-transition-name: qc-7; }
.carousel__cards .qcard:nth-child(8) { view-transition-name: qc-8; }
.carousel__cards .qcard:nth-child(9) { view-transition-name: qc-9; }
.qstrip .qthumb:nth-child(1) { view-transition-name: qt-1; }
.qstrip .qthumb:nth-child(2) { view-transition-name: qt-2; }
.qstrip .qthumb:nth-child(3) { view-transition-name: qt-3; }
.qstrip .qthumb:nth-child(4) { view-transition-name: qt-4; }
.qstrip .qthumb:nth-child(5) { view-transition-name: qt-5; }
.qstrip .qthumb:nth-child(6) { view-transition-name: qt-6; }
.qstrip .qthumb:nth-child(7) { view-transition-name: qt-7; }
.qstrip .qthumb:nth-child(8) { view-transition-name: qt-8; }
.qstrip .qthumb:nth-child(9) { view-transition-name: qt-9; }
/* VT:end */

@media (prefers-reduced-motion: no-preference) {
  ::view-transition-group(*) { animation-duration: .45s; animation-timing-function: var(--ease); }
}
/* only the carousel moves; the page behind it must not cross-fade */
::view-transition-old(root), ::view-transition-new(root) { animation: none; mix-blend-mode: normal; }

@media (prefers-reduced-motion: reduce) {
  .qcard, .qthumb { transition: none !important; }
  .js .qcard.is-active .qcard__role,
  .js .qcard.is-active h3,
  .js .qcard.is-active .qcard__quote,
  .js .qcard.is-active .qcard__des { animation: none !important; }
}

/* A short viewport cannot hold a pinned stage without hiding the page, so the
   track collapses and the carousel becomes an ordinary block the buttons drive. */
@media (max-height: 620px), (max-width: 560px) {
  .qtrack__steps { padding-top: 0; }
  .qstep { height: 0; }
  .qtrack__sticky { position: static; }
}

/* ================= 8. PROCESS + CONTACT ================================== */
.steps { display: grid; gap: var(--s-4); }
@media (min-width: 760px)  { .steps { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 1040px) { .steps { grid-template-columns: repeat(5, 1fr); } }
.steps > * { min-width: 0; }

.band {
  border-top: 1px solid var(--line);
  background: var(--surface);
  padding-block: var(--section-y);
}
.band__grid { display: grid; gap: var(--s-7); align-items: start; }
@media (min-width: 880px) { .band__grid { grid-template-columns: 1.2fr .8fr; } }
.band h2 { max-width: 16ch; }
.band__cta { display: flex; flex-wrap: wrap; gap: var(--s-3); margin-top: var(--s-6); }
.band__meta { display: grid; gap: var(--s-4); }
.band__meta a { display: inline-flex; align-items: center; gap: var(--s-2); color: var(--muted); }
@media (hover: hover) and (pointer: fine) { .band__meta a:hover { color: var(--text); } }
.band__mail { font-size: 1.05rem; font-weight: 600; color: var(--text); }

.footer { border-top: 1px solid var(--line); padding-block: var(--s-6); }
.footer__inner {
  display: flex; flex-wrap: wrap; gap: var(--s-3) var(--s-6);
  justify-content: space-between; align-items: center;
}
.footer p { font-size: .875rem; color: var(--muted); }

.totop {
  position: fixed; right: var(--gutter); bottom: var(--gutter); z-index: 50;
  width: 44px; height: 44px;
  display: grid; place-items: center;
  border-radius: 50%; border: 1px solid var(--line-2);
  background: var(--void); color: var(--text);
  opacity: 0; visibility: hidden; transform: translateY(8px);
  transition: opacity .22s var(--ease), transform .22s var(--ease), visibility .22s;
}
.totop.is-on { opacity: 1; visibility: visible; transform: none; }

/* ============================== FAQ ====================================
   Native <details>/<summary>: keyboard operable, announced correctly, and it
   still opens with JavaScript off. The name attribute makes it an exclusive
   accordion where that is supported and simply allows several open panels
   where it is not, which is a fine fallback.
   ===================================================================== */
.faq { border-top: 1px solid var(--line); max-width: 92ch; }

.faq__item { border-bottom: 1px solid var(--line); }

.faq__item summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-5);
  padding-block: var(--s-5);
  cursor: pointer;
  list-style: none;
  font-size: clamp(1.02rem, .98rem + .3vw, 1.2rem);
  font-weight: 600;
  color: var(--text);
}
.faq__item summary::-webkit-details-marker { display: none; }
.faq__item summary:focus-visible { outline: 2px solid var(--blue); outline-offset: 3px; }

.faq__mark {
  flex: none;
  color: var(--muted);
  transition: transform .25s var(--ease), color .25s var(--ease);
}
.faq__item[open] .faq__mark { transform: rotate(90deg); color: var(--text); }

.faq__item p {
  color: var(--muted);
  max-width: 72ch;
  padding-bottom: var(--s-5);
}

@media (hover: hover) and (pointer: fine) {
  .faq__item summary:hover .faq__mark { color: var(--text); }
}

/* ============================ studio page =============================== */
.members { display: grid; gap: clamp(1.25rem, 3vw, var(--s-6)); }
@media (min-width: 680px)  { .members { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1040px) { .members { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 1500px) { .members { grid-template-columns: repeat(4, 1fr); } }

.member {
  display: flex; flex-direction: column;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--void);
  transition: border-color .22s var(--ease), transform .22s var(--ease);
}
@media (hover: hover) and (pointer: fine) {
  .member:hover { border-color: var(--line-2); transform: translateY(-2px); }
}
.member__shot { aspect-ratio: 16 / 10; overflow: hidden; background: var(--surface-2); border-bottom: 1px solid var(--line); }
.member__shot img { width: 100%; height: 100%; object-fit: cover; object-position: center 32%; }
.member__body { padding: var(--s-5); display: flex; flex-direction: column; gap: var(--s-2); }
.member__role {
  display: block;
  font-size: .68rem; letter-spacing: .14em; text-transform: uppercase;
  font-weight: 600; color: var(--muted);
}
.member h3 { font-size: 1.25rem; }
.member p { color: var(--muted); font-size: .92rem; }

.profile + .profile { margin-top: var(--s-8); padding-top: var(--s-8); border-top: 1px solid var(--line); }

/* ============== CASE STUDIES - cards on the home page =================== */
.caselist {
  list-style: none; margin: 0; padding: 0;
  display: grid; gap: var(--s-4);
}
@media (min-width: 760px)  { .caselist { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1100px) { .caselist { grid-template-columns: repeat(3, 1fr); } }
.caselist > * { min-width: 0; }

.casecard {
  display: grid;
  grid-auto-rows: max-content;
  align-content: start;
  gap: var(--s-3);
  height: 100%;
  padding: var(--s-5);
  background: var(--void);
  border: 1px solid var(--line);
  border-radius: var(--radius-sheet);
  box-shadow: var(--shadow-float);
  color: inherit; text-decoration: none;
  transition: border-color .25s var(--ease), transform .25s var(--ease);
}
.casecard__meta {
  font-size: .6875rem; font-weight: 600; letter-spacing: .12em;
  text-transform: uppercase; color: var(--muted); margin: 0;
}
.casecard__num { font-variant-numeric: tabular-nums; color: var(--muted); margin-right: var(--s-2); }
.casecard__title { font-size: 1.1875rem; line-height: 1.2; letter-spacing: -.015em; margin: 0; }
.casecard__deck { font-size: .9375rem; line-height: 1.55; color: var(--muted); margin: 0; }
.casecard__stat {
  margin: 0; padding-top: var(--s-3); border-top: 1px solid var(--line);
  display: flex; align-items: baseline; gap: var(--s-2); flex-wrap: wrap;
}
.casecard__stat b { font-size: 1.25rem; letter-spacing: -.02em; }
.casecard__stat span { font-size: .75rem; color: var(--muted); text-transform: uppercase; letter-spacing: .1em; }
.casecard__go { font-size: .875rem; font-weight: 600; color: var(--blue); }
@media (hover: hover) and (pointer: fine) {
  .casecard:hover { border-color: var(--line-2); transform: translateY(-6px); }
}
.casecard:focus-visible { outline: 2px solid var(--blue); outline-offset: 3px; }
.work__more { margin-top: var(--s-6); }

/* ============== CASE STUDIES - the page ================================= */
.cases { display: grid; gap: var(--s-8); }
.case { display: grid; gap: var(--s-4); scroll-margin-top: calc(var(--nav-h) + var(--s-5)); }
.case + .case { padding-top: var(--s-8); border-top: 1px solid var(--line); }
.case__meta {
  font-size: .6875rem; font-weight: 600; letter-spacing: .12em;
  text-transform: uppercase; color: var(--muted); margin: 0;
}
.case__num { font-variant-numeric: tabular-nums; color: var(--muted); margin-right: var(--s-2); }
.case__title {
  font-size: clamp(1.75rem, 1.2rem + 2vw, 3rem);
  line-height: 1.04; letter-spacing: -.028em; margin: 0; max-width: 20ch;
}
.case__deck { font-size: clamp(1.0625rem, 1rem + .3vw, 1.25rem); line-height: 1.5; color: var(--muted); max-width: 62ch; margin: 0; }

.casestats { display: grid; gap: var(--s-4); grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); }
.casestat {
  padding: var(--s-4);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  display: grid; gap: var(--s-1);
}
.casestat b { font-size: clamp(1.25rem, 1rem + .8vw, 1.75rem); letter-spacing: -.02em; line-height: 1.1; }
.casestat span { font-size: .75rem; color: var(--muted); text-transform: uppercase; letter-spacing: .1em; }

.case__body { max-width: var(--prose); display: grid; gap: var(--s-3); }
.case__body h3 { font-size: .8125rem; letter-spacing: .12em; text-transform: uppercase; color: var(--muted); margin-top: var(--s-4); }
.case__body h3:first-child { margin-top: 0; }
.case__body p { color: var(--muted); }
.ticks { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s-3); }
.ticks li { padding-left: var(--s-5); position: relative; color: var(--muted); }
/* A square, not a drawn checkmark: an icon has to come from the sprite. */
.ticks li::before {
  content: ''; position: absolute; left: 0; top: .55em;
  width: 8px; height: 8px; border-radius: 2px; background: var(--blue);
}
.case__also { font-size: .9375rem; padding-top: var(--s-4); border-top: 1px solid var(--line); }
.case__stack { margin-top: var(--s-2); }
.band__cta { display: grid; gap: var(--s-3); align-content: start; justify-items: start; }
.band__note { font-size: .875rem; color: var(--band-muted); }

/* ============== PROCESS - diagram, then cards =========================== */
.flow { margin-bottom: var(--s-6); overflow-x: auto; }
.flow__svg { width: 100%; min-width: 640px; height: auto; display: block; }
.flow__rail { stroke: var(--line-2); stroke-width: 2; }
.flow__node { fill: var(--void); stroke: var(--text); stroke-width: 2; }
.flow__num { fill: var(--text); font: 700 14px/1 'Outfit', system-ui, sans-serif; }
.flow__label { fill: var(--text); font: 600 14px/1 'Outfit', system-ui, sans-serif; }
.flow__arc { fill: none; stroke: var(--line-2); stroke-width: 2; stroke-dasharray: 4 5; }
.flow__head { fill: var(--line-2); }
.flow__arclabel { fill: var(--muted); font: 600 11px/1 'Outfit', system-ui, sans-serif; letter-spacing: .1em; text-transform: uppercase; }

.steps { list-style: none; margin: 0; padding: 0; }
.stepcard {
  padding: var(--s-5);
  background: var(--void);
  border: 1px solid var(--line);
  border-radius: var(--radius-ios);
  box-shadow: var(--shadow);
  display: grid; gap: var(--s-2); align-content: start;
}
.stepcard__num {
  font-size: .6875rem; font-weight: 700; letter-spacing: .12em;
  font-variant-numeric: tabular-nums; color: var(--blue);
}
.stepcard h3 { font-size: 1rem; margin: 0; }
.stepcard p { font-size: .9rem; color: var(--muted); margin: 0; }

/* ============== ACCENT TILES - the logo palette, nothing else ========== */
/* Three families and three only, sampled from the wordmark: the ink, the
   swoosh and the Q. A fourth hue would not be the brand. */
.casecard__icon, .case__icon {
  display: inline-grid;
  place-items: center;
  width: 44px; height: 44px;
  border-radius: 13px;            /* the iOS app-tile corner, not a circle */
  border: 1px solid transparent;
}
.casecard__icon .icon, .case__icon .icon { width: 22px; height: 22px; }
.case__icon { width: 52px; height: 52px; border-radius: 15px; }
.case__icon .icon { width: 26px; height: 26px; }

.casecard__icon--blue,  .case__icon--blue  { background: var(--blue);  color: #FFFFFF; }
.casecard__icon--coral, .case__icon--coral { background: var(--coral); color: #FFFFFF; }
.casecard__icon--ink,   .case__icon--ink   { background: var(--text);  color: var(--void); }

.casecard__site, .case__site {
  display: inline-flex; align-items: center; gap: var(--s-2);
  font-size: .8125rem; color: var(--muted);
}
.casecard__site .icon, .case__site .icon { width: 15px; height: 15px; }
.case__site a { color: var(--blue); }
.casecard__stat { margin-top: auto; }
.casecard__go { display: inline-flex; align-items: center; gap: var(--s-2); }
.casecard__go .icon { width: 16px; height: 16px; }

/* ============== MODAL - one dialog, cloned into ======================== */
/* The long write-ups are hidden ONLY under .has-modal, which the driver adds
   to <html> after it has confirmed <dialog>.showModal exists and that every
   trigger resolves to a real source. Hiding them from CSS alone would strand
   the content on any engine without dialog support, and with no script at
   all nothing is hidden and every anchor still jumps to its article. */
.has-modal .post:not(.modal__content),
.has-modal #full:not(.modal__content) { display: none; }
html.is-locked, html.is-locked body { overflow: hidden; }

.modal {
  /* The reset above sets * { margin: 0 }, which beats the UA stylesheet's
     dialog { margin: auto } and parks a modal dialog in the top left. */
  margin: auto;
  width: min(900px, calc(100% - var(--s-5)));
  max-width: none;
  max-height: 88vh;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius-sheet);
  background: var(--void);
  color: var(--text);
  box-shadow: var(--shadow-float);
  overflow: hidden;
}
/* Flat, never a gradient, and dark enough that the sheet reads as lifted. */
.modal::backdrop { background: rgba(15, 23, 42, .64); }
:root[data-theme='dark'] .modal::backdrop { background: rgba(0, 0, 0, .74); }

.modal__shell {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  max-height: 88vh;
}
.modal__bar {
  position: relative;
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: var(--s-4);
  padding: var(--s-5) var(--s-5) var(--s-4) var(--s-6);
  border-bottom: 1px solid var(--line);
  background: var(--void);
}
/* the iOS sheet grabber, centred over the bar */
.modal__grab {
  position: absolute;
  top: 8px; left: 50%;
  transform: translateX(-50%);
  width: 36px; height: 5px;
  border-radius: 999px;
  background: var(--line-2);
}
.modal__title {
  font-size: 1rem; font-weight: 600; line-height: 1.3;
  letter-spacing: -.01em; margin: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.modal__close {
  display: inline-grid; place-items: center;
  width: 36px; height: 36px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--text);
  cursor: pointer;
}
.modal__close .icon { width: 18px; height: 18px; }
@media (hover: hover) and (pointer: fine) {
  .modal__close:hover { border-color: var(--text); }
}
.modal__close:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }

.modal__body {
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--s-6);
}
/* The clone arrives carrying the page styles it had; these undo the parts
   that only made sense in a stacked list. */
.modal__content { display: grid; gap: var(--s-4); max-width: none; }
.modal__content.case + .case,
.modal__content.post { padding-top: 0; border-top: 0; margin: 0; }
.modal__content .case__title,
.modal__content h2 { font-size: clamp(1.5rem, 1.2rem + 1.4vw, 2.25rem); line-height: 1.08; }
.modal__content .post__body { max-width: var(--prose); }
.modal__content img { max-width: 100%; height: auto; border-radius: var(--radius); }

@media (max-width: 640px) {
  .modal { width: 100%; max-height: 92vh; border-radius: var(--radius-sheet) var(--radius-sheet) 0 0; }
  .modal__shell { max-height: 92vh; }
  .modal__body { padding: var(--s-5) var(--s-4) var(--s-7); }
  .modal__bar { padding: var(--s-5) var(--s-3) var(--s-3) var(--s-4); }
}
@media (prefers-reduced-motion: reduce) {
  .modal, .modal__close { transition: none !important; }
}

/* ============== SKILLS - what each member actually does ================= */
/* A real list, not a row of loose spans: a screen reader announcing
   "list, 6 items" is more use than six unlabelled chips. Colours come from
   the section tokens, so the same rule works on the page and inside the
   dark band without a second copy. */
.skills-rail {
  list-style: none;
  margin: var(--s-3) 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
}
.skills-rail li {
  font-size: .75rem;
  font-weight: 500;
  line-height: 1;
  padding: .42rem .62rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  color: var(--muted);
  white-space: nowrap;
}

/* On a card the chips sit over a photographic scrim, where a token colour
   cannot be trusted to clear AA against whatever pixel is underneath. */
.qcard__skills { margin-top: var(--s-4); }
.qcard__skills li {
  border-color: rgba(255, 255, 255, .34);
  color: #FFFFFF;
  background: rgba(10, 15, 28, .42);
}

.profile .skills-rail { margin-top: var(--s-4); }
.member .skills-rail { margin-top: var(--s-3); }

@media (max-width: 640px) {
  /* the quote carries the card at phone size; chips would wrap to four rows */
  .qcard__skills { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .skills-rail li { transition: none !important; }
}

/* =============================== blog ==================================== */
/* ============== BLOG INDEX - iOS cards, lifted off the page ============= */
.postcards {
  display: grid;
  gap: var(--s-5);
  margin-bottom: var(--s-8);
}
@media (min-width: 720px)  { .postcards { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1080px) { .postcards { grid-template-columns: repeat(3, 1fr); } }
.postcards > * { min-width: 0; }

.postcard {
  display: grid;
  grid-template-rows: auto 1fr;
  background: var(--void);
  border: 1px solid var(--line);
  border-radius: var(--radius-sheet);
  overflow: hidden;
  box-shadow: var(--shadow-float);
  color: inherit;
  text-decoration: none;
  /* transform only. Transitioning box-shadow repaints the whole card every
     frame, which is why this project forbids it. */
  transition: transform .3s var(--ease), border-color .3s var(--ease);
}
.postcard__shot {
  display: block;
  aspect-ratio: 16 / 10;
  background: var(--surface);
  border-bottom: 1px solid var(--line);
  overflow: hidden;
}
.postcard__shot img {
  width: 100%; height: 100%;
  object-fit: cover; object-position: top center;
  display: block;
}
.postcard__body {
  display: grid;
  align-content: start;
  gap: var(--s-2);
  padding: var(--s-5);
}
.postcard__meta {
  font-size: .6875rem; font-weight: 600;
  letter-spacing: .12em; text-transform: uppercase;
  color: var(--muted);
}
.postcard__title {
  font-family: 'Outfit', system-ui, sans-serif;
  font-size: 1.125rem; font-weight: 600;
  line-height: 1.25; letter-spacing: -.015em;
  color: var(--text);
}
.postcard__excerpt {
  font-size: .9375rem; line-height: 1.55;
  color: var(--muted);
}
.postcard__go {
  margin-top: var(--s-2);
  font-size: .875rem; font-weight: 600;
  color: var(--blue);
}
@media (hover: hover) and (pointer: fine) {
  .postcard:hover { transform: translateY(-6px); border-color: var(--line-2); }
}
.postcard:focus-visible { outline: 2px solid var(--blue); outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) {
  .postcard { transition: none !important; }
}

.post-index { display: grid; gap: 0; border-top: 1px solid var(--line); }
.post-index a {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s-2) var(--s-5);
  padding-block: var(--s-4);
  border-bottom: 1px solid var(--line);
  transition: padding-left .2s var(--ease);
}
.post-index b { font-weight: 600; font-size: 1.05rem; }
.post-index span { font-size: .8rem; color: var(--muted); margin-left: auto; }
@media (hover: hover) and (pointer: fine) {
  .post-index a:hover { padding-left: var(--s-3); }
}

.post { max-width: 72ch; padding-block: clamp(2.5rem, 6vw, var(--s-8)); border-top: 1px solid var(--line); }
.post:first-of-type { border-top: 0; }
.post__meta { font-size: .8rem; color: var(--muted); }
.post h2 { font-size: clamp(1.5rem, 1.2rem + 1.6vw, 2.25rem); margin-block: var(--s-3) var(--s-4); }
.post__tags { display: flex; flex-wrap: wrap; gap: var(--s-2); margin-bottom: var(--s-6); }

.post__body > * + * { margin-top: var(--s-4); }
.post__body p { color: var(--text); }
.post__body h2 { font-size: clamp(1.2rem, 1.1rem + .6vw, 1.5rem); margin-top: var(--s-7); }
.post__body h3 { font-size: 1.1rem; margin-top: var(--s-6); }
.post__body ul, .post__body ol { display: grid; gap: var(--s-2); padding-left: var(--s-5); }
.post__body ul li { list-style: none; position: relative; padding-left: var(--s-4); color: var(--muted); }
.post__body ul li::before { content: ''; position: absolute; left: 0; top: .7em; width: 7px; height: 1px; background: var(--line-2); }
.post__body ol { list-style: decimal; }
.post__body ol li { color: var(--muted); }
.post__body strong { color: var(--text); font-weight: 600; }
.post__body a { color: var(--blue); text-decoration: underline; text-underline-offset: 3px; }
.post__body blockquote { border-left: 2px solid var(--line-2); padding-left: var(--s-4); color: var(--muted); }

.post__body :not(pre) > code {
  font-family: ui-monospace, 'Cascadia Code', Consolas, monospace;
  font-size: .875em; color: var(--text);
  background: var(--surface-2); border: 1px solid var(--line);
  border-radius: 5px; padding: .1em .36em; overflow-wrap: anywhere;
}
.post__body pre {
  max-width: 100%; overflow-x: auto;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); padding: var(--s-4) var(--s-5);
  font-size: .85rem; line-height: 1.6;
  -webkit-overflow-scrolling: touch;
}
.post__body pre code {
  font-family: ui-monospace, 'Cascadia Code', Consolas, monospace;
  color: var(--text); white-space: pre; background: none; border: 0; padding: 0;
}
.post__body figure { border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; background: var(--surface); }
.post__body figure img { width: 100%; }
.post__body figcaption { border-top: 1px solid var(--line); padding: var(--s-3) var(--s-4); font-size: .8rem; color: var(--muted); }
.post__body hr { height: 1px; border: 0; background: var(--line); }
.post__top { display: inline-flex; align-items: center; gap: var(--s-2); margin-top: var(--s-6); font-size: .9rem; color: var(--muted); }

/* ============================== motion =================================== */
/* Content is visible by default. The start state is armed only once the script
   has added .js to <html>, so with JavaScript off nothing is ever hidden. */
.reveal { transition: opacity .55s var(--ease), transform .55s var(--ease); }
.js .reveal:not(.in) { opacity: 0; transform: translateY(18px); }

/* A theme flip must be instant: otherwise every element animates through
   intermediate colours that are briefly unreadable. */
:root.theme-swap, :root.theme-swap *, :root.theme-swap *::before, :root.theme-swap *::after {
  transition: none !important;
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .gallery, .rail { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }
  .js .reveal:not(.in) { opacity: 1; transform: none; }
}

@media print {
  .nav, .menu, .totop, .gallery__nav { display: none !important; }
  body { background: #fff; color: #000; }
}
