/* ==========================================================================
   WebMint — stylesheet
   Built to DESIGN.md (the Framer design analysis): a dark-canvas marketing
   system, poster-grade display type, white pill CTAs, and gradient spotlight
   cards as the atmosphere device.

   Token names below deliberately match DESIGN.md so the mapping is auditable.

   The one substitution: DESIGN.md's accent is #0099ff. This brand is green, and
   the accent plays exactly the role the document assigns — links, focus and
   selection ONLY, never a surface. See the README.

   Structure:
     1.  Tokens                  9.  Selected work + spotlight cards
     2.  Base                   10.  Packages (pricing cards)
     3.  Layout                 11.  Process
     4.  Typography             12.  FAQ rows
     5.  Buttons                13.  Contact
     6.  Top nav                14.  Footer
     7.  Hero                   15.  404
     8.  Services               16.  Dynamic layer
                                17.  Motion
   ========================================================================== */

/* 1. Tokens --------------------------------------------------------------- */

:root {
  color-scheme: light;

  /* Surface — DESIGN.md's system inverted for light.
     DESIGN.md itself says "don't ship a light-mode page", so this is a
     deliberate departure from the document, not an oversight. The structure it
     defines — surface lift, binary ink hierarchy, pills, display tracking — is
     unchanged; only the luminance is flipped. */
  --canvas:        #F7F6F3;  /* page ground, a warm paper white */
  --surface-1:     #FFFFFF;  /* cards */
  --surface-2:     #F1EFEA;  /* recessed fill: secondary pills, inputs */
  --hairline:      #E4E1DA;
  --hairline-soft: #EDEBE5;
  --inverse-canvas:#111110;  /* the primary pill is ink on light */
  --inverse-ink:   #FFFFFF;

  /* Text — still a binary hierarchy: ink or ink-muted. Nothing between. */
  --ink:       #111110;
  --ink-muted: #6B6862;

  /* Accent. NOTE: the brand green is only 2.5:1 on white, so on a light ground
     it can only ever be a FILL (with near-black text on it, 7.5:1). Green text
     must use --accent-ink, which is a darkened form of the same hue at 5.4:1. */
  --accent:      #50B850;  /* brand green — fills only */
  --accent-ink:  #1F7A34;  /* green that is safe as text */
  --accent-tint: #F1F9F2;  /* pale green wash */

  /* Gradient spotlight family — DESIGN.md's decorative signature */
  --gradient-violet:  #6a4cf5;
  --gradient-magenta: #d44df0;
  --gradient-orange:  #ff7a3d;
  --gradient-coral:   #ff5577;
  --semantic-success: #22c55e;

  /* Radius — DESIGN.md §Shapes */
  --r-xs: 4px;
  --r-sm: 6px;
  --r-md: 10px;
  --r-lg: 15px;
  --r-xl: 20px;
  --r-xxl: 30px;
  --r-pill: 100px;

  /* Spacing — DESIGN.md uses a 5px base, not 4/8/16 */
  --sp-hair: 1px;
  --sp-xxs: 4px;
  --sp-xs: 8px;
  --sp-sm: 12px;
  --sp-md: 15px;
  --sp-lg: 20px;
  --sp-xl: 30px;
  --sp-xxl: 40px;
  --sp-section: clamp(64px, 8vw, 96px);

  /* Type — one family, per DESIGN.md's substitute note */
  --sans: 'Inter', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto,
          'Helvetica Neue', Arial, sans-serif;

  /* Display scale. Tracking is expressed in em so the PERCENTAGE holds as the
     size scales — DESIGN.md is explicit that the compression is not to be
     reduced, only the size. */
  --display-xxl: clamp(2.75rem, 8.5vw, 6.875rem);  /* 110px */
  --display-xl:  clamp(2.25rem, 6.6vw, 5.3125rem); /* 85px  */
  --display-lg:  clamp(1.9rem, 4.8vw, 3.875rem);   /* 62px  */
  --display-md:  clamp(1.5rem, 2.5vw, 2rem);       /* 32px  */

  --track-display: -.05em;    /* -5.5px at 110px = 5% */
  --track-md:      -.031em;   /* -1.0px at 32px  */
  --track-text:    -.01em;    /* -0.15px at 15px = 1% */

  /* Elevation — DESIGN.md §Elevation & Depth */
  --elev-2: 0 1px 2px rgba(17,17,16,.05), 0 10px 30px rgba(17,17,16,.07);
  --ring-selected: 0 0 0 1px rgba(80,184,80,.4);

  /* Motion. Every entrance uses this one curve, so nothing arrives on a timing
     of its own; the hover transitions use a plain ease, because a 1px lift over
     250ms wants a different feel to a 22px fade-up over 750ms. */
  --ease-reveal: cubic-bezier(.22,.61,.36,1);

  --wrap: 1199px;             /* DESIGN.md's container breakpoint */
}

/* 2. Base ----------------------------------------------------------------- */

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

html {
  scroll-behavior: smooth;
  scroll-padding-top: 72px;
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--canvas);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 15px;
  font-weight: 400;
  line-height: 1.3;
  letter-spacing: var(--track-text);
  -webkit-font-smoothing: antialiased;
}

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

/* DESIGN.md §Elevation: the accent ring is the focus signal. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: var(--r-xs);
}

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

.skip-link {
  position: fixed;
  top: var(--sp-sm); left: var(--sp-sm);
  z-index: 300;
  padding: 10px 20px;
  background: var(--inverse-canvas);
  color: var(--inverse-ink);
  font-size: 14px;
  font-weight: 500;
  text-decoration: none;
  border-radius: var(--r-pill);
  transform: translateY(-250%);
}
.skip-link:focus { transform: translateY(0); }

/* 3. Layout --------------------------------------------------------------- */

.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: clamp(20px, 4vw, 32px);
}

.cols { display: grid; gap: var(--sp-xxl); }
@media (min-width: 900px) {
  .cols--8-4 { grid-template-columns: minmax(0, 8fr) minmax(0, 4fr); }
  .cols--7-5 { grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); }
  .cols--6-6 { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  .cols--5-7 { grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); }
  .cols--4-8 { grid-template-columns: minmax(0, 4fr) minmax(0, 8fr); }
}

.section { padding-block: var(--sp-section); }

/* DESIGN.md is explicit: canvas is the surface for hero, body, pricing, FAQ and
   footer alike — "no light interludes", and "the dark canvas IS the
   whitespace". Sections therefore separate by DENSITY (a band of charcoal cards
   versus flat black), not by a background swap.
   This also matters practically: a surface-1 card on a surface-1 band would be
   invisible. Cards are always one step up from the canvas. */
.section--surface { background: var(--canvas); }

.rule { border: 0; border-top: 1px solid var(--hairline-soft); margin: 0; }

/* 4. Typography — DESIGN.md §Typography ------------------------------------ */

h1, h2, h3, h4 { margin: 0; font-weight: 500; }
p { margin: 0; }

.display-xxl {
  font-size: var(--display-xxl);
  font-weight: 500;
  line-height: .85;
  letter-spacing: var(--track-display);
}
.display-xl {
  font-size: var(--display-xl);
  font-weight: 500;
  line-height: .95;
  letter-spacing: var(--track-display);
}
.display-lg {
  font-size: var(--display-lg);
  font-weight: 500;
  line-height: 1;
  letter-spacing: var(--track-display);
}
.display-md {
  font-size: var(--display-md);
  font-weight: 500;
  line-height: 1.13;
  letter-spacing: var(--track-md);
}

/* Deliberate line breaks on wide screens, natural wrapping on narrow ones. */
.dl { display: block; }
@media (max-width: 719px) { .dl { display: inline; } }

.headline { font-size: 22px; font-weight: 700; line-height: 1.2; letter-spacing: -.036em; }
.subhead  { font-size: 24px; font-weight: 400; line-height: 1.3; letter-spacing: -.0004em; color: var(--ink-muted); }
.body-lg  { font-size: 18px; font-weight: 400; line-height: 1.3; letter-spacing: var(--track-text); color: var(--ink-muted); }
.body-sm  { font-size: 14px; font-weight: 500; line-height: 1.4; letter-spacing: var(--track-text); }
.caption  { font-size: 13px; font-weight: 500; line-height: 1.2; letter-spacing: var(--track-text); color: var(--ink-muted); }
.micro    { font-size: 12px; font-weight: 400; line-height: 1.2; letter-spacing: var(--track-text); color: var(--ink-muted); }

.eyebrow {
  display: block;
  margin-bottom: var(--sp-lg);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: var(--track-text);
  color: var(--ink-muted);
}
.lead { margin-top: var(--sp-lg); max-width: 52ch; }

/* 5. Buttons — DESIGN.md §Buttons ------------------------------------------
   Every CTA is a pill. Secondary actions are charcoal pills; bordered "ghost"
   buttons are explicitly not part of this vocabulary. The accent is never a
   button fill. */

/* Links must be --accent-ink, never --accent: the brand green is 2.5:1 on a
   light ground and would fail as text. */
a { color: var(--accent-ink); text-decoration: none; }
a:hover { text-decoration: underline; text-underline-offset: .2em; }

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-xs);
  min-height: 44px;
  padding: 10px 20px;
  border: 0;
  border-radius: var(--r-pill);
  font-family: inherit;
  font-size: 14px;
  font-weight: 500;
  line-height: 1;
  letter-spacing: var(--track-text);
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  transition: transform .15s ease, background-color .15s ease, color .15s ease;
}
a.btn { text-decoration: none; }
a.btn:hover { text-decoration: none; }

/* button-primary — the ink pill. On light, the primary CTA is near-black on
   white, which is the inverse of DESIGN.md's white-on-dark and 18.9:1. */
.btn--primary { background: var(--inverse-canvas); color: var(--inverse-ink); }
/* Hover must stay dark: the palette flip means a "lighten on hover" rule that
   suited the dark theme would put white type on a near-white pill. */
.btn--primary:hover { background: #33322F; color: var(--inverse-ink); }

/* button-secondary — the soft grey pill. DESIGN.md specifies surface-1, but its
   own pricing-card is also surface-1, so at that value the pill vanishes on a
   pricing card. surface-2 is one step away from any card it sits on. */
.btn--secondary { background: var(--surface-2); color: var(--ink); }
.btn--secondary:hover { background: #E6E3DC; color: var(--ink); }

/* button-translucent — for use on top of a colour block. */
.btn--translucent { background: var(--surface-2); color: var(--ink); border-radius: var(--r-xxl); padding: 8px 16px; }
.btn--translucent:hover { background: #E6E3DC; color: var(--ink); }

/* A 1px lift, and only on hover. `transform`, never `translate`: the reveal
   animates `translate` and carries a delay, so a lift written that way would
   start late and take three-quarters of a second. The transition for this is
   already on `.btn` above, and `.btn:active` below still wins when a hovered
   button is pressed. */
.btn:hover { transform: translateY(-1px); }

/* Pressed state shrinks rather than darkening — DESIGN.md button-primary-pressed. */
.btn:active { transform: scale(.97); }
.btn--sm { min-height: 36px; padding: 8px 16px; font-size: 14px; }
.btn--block { width: 100%; }

.btn-row { display: flex; flex-wrap: wrap; gap: var(--sp-sm); align-items: center; }
@media (max-width: 520px) { .btn-row .btn { flex: 1 1 100%; } }

/* 6. Top nav — DESIGN.md §Navigation ---------------------------------------- */

.site-header {
  position: sticky;
  top: 0;
  z-index: 200;
  background: var(--canvas);
  border-bottom: 1px solid transparent;
  transition: border-color .2s ease, background-color .2s ease;
}
.site-header.is-stuck {
  background: rgba(247,246,243,.9);
  backdrop-filter: saturate(180%) blur(14px);
  border-bottom-color: var(--hairline-soft);
}

/* Three columns so the nav sits dead centre of the bar, regardless of how wide
   the logo or the CTA happen to be. The middle column collapses to nothing when
   the nav is hidden on mobile, leaving logo left / controls right. */
.masthead {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: var(--sp-lg);
  min-height: 56px;
}
.brand { justify-self: start; }
.nav--desk { justify-self: center; }
.masthead__end {
  grid-column: 3;
  justify-self: end;
  display: flex;
  align-items: center;
  gap: var(--sp-sm);
}

.brand { display: inline-flex; align-items: center; }
/* 100px of actual logo. The asset is cropped to its artwork, so the width set
   here is the width you see — the supplied master carried ~25% transparent
   padding, which used to draw only ~99px of mark inside a 132px box. */
.brand img { width: 100px; }
.brand:hover { text-decoration: none; opacity: .85; }

.nav ul { display: flex; align-items: center; gap: var(--sp-xs); margin: 0; padding: 0; list-style: none; }
.nav a {
  display: block;
  padding: var(--sp-xs) var(--sp-sm);
  border-radius: var(--r-pill);
  font-size: 14px;
  font-weight: 500;
  letter-spacing: var(--track-text);
  color: var(--ink-muted);
  text-decoration: none;
  transition: color .15s ease, background-color .15s ease;
}
.nav a:hover { color: var(--ink); background: var(--surface-1); text-decoration: none; }
/* Set by the section highlight in main.js. Deliberately the same treatment as
   hover, so the nav reads identically whether you are pointing at a link or
   reading the section it points to. */
.nav a[aria-current="true"] { color: var(--ink); background: var(--surface-1); }

.nav-toggle { display: none; }
.nav-toggle > summary {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-xs);
  min-height: 40px;
  padding: 8px 16px;
  background: var(--surface-1);
  border-radius: var(--r-pill);
  font-size: 14px;
  font-weight: 500;
  color: var(--ink);
  cursor: pointer;
  list-style: none;
}
.nav-toggle > summary::-webkit-details-marker { display: none; }
.nav-toggle__bars,
.nav-toggle__bars::before,
.nav-toggle__bars::after {
  display: block; width: 14px; height: 1.5px;
  border-radius: 2px; background: currentColor;
}
.nav-toggle__bars { position: relative; }
.nav-toggle__bars::before,
.nav-toggle__bars::after { content: ""; position: absolute; left: 0; }
.nav-toggle__bars::before { top: -5px; }
.nav-toggle__bars::after { top: 5px; }

/* "Contact on WhatsApp" is a long label; below this width the bar cannot hold
   the logo, the pill and the menu button at once, so the pill moves into the
   menu panel, where it appears as a full-width button.

   The child combinator here is load-bearing. The panel's own WhatsApp button
   lives INSIDE `.masthead__end`, so a plain descendant selector (`.masthead__end
   .btn`) hid both of them — which left the mobile menu with no way to reach
   WhatsApp at all, on the width most of this site's visitors are using. Only the
   pill sitting directly on the bar should disappear. */
@media (max-width: 599px) {
  .masthead__end > .btn { display: none; }
}

@media (max-width: 999px) {
  .nav-toggle { display: block; }
  .nav--desk { display: none; }

  /* Anchored to .site-header, which is positioned because it is sticky. */
  .nav-toggle__panel {
    position: absolute;
    left: 20px; right: 20px;
    top: calc(100% + 8px);
    padding: var(--sp-md);
    background: var(--surface-1);
    border: 1px solid var(--hairline);
    border-radius: var(--r-xl);
    box-shadow: var(--elev-2);
    max-height: calc(100vh - 90px);
    overflow-y: auto;
  }
  .nav-toggle__panel .nav ul { flex-direction: column; align-items: stretch; gap: 2px; }
  .nav-toggle__panel .nav a { padding: var(--sp-sm) var(--sp-md); font-size: 15px; border-radius: var(--r-md); }
  /* The panel is already surface-1, so the desk nav's white pill would be a white
     pill on a white card. One surface step over instead. */
  .nav-toggle__panel .nav a[aria-current="true"] { background: var(--surface-2); }
  .nav-toggle__panel .btn { margin-top: var(--sp-sm); }

  /* <details> content is display:none while closed, and a transition cannot
     start from a box that does not exist — but an animation can, because it
     begins the moment the element becomes displayed. So the panel animates open
     and closes instantly. Animating the close would mean replacing the native
     element and giving up the keyboard behaviour that comes with it, which is
     not a trade worth making for a menu. `both` holds the from-state during the
     zero-length delay, so there is no one-frame flash of the finished panel.
     The reduced-motion block at the foot of this file already neutralises it. */
  @keyframes panelIn {
    from { opacity: 0; transform: translateY(-6px); }
    to   { opacity: 1; transform: translateY(0); }
  }
  .nav-toggle[open] .nav-toggle__panel { animation: panelIn .18s ease-out both; }

  /* The bars fold into a cross while the menu is open. Unlike the panel, the
     summary stays displayed throughout, so this one can be a transition. */
  .nav-toggle__bars,
  .nav-toggle__bars::before,
  .nav-toggle__bars::after { transition: background-color .2s ease, transform .2s ease, top .2s ease; }
  .nav-toggle[open] .nav-toggle__bars { background: transparent; }
  .nav-toggle[open] .nav-toggle__bars::before { top: 0; transform: rotate(45deg); }
  .nav-toggle[open] .nav-toggle__bars::after  { top: 0; transform: rotate(-45deg); }
}

/* 7. Hero ------------------------------------------------------------------ */

.hero { padding-block: clamp(48px, 7vw, 96px) var(--sp-section); }

/* Centred hero. The measure is set in ch on the headline and the lead, and each
   is centred with margin-inline, so the block stays optically centred rather
   than stretching to the container and centring the text inside a wide box. */
.hero { text-align: center; }
.hero h1 { max-width: 20ch; margin-inline: auto; }
.hero__lead { margin-top: var(--sp-xl); max-width: 54ch; margin-inline: auto; }
.hero__actions { margin-top: var(--sp-xl); justify-content: center; }
/* The caption is a flex row (dot + text), so it needs its own centring. */
.hero figcaption { justify-content: center; }

/* The demo, in a product mockup tile — DESIGN.md §Components. */
.mockup-tile {
  margin: 0;
  padding: var(--sp-md);
  background: var(--surface-1);
  border-radius: var(--r-xl);
  box-shadow: var(--elev-2);
}
.mockup-tile__frame {
  display: block;
  border-radius: var(--r-lg);
  overflow: hidden;
  background: var(--canvas);
}
.mockup-tile img { width: 100%; }
.mockup-tile figcaption {
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
  padding: var(--sp-md) var(--sp-xxs) var(--sp-xxs);
  font-size: 13px;
  font-weight: 500;
  color: var(--ink-muted);
}
.mockup-tile figcaption::before {
  content: "";
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--accent);
  flex: none;
}

/* A cut-out device mockup carries its own transparency, so it needs no frame —
   the tile chrome would only draw a card around nothing. */
.mockup-tile--bare { padding: 0; background: none; box-shadow: none; }
.mockup-tile--bare .mockup-tile__frame {
  background: none;
  border-radius: 0;
  overflow: visible;
}

.hero__showcase { margin-top: clamp(40px, 6vw, 72px); }

/* 8. Services — a horizontal snap rail of cards ----------------------------
   The rail is a native scroll container: touch drag, trackpad swipe, keyboard
   arrows and Tab all work with no JavaScript at all. JS only adds the
   prev/next buttons and the dots. */

.services {
  display: flex;
  gap: var(--sp-md);
  margin: 0;
  padding: var(--sp-xs) var(--sp-xs) var(--sp-lg);
  list-style: none;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  scrollbar-width: none;   /* the dots underneath carry the position instead */
}
.services::-webkit-scrollbar { display: none; }
.services > * { flex: 0 0 min(80%, 292px); scroll-snap-align: start; }
@media (min-width: 700px) { .services > * { flex-basis: 304px; } }

/* A simpler card than before: one green number tile, a short plain-language
   title, and a single line of explanation. */
.service {
  display: flex;
  flex-direction: column;
  padding: var(--sp-lg);
  background: var(--surface-1);
  border: 1px solid var(--hairline);
  border-radius: var(--r-xl);
  transition: border-color .2s ease, box-shadow .2s ease, transform .2s ease;
}
.service:hover {
  border-color: transparent;
  box-shadow: 0 10px 28px rgba(17, 17, 16, .09);
  transform: translateY(-4px);
}
.service__num {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  margin-bottom: var(--sp-lg);
  border-radius: var(--r-md);
  background: var(--accent-tint);
  color: var(--accent-ink);
  font-size: 17px;
  font-weight: 600;
  letter-spacing: -.02em;
}
.service h3 {
  font-size: 20px;
  font-weight: 700;
  line-height: 1.2;
  letter-spacing: -.02em;
}
.service p {
  margin-top: var(--sp-xs);
  font-size: 14px;
  line-height: 1.5;
  color: var(--ink-muted);
}

/* 9. Selected work + gradient spotlight cards ------------------------------ */

.work-grid { display: grid; gap: var(--sp-lg); }
@media (min-width: 900px) {
  .work-grid { grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr); align-items: stretch; }
}

/* gradient-spotlight-card — the signature atmosphere device.
   DESIGN.md: cards, never section grounds; one or two per long page. */
.spotlight {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: var(--sp-xl);
  padding: var(--sp-xxl);
  border-radius: var(--r-xxl);
  font-size: 24px;
  font-weight: 400;
  line-height: 1.3;
}
.spotlight p { max-width: 22ch; }
/* Gradient stops are tuned so the card's text colour clears AA against EVERY
   stop, not just the average — a spotlight is a gradient, and the worst pixel
   is what counts.
     violet  takes white      (lightest stop #7350f0 -> 5.1:1)
     magenta takes near-black (darkest stop  #b838d4 -> 4.6:1)
   DESIGN.md's own figures for white-on-orange would only reach 2.6:1, so the
   warm variants take near-black instead. */
/* The violet panel is a dark fill in either theme, so its text is always white.
   It must NOT use --ink: that token flipped to near-black with the light
   palette, which put dark type on violet at 1.8:1. */
.spotlight--violet {
  background: radial-gradient(115% 115% at 22% 12%, #7350f0 0%, #5738e0 52%, #3a20af 100%);
  color: #FFFFFF;
}
/* Only the violet variant is in use — it is the page's single colour block, in
   the work section. The other gradient stops stay defined as tokens in :root if
   you want a second one; see the README. */
/* No opacity tricks — hierarchy on a gradient comes from size alone. */
.spotlight__label { display: block; margin-bottom: var(--sp-md); font-size: 13px; font-weight: 500; }


.work-meta { display: grid; gap: var(--sp-lg); margin: clamp(32px, 4vw, 48px) 0 0; }
@media (min-width: 700px) { .work-meta { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.work-meta div { border-top: 1px solid var(--hairline); padding-top: var(--sp-md); }
.work-meta dt { font-size: 13px; font-weight: 500; color: var(--ink-muted); margin-bottom: var(--sp-xxs); }
.work-meta dd { margin: 0; font-size: 18px; font-weight: 500; }

.work-notes { margin-top: clamp(40px, 5vw, 64px); }
.disclaimer {
  align-self: start;
  padding: var(--sp-lg);
  background: var(--surface-1);
  border-radius: var(--r-lg);
  font-size: 14px;
  font-weight: 500;
  line-height: 1.4;
  color: var(--ink-muted);
}
.disclaimer strong { color: var(--ink); font-weight: 700; }

.built { margin: 0; padding: 0; list-style: none; align-self: start; }
.built li {
  padding-block: var(--sp-md);
  border-top: 1px solid var(--hairline-soft);
  font-size: 15px;
  line-height: 1.4;
  color: var(--ink-muted);
}
.built li:last-child { border-bottom: 1px solid var(--hairline-soft); }
.built strong { color: var(--ink); font-weight: 500; }

.work-actions { margin-top: var(--sp-xl); }
.work-actions .note { margin-top: var(--sp-md); max-width: 60ch; }

/* 10. Packages — pricing cards -------------------------------------------- */

/* Heading with its supporting line beneath it, rather than in a right column. */
.pricing-head { max-width: 46rem; margin-bottom: clamp(32px, 4vw, 48px); }
.pricing-head h2 + p { margin-top: var(--sp-lg); }

.packages {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
  gap: var(--sp-lg);
  align-items: stretch;
}

/* pricing-card — surface-1, xl radius, 24px padding. */
.pricing-card {
  display: flex;
  flex-direction: column;
  padding: 24px;
  background: var(--surface-1);
  border-radius: var(--r-xl);
}
/* pricing-card-featured. On dark, "one surface step up" was a lighter charcoal.
   On a light ground a step of grey is barely visible, so emphasis comes from the
   pale brand-green wash plus a deeper shadow. */
.pricing-card--featured {
  /* Deeper than --accent-tint: against the warm canvas a very pale wash is
     only a delta of ~10 and reads as no card at all. */
  background: #E9F5EB;
  box-shadow: 0 16px 40px rgba(31, 122, 52, .14);
}

/* A hover lift, matching the 4px the service cards in the rail already take —
   these cards are the commercial heart of the page and should acknowledge the
   pointer. The transition that drives it CANNOT be declared here: every one of
   these cards carries `.reveal`, and `.js .reveal.is-in` owns the `transition`
   shorthand on revealed elements at a higher specificity, so a rule here would
   be discarded and the lift would snap. It lives in section 16 instead, beside
   the rule it has to out-specify. */
.pricing-card:hover { transform: translateY(-4px); }
.pricing-card:not(.pricing-card--featured):hover {
  box-shadow: 0 16px 36px rgba(17, 17, 16, .12);
}
/* The featured card's shadow is green on purpose, so it deepens rather than
   being swapped for a neutral one. */
.pricing-card--featured:hover { box-shadow: 0 20px 46px rgba(31, 122, 52, .20); }

/* The name and the Recommended tag share a row. */
.pricing-card__head { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-xs) var(--sp-sm); }
.pricing-card__name { font-size: 22px; font-weight: 700; line-height: 1.2; letter-spacing: -.036em; }
.pricing-card__tagline { margin-top: var(--sp-xs); font-size: 15px; color: var(--ink-muted); }
.pricing-card__badge {
  padding: 4px 10px;
  background: var(--ink);
  border-radius: var(--r-pill);
  font-size: 12px;
  font-weight: 500;
  color: var(--inverse-ink);
}

.pricing-card__price {
  margin-top: var(--sp-lg);
  padding-top: var(--sp-lg);
  border-top: 1px solid var(--hairline);
}
/* The struck-through original, then the price and its saving on one row. */
.pricing-card__was {
  display: block;
  font-size: 15px;
  font-weight: 500;
  color: var(--ink-muted);
  text-decoration: line-through;
  text-decoration-thickness: 1px;
}
.pricing-card__now {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-xxs) var(--sp-sm);
  margin-top: 2px;
}
/* The number is the loudest thing on the card, so it takes the bold weight. The
   struck-through original stays at 500 and muted — bolding it too would make the
   old price compete with the new one. */
.pricing-card__amount { font-size: var(--display-md); font-weight: 700; letter-spacing: var(--track-md); line-height: 1.13; }

/* The saving is what these cards are selling, so it carries the brand green as
   a FILL with near-black on it (7.5:1) — the accent is never text on a light
   ground. The subtle variant sits on the pale wash; the best offer gets the
   solid fill so the deepest discount reads first. */
.pricing-card__save {
  padding: 4px 10px;
  border-radius: var(--r-pill);
  background: var(--accent-tint);
  color: var(--accent-ink);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .01em;
  white-space: nowrap;
}
.pricing-card__save--best { background: var(--accent); color: var(--ink); }

.pricing-card__unit { display: block; margin-top: var(--sp-xs); font-size: 13px; font-weight: 500; color: var(--ink-muted); }

.pricing-card__inherits { margin-top: var(--sp-lg); font-size: 14px; font-weight: 500; color: var(--ink); }
.pricing-card__includes { margin: var(--sp-lg) 0 0; padding: 0; list-style: none; }
.pricing-card__includes:last-of-type { flex: 1 1 auto; }
.pricing-card__inherits + .pricing-card__includes { margin-top: var(--sp-xs); }
.pricing-card__includes li {
  position: relative;
  padding: var(--sp-xs) 0 var(--sp-xs) var(--sp-lg);
  font-size: 14px;
  font-weight: 500;
  line-height: 1.4;
  color: var(--ink-muted);
}
.pricing-card__includes li::before {
  content: "";
  position: absolute;
  left: 0; top: .55em;
  width: 12px; height: 12px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M4.5 10.5l3.6 3.6L15.5 6.7' fill='none' stroke='%2350B850' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: center;
  background-size: 12px;
}

.pricing-card__cta { margin-top: var(--sp-xl); }
.pricing-card__note { margin-top: var(--sp-sm); font-size: 12px; line-height: 1.2; color: var(--ink-muted); }


/* 11. Process — sticky index beside scrolling steps ------------------------
   The heading and the numbered index stay pinned while the six steps scroll
   past. Without JavaScript the index is still a working list of anchor links;
   JS only adds the "you are here" highlight. */

.steps-v { margin: 0; padding: 0; list-style: none; }
.step-v {
  display: grid;
  grid-template-columns: 56px minmax(0, 1fr);
  gap: var(--sp-lg);
  padding-block: var(--sp-xl);
  border-top: 1px solid var(--hairline-soft);
}
.step-v:last-child { border-bottom: 1px solid var(--hairline-soft); }
.step-v__num {
  font-size: 13px;
  font-weight: 500;
  letter-spacing: var(--track-text);
  color: var(--ink-muted);
  padding-top: 4px;
  transition: color .25s ease;
}
.step-v h3 { margin-bottom: var(--sp-xs); font-size: 18px; font-weight: 700; letter-spacing: -.02em; }
.step-v p { font-size: 15px; line-height: 1.4; color: var(--ink-muted); max-width: 54ch; }
/* Added by IntersectionObserver; absent without JS, which is fine. */
.step-v.is-active .step-v__num { color: var(--accent-ink); }

.step-index { margin: var(--sp-xl) 0 0; padding: 0; list-style: none; display: grid; gap: 2px; }
.step-index a {
  display: flex;
  gap: var(--sp-sm);
  padding: 6px 10px;
  margin-left: -10px;
  border-radius: var(--r-md);
  font-size: 13px;
  font-weight: 500;
  color: var(--ink-muted);
  text-decoration: none;
  transition: color .2s ease, background-color .2s ease;
}
.step-index a:hover { color: var(--ink); background: var(--surface-1); }
.step-index a[aria-current="true"] { color: var(--ink); background: var(--surface-1); }
.step-index span:first-child { color: var(--accent-ink); }

.process-note { margin-top: var(--sp-xl); max-width: 70ch; }

/* 12. FAQ rows — DESIGN.md §Comparison & FAQ ------------------------------- */

/* The questions are split into three labelled themes. The groups stack, and on
   wide screens each becomes a 3/9 split: theme label on the left, questions on
   the right. */
.faq-groups { counter-reset: qa; display: grid; gap: clamp(40px, 5vw, 60px); }

/* Each theme is a <details>, so it can be folded shut — see the note in
   index.html, and the folding itself in main.js. */
.faq-group { display: grid; gap: var(--sp-md) var(--sp-xxl); }
@media (min-width: 900px) {
  .faq-group { grid-template-columns: minmax(0, 3fr) minmax(0, 9fr); }
  /* Folded on a wide screen, the label takes the whole row rather than sitting
     beside a column with nothing in it. */
  .faq-group:not([open]) { grid-template-columns: minmax(0, 1fr); }
}

/* Sticky, so the theme name stays beside you as you read down its questions —
   which also gives the otherwise empty left column a job. It is the control
   too, so it carries a chevron. */
.faq-group__label {
  align-self: start;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 0 var(--sp-md);
  cursor: pointer;
  list-style: none;
}
.faq-group__label::-webkit-details-marker { display: none; }
.faq-group__label h3 {
  grid-column: 1; grid-row: 1;
  font-size: 13px;
  font-weight: 500;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--accent-ink);
}
.faq-group__label .micro { grid-column: 1; grid-row: 2; margin-top: var(--sp-xxs); }
/* The chevron: a rotated border corner rather than an image, so it needs no
   request and follows the text colour. Down when the theme is folded, up when
   it is open. */
.faq-group__label::after {
  content: "";
  grid-column: 2;
  grid-row: 1 / span 2;
  width: 8px;
  height: 8px;
  border-right: 1.5px solid var(--ink-muted);
  border-bottom: 1.5px solid var(--ink-muted);
  transform: rotate(45deg);
  transition: transform .2s ease;
}
.faq-group[open] .faq-group__label::after { transform: rotate(-135deg); }
@media (min-width: 900px) {
  .faq-group__label { position: sticky; top: 88px; }
}

.faq-group__items { border-top: 1px solid var(--hairline); }

/* On a phone the three themes arrive folded, so each is a row you tap and each
   needs its own separator — the questions inside are hidden, so the line the
   items normally draw is not there to do that job. */
@media (max-width: 899px) {
  /* Folded, the three themes are a list of rows rather than three blocks, so
     they sit closer together than the 40px the wide layout uses. */
  .faq-groups { gap: var(--sp-lg); }
  .faq-group { gap: 0; }
  .faq-group__label {
    min-height: 56px;
    padding-block: var(--sp-md);
    border-top: 1px solid var(--hairline);
  }
  .faq-group__items { border-top: 0; }
}

.qa {
  counter-increment: qa;
  border-bottom: 1px solid var(--hairline-soft);
}
.qa > summary {
  position: relative;
  display: flex;
  gap: var(--sp-md);
  padding: var(--sp-lg) 44px var(--sp-lg) 0;
  cursor: pointer;
  list-style: none;
  font-size: 17px;
  font-weight: 500;
  line-height: 1.35;
  letter-spacing: var(--track-text);
}
.qa > summary::-webkit-details-marker { display: none; }
/* The running number comes from a counter on the container, so the three groups
   read as one sequence 01–12 rather than restarting at 01 each time. */
.qa > summary::before {
  content: counter(qa, decimal-leading-zero);
  flex: none;
  padding-top: 2px;
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0;
  color: var(--accent-ink);
}
/* Once main.js has numbered the rows, its numbers win — and they have to, for a
   reason that is far from obvious: a folded theme is skipped content, and CSS
   counters are not incremented inside content that is being skipped. With the
   first theme folded, the second one restarts at 01. main.js counts the rows in
   document order instead, which survives folding and still renumbers itself if
   a question is moved between themes. The counter above remains the fallback
   for the no-JavaScript case, where nothing is ever folded. */
.faq-groups.is-numbered .qa > summary::before { content: attr(data-qa); }
.qa__mark {
  position: absolute;
  right: 0; top: 50%;
  width: 28px; height: 28px;
  margin-top: -14px;
  background: var(--surface-2);   /* reads as a control, not a white void */
  border-radius: 50%;
  transition: background-color .2s ease;
}
/* A quiet acknowledgment that the whole row is the control, not just the mark. */
.qa > summary:hover .qa__mark { background: var(--accent-tint); }
.qa__mark::before,
.qa__mark::after {
  content: ""; position: absolute; top: 50%; left: 50%;
  background: var(--accent-ink); border-radius: 2px; transform: translate(-50%, -50%);
}
.qa__mark::before { width: 12px; height: 1.5px; }
.qa__mark::after { width: 1.5px; height: 12px; transition: transform .2s ease; }
.qa[open] .qa__mark::after { transform: translate(-50%, -50%) scaleY(0); }
/* While an answer collapses it is still on screen and `[open]` is still true —
   that is the whole point of animating the close — so without this the plus
   would not come back until the animation had already finished. It has to
   out-specify `.qa[open]` above rather than rely on source order. */
.qa[open].is-closing .qa__mark::after { transform: translate(-50%, -50%) scaleY(1); }
.qa[open] { border-bottom-color: var(--hairline); }

/* Indented to clear the number column, so the answer sits under its question.
   The VERTICAL padding deliberately lives on the children rather than here, and
   that is load-bearing: this box is `box-sizing: border-box` (set globally
   above), and such an element can never be shorter than its own padding — so
   `height: 0` would still draw a 32px stub and the open/close animation in
   main.js would stop short and then snap. Padding on the children counts inside
   `height: auto` just the same, so nothing shifts at rest. `:first-child` and
   `:last-child` rather than `p`, so a list added to an answer later keeps its
   spacing. */
.qa__a {
  padding: 0 44px 0 30px;
  max-width: 84ch;
  /* JS owns the animation; the duration is declared here so the reduced-motion
     override at the foot of this file still reaches it, and so there is one
     source of truth for the timing rather than two that can drift. */
  transition: height .32s cubic-bezier(.22,.61,.36,1);
}
.qa__a > :first-child { padding-top: var(--sp-sm); }
.qa__a > :last-child  { padding-bottom: var(--sp-lg); }
.qa__a p { font-size: 15px; line-height: 1.45; color: var(--ink-muted); }
.qa__a p + p { margin-top: var(--sp-sm); }

/* Closes the section: a full-width invitation rather than a card beside the
   list, since the list now runs the full width itself. */
.faq-ask {
  display: grid;
  gap: var(--sp-lg);
  margin-top: clamp(40px, 5vw, 64px);
  padding: clamp(24px, 4vw, 40px);
  background: var(--accent-tint);
  border-radius: var(--r-xxl);
}
@media (min-width: 760px) {
  .faq-ask { grid-template-columns: minmax(0, 1fr) auto; align-items: center; }
}
.faq-ask h3 { font-size: 22px; font-weight: 700; letter-spacing: -.022em; }
.faq-ask p { margin-top: var(--sp-xs); max-width: 52ch; font-size: 15px; line-height: 1.45; color: var(--ink-muted); }

/* 13. Contact -------------------------------------------------------------- */

/* A centred single column, so the page opens on the centred hero and closes on
   the same axis. Everything between them is left-aligned, which is what makes
   the pairing read as deliberate rather than inconsistent. */
.contact-close { max-width: 44rem; margin-inline: auto; text-align: center; }
.contact-close__lead { margin-top: var(--sp-lg); }
.contact-close__actions { margin-top: var(--sp-xl); justify-content: center; }

/* Ways to reach us on one line. A bordered list beside a card made this read
   like a data table and fought everything else on the page. */
.contact-inline {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--sp-sm) var(--sp-xl);
  margin: var(--sp-xxl) 0 0;
  padding: 0;
  list-style: none;
}
.contact-inline li { display: flex; align-items: baseline; gap: var(--sp-xs); font-size: 15px; font-weight: 500; }
.contact-inline span { font-size: 13px; color: var(--ink-muted); }
.contact-inline a { text-decoration: none; }
.contact-inline a:hover { text-decoration: underline; }

.contact-form { margin-top: var(--sp-xxl); }
.contact-form > summary { list-style: none; cursor: pointer; }
.contact-form > summary::-webkit-details-marker { display: none; }
.contact-form[open] > summary { margin-bottom: var(--sp-xl); }
/* The form stays LEFT-aligned inside the centred block — centred field labels
   and inputs read badly. */
.contact-form form { max-width: 32rem; margin-inline: auto; text-align: left; }

.field { display: grid; gap: var(--sp-xs); margin-bottom: var(--sp-md); }
.field label { font-size: 13px; font-weight: 500; color: var(--ink-muted); }
.field input, .field select, .field textarea {
  width: 100%;
  padding: 10px 14px;
  background: var(--surface-2);
  border: 1px solid var(--hairline);
  border-radius: var(--r-md);
  color: var(--ink);
  font: inherit;
  font-size: 15px;
  letter-spacing: var(--track-text);
}
.field textarea { min-height: 96px; resize: vertical; }
/* Placeholder text is still text: it has to clear AA, so it uses ink-muted
   rather than a dimmer grey that would only reach 3.2:1 on surface-2. */
.field ::placeholder { color: var(--ink-muted); }
/* text-input-focused — the level-3 accent ring, surface unchanged. */
.field input:focus-visible, .field select:focus-visible, .field textarea:focus-visible {
  outline: none;
  border-color: transparent;
  box-shadow: var(--ring-selected);
}
.form-note { margin: var(--sp-lg) auto 0; max-width: 44rem; font-size: 13px; line-height: 1.5; color: var(--ink-muted); }
.form-fallback {
  margin: var(--sp-lg) auto 0; max-width: 32rem; padding: var(--sp-sm) var(--sp-md);
  background: var(--surface-1); border: 1px solid var(--hairline); border-radius: var(--r-md);
  font-size: 14px; line-height: 1.5; color: var(--ink-muted); text-align: left;
}

/* 14. Footer ---------------------------------------------------------------
   One centred column: links, then the mark at full size, then the small print.
   The logo is the point of the footer, so it gets the space. */

.site-footer {
  padding-block: clamp(40px, 5vw, 64px) clamp(32px, 4vw, 48px);
  background: var(--surface-2);
  border-top: 1px solid var(--hairline);
  text-align: center;
}

.footer-nav ul {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--sp-xs) var(--sp-lg);
  margin: 0;
  padding: 0;
  list-style: none;
}
.footer-nav a { font-size: 15px; font-weight: 500; color: var(--ink-muted); text-decoration: none; }
.footer-nav a:hover { color: var(--ink); text-decoration: underline; }

/* Sized off the 900px asset, so it stays sharp — the 520px header logo would
   visibly soften at this width. */
/* The transition is safe to put here only because this mark is deliberately NOT
   given `.reveal` — see the exclusions noted beside the reveal block. A revealed
   element's `transition` shorthand and lingering `transition-delay` would both
   land on this hover and make it sluggish. */
.footer-mark {
  display: inline-block;
  margin-top: clamp(40px, 5vw, 72px);
  transition: opacity .2s ease;
}
.footer-mark img { width: clamp(240px, 44vw, 560px); height: auto; }
.footer-mark:hover { opacity: .82; }

.footer-meta {
  margin-top: clamp(32px, 4vw, 52px);
  display: grid;
  gap: var(--sp-xs);
  font-size: 13px;
  line-height: 1.6;
  color: var(--ink-muted);
}
.footer-meta a { color: var(--ink-muted); text-decoration: none; }
.footer-meta a:hover { color: var(--ink); text-decoration: underline; }

/* 15. 404 ------------------------------------------------------------------ */

.error-page { min-height: 100vh; display: grid; align-content: center; padding-block: var(--sp-section); }
.error-page .brand { margin-bottom: var(--sp-xxl); }
.error-page h1 { margin-top: var(--sp-md); }
.error-page p { margin-top: var(--sp-lg); max-width: 46ch; }
.error-page .btn-row { margin-top: var(--sp-xl); }
.error-page__links { margin-top: var(--sp-xl); }

/* 16. Dynamic layer --------------------------------------------------------
   Everything below is progressive enhancement. The content is complete and
   readable with no JavaScript and no animation; these add movement on top. */

/* --- Scroll progress bar (in the sticky header) --------------------------- */
.progress {
  position: absolute;
  left: 0; bottom: -1px;
  height: 2px;
  width: 100%;
  transform: scaleX(var(--progress, 0));
  transform-origin: 0 50%;
  background: var(--accent);
}

/* --- Back to top ----------------------------------------------------------
   Gated on `.js` for the same reason as the rail controls further down:
   without JavaScript nothing would ever reveal it. The 2.5s safety net in
   <head> also strips `.js` again if main.js fails, so the control cannot be
   left floating with no way to hide it.
   The scroll itself needs no script — it is a plain `#top` anchor, which
   browsers resolve to the start of the document (see the note in index.html
   about why the header does not carry that id). That gives native smooth
   scrolling and correct focus handling for free. */
.to-top { display: none; }
.js .to-top {
  display: grid;
  place-items: center;
  position: fixed;
  right: clamp(16px, 4vw, 32px);
  bottom: clamp(16px, 4vw, 32px);
  z-index: 150;              /* below the header's 200, so the menu panel wins */
  width: 44px;               /* the 44px target DESIGN.md asks of every control */
  height: 44px;
  border-radius: 50%;
  background: var(--inverse-canvas);
  color: var(--inverse-ink);
  box-shadow: var(--elev-2);
  opacity: 0;
  visibility: hidden;
  transform: translateY(8px);
  /* `visibility` is a discrete property, but transitioning it holds the element
     visible for the whole fade out and flips it at the end — which is exactly
     what is wanted, and it also keeps the control out of the tab order while
     hidden. */
  transition: opacity .25s ease, visibility .25s ease, transform .25s ease,
              background-color .2s ease;
}
.js .to-top.is-visible { opacity: 1; visibility: visible; transform: none; }
.js .to-top:hover { background: #33322F; }
a.to-top, a.to-top:hover { text-decoration: none; }

/* --- Sticky split: pinned column beside scrolling content ----------------- */
.sticky-split { display: grid; gap: var(--sp-xxl); }
@media (min-width: 900px) {
  .sticky-split { grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); align-items: start; }
  .sticky-split__aside { position: sticky; top: 88px; }
}

/* --- Hero atmosphere: a soft gradient that drifts on scroll ---------------- */
.hero { position: relative; overflow: hidden; }
.hero > .wrap { position: relative; z-index: 1; }
/* The wash is composed symmetrically about the centre, so it sits behind a
   centred headline instead of drifting to one side. */
.hero__glow {
  position: absolute;
  top: -35%; left: 0;
  width: 100%; height: 150%;
  pointer-events: none;
  z-index: 0;
  /* Much softer than the dark version: on a light ground the same alpha would
     swamp the type instead of glowing behind it. */
  opacity: .2;
  background:
    radial-gradient(46% 44% at 50% 34%, rgba(106, 76, 245, .5) 0%, transparent 70%),
    radial-gradient(38% 40% at 24% 20%, rgba(212, 77, 240, .35) 0%, transparent 72%),
    radial-gradient(38% 40% at 76% 20%, rgba(80, 184, 80, .45) 0%, transparent 72%);
  filter: blur(70px);
  will-change: transform, opacity;
  transform: translate3d(0, var(--lift, 0px), 0);
  /* A slow breath, so the hero is never completely static. Animates opacity
     only — the parallax owns `transform`, and opacity on a composited layer is
     cheap to animate. */
  animation: glowBreathe 20s ease-in-out infinite;
}
@keyframes glowBreathe {
  0%, 100% { opacity: .16; }
  50%      { opacity: .30; }
}

/* --- Hero dot field -------------------------------------------------------
   One repeating background grid, revealed by a radial mask. With a real
   pointer the mask follows the cursor, so the grid lights up around it — see
   the spotlight wiring in main.js. On touch, and with JavaScript off, the mask
   stays in its broad default position and the texture is simply there. */
.hero__dots {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image: radial-gradient(rgba(17, 17, 16, .16) 1px, transparent 1px);
  background-size: 26px 26px;
  opacity: .9;
  -webkit-mask-image: radial-gradient(66% 60% at 50% 28%, #000 0%, transparent 78%);
          mask-image: radial-gradient(66% 60% at 50% 28%, #000 0%, transparent 78%);
}
@media (hover: hover) and (pointer: fine) {
  .hero__dots {
    -webkit-mask-image: radial-gradient(250px 250px at var(--mx, 50%) var(--my, 30%), #000 0%, transparent 72%);
            mask-image: radial-gradient(250px 250px at var(--mx, 50%) var(--my, 30%), #000 0%, transparent 72%);
  }
}

/* --- Ticker: an infinite marquee, pure CSS --------------------------------
   Two identical groups, each carrying its own trailing gap, so translating by
   exactly -50% lands seam-perfectly on the start of the second group. */
.ticker {
  border-block: 1px solid var(--hairline-soft);
  padding-block: 20px;
  overflow: hidden;
  mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}
.ticker__track { display: flex; width: max-content; animation: ticker 46s linear infinite; }
.ticker:hover .ticker__track { animation-play-state: paused; }
.ticker__group {
  display: flex;
  align-items: center;
  gap: 40px;
  padding: 0 40px 0 0;
  margin: 0;
  list-style: none;
  font-size: 18px;
  font-weight: 500;
  letter-spacing: var(--track-text);
  color: var(--ink-muted);
  white-space: nowrap;
}
.ticker__group li { display: flex; align-items: center; gap: 40px; }
.ticker__group li::after {
  content: "";
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--accent);
  flex: none;
}
@keyframes ticker { from { transform: translateX(0); } to { transform: translateX(-50%); } }

/* --- Figures band --------------------------------------------------------- */
.stats { display: grid; gap: var(--sp-xl); margin: 0; padding: 0; list-style: none; }
@media (min-width: 760px) { .stats { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.stat { padding-top: var(--sp-lg); border-top: 1px solid var(--hairline); }
.stat__value {
  display: block;
  font-size: var(--display-md);
  font-weight: 500;
  line-height: 1.13;
  letter-spacing: var(--track-md);
  color: var(--ink);
}
.stat__label { display: block; margin-top: var(--sp-xs); font-size: 14px; font-weight: 500; color: var(--ink-muted); }

/* --- Rail controls (added by JS; the rail itself needs none) --------------
   One pill holding previous / pause / next, rather than loose circles. */
.rail-head { display: flex; align-items: flex-end; justify-content: space-between; gap: var(--sp-lg); }

.rail-controls {
  display: none;            /* shown only when JS is running */
  flex: none;
  gap: 2px;
  padding: 4px;
  background: var(--surface-2);
  border-radius: var(--r-pill);
}
.js .rail-controls { display: flex; }

.rail-btn {
  display: inline-grid;
  place-items: center;
  width: 40px; height: 40px;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--ink);
  cursor: pointer;
  transition: background-color .2s ease, color .2s ease, opacity .2s ease;
}
.rail-btn:hover:not(:disabled) { background: var(--surface-1); }
.rail-btn:disabled { opacity: .3; cursor: default; }

/* The play/pause button is the mechanism WCAG 2.2.2 requires for content that
   moves on its own, so it reads as the primary control in the cluster. While
   the rail is running it shows a tinted pause; once stopped it goes solid ink,
   because a stopped slider is the state the visitor chose. */
.rail-btn--play { background: var(--accent-tint); color: var(--accent-ink); }
.rail-btn--play:hover { background: var(--surface-1); color: var(--ink); }
.rail-btn--play .icon-play { display: none; }
.rail-btn--play.is-paused { background: var(--ink); color: var(--inverse-ink); }
.rail-btn--play.is-paused:hover { background: #33322F; color: var(--inverse-ink); }
.rail-btn--play.is-paused .icon-pause { display: none; }
.rail-btn--play.is-paused .icon-play { display: block; }

.rail-dots { display: none; gap: 6px; margin-top: var(--sp-lg); padding: 0; list-style: none; }
.js .rail-dots { display: flex; }
.rail-dots button {
  width: 24px; height: 4px;
  padding: 0;
  border: 0;
  border-radius: 2px;
  background: var(--hairline);
  cursor: pointer;
  transition: background-color .2s ease;
}
.rail-dots button:hover { background: var(--ink-muted); }
.rail-dots button[aria-current="true"] { background: var(--accent); }

/* --- Scroll reveal --------------------------------------------------------
   The hidden state lives under `.js`, which the inline script in <head> sets.
   With JavaScript off nothing is ever hidden — the page simply appears. The
   script also reveals everything on a timer as a safety net, so content can
   never be stranded invisible if the observer fails.

   `.reveal` covers the section heading groups, the feature blocks and the
   process steps. Four things deliberately do NOT carry it, and each has a
   reason that would be easy to undo by accident:

     * The eight `.service` cards. The rail scrolls sideways, so cards 2–8 cross
       the reveal line while off-screen to the right, all at once: eight elements
       in the pending list for an animation nobody can see.
     * `.brand` and `.footer-mark`. Both already change `opacity` on hover. A
       revealed element also carries the `transition` shorthand AND a lingering
       `transition-delay`, so revealing them would make the hover start late and
       run long.
     * `.faq-group__items` — the questions inside a theme. The reveal sits on the
       theme itself (`.faq-group`) instead. A folded theme's questions are hidden,
       and revealing them means they fade in over three-quarters of a second
       *after* the theme is tapped open — which reads as a pause, not an
       entrance. The theme's own row is always rendered whether it is folded or
       not, so it reveals correctly either way, and the questions simply appear.
     * `.rail-controls`, which are `display: none` without `.js`, so they measure
       as zero and would reveal on the very first frame regardless. */
/* The reveal animates the `translate` property, not `transform` — because the
   parallax below drives `transform` on the same elements. Both apply (translate
   composes first, then transform), and a transition on one no longer cancels
   the other. Previously `.reveal.is-in { transform: none }` out-specified
   `.lift`, so every tile that both revealed AND parallaxed lost its parallax. */
.js .reveal { opacity: 0; translate: 0 22px; }
.js .reveal.is-in {
  opacity: 1;
  translate: 0 0;
  transition: opacity .75s var(--ease-reveal), translate .75s var(--ease-reveal);
  transition-delay: var(--reveal-delay, 0s);
}

/* The pricing cards both reveal AND lift on hover, so they need both
   transitions — and this has to out-specify `.js .reveal.is-in` above (0,3,0),
   which owns the `transition` shorthand on every revealed element. It is
   declared here, beside the rule it is fighting, rather than in section 10 with
   the hover state, so the reason travels with the code.
   The delays are written per property, and that is the whole point: the reveal's
   `--reveal-delay` must not leak onto the hover, or a pointer resting on the
   card would wait a beat before anything moved. */
.js .pricing-card.reveal.is-in {
  transition:
    opacity .75s var(--ease-reveal) var(--reveal-delay, 0s),
    translate .75s var(--ease-reveal) var(--reveal-delay, 0s),
    transform .25s ease 0s,
    box-shadow .25s ease 0s;
}

/* --- Floating mockup: a light parallax lift ------------------------------- */
.js .lift { transform: translateY(var(--lift, 0px)); will-change: transform; }

/* --- Work stack: the scrolling column beside the pinned aside ------------- */
.work-stack { display: grid; gap: var(--sp-lg); }
/* Inside the narrow pinned column the three metadata rows stack. */
.sticky-split__aside .work-meta { grid-template-columns: minmax(0, 1fr); gap: var(--sp-md); }

/* 17. Motion --------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
  .btn:active { transform: none; }
  /* Nothing that moves should move — but the hero keeps its texture. Reduced
     motion means less animation, not fewer visuals, so the glow and the dot
     grid stay exactly where they are and simply stop breathing. */
  .ticker__track { animation: none; }
  .hero__glow,
  .hero__dots { animation: none; }
  .js .reveal { opacity: 1; translate: none; }
  .js .lift { transform: none; }
  .services { scroll-behavior: auto; }
}
