/* =============================================================
   Sozou.

   Editorial bones, modern motion. A high-contrast serif for
   display, a geometric sans for everything functional, near-black
   on warm white, hairline rules, and a great deal of air. Then a
   motion layer over the top: things arrive as you reach them,
   surfaces have depth, and hover is an answer rather than a
   colour change.

   Two rules the motion follows, because animation that ignores
   them is worse than none:

   1. NOTHING IS HIDDEN BY DEFAULT. Reveal states are applied by
      JS only once it has confirmed IntersectionObserver works.
      A stylesheet that starts elements at opacity:0 and relies
      on script to bring them back shows a blank page to anyone
      whose JS failed to load.
   2. prefers-reduced-motion turns all of it off. Not "less of
      it" — off. Vestibular disorders are not a design taste.
   ============================================================= */

/* Palette sampled from the mark itself rather than taken from the brand
   document, because the two disagreed: the artwork is navy and the document
   lists a violet system. The artwork wins — it is what people will see.

     #0C2454   the enso and the kanji (4,406px of the mark)
     #486CFC   the spark, the single bright dot (87px)

   The paper is COOL, not the warm cream this file started with. Warm cream
   against a cool navy produces a discord that is hard to name and easy to
   feel; every neutral here leans very slightly blue so the mark sits in the
   page instead of on top of it. */
:root {
  /* — paper — */
  --bg:        #FAFAFC;
  --bg-2:      #F1F2F7;
  --surface:   #FFFFFF;
  --hair:      rgba(12, 36, 84, .12);
  --hair-2:    rgba(12, 36, 84, .22);

  /* — ink — */
  --ink:       #16181F;
  --ink-2:     #3F4453;
  --ink-3:     #626878;
  --on-ink:    #FAFAFC;

  /* — the brand. Navy carries structure; the spark is for emphasis only,
       because a spark used everywhere stops being one. — */
  --navy:      #0C2454;
  --navy-2:    #18306C;
  --spark:     #486CFC;
  --bronze:    #0C2454;              /* legacy alias, so nothing breaks mid-edit */
  --bronze-2:  #18306C;
  --bronze-soft: rgba(12, 36, 84, .07);

  --display: "Cormorant Garamond", Georgia, "Times New Roman", serif;
  --sans:    "Manrope", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --mono:    ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  --ls:   .04em;
  --r:    5px;
  --pad:  clamp(20px, 5vw, 64px);
  --maxw: 1180px;
  --measure: 34rem;

  --ease:  cubic-bezier(.22, .7, .24, 1);
  --fast:  160ms;
  --mid:   340ms;
  --slow:  700ms;

  color-scheme: light;
}

* { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* No page scrollbar (owner's call, 2026-09-14). A classic scrollbar is a
     15px strip no page can draw under, so the header and the full-bleed work
     frame both stopped 15px short of the window's right edge. Scrolling is
     untouched: wheel, trackpad, keys and touch all still work, and the 1px
     progress line (motion.js) shows position. Scoped to the root only, so
     panels that scroll on their own (the Kikuemon chat log, long lists)
     keep their scrollbars. */
  scrollbar-width: none;
}
html::-webkit-scrollbar { display: none; }   /* Safari and older Chromium, which ignore scrollbar-width */

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font: 400 16.5px/1.65 var(--sans);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;

  /* clip, NOT hidden — and this one line decides whether the sticky header
     works at all.

     Setting overflow-x: hidden forces the computed overflow-y to `auto`,
     which makes <body> a scroll container. position: sticky then has nothing
     to stick to and the bar scrolls away with the page; measured, its top was
     exactly -scrollY at every offset. `clip` suppresses the same horizontal
     overflow WITHOUT creating a scroll container, so sticky survives.

     Both are declared: a browser too old for `clip` falls back to `hidden`
     and loses only the sticky header, not the layout. */
  overflow-x: hidden;
  overflow-x: clip;
}

h1, h2, h3 {
  margin: 0;
  font-family: var(--display);
  font-weight: 300;
  letter-spacing: -.015em;
  line-height: 1.06;
}
p { margin: 0; }
a { color: inherit; text-decoration: none; }
img { max-width: 100%; display: block; }

.wrap { max-width: var(--maxw); margin: 0 auto; padding: 0 var(--pad); }
.measure { max-width: var(--measure); }

/* Off-screen but still read by a screen reader / found by find-in-page —
   for the one real <h1> a page needs when its visible heading is an h2
   (or, here, when the h1's section is deliberately a bare motion graphic). */
.vh {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* =============================================================
   The bar
   ============================================================= */

/* Transparent at rest, glass once the page moves.

   At the top there is nothing behind the bar to separate it from, so the
   material is just a band of slightly-different white across the hero. The
   moment content starts passing underneath it, the blur earns its place. */
.bar {
  position: sticky;
  top: 0;
  z-index: 50;
  background: transparent;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  transition: background var(--mid) var(--ease),
              backdrop-filter var(--mid) var(--ease),
              transform 320ms var(--ease);
  /* No rule under the bar, scrolled or otherwise. The blur already
     separates it from what passes beneath, and a hairline on top of that
     draws a line the eye has to cross on every page. */
}

/* NOT capped at --maxw. The bar spans the window so the mark sits against
   the left edge and the call to action against the right; boxing it into the
   1180px content column would float both of them in the middle of a wide
   screen with empty gutters either side. The gutter is tight for the same
   reason. */
.bar-in {
  max-width: none;
  margin: 0;
  padding: 0 clamp(16px, 2.4vw, 30px);
  height: 52px;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  gap: 20px;
}
.wordmark {
  justify-self: start;
  font-family: var(--display);
  font-size: 1.42rem;
  letter-spacing: .005em;
}
.bar nav { display: flex; gap: clamp(16px, 2.4vw, 30px); }
.bar nav a {
  position: relative;
  font-size: .9rem;
  color: var(--ink-3);
  padding: 4px 0;
  transition: color var(--fast) var(--ease);
}
.bar nav a:hover { color: var(--ink); }
/* The underline grows from the left rather than fading in. Movement
   reads as a response; a fade reads as a rendering delay. */
.bar nav a::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--mid) var(--ease);
}
.bar nav a:hover::after { transform: scaleX(1); }
.bar-cta { justify-self: end; white-space: nowrap; }

/* Phones: two rows. The one-row bar needs about 660px (mark, four links, the
   button), and below that the grid squeezed the nav on top of the wordmark and
   wrapped "Book a call" onto three lines. The four links stay visible on a
   second row rather than going behind a menu button: four short words fit
   across a 320px screen, and a link you can see beats one you have to open a
   menu to find. The mark and the call to action keep the first row at the
   usual 52px. The whole bar still hides on scroll-down, as one piece. */
@media (max-width: 760px) {
  .bar-in {
    height: auto;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-rows: 52px auto;
    grid-template-areas: "mark cta" "nav nav";
    row-gap: 0;
  }
  .bar-in > .wordmark { grid-area: mark; }
  .bar-in > .bar-cta { grid-area: cta; }
  .bar nav {
    grid-area: nav;
    justify-content: center;
    gap: clamp(18px, 6vw, 32px);
    padding-bottom: 6px;
  }
  /* 8px above and below: a 32px-tall target, not the 26px a mouse gets. */
  .bar nav a { font-size: .86rem; padding: 8px 0; }
  .bar nav a::after { bottom: 6px; }
  /* Anchors land below the taller bar, not under its second row. `html [id]`
     and not `[id]`: the base rule sits much later in this file and would win
     a tie on source order. */
  html [id] { scroll-margin-top: calc(97px + 20px); }   /* the two-row bar measures 97px */
}

/* =============================================================
   Buttons
   ============================================================= */

.btn {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  font: 500 .9rem/1 var(--sans);
  padding: 13px 22px;
  border: 1px solid var(--ink);
  border-radius: var(--r);
  color: var(--ink);
  background: none;
  cursor: pointer;
  position: relative;
  overflow: hidden;
  transition: color var(--mid) var(--ease), border-color var(--mid) var(--ease);
}
/* The fill sweeps up from the bottom edge. One transform, composited,
   no layout work — which is why it stays smooth on a phone. */
.btn::before {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--ink);
  transform: translateY(101%);
  transition: transform var(--mid) var(--ease);
  /* 0, NOT -1. `.btn` is position:relative with z-index:auto, so it is not a
     stacking context — at -1 this pseudo paints BEHIND the parent's own
     background. On a transparent button you cannot tell; on .btn-primary,
     which has an opaque background, the sweep never appears at all, and
     :hover then sets the label to var(--ink) over a var(--ink) background:
     contrast 1:1, an invisible "Book a call". It looked fine only because
     motion.js puts an inline transform on magnetic buttons, and that DOES
     make a stacking context — so the bug hid from every mouse desktop and
     showed up under prefers-reduced-motion, with JS off, and on touch.
     At 0 the pseudo paints above the background and below .btn span (z 1),
     in both cases. */
  z-index: 0;
}
.btn:hover { color: var(--on-ink); }
.btn:hover::before { transform: translateY(0); }
.btn span, .btn svg { position: relative; z-index: 1; }

.btn-primary { background: var(--ink); color: var(--on-ink); }
.btn-primary::before { background: var(--bg); }
.btn-primary:hover { color: var(--ink); }

.btn-ghost { border-color: transparent; padding-left: 4px; padding-right: 4px; }
.btn-ghost::before { display: none; }
.btn-ghost:hover { color: var(--bronze); }

.btn-sm { padding: 9px 16px; font-size: .84rem; }

/* =============================================================
   Motion — reveal on approach
   Applied by motion.js only after it confirms support, so the
   default state of every element is visible.
   ============================================================= */

/* 40px and a slight scale, not 22px and opacity.

   The first version was measured working perfectly and still went unnoticed:
   too little travel, over too long, triggered too early. An effect nobody
   perceives is pure cost — it runs, it burns frames, and it buys nothing.
   The scale is what makes it read as ARRIVING rather than merely fading. */
html.motion [data-rise] {
  opacity: 0;
  transform: translate3d(0, 40px, 0) scale(.985);
  transition: opacity 620ms var(--ease), transform 620ms var(--ease);
}
html.motion [data-rise].in {
  opacity: 1;
  transform: none;
}
/* Staggering is a CSS variable the script sets, so the delay lives with
   the element rather than in a pile of nth-child rules. */
html.motion [data-rise] { transition-delay: calc(var(--i, 0) * 85ms); }

/* =============================================================
   Hero
   ============================================================= */

/* Full viewport. svh, not vh: on a phone the URL bar collapses on first
   scroll and 100vh is measured against the EXPANDED bar, so a vh hero is
   taller than the screen and the first thing anyone does is scroll past a
   slice of nothing. */
.hero {
  position: relative;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: calc(52px + 3vh) 0 5vh;
}
/* A slow bronze wash that drifts as you scroll. Set by motion.js via
   --sy so it costs one custom property write per frame, not a reflow. */
/* This wash is what makes the transparent header mean anything.

   A transparent bar over a plain white page is indistinguishable from a white
   bar — measured: every layer beneath it was transparent down to the body, so
   it was revealing the same paper it would otherwise have painted. Something
   has to sit behind it. This does, and it deliberately starts ABOVE the hero's
   top edge so it passes under the bar rather than beginning below it. */
.hero::before {
  content: "";
  position: absolute;
  top: -46%; right: -14%;
  width: 72vw; height: 72vw;
  max-width: 900px; max-height: 900px;
  background: radial-gradient(circle at 50% 50%,
              rgba(72, 108, 252, .22), rgba(72, 108, 252, .06) 45%,
              rgba(72, 108, 252, 0) 70%);
  transform: translate3d(0, calc(var(--sy, 0) * -.06px), 0);
  pointer-events: none;
}

/* A second, cooler wash from the left, so the top of the page reads as a field
   rather than one blob in a corner. */
.hero::after {
  content: "";
  position: absolute;
  top: -30%; left: -18%;
  width: 58vw; height: 58vw;
  max-width: 720px; max-height: 720px;
  background: radial-gradient(circle at 50% 50%,
              rgba(12, 36, 84, .10), rgba(12, 36, 84, 0) 66%);
  transform: translate3d(0, calc(var(--sy, 0) * -.03px), 0);
  pointer-events: none;
}

/* overflow was hidden, which clipped both washes at the hero's own top edge —
   exactly the region the bar sits over. Visible lets them run up behind it.

   Sideways it has to be clipped here, though, because body's overflow-x:clip
   does not do it. While <html> is overflow:visible, body's value is handed to
   the viewport rather than applied to body. Measured before this line: the
   ::before's right:-14% made the home page 428px wide on a 375px phone, and
   mobile Chrome widened the layout viewport to match, which parked every
   right-anchored fixed element (the Kikuemon launcher) ~40px off-screen.
   clip rather than hidden: clip leaves overflow-y visible, and hidden would
   turn the hero into a scroll container and cut the washes off at the top again.
   Safari 15 has no clip (iPhone 6s/7/SE1 and iPad Air 2 stop there), so it gets
   the old hidden instead: the washes cut at the top edge beats a page that
   drags sideways. */
.hero { overflow: hidden; }
@supports (overflow-x: clip) {
  .hero { overflow: visible; overflow-x: clip; }
}
.hero > .wrap { position: relative; z-index: 1; }

/* =============================================================
   About page — hero enso + the portrait
   ============================================================= */

/* The about hero is a lockup and nothing else: the mark, "Sozou" under it,
   then the tagline. No button, no copy. Full height on phones too (the home
   hero drops to auto there), because here the mark IS the section.

   Every animated state below is scoped to .motion; without it (no JS,
   reduced motion) each layer's base style is its finished position, so
   the fallback is the lockup, flat and complete, not a blank. Once the
   intro has played it holds still: no idle drift, only the pointer tilt. */
/* Tucked up under the transparent header, as the home hero is, so it is
   exactly one screen: before, it started below the header yet was still
   100svh tall AND padded for the header, which sat the lockup ~52px low
   and pushed the scroll cue off the bottom. The extra bottom padding
   keeps the cue clear of the tagline and lifts the lockup a little above
   centre. --ah-bar is the header's height (two rows on phones). */
.hero.about-hero {
  --ah-bar: 52px;
  margin-top: calc(-1 * var(--ah-bar));
  min-height: 100svh;
  padding: calc(var(--ah-bar) + 2vh) 0 calc(3vh + 64px);
}
@media (max-width: 760px) { .hero.about-hero { --ah-bar: 91px; } }
.about-hero .wrap {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  text-align: center;
}

/* The ring's own sweep angle. Registered so it can animate at all; the
   initial value is past 360deg so an un-animated ring is fully shown.
   A browser without @property never resolves var(--sweep), the mask is
   invalid, and the ring is simply drawn — the right fallback. */
@property --sweep { syntax: '<angle>'; inherits: false; initial-value: 380deg; }

/* Wrappers, one job each, so no animation overwrites another's transform:
   scroll (recede as you leave) > tilt (pointer, eased) > scene (the
   arrival). Opacity, filter and mask live only on leaves, because on a
   preserve-3d element any of them flattens its children onto one plane.
   The stage is capped by height as well as width so the whole lockup
   (mark + name + tagline) fits a short laptop screen. */
.ah-stage {
  position: relative;
  width: min(64vw, 380px, 46svh);
  aspect-ratio: 1;
  container-type: inline-size;      /* cqw below = % of the stage width */
  perspective: 1300px;
}
.ah-scroll, .ah-tilt, .ah-scene { position: absolute; inset: 0; }
.ah-scroll, .ah-tilt, .ah-scene, .ah-kanji, .ah-k {
  transform-style: preserve-3d;
}
.ah-tilt {
  transform: rotateX(var(--ty, 0deg)) rotateY(var(--tx, 0deg));
  transition: transform 1.2s cubic-bezier(.2, .8, .2, 1);
}
.motion .ah-scroll {
  transform: translateZ(calc(var(--sy, 0) * -.3px)) rotateX(calc(var(--sy, 0) * .02deg));
}

/* The brush — the real one, cut from mark.png by tools/make_enso_ring.py.
   The mask sweeps clockwise from 205deg, the heavy blob under the spark,
   which is where the stroke was actually started: loaded brush up the left
   side, dry-brush tail along the bottom. Centred on the ring, not the
   canvas (the art sits 1.3% left and 0.6% high). */
.ah-ring, .ah-ghost {
  position: absolute; inset: 0; width: 100%; height: 100%;
  -webkit-mask-image: conic-gradient(from 205deg at 48.7% 49.4%,
              #000 calc(var(--sweep) - 10deg), transparent var(--sweep));
          mask-image: conic-gradient(from 205deg at 48.7% 49.4%,
              #000 calc(var(--sweep) - 10deg), transparent var(--sweep));
}
/* A soft ink shadow further back: it is what the tilt slides the ring
   across, so the depth reads even at rest. */
.ah-ghost { transform: translateZ(-16cqw) scale(1.06); filter: blur(12px); opacity: .16; }

.ah-spark {
  position: absolute;
  left: 23.2%; top: 80.2%;
  width: 22%; aspect-ratio: 1;
  margin: -11% 0 0 -11%;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(72, 108, 252, .55) 0,
              rgba(72, 108, 252, .2) 30%, rgba(72, 108, 252, 0) 68%);
  transform: translateZ(4cqw);
}
.ah-spark::after {
  content: "";
  position: absolute;
  inset: 30%;
  border-radius: 50%;
  border: .35cqw solid rgba(72, 108, 252, .7);
  opacity: 0;
}

/* 想像, live, in the logo's own face (C:\Sozou\logo\bake.py set it in Zen
   Kaku Gothic New Bold). Size, gap and offset were measured against
   mark.png's pixels, flat, including the 3% the translateZ adds: width
   and centre within 0.1% of the ring, height 2.8% over (the browser's
   rendering of the face is a touch taller than FreeType's). Owner,
   2026-09-29: first lighter (Regular), then "a more stylish modern" face,
   so the hero is now MURECHO 400 (a clean geometric Japanese sans), chosen
   over Zen Maru Gothic, Shippori Mincho and a brush script in a side-by-side;
   the in-text 想像 heading keeps Zen Kaku Gothic New 700. One link loads a
   two-glyph subset of each. */
.ah-kanji {
  position: absolute; inset: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  font: 400 18.7cqw/1 'Murecho', 'Zen Kaku Gothic New', 'Hiragino Kaku Gothic ProN', 'Yu Gothic', sans-serif;
  transform: translateY(.95cqw) translateZ(9cqw);
}
.ah-k { position: relative; display: block; }
.ah-k + .ah-k { margin-top: .01em; }
.ah-k b { position: relative; display: block; font-weight: inherit; color: #162A5A; }
.ah-k i {
  position: absolute; inset: 0;
  font-style: normal;
  color: #0B1530;
  transform: translateZ(calc(var(--z) * -.45cqw));
}

/* "Sozou" in the display serif, as in the header wordmark, set large in the
   mark's navy; the tagline in the same face's italic. The name sits a
   little into the ring's transparent margin so the lockup reads as one. */
.ah-name {
  margin: clamp(2px, 1vh, 10px) 0 0;
  font: 400 clamp(2.3rem, 4.6vw, 3.3rem)/1 var(--display);
  letter-spacing: .02em;
  color: #162A5A;
  perspective: 600px;
}
.ah-name i { display: inline-block; font-style: normal; transform-origin: 50% 90%; }
.ah-tag {
  margin: clamp(8px, 1.4vh, 14px) 0 0;
  font: italic 300 clamp(1.05rem, 1.6vw, 1.3rem)/1.3 var(--display);
  letter-spacing: .03em;
  color: var(--ink-2);
}

/* A thin line with a bead running down it: there is more below. */
.ah-cue {
  position: absolute;
  left: 50%; bottom: clamp(18px, 4vh, 36px);
  width: 1px; height: 44px;
  margin-left: -.5px;
  overflow: hidden;
  background: linear-gradient(rgba(12, 36, 84, 0), rgba(12, 36, 84, .26));
  z-index: 1;
}
.ah-cue::after {
  content: "";
  position: absolute;
  left: 0; top: -40%;
  width: 1px; height: 40%;
  background: var(--navy);
}

/* ---- the sequence (motion only) ----
   0s     the scene starts lying back, tipped and far off, and rises for
          3.4s — slow enough that the brush visibly paints on a surface
          still turning to face you (an ease-out had it upright before a
          single stroke showed, and read as flat)
   .18s   the spark lands
   .28s   the brush sweeps round from it (2.1s)
   1.35s  想, then 像, flip in from in front of the screen
   2.25s+ S-o-z-o-u flip up letter by letter under the mark
   2.4s   the ring closes; a ripple leaves the spark
   2.95s  the tagline rises in
   after  still; only the pointer tilt (about.js) moves it */
.motion .ah-scene { animation: ah-arrive 3.4s cubic-bezier(.45, 0, .15, 1) both; }
@keyframes ah-arrive {
  from { transform: rotateX(62deg) rotateZ(-26deg) translateZ(-24cqw) scale(.86); }
  to   { transform: none; }
}

.motion .ah-ring, .motion .ah-ghost {
  animation: ah-sweep 2.1s cubic-bezier(.55, .05, .35, 1) .28s both;
}
@keyframes ah-sweep { from { --sweep: 0deg; } to { --sweep: 380deg; } }

.motion .ah-spark { animation: ah-land 1s cubic-bezier(.2, .9, .3, 1.3) .18s both; }
.motion .ah-spark::after { animation: ah-ripple 1.6s cubic-bezier(.2, .7, .2, 1) 2.4s forwards; }
@keyframes ah-land {
  from { opacity: 0; scale: .2; }
  60%  { opacity: 1; scale: 1.25; }
  to   { opacity: 1; scale: 1; }
}
@keyframes ah-ripple {
  from { opacity: .9; scale: .4; }
  to   { opacity: 0; scale: 3.2; }
}

/* The characters: the transform on .ah-k (3D, so the extrusion shows
   while it turns), the fade and blur on its layers (leaves). The fade is
   short and the turn long and soft, so they are solid while still
   visibly flipping; with a long fade the flip was over before they could
   be seen. */
.ah-k:first-child { --kd: 1.35s; }
.ah-k:last-child  { --kd: 1.6s; }
.motion .ah-k { animation: ah-k-in 1.9s cubic-bezier(.3, .7, .2, 1) var(--kd) both; }
.motion .ah-k > * { animation: ah-k-fade .55s ease-out var(--kd) both; }
@keyframes ah-k-in {
  from { transform: translate3d(0, -4cqw, 50cqw) rotateX(-75deg) rotateY(25deg); }
  to   { transform: none; }
}
@keyframes ah-k-fade {
  from { opacity: 0; filter: blur(8px); }
  to   { opacity: 1; filter: none; }
}

/* The name: each letter tips up off its baseline, the same flip the
   characters make, 70ms apart. Letters are leaves, so the blur and fade
   sit on them directly. */
.motion .ah-name i {
  animation: ah-letter .9s cubic-bezier(.2, .8, .2, 1) calc(2.25s + var(--i) * .07s) both;
}
@keyframes ah-letter {
  from { opacity: 0; transform: translateY(.35em) rotateX(-85deg); filter: blur(6px); }
  to   { opacity: 1; transform: none; filter: none; }
}
.motion .ah-tag { animation: ah-rise 1s cubic-bezier(.2, .8, .2, 1) 2.95s both; }
@keyframes ah-rise {
  from { opacity: 0; transform: translateY(10px); filter: blur(4px); }
  to   { opacity: 1; transform: none; filter: none; }
}

.motion .ah-cue { animation: ah-fade .8s ease-out 3.8s both; }
.motion .ah-cue::after { animation: ah-cue 2.2s cubic-bezier(.6, 0, .4, 1) 4s infinite; }
@keyframes ah-fade { from { opacity: 0; } }
@keyframes ah-cue { to { top: 100%; } }

/* Photo card on the left, pinned while the story scrolls on the right.
   One column on phones and narrow tablets, card centred on top. Its own
   wider wrap (the site's .wrap caps content at ~1050px) so the two columns
   can spread out; the story column is still capped at 50rem so lines stay
   readable. */
.about-wrap { max-width: 1300px; }
.about-body {
  display: grid;
  grid-template-columns: 280px minmax(0, 50rem);
  gap: clamp(40px, 6vw, 100px);
  justify-content: center;
  align-items: start;
  padding-top: clamp(28px, 5vw, 72px);
}
.about-who {
  position: sticky;
  z-index: 2;                       /* the avatar's jump-out zoom paints over the story */
  top: 96px;                        /* clears the header when it slides back */
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 8px;
}
/* The avatar is a card with two faces: the photo on the front, a 3D cartoon
   of him on the back. The cartoon is ONE sprite of six frames side by side
   (hand in, raised, hand out, lowering, arm down, blink), so each frame is a
   background-position-x step: 0% 20% 40% 60% 80% 100%. Without JS it is just
   the photo: the cartoon only exists once about.js adds .aw-on. .is-digital
   turns the card round. */
.aw { position: relative; width: 180px; height: 180px; perspective: 900px; }
.aw-card { position: relative; width: 100%; height: 100%; transform-style: preserve-3d; }
html.motion .aw-card { transition: transform .8s cubic-bezier(.3, .7, .2, 1); }
.aw.is-digital .aw-card { transform: rotateY(180deg); }
.aw.aw-still .aw-card { transition: none; }
.aw-face {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  border-radius: 50%;
  object-fit: cover;
  box-shadow: 0 0 0 4px var(--bg-2);
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
}
/* No ring on the cartoon (owner). clip-path rather than overflow + radius:
   the character sways INSIDE the circle, and WebKit does not always clip a
   transformed child to a rounded corner. */
.aw-digital {
  display: none;
  box-shadow: none;
  transform: rotateY(180deg);
  background: #EEF1F9;
  clip-path: circle(50% at 50% 50%);
}
.aw-on .aw-digital { display: block; }

/* Alive, not a still: the character breathes and sways a little inside the
   circle (.aw-live), and blinks now and then (.aw-frames.idle). Scaled up 8%
   so the sway never shows the picture's edge; pivoting low, like a body. */
.aw-live { position: absolute; inset: 0; transform: scale(1.08); transform-origin: 50% 72%; }
.aw-frames { position: absolute; inset: 0; background: var(--sprite) 80% 0 / 600% 100% no-repeat; }
html.motion .aw.is-digital .aw-live { animation: aw-sway 5s ease-in-out infinite; }
html.motion .aw-frames.idle { animation: aw-blink 4.6s step-end infinite; }
html.motion .aw-frames.waving { animation: aw-wave 2.4s step-end both; }
.aw:not(.is-digital) .aw-frames { animation-play-state: paused; }
@keyframes aw-sway {
  0%, to { transform: scale(1.08) translateY(0) rotate(0deg); }
  25%    { transform: scale(1.085) translateY(-1.1%) rotate(-1.3deg); }
  50%    { transform: scale(1.095) translateY(-.3%) rotate(0deg); }
  75%    { transform: scale(1.085) translateY(-.9%) rotate(1.3deg); }
}
@keyframes aw-blink {
  0%       { background-position-x: 80%; }
  88%      { background-position-x: 100%; }
  91.5%, to { background-position-x: 80%; }
}
/* Raise the hand, wave twice, lower it, rest (then about.js switches to idle). */
@keyframes aw-wave {
  0%       { background-position-x: 80%; }
  5%       { background-position-x: 60%; }
  10%      { background-position-x: 20%; }
  18%      { background-position-x: 40%; }
  27%      { background-position-x: 20%; }
  36%      { background-position-x: 0%; }
  45%      { background-position-x: 20%; }
  54%      { background-position-x: 40%; }
  63%      { background-position-x: 20%; }
  72%      { background-position-x: 0%; }
  81%      { background-position-x: 20%; }
  88%      { background-position-x: 60%; }
  94%, to  { background-position-x: 80%; }
}

/* The first time he appears (and only then) he jumps out at you: the whole
   avatar glides up toward the screen while he waves, then eases back into
   place. A one-second eased glide in (gentle start, no overshoot), a hold, a
   gentle glide back: 3.6s in all, which about.js knows as JUMP_MS. On .aw
   itself, so it never fights the card's own rotateY. The shadow is a
   ready-painted ::before whose OPACITY fades, not an animated box-shadow:
   opacity and transform stay on the compositor, a changing shadow repaints
   every frame and stutters. */
.aw::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  box-shadow: 0 28px 44px -16px rgba(12, 36, 84, .45);
  opacity: 0;
  pointer-events: none;
}
.aw.jumping { z-index: 5; will-change: transform; animation: aw-jump 3.6s both; }
.aw.jumping::before { animation: aw-lift 3.6s both; }
/* The name sits just under the avatar, so the zoomed circle half-covers it;
   let it step aside while he is out. */
.about-who h2 { transition: opacity .35s var(--ease); }
.about-who:has(.aw.jumping) h2 { opacity: 0; }
@keyframes aw-jump {
  0%       { transform: none; animation-timing-function: cubic-bezier(.45, 0, .25, 1); }
  28%, 75% { transform: translateY(-6%) scale(1.5); }
  75%      { animation-timing-function: cubic-bezier(.45, 0, .35, 1); }
  to       { transform: none; }
}
@keyframes aw-lift {
  0%       { opacity: 0; animation-timing-function: cubic-bezier(.45, 0, .25, 1); }
  28%, 75% { opacity: 1; }
  75%      { animation-timing-function: cubic-bezier(.45, 0, .35, 1); }
  to       { opacity: 0; }
}

/* Turns him round: a small round thumbnail under the LinkedIn link of the
   OTHER face — the resting cartoon while the photo shows, the photo while the
   cartoon does. No ring and no tooltip (owner); aria-label carries the name
   for screen readers. */
.aw-btn {
  position: relative;
  display: block;
  width: 42px; height: 42px;
  margin-top: 10px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  overflow: hidden;
  background: #EEF1F9;
  box-shadow: 0 6px 16px -8px rgba(12, 36, 84, .45);
  cursor: pointer;
  transition: transform var(--fast) var(--ease), box-shadow var(--fast) var(--ease);
}
.aw-btn[hidden] { display: none; }
.aw-btn:hover { transform: scale(1.08); box-shadow: 0 8px 18px -8px rgba(12, 36, 84, .5); }
.aw-btn:focus-visible { outline: 2px solid var(--spark); outline-offset: 2px; }
.aw-btn-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
/* Their own small images (heng-thumb-*.webp, 3x the button), not the 540px
   sprite frames shrunk thirteen times on the fly, which came out pixelated. */
.aw-btn[aria-pressed="true"] .aw-btn-digital, .aw-btn[aria-pressed="false"] .aw-btn-real { display: none; }
html.motion .aw-btn.pop { animation: aw-pop .5s var(--ease) both; }
@keyframes aw-pop { from { opacity: 0; transform: translateY(6px); } }

/* The intro, only when about.js runs it (.aw-seq, and only under html.motion):
   the avatar arrives first and waves, then the story arrives as he turns
   round to the photo. Without it nothing here is hidden. */
.aw-seq .about-who { opacity: 0; transform: translateY(18px); transition: opacity .7s var(--ease), transform .7s var(--ease); }
.aw-seq.aw-seen .about-who { opacity: 1; transform: none; }
/* The LinkedIn link belongs to the real him: it waits until the card has
   turned to the photo (owner). */
.aw-seq .about-li { opacity: 0; transition: opacity .6s var(--ease); }
.aw-seq.aw-photo .about-li { opacity: 1; }
.aw-seq .about-story > * {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity .7s var(--ease), transform .7s var(--ease);
  transition-delay: calc(var(--i, 0) * 90ms);
}
.aw-seq.aw-told .about-story > * { opacity: 1; transform: none; }
.about-who h2 {
  margin: 6px 0 0;
  font: 600 1.08rem/1.3 var(--sans);
  letter-spacing: -.005em;
}
.about-body .post { max-width: none; margin: 0; padding-top: 0; }
@media (max-width: 860px) {
  .about-body { grid-template-columns: minmax(0, 34rem); gap: 28px; }
  .about-who { position: static; }
}
.about-li {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: .84rem;
  color: var(--ink-3);
  transition: color var(--fast) var(--ease);
}
.about-li svg {
  width: 15px; height: 15px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.about-li:hover { color: var(--navy); }
.about-wave { display: inline-block; font-size: 1.2em; line-height: 1; margin-right: .1em; }

/* The in-text 想像 heading, in the logo's face (same two-glyph subset). */
.post h2.about-kj {
  font: 700 1.7rem/1.2 'Zen Kaku Gothic New', 'Hiragino Kaku Gothic ProN', 'Yu Gothic', var(--sans);
  letter-spacing: .08em;
  color: #162A5A;
}

/* Full width under both columns, the rule spanning the same width. */
.post.about-cta {
  max-width: calc(280px + 50rem + clamp(40px, 6vw, 100px));
  margin: 3.2em auto 0;
  padding-top: 2.4em;
  border-top: 1px solid var(--hair);
  text-align: center;
}
.post.about-cta h2 { margin-top: 0; }   /* .post h2's margin loads later */
.about-cta-row {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px;
  margin: 1.5em 0 1.1em;
}
.about-cta-row svg {
  width: 16px; height: 16px;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.post .about-cta-alt { font-size: .86rem; color: var(--ink-3); }

/* --- About: the statement under the hero, then how we work ---
   The statement follows the MM Carz About page's second section: one tall,
   airy beat with a headline, a short paragraph and an outline button. Its
   backdrop is not a photograph: a soft sky wash, with the logo's own brushstroke
   ring ghosted large behind the words (--ring is set inline so it keeps the
   hashed asset URL). It starts and ends on the page colour, so it melts out of
   the hero above and into the page below. */
.about-mission {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  display: grid;
  place-items: center;
  min-height: 100svh;
  padding: clamp(96px, 16vh, 170px) clamp(20px, 5vw, 56px);
  text-align: center;
  background:
    radial-gradient(ellipse 65% 42% at 26% 92%, rgba(255, 255, 255, .9), rgba(255, 255, 255, 0) 70%),
    linear-gradient(180deg, rgba(250, 250, 252, 0) 0%, #E8EEFD 32%, #CBDBFB 70%, rgba(250, 250, 252, 0) 100%);
}
.about-mission::before {
  content: "";
  position: absolute;
  z-index: -1;
  top: 50%;
  right: -14%;
  width: min(118vw, 1240px, 108svh);   /* never taller than the section, so its ends fade inside it */
  aspect-ratio: 1;
  transform: translateY(-50%);
  background: var(--ring) center / contain no-repeat;
  opacity: .1;
  pointer-events: none;
  /* The ring is taller than the section, so its ends would be sliced off in a
     hard line at the seams; fade them instead. */
  -webkit-mask-image: linear-gradient(to bottom, transparent 4%, #000 24%, #000 76%, transparent 96%);
  mask-image: linear-gradient(to bottom, transparent 4%, #000 24%, #000 76%, transparent 96%);
}
.about-mission-in { max-width: 36rem; }
.about-mission h2 {
  margin: 0;
  font: 500 clamp(1.5rem, 2.5vw, 2.1rem)/1.15 var(--sans);
  letter-spacing: -.04em;
  color: var(--ink);
}
.about-mission p {
  max-width: 30em;
  margin: 1.1rem auto 0;
  font-size: clamp(.95rem, 1.05vw, 1.02rem);
  line-height: 1.75;
  color: var(--ink-2);
  text-wrap: balance;
}
.about-mission p:last-child { margin-top: 2.2rem; }

.kicker {
  font-size: .78rem;
  letter-spacing: var(--ls);
  color: var(--bronze);
  margin-bottom: 18px;
}
.hero h1 {
  font-size: clamp(2.6rem, 7vw, 5.2rem);
  line-height: 1.0;
  letter-spacing: -.022em;
  max-width: 15ch;
  text-wrap: balance;
}
.hero h1 em {
  font-style: italic;
  color: var(--navy);
}
.hero-sub {
  margin: clamp(22px, 3vw, 34px) 0 0;
  max-width: 46ch;
  font-size: 1.06rem;
  color: var(--ink-2);
  line-height: 1.7;
}
.hero-acts { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 34px; }

/* =============================================================
   Sections
   ============================================================= */

section { padding: clamp(56px, 9vw, 118px) 0; }
.sec-head { margin-bottom: clamp(30px, 5vw, 56px); }
.sec-head h2 {
  font-size: clamp(1.9rem, 4.2vw, 3rem);
  max-width: 20ch;
  text-wrap: balance;
}
.sec-head p { margin-top: 14px; max-width: 46ch; color: var(--ink-2); }

/* /projects/ only: each business's heading is its full logo (the light-
   background lockup: coloured mark + black wordmark) with the name set
   beside it after a hairline (owner, 2026-09-28: "place the words next to
   the logo"). The img is alt="" because the name is right there as text. */
.sec-head .proj-logo { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 10px clamp(14px, 2.4vw, 28px); margin: 0; max-width: none; }
.proj-logo img { display: block; height: clamp(34px, 5.4vw, 66px); width: auto; max-width: 100%; }
.proj-logo span { font-family: var(--display); font-weight: 400; font-size: clamp(1.6rem, 4.4vw, 3.1rem); line-height: 1.1; color: var(--ink); padding-left: clamp(14px, 2.4vw, 28px); border-left: 1px solid var(--hair); white-space: nowrap; }

/* /projects/ (third pass, owner, 2026-09-28): each business is a .proj
   section — logo and name, the edge-to-edge gallery, then claim-and-proof
   rows where the proof takes the larger share. */
/* The first section sits well clear of the nav (owner, 2026-09-29: "lower
   the top section further down from the header" — an earlier pass had
   pulled it up). The head itself adds nothing; the section padding is the
   gap. Logo, name and line are centred (owner, 2026-09-29). The second
   business starts after space alone, no hairline (owner, 2026-09-29). */
.proj:first-child { padding-top: clamp(64px, 10vw, 136px); }
.proj-head { padding-top: 0; text-align: center; }
.proj-head p { max-width: 62ch; margin-left: auto; margin-right: auto; }
.proj + .proj { padding-top: clamp(40px, 6vw, 80px); }

/* The gallery: pages of the live site crossfading one into the next, then a
   montage of the app. Full-bleed (outside every .wrap, no chrome, no radius).
   The stage is exactly the stills' own 16:9, so every page shows WHOLE —
   never cropped (owner, 2026-09-29: "show the full viewport"). On a short,
   wide window that makes it taller than the screen; the reader scrolls.
   The slides are stacked. The one coming in (.is-on) fades up over the one
   going out (.was-on), which stays solid underneath until it is covered, so
   there is no dip to the dark ground halfway through. Everything else is
   visibility:hidden once faded, which also takes it out of the a11y tree.
   Without JS the first slide shows and the controls stay hidden. */
.proj-gal { margin-top: 0; }
.proj-gal__stage { position: relative; width: 100%; aspect-ratio: 16 / 9; overflow: hidden; background: #0d0f14; touch-action: pan-y; container-type: size; }
.proj-gal__slide { position: absolute; inset: 0; margin: 0; opacity: 0; visibility: hidden; transition: opacity 1s ease, visibility 0s linear 1s; }
.proj-gal__slide.was-on { opacity: 1; visibility: visible; z-index: 1; transition: none; }
.proj-gal__slide.is-on { opacity: 1; visibility: visible; z-index: 2; transition: opacity 1s ease; }
.proj-gal__slide > img { display: block; width: 100%; height: 100%; object-fit: cover; object-position: top; }

/* The montage: the app's icon and name, then its screens as a staggered row
   of phones. The stage is a size container, so the phones are sized against
   BOTH its width (a 16:9 stage) and its height (a stage capped by a short,
   wide window) and never spill out of either. Below 560 wide the whole
   stage is ~210px tall: the role line and the fourth phone go. */
.proj-gal__apps { display: flex; align-items: center; justify-content: center; gap: 3.2cqw; padding: 0 4cqw;
  background: radial-gradient(90% 120% at 18% 20%, rgba(0, 85, 200, .34), transparent 60%),
              radial-gradient(70% 110% at 92% 95%, rgba(200, 16, 46, .26), transparent 62%), #0b0e16; }
.proj-gal__app { display: flex; flex-direction: column; align-items: flex-start; gap: 1.4cqh; color: #fff; max-width: 17cqw; }
.proj-gal__icon { display: block; width: clamp(40px, 16cqh, 150px); height: auto; border-radius: 22.5%; box-shadow: 0 18px 40px -18px rgba(0, 0, 0, .7); }
.proj-gal__app b { font-family: var(--display); font-weight: 400; font-size: clamp(.95rem, 4.6cqh, 2.6rem); line-height: 1.05; margin-top: 1cqh; }
.proj-gal__app span { font-size: clamp(.62rem, 1.9cqh, 1rem); letter-spacing: .08em; text-transform: uppercase; color: rgba(255, 255, 255, .62); }
.proj-gal__phones { display: flex; align-items: center; gap: 1.8cqw; }
.proj-gal__phone { display: block; height: min(31.4cqw, 66cqh); width: auto; border-radius: min(2.1cqw, 4.4cqh); box-shadow: 0 2.2cqw 4cqw -1.6cqw rgba(0, 0, 0, .75); transform: translateY(var(--dy, 0)); }
.proj-gal__phone:nth-child(even) { --dy: 7cqh; }
.proj-gal__phone:nth-child(odd) { --dy: -3.5cqh; }
@container (max-width: 560px) {
  .proj-gal__app { max-width: 24cqw; }
  .proj-gal__app span, .proj-gal__phone:nth-child(4) { display: none; }
}
/* The phones rise into place as the montage comes up. */
.proj-gal__apps .proj-gal__phone { opacity: 0; translate: 0 5cqh; transition: opacity .8s ease, translate .9s cubic-bezier(.2, .7, .2, 1); }
.proj-gal__apps.is-on .proj-gal__phone { opacity: 1; translate: 0 0; }
.proj-gal__apps.is-on .proj-gal__phone:nth-child(2) { transition-delay: .12s; }
.proj-gal__apps.is-on .proj-gal__phone:nth-child(3) { transition-delay: .24s; }
.proj-gal__apps.is-on .proj-gal__phone:nth-child(4) { transition-delay: .36s; }

/* Under the stage: the controls, then which page this is. Controls first so
   a longer label never shifts them, and all of it at the left — the far
   right is where the site's own fixed launcher sits. */
.proj-gal__bar { display: flex; align-items: center; justify-content: flex-start; gap: 8px 28px; flex-wrap: wrap; padding-top: 14px; }
.proj-gal__now { display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 12px; margin: 0; font-size: .86rem; color: var(--ink-3); min-width: 0; }
.proj-gal__site { font-weight: 600; color: var(--ink); }
.proj-gal__label { color: var(--ink-2); }
.proj-gal__count { font-variant-numeric: tabular-nums; }
.proj-gal__ctrl { display: flex; align-items: center; gap: 10px; }
.proj-gal__ctrl[hidden] { display: none; }
.proj-gal__dots { display: flex; align-items: center; }
.proj-gal__dot { width: 26px; height: 26px; padding: 0; border: 0; background: none; cursor: pointer; display: grid; place-items: center; }
.proj-gal__dot::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--ink-3); opacity: .38; transition: opacity .2s, width .3s ease; }
.proj-gal__dot[aria-current="true"]::before { width: 18px; border-radius: 4px; opacity: 1; background: var(--ink); }
.proj-gal__dot:hover::before { opacity: .8; }
.proj-gal__pause { width: 30px; height: 30px; padding: 0; border: 1px solid var(--hair); border-radius: 50%; background: var(--surface); color: var(--ink); cursor: pointer; display: grid; place-items: center; }
.proj-gal__pause svg { width: 12px; height: 12px; fill: currentColor; }
.proj-gal__pause .i-play, .proj-gal.is-paused .proj-gal__pause .i-pause { display: none; }
.proj-gal.is-paused .proj-gal__pause .i-play { display: block; }
.proj-gal__dot:focus-visible, .proj-gal__pause:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
  .proj-gal__slide, .proj-gal__slide.is-on, .proj-gal__apps .proj-gal__phone { transition: none; }
}

.proj-h2 { font-family: var(--display); font-weight: 400; font-size: clamp(1.6rem, 3.4vw, 2.1rem); margin: clamp(48px, 7vw, 96px) 0 0; }

/* Claim + proof. The proof column is the wide one (1.55fr) and the row sits in
   .wrap-wide (1520) instead of .wrap (1180), so a clip is ~900px on a laptop
   rather than ~560. Each clip keeps its own aspect — no forced crop; portrait
   proof (a chat panel, a phone screen) gets .proj-feat__media--phone instead.
   The grid children are .proj-feat__side (proof + its caption) and the text. */
.proj-feats { display: flex; flex-direction: column; gap: clamp(48px, 7vw, 96px); margin: clamp(28px, 4vw, 48px) 0 clamp(24px, 4vw, 48px); }
.proj-feat { display: grid; grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr); gap: clamp(24px, 4vw, 56px); align-items: center; }
.proj-feat--flip { grid-template-columns: minmax(0, 1fr) minmax(0, 1.55fr); }
.proj-feat--flip .proj-feat__side { order: 2; }
.proj-feat--flip .proj-feat__text { order: 1; }
.proj-feat__side { min-width: 0; }
.proj-feat__media { border-radius: 10px; overflow: hidden; border: 1px solid var(--hair); background: var(--surface); }
.proj-feat__media video, .proj-feat__media img { display: block; width: 100%; height: auto; }
.proj-feat__text { max-width: 30rem; }
.proj-feat__text h3 { font-size: 1.2rem; font-weight: 600; margin: 0 0 10px; color: var(--ink); }
.proj-feat__text p { color: var(--ink-2); line-height: 1.7; margin: 0; }
/* Portrait proof shown whole on a dark ground, instead of being cropped into a
   landscape box or stretching its row to phone height. Dark because the chat
   capture's own edges are the dark page it floated over. */
.proj-feat__media--phone { display: flex; justify-content: center; align-items: center; padding: clamp(18px, 3vw, 36px); background: #0d0f14; border-color: transparent; }
.proj-feat__media--phone video, .proj-feat__media--phone img { width: auto; max-width: 100%; height: clamp(440px, 42vw, 640px); border-radius: 14px; }
/* What was blanked out of a capture of a live system — said, not hidden. */
.proj-cap { margin: 10px 2px 0; font-size: .8rem; line-height: 1.5; color: var(--ink-3); }

/* The two "Visit" buttons, centred under the last section (the note they
   sat in was removed, owner 2026-09-29). */
.proj:last-of-type { padding-bottom: clamp(24px, 3vw, 40px); }
.proj-cta { display: flex; flex-wrap: wrap; justify-content: center; gap: 12px; margin: 0 0 clamp(56px, 8vw, 96px); }

/* A full-width proof (a row of phones) with its claim above it. */
.proj-block { margin: 0; }
.proj-block__text { display: flex; align-items: flex-start; gap: clamp(14px, 2vw, 22px); max-width: 46rem; margin-bottom: clamp(20px, 3vw, 36px); }
/* The app's own home-screen icon beside its claim (owner, 2026-09-28). */
.proj-appicon { flex: none; display: block; width: clamp(56px, 6vw, 76px); height: auto; border-radius: 22.5%; box-shadow: 0 14px 30px -16px rgba(12, 36, 84, .55); }
/* "Connected to Xero": the integration's own mark above the claim. */
.proj-with { display: flex; align-items: center; gap: 8px; margin: 0 0 12px; font-size: .78rem; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--ink-3); }
.proj-with img { display: block; width: 24px; height: 24px; }
.proj-block__text h3 { font-size: 1.2rem; font-weight: 600; margin: 0 0 10px; color: var(--ink); }
.proj-block__text p { color: var(--ink-2); line-height: 1.7; margin: 0; }
.proj-block .proj-cap { margin-top: 18px; }

/* App screens as a row of phones (--n of them, set inline). Each keeps its true
   phone proportion; the radius stands in for the handset; the row is capped so
   three phones don't blow up to a metre tall. Below 860 the row scrolls inside
   itself (snap per phone) and the page never scrolls sideways. */
.proj-phones { --n: 5; display: grid; grid-template-columns: repeat(var(--n), minmax(0, 1fr)); gap: clamp(12px, 2vw, 28px); max-width: calc(var(--n) * 270px + (var(--n) - 1) * 28px); margin: 0; padding: 0; list-style: none; }
.proj-phones li { margin: 0; }
.proj-phones img { display: block; width: 100%; height: auto; border-radius: clamp(16px, 2.2vw, 30px); border: 1px solid var(--hair); box-shadow: 0 28px 60px -34px rgba(12, 36, 84, .45); }
.proj-phones span { display: block; margin-top: 12px; font-size: .82rem; color: var(--ink-3); text-align: center; }

@media (max-width: 860px) {
  .proj-feat, .proj-feat--flip { grid-template-columns: 1fr; }
  .proj-feat--flip .proj-feat__side, .proj-feat--flip .proj-feat__text { order: unset; }
  .proj-feat__media--phone video, .proj-feat__media--phone img { height: clamp(380px, 110vw, 560px); }
  .proj-phones { grid-template-columns: none; grid-auto-flow: column; grid-auto-columns: min(62%, 260px); max-width: none; overflow-x: auto; scroll-snap-type: x mandatory; padding-bottom: 8px; scrollbar-width: thin; }
  .proj-phones li { scroll-snap-align: start; }
}

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

/* --- the offer grid --- */

.offer {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
  gap: 1px;
  background: var(--hair);
  border: 1px solid var(--hair);
  border-radius: var(--r);
  overflow: hidden;
}
.offer > article {
  background: var(--bg);
  padding: clamp(24px, 3vw, 36px);
  transition: background var(--mid) var(--ease);
}
.offer > article:hover { background: var(--surface); }
.offer h3 {
  font-family: var(--sans);
  font-weight: 600;
  font-size: .95rem;
  letter-spacing: -.005em;
  margin-bottom: 10px;
}
.offer p { color: var(--ink-2); font-size: .94rem; line-height: 1.7; }
.offer .num {
  font-family: var(--display);
  font-size: 2.4rem;
  line-height: 1;
  color: var(--bronze);
  display: block;
  margin-bottom: 14px;
}

/* --- the case-study card, with depth ---
   The tilt is tiny on purpose. Big 3D tilts look like a demo of 3D
   tilts; two degrees reads as the surface responding to you. */

.card3d {
  position: relative;
  display: block;
  background: var(--surface);
  border: 1px solid var(--hair);
  border-radius: var(--r);
  padding: clamp(28px, 4vw, 52px);
  transform-style: preserve-3d;
  transition: transform var(--mid) var(--ease), box-shadow var(--mid) var(--ease),
              border-color var(--mid) var(--ease);
  will-change: transform;
}
.card3d:hover {
  border-color: var(--hair-2);
  box-shadow: 0 30px 70px -40px rgba(12, 36, 84, .28);
}
.card3d > * { transform: translateZ(24px); }

.case-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 22px;
}
.tag {
  font-size: .74rem;
  letter-spacing: var(--ls);
  color: var(--bronze);
  background: var(--bronze-soft);
  border: 1px solid rgba(12, 36, 84, .20);
  border-radius: 999px;
  padding: 5px 12px;
}
.case h3 { font-size: clamp(1.6rem, 3.4vw, 2.4rem); margin-bottom: 14px; }
.case p { color: var(--ink-2); max-width: 52ch; line-height: 1.75; }

.stats {
  display: flex;
  flex-wrap: wrap;
  gap: clamp(24px, 5vw, 56px);
  margin-top: clamp(26px, 4vw, 40px);
  padding-top: 24px;
  border-top: 1px solid var(--hair);
}
.stat b {
  display: block;
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(1.6rem, 3vw, 2.3rem);
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
.stat span { display: block; margin-top: 6px; font-size: .8rem; color: var(--ink-3); }

/* =============================================================
   Research card row  (home)

   The MM Carz "News & insights" row, measured off the live page on
   2026-09-22 and matched: a band at least a screen tall, a centred head, then
   FOUR white cards running edge to edge, window side to window side, with
   nothing between them but a 10-16px gap, and one centred call to action
   under the row. The cards have no border, no radius and no lift; only the
   picture moves on hover, a slow zoom. What separates white cards from the
   page is the band behind them. MM Carz fades through warm greys; this fades
   through the site's own cool --bg-2, so the paper stays cool.

   Edge to edge because the grid sits OUTSIDE .wrap in home(). The head and
   the call to action stay inside it.

   Each card opens with its post's 16:9 feature image (`image:` in the post
   header). A post without one is set as type alone, not given an empty box.
   ============================================================= */

.rnews {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: clamp(40px, 6vh, 88px) 0;
  background: linear-gradient(180deg, var(--bg) 0%, var(--bg-2) 15%, var(--bg-2) 72%, var(--bg) 100%);
}
/* A flex column would shrink .wrap to its content; it has to span the band
   for margin:auto to centre the head and the call to action. */
.rnews > .wrap { width: 100%; }

.rnews-head {
  text-align: center;
  max-width: 760px;
  margin: 0 auto clamp(34px, 5vh, 60px);
}
.rnews-head h2 { margin-bottom: clamp(9px, 1.3vh, 13px); }
.rnews-head p { color: var(--ink-3); margin: 0; }

.rnews-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: clamp(10px, 1vw, 16px);
  align-items: stretch;
}
/* home() shows up to four. With fewer, the row still spans the window, in as
   many equal columns as there are cards. A lone card is capped and centred
   rather than stretched into a banner. */
.rnews-grid[data-n="3"] { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.rnews-grid[data-n="2"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.rnews-grid[data-n="1"] { grid-template-columns: minmax(0, 480px); justify-content: center; }

.rcard {
  display: flex;
  flex-direction: column;
  background: var(--surface);
  padding-bottom: clamp(30px, 3.4vh, 44px);
}

.rcard-media {
  display: block;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background: var(--hair);
}
.rcard-media img {
  display: block; width: 100%; height: 100%; object-fit: cover;
  transition: transform .9s cubic-bezier(.2, .7, .2, 1);
}
.rcard:hover .rcard-media img { transform: scale(1.045); }

.rcard-body {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  flex: 1;
  padding: clamp(22px, 2.6vh, 34px) clamp(20px, 2vw, 40px) 0;
}
.rcard h3 {
  font-family: var(--display);
  font-weight: 400;
  /* MM Carz sets 1-1.18rem in a sans-leaning serif; Cormorant's small x-height
     needs a step more to read at the same weight. */
  font-size: clamp(1.12rem, 1.3vw, 1.3rem);
  line-height: 1.2;
  margin: 0 0 clamp(8px, 1.1vh, 11px);
  /* Clamped so a long title cannot make one card taller than its neighbours;
     the min-height keeps a one-line title from shortening its own card. */
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
  min-height: 2.4em;
}
.rcard p {
  font-size: clamp(.8rem, .9vw, .86rem);
  line-height: 1.55;
  color: var(--ink-3);
  margin: 0 0 clamp(12px, 1.6vh, 16px);
  max-width: 30ch;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
  min-height: 3.1em;
}
.rcard-meta {
  display: inline-flex;
  align-items: center;
  flex-wrap: wrap;
  justify-content: center;
  gap: 8px;
  font-size: .72rem;
  letter-spacing: var(--ls);
  color: var(--ink-3);
  margin-top: auto;                 /* pins meta + Read to the card foot */
  padding-top: 4px;
  margin-bottom: clamp(16px, 2vh, 22px);
}
.rcard-meta i { width: 3px; height: 3px; border-radius: 50%; background: var(--hair-2); flex: 0 0 auto; }
.rcard-more {
  font-size: .84rem;
  font-weight: 500;
  letter-spacing: .03em;
  color: var(--ink);
  padding-bottom: 3px;
  border-bottom: 1px solid var(--ink);
  transition: color var(--fast) var(--ease), border-color var(--fast) var(--ease);
}
.rcard:hover .rcard-more { color: var(--spark); border-bottom-color: var(--spark); }

.rnews-cta {
  /* flex, not inline-flex: an inline-level box ignores `margin: auto`, which is
     what left-aligned this under a centred heading. */
  display: flex;
  align-items: center;
  gap: 10px;
  margin: clamp(28px, 4vh, 48px) auto 0;
  width: fit-content;
  font-size: .92rem;
  font-weight: 500;
  letter-spacing: .03em;
  color: var(--ink);
  padding-bottom: 5px;
  border-bottom: 1px solid var(--ink);
  transition: gap var(--mid) var(--ease), color var(--fast) var(--ease),
              border-color var(--fast) var(--ease);
}
.rnews-cta:hover { gap: 16px; color: var(--spark); border-bottom-color: var(--spark); }
.rnews-cta svg { width: 19px; height: 11px; flex: 0 0 auto; }
.rnews-empty { text-align: center; color: var(--ink-3); }

/* A short pull from /about/, not a restatement of it. */
.about-snip-wrap { display: flex; flex-direction: column; align-items: center; text-align: center; gap: clamp(16px, 3vw, 28px); }
.about-snip-mark { width: 68px; height: 68px; flex: none; mix-blend-mode: multiply; }
.about-snip-body { max-width: 62ch; }
.about-snip-lede { margin-top: 4px; font-size: 1.06rem; line-height: 1.75; color: var(--ink-2); }
.about-snip-cta { margin: clamp(20px, 3vh, 28px) auto 0; }
@media (max-width: 640px) {
  .about-snip-mark { width: 56px; height: 56px; }
}

/* The MM Carz breakpoints: two across below 1080px, one below 560px. The
   [data-n] in these selectors is what lets them beat the per-count rules. */
@media (max-width: 1080px) {
  .rnews-grid[data-n] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .rnews-grid[data-n="1"] { grid-template-columns: minmax(0, 480px); }
}
@media (max-width: 560px) {
  .rnews-grid[data-n] { grid-template-columns: 1fr; }
  .rcard p { max-width: 40ch; }
  .rcard h3 { min-height: 0; }
}

/* =============================================================
   Article
   ============================================================= */

.post { max-width: var(--measure); margin: 0 auto; padding-top: clamp(30px, 6vw, 70px); }
.post h1 { font-size: clamp(2rem, 4.8vw, 3.1rem); margin: 8px 0 18px; }
.post h2 {
  font-family: var(--sans);
  font-weight: 600;
  font-size: 1.08rem;
  letter-spacing: -.005em;
  margin: 2.4em 0 .7em;
}
.post p, .post ul, .post ol { margin: 0 0 1.2em; }
.post ul, .post ol { padding-left: 1.15em; }
.post li { margin-bottom: .45em; }
.post .meta { font-size: .84rem; color: var(--ink-3); margin: 0; letter-spacing: var(--ls); }
.post .lede { font-size: 1.14rem; color: var(--ink-2); margin: 0 0 2em; line-height: 1.72; }
/* =============================================================
   Research article  (/research/<post>/, article() in build.py)

   The MM Carz single-insight page (theme style.css, .mm-article*, and the
   live page, measured 2026-09-22), in this site's own type and colours.
   Values carried over: the 16px-radius cover at 1.9:1 with its long soft
   shadow; the hero grid (cover up to 1024px, heading 320px+, gap 40-88px),
   stacking under 900px; the side padding clamp(24px, 4vw, 75px); the
   two-column read with a 460px rail (380px under 1180, stacked under 1024);
   the 40px round tool buttons; and the slim reading bar. The cover runs
   full-bleed and square-cornered on a phone held upright.

   Departures, each for a reason: the title is Cormorant, as every display
   line on this site is, set larger to match MM Carz's visual size; the
   article column is capped at 46rem because a 1,100px line is a chore to
   read; and the tint is fixed navy-to-paper rather than sampled from each
   picture, since the build never reads pixels.
   ============================================================= */

.art { --art-pad: clamp(24px, 4vw, 75px); --bar-h: 52px; padding-bottom: clamp(60px, 9vw, 120px); }
.art-wrap { max-width: 2100px; margin: 0 auto; padding: 0 var(--art-pad); }

/* The band runs up UNDER the transparent header: pulled up by the header's
   height and padded back down by the same, as MM Carz does.

   The tint is a masked layer that fades to TRANSPARENT at its foot, not a
   background that ends on the paper colour. The cursor glow (motion.js,
   .field-glow) sits behind <main>; an opaque band hid the glow above its
   bottom edge and let it show below, so moving the mouse over the tools drew
   a hard line across the page just above them. Faded out, there is no edge. */
.art-hero {
  position: relative;
  isolation: isolate;
  margin-top: calc(-1 * var(--bar-h));
  padding: calc(var(--bar-h) + clamp(24px, 3vw, 44px)) 0 clamp(20px, 2.4vw, 32px);
}
.art-hero::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: radial-gradient(130% 160% at 12% -10%,
    color-mix(in srgb, var(--navy) 16%, #fff) 0%,
    color-mix(in srgb, var(--spark) 8%, #fff) 52%,
    color-mix(in srgb, var(--navy) 3%, #fff) 100%);
  -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 55%, transparent 100%);
          mask-image: linear-gradient(180deg, #000 0%, #000 55%, transparent 100%);
}
.art-hero-in {
  display: grid;
  grid-template-columns: minmax(0, 1024px) minmax(320px, 1fr);
  gap: clamp(40px, 4.5vw, 88px);
  align-items: center;
}
.art-hero-in:not(:has(.art-cover)) { grid-template-columns: minmax(0, 760px); }
.art-cover {
  margin: 0;
  border-radius: 16px;
  overflow: hidden;
  aspect-ratio: 1.9 / 1;
  background: var(--hair);
  box-shadow: 0 24px 60px -30px rgba(12, 22, 44, .44);
}
.art-cover img { display: block; width: 100%; height: 100%; object-fit: cover; }

.art-cat {
  display: inline-block;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .16em;
  color: var(--navy);
  margin-bottom: 16px;
}
.art-title {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(1.9rem, 2.8vw, 2.6rem);
  line-height: 1.12;
  letter-spacing: -.005em;
  margin: 0 0 16px;
}
.art-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 9px; font-size: 14px; color: var(--ink-3); margin: 0; }
.art-meta span[aria-hidden] { opacity: .5; }

/* --- reader tools --- */
.art-actions { display: flex; align-items: center; justify-content: center; gap: 4px; padding: 6px 16px 14px; }
.art-act {
  position: relative;
  height: 40px; min-width: 40px; padding: 0 11px;
  border: 0; border-radius: 20px; background: none;
  color: var(--ink-3); cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  font: 600 15px/1 var(--sans);
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}
.art-act[hidden] { display: none; }
.art-act:hover { background: var(--bg-2); color: var(--ink); }
.art-act svg { width: 20px; height: 20px; }
/* The five drawings are not the same size inside their 24-unit box, so their
   tops and bottoms sat at different heights. Measured on the painted pixels
   (4x): the bubble and headphones were 2-3px shorter than the heart and the
   arrow, and the digits sat ~0.6px above the icons. Scaled and nudged so that
   tops, bottoms and centres line up. */
.art-act[data-act="comment"] svg { transform: translateY(-.3px) scale(1.17); }
.art-act[data-act="listen"] svg { transform: scale(1.1); }
.art-act-text { gap: 0; }
/* "Aa" is one run of text so the two letters share a baseline (they used to be
   two flex items, each centred on its own line box). */
.art-aa { font-size: 19px; line-height: 1; position: relative; top: .5px; }
.art-count { font-size: 12.5px; font-weight: 600; font-variant-numeric: tabular-nums; position: relative; top: 1px; }
.art-count[hidden] { display: none; }
.art-act[data-act="like"][aria-pressed="true"] { color: #B03A48; }
.art-act[data-act="like"][aria-pressed="true"] svg { fill: currentColor; }
.art-aa small { font-size: .72em; }
.art-act[aria-pressed="true"] { color: var(--spark); }
.art-act.is-copied::after,
.art-rb-share.is-copied::after {
  content: "Link copied";
  position: absolute; left: 50%; bottom: calc(100% + 6px); transform: translateX(-50%);
  font: 600 10px/1 var(--sans); color: #fff; background: var(--ink);
  padding: 4px 8px; border-radius: 6px; white-space: nowrap; z-index: 6;
}
.art-rb-share.is-copied::after { bottom: auto; top: calc(100% + 4px); left: auto; right: 0; transform: none; }

/* --- the read: article + rail --- */
.art-cols {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 460px;
  gap: clamp(48px, 8vw, 145px);
  align-items: start;
  padding-top: clamp(34px, 4vw, 56px);
}
.art-main { min-width: 0; max-width: 1100px; }
@media (min-width: 1600px) { .art-main { max-width: none; } }
.art-lede { font-size: 1.12rem; line-height: 1.7; color: var(--ink-2); margin: 0 0 1.4em; }
.art-body { font-size: 15.5px; line-height: 1.74; color: var(--ink); }
.art-body.is-lg { font-size: 17px; line-height: 1.76; }
.art-body.is-xl { font-size: 19px; line-height: 1.8; }
.art-body > p { margin: 0 0 1.3em; }
.art-body h2 {
  font-family: var(--display); font-weight: 400;
  font-size: clamp(1.5rem, 2.3vw, 1.9rem); line-height: 1.2; letter-spacing: -.005em;
  margin: 1.7em 0 .55em;
}
.art-body h3 { font-family: var(--display); font-weight: 500; font-size: clamp(1.2rem, 1.7vw, 1.4rem); margin: 1.5em 0 .5em; }
.art-body ul, .art-body ol { margin: 0 0 1.35em; padding-left: 1.3em; }
.art-body li { margin: 0 0 .5em; }
.art-body a, .art-credit a {
  color: var(--navy);
  text-decoration: underline;
  text-decoration-color: rgba(12, 36, 84, .35);
  text-underline-offset: 3px;
  transition: text-decoration-color var(--fast) var(--ease);
}
.art-body a:hover, .art-credit a:hover { text-decoration-color: var(--navy); }
.art-body blockquote {
  margin: 1.6em 0; padding: 4px 0 4px 24px;
  border-left: 2px solid var(--ink);
  font-family: var(--display); font-size: 1.3em; line-height: 1.45;
}
.art-body blockquote p { margin: 0; }
.art-body strong { font-weight: 600; }
.art-credit { margin: 2.2em 0 0; padding-top: 1.4em; border-top: 1px solid var(--hair); font-size: 13px; color: var(--ink-3); }
/* the byline: a small "Written by" kicker (matches .art-more h2's label
   style), then two overlapping round faces and both names */
.art-author { margin: 2.4em 0 0; }
.art-author-label {
  font: 600 12px/1.3 var(--sans); text-transform: none; letter-spacing: 0;
  color: var(--ink-3); margin: 0 0 10px;
}
.art-author-row { display: flex; align-items: center; gap: 12px; }
.art-author-avs { display: flex; flex: none; }
.art-author-av {
  width: 44px; height: 44px; border-radius: 50%; object-fit: cover;
  background: var(--bg-2); box-shadow: 0 0 0 2px var(--bg);
}
.art-author-av-co { margin-left: -16px; }
.art-author-name { font: 600 15px/1.2 var(--sans); color: var(--ink); }
.art-foot { margin: 1.6em 0 0; }
.art-back {
  display: inline-flex; align-items: center; gap: 10px;
  font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: .14em; color: var(--ink);
}
.art-back:hover { color: var(--spark); }
.art-back svg { width: 22px; height: 13px; }

/* --- the rail: comments, subscribe, more research (MM Carz's order) --- */
.art-rail { display: flex; flex-direction: column; gap: clamp(30px, 3vw, 44px); min-width: 0; }
.art-rail[hidden], .art-comments[hidden] { display: none; }
/* These are <section>s, and every section on the site gets up to 118px of
   padding top and bottom; in a rail that is a hole, not breathing room. */
.art-comments, .art-more { padding: 0; }

/* comments — .mm-article__comments and friends, ported */
.art-chead { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 18px; }
.art-ctitle { display: inline-flex; align-items: center; gap: 9px; font: 600 1.15rem/1.2 var(--sans); letter-spacing: -.01em; margin: 0; color: var(--ink); }
.art-ctitle svg { width: 19px; height: 19px; color: var(--ink-3); }
.art-cn { font-size: 13px; font-weight: 600; color: var(--ink-3); font-variant-numeric: tabular-nums; }
.art-cn[hidden] { display: none; }
.art-csort { position: relative; }
.art-csort-btn { display: inline-flex; align-items: center; gap: 6px; border: 0; background: none; padding: 6px 4px; font: 12.5px var(--sans); color: var(--ink-3); cursor: pointer; }
.art-csort-btn:hover { color: var(--ink); }
.art-csort-btn svg { width: 15px; height: 15px; }
.art-csort-menu { position: absolute; top: calc(100% + 4px); right: 0; z-index: 20; min-width: 150px; padding: 6px; background: var(--surface); border: 1px solid var(--hair); border-radius: 10px; box-shadow: 0 12px 30px -10px rgba(12, 22, 44, .28); }
.art-csort-menu[hidden] { display: none; }
.art-csort-menu button { display: block; width: 100%; text-align: left; border: 0; background: none; padding: 8px 11px; border-radius: 7px; font: 13px var(--sans); color: var(--ink); cursor: pointer; }
.art-csort-menu button:hover { background: var(--bg-2); }
.art-csort-menu button.is-active { color: var(--navy); font-weight: 600; }

.art-cform { margin-bottom: 26px; }
.art-composer { display: grid; grid-template-columns: 40px minmax(0, 1fr); gap: 12px; align-items: start; }
.art-cav-wrap { position: relative; width: 40px; height: 40px; }
.art-cav-btn { position: relative; width: 40px; height: 40px; padding: 0; border: 0; background: none; cursor: pointer; }
.art-cav-circle { width: 40px; height: 40px; border-radius: 50%; background: color-mix(in srgb, var(--navy) 14%, #fff); color: var(--navy); display: inline-flex; align-items: center; justify-content: center; font: 700 16px/1 var(--sans); }
.art-cav-edit { position: absolute; right: -3px; bottom: -3px; width: 17px; height: 17px; border-radius: 50%; background: var(--ink); color: #fff; display: inline-flex; align-items: center; justify-content: center; box-shadow: 0 0 0 2px var(--bg); }
.art-cav-edit svg { width: 9px; height: 9px; }
.art-avpop { position: absolute; top: calc(100% + 8px); left: 0; z-index: 30; width: 236px; padding: 12px; background: var(--surface); border: 1px solid var(--hair); border-radius: 12px; box-shadow: 0 16px 38px -12px rgba(12, 22, 44, .32); }
.art-avpop[hidden] { display: none; }
.art-avpop-label { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: .14em; color: var(--ink-3); margin: 0 0 8px; }
.art-avpop-label + .art-avpop-row { margin-bottom: 12px; }
.art-avpop-row { display: flex; flex-wrap: wrap; gap: 6px; }
.art-avpick { width: 32px; height: 32px; border: 1px solid var(--hair); border-radius: 50%; background: var(--bg-2); font-size: 16px; line-height: 1; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; padding: 0; transition: transform .12s ease; }
.art-avpick:hover { transform: scale(1.08); }
.art-avpick.is-active { border-color: var(--ink); box-shadow: 0 0 0 2px var(--ink); }
.art-avpick [data-avpick-letter] { font: 700 14px/1 var(--sans); color: var(--ink-3); }
.art-avcolor { width: 26px; height: 26px; border: 0; border-radius: 50%; cursor: pointer; padding: 0; box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .08); transition: transform .12s ease; }
.art-avcolor:hover { transform: scale(1.12); }
.art-avcolor.is-active { box-shadow: 0 0 0 2px #fff, 0 0 0 4px var(--ink); }
.art-cfield { min-width: 0; display: flex; flex-direction: column; gap: 12px; }
.art-cinput { position: relative; }
.art-cbody { display: block; width: 100%; border: 0; border-bottom: 1px solid var(--hair-2); border-radius: 0; background: none; padding: 8px 34px 8px 2px; font: 16px/1.55 var(--sans); color: var(--ink); resize: none; min-height: 24px; overflow: hidden; }
.art-cform.is-active .art-cbody { padding: 10px 34px 10px 2px; min-height: 48px; }
.art-cbody:focus { outline: none; border-bottom-color: var(--ink); }
/* The site-wide `.experience-site :focus-visible` ring (blue box, 5px out) is
   the same specificity as the rules above and loads after them, so it won.
   These fields say "focused" with their underline going dark instead. */
.art .art-cbody:focus-visible,
.art .art-cextra input:focus-visible,
.art .art-sub input[type="email"]:focus-visible { outline: none; }
.art-cemoji { position: absolute; right: 2px; bottom: 4px; width: 26px; height: 26px; border: 0; background: none; border-radius: 50%; color: var(--ink-3); cursor: pointer; display: inline-flex; align-items: center; justify-content: center; }
.art-cemoji:hover { background: var(--bg-2); color: var(--ink); }
.art-cemoji svg { width: 18px; height: 18px; }
.art-emojipop { position: absolute; right: 0; bottom: calc(100% + 6px); z-index: 20; display: grid; grid-template-columns: repeat(6, 1fr); gap: 2px; padding: 8px; background: var(--surface); border: 1px solid var(--hair); border-radius: 12px; box-shadow: 0 12px 30px -10px rgba(12, 22, 44, .28); }
.art-emojipop[hidden] { display: none; }
.art-emojiitem { width: 30px; height: 30px; border: 0; background: none; border-radius: 8px; font-size: 17px; line-height: 1; cursor: pointer; }
.art-emojiitem:hover { background: var(--bg-2); }
.art-cextra { display: none; gap: 10px; }
.art-cform.is-active .art-cextra { display: flex; }
.art-cextra input { flex: 1; min-width: 0; border: 0; border-bottom: 1px solid var(--hair-2); background: none; padding: 7px 2px; font: 16px var(--sans); color: var(--ink); }
.art-cextra input:focus { outline: none; border-bottom-color: var(--ink); }
.art-cform-foot { display: none; align-items: center; justify-content: flex-end; gap: 10px; margin-top: 14px; }
.art-cform.is-active .art-cform-foot { display: flex; }
.art-cstatus { margin-right: auto; font-size: 12px; }
.art-cstatus.is-ok { color: #2F7D6B; }
.art-cstatus.is-err { color: #B03A48; }
.art-cstatus[hidden] { display: none; }
.art-ccancel { font: 600 12.5px var(--sans); color: var(--ink-3); background: none; border: 0; padding: 10px 12px; cursor: pointer; }
.art-ccancel:hover { color: var(--ink); }
.art-cbtn, .art-subbtn { font: 600 11px/1 var(--sans); text-transform: uppercase; letter-spacing: .14em; color: #fff; background: var(--navy); border: 0; border-radius: 8px; padding: 11px 20px; cursor: pointer; transition: opacity .2s ease; }
.art-cbtn:hover, .art-subbtn:hover { opacity: .88; }
.art-cbtn:disabled { opacity: .5; cursor: default; }
.art-cempty { font-size: 13.5px; color: var(--ink-3); font-style: italic; margin: 0; }
.art-clist { display: flex; flex-direction: column; gap: 20px; }
.art-citem { display: grid; grid-template-columns: 34px minmax(0, 1fr); gap: 12px; }
.art-cav { width: 34px; height: 34px; border-radius: 50%; background: var(--bg-2); color: var(--ink-3); display: inline-flex; align-items: center; justify-content: center; font: 700 14px/1 var(--sans); }
.art-cmain { min-width: 0; }
.art-cmeta { display: flex; align-items: baseline; gap: 8px; margin-bottom: 3px; }
.art-cauthor { font-size: 13.5px; font-weight: 600; color: var(--ink); }
.art-ctime { font-size: 11.5px; color: var(--ink-3); }
.art-ctext { font-size: 14px; line-height: 1.6; color: var(--ink); margin: 0; overflow-wrap: break-word; white-space: pre-line; overflow: clip; }
.art-citem { overflow: clip; }
.art-creport { display: inline-block; margin-top: 4px; font-size: 11px; color: var(--ink-3); text-decoration: none; opacity: 0; transition: opacity var(--fast) var(--ease); }
.art-citem:hover .art-creport, .art-creport:focus-visible { opacity: 1; }
.art-creport:hover { color: var(--ink); text-decoration: underline; }
@media (hover: none) { .art-creport { opacity: 1; } }

/* subscribe — MM Carz's plain block: no card, a rule above it, an underlined
   field and a full-width button */
.art-sub { border-top: 1px solid var(--hair); padding: clamp(24px, 3vw, 32px) 0 0; }
.art-sub h2 { font: 600 15px/1.3 var(--sans); letter-spacing: 0; text-transform: none; color: var(--ink); margin: 0 0 4px; }
.art-sub p { font-size: 12.5px; color: var(--ink-3); line-height: 1.5; margin: 0 0 14px; }
.art-sub .field { display: block; }
.art-sub .field > span { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
.art-sub input[type="email"] { width: 100%; border: 0; border-bottom: 1px solid var(--hair-2); background: none; padding: 8px 2px; font: 16px var(--sans); color: var(--ink); }
.art-sub input[type="email"]:focus { outline: none; border-bottom-color: var(--ink); }
/* Tight and left, not the full-width bar it used to be: the field above it
   already spans the rail, so a full-bleed button under it read as a stretch.
   The form is plain block flow, so a button with no explicit width just sizes
   to its label and sits at the left edge on its own. */
.art-subbtn { margin-top: 14px; width: auto; padding: 11px 26px; }
.art-sub .form-msg { font-size: 12.5px; margin: 12px 0 0; }
.art-sub .form-msg:empty { display: none; }

/* more research — thumb, title, views and age */
.art-more h2 { font: 600 12px/1 var(--sans); text-transform: uppercase; letter-spacing: .14em; color: var(--ink-3); margin: 0 0 14px; }
.art-more ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 18px; }
.art-more a { display: grid; grid-template-columns: 148px minmax(0, 1fr); gap: 14px; align-items: start; }
.art-more-th { display: block; aspect-ratio: 16 / 10; border-radius: 9px; overflow: hidden; background: var(--hair); }
.art-more-th img { display: block; width: 100%; height: 100%; object-fit: cover; transition: transform .6s cubic-bezier(.2, .7, .2, 1); }
.art-more a:hover .art-more-th img { transform: scale(1.05); }
.art-more-tx { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.art-more-tx b {
  font-weight: 600; font-size: 13.5px; line-height: 1.32; color: var(--ink);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.art-more a:hover .art-more-tx b { color: var(--navy); }
.art-more-tx > span { font-size: 12px; color: var(--ink-3); }
.art-more-tx [data-rviews][hidden] { display: none; }

/* topic pills over the list (MM Carz's "More from" bar): they scroll across,
   and a chevron shows on whichever side has more to reach */
.art-rcats { position: relative; margin: 0 0 16px; }
.art-rcats[hidden] { display: none; }
.art-rcats-track { display: flex; gap: 8px; overflow-x: auto; scroll-behavior: smooth; scrollbar-width: none; }
.art-rcats-track::-webkit-scrollbar { display: none; }
.art-pill {
  flex: 0 0 auto; white-space: nowrap; cursor: pointer;
  font: 500 12px/1 var(--sans); color: var(--ink);
  background: var(--bg-2); border: 1px solid transparent; border-radius: 999px;
  padding: 8px 14px;
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}
.art-pill:hover { background: var(--hair); }
.art-pill.is-active { background: var(--ink); color: #fff; }
.art .art-pill:focus-visible { outline-offset: 2px; }
.art-rcats-nav {
  position: absolute; top: 0; z-index: 2; width: 40px; height: 100%;
  border: 0; padding: 0 2px; cursor: pointer; color: var(--ink);
  display: inline-flex; align-items: center;
}
.art-rcats-nav[hidden] { display: none; }
.art-rcats-nav svg { width: 18px; height: 18px; }
.art-rcats-prev { left: 0; justify-content: flex-start; background: linear-gradient(90deg, var(--bg) 42%, transparent); }
.art-rcats-next { right: 0; justify-content: flex-end; background: linear-gradient(90deg, transparent, var(--bg) 42%); }

/* --- the reading bar: takes the header's place while you read down --- */
.art-readbar {
  position: fixed; top: 0; left: 0; right: 0; z-index: 60;
  background: rgba(250, 250, 252, .9);
  -webkit-backdrop-filter: saturate(180%) blur(18px); backdrop-filter: saturate(180%) blur(18px);
  border-bottom: 1px solid var(--hair);
  transform: translateY(-101%);
  transition: transform 300ms var(--ease);
}
.art-readbar.is-shown { transform: none; }
.art-rb-in {
  height: 52px; display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center; gap: 16px; padding: 0 clamp(16px, 2.4vw, 30px);
}
.art-rb-mark { justify-self: start; display: inline-flex; align-items: center; gap: 8px; }
.art-rb-mark img { mix-blend-mode: multiply; }
.art-rb-mark b { font-family: var(--display); font-weight: 400; font-size: 1.2rem; }
.art-rb-title { max-width: min(52ch, 42vw); font-size: 14px; font-weight: 600; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.art-rb-shares { justify-self: end; display: flex; align-items: center; gap: 2px; }
.art-rb-share {
  position: relative; width: 34px; height: 34px; border: 0; border-radius: 50%; background: none;
  color: var(--ink-3); cursor: pointer; display: inline-flex; align-items: center; justify-content: center;
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}
.art-rb-share[hidden] { display: none; }
.art-rb-share:hover { background: var(--bg-2); color: var(--ink); }
.art-rb-share svg { width: 15px; height: 15px; }
.art-rb-progress { position: absolute; left: 0; right: 0; bottom: -1px; height: 2px; background: var(--spark); transform: scaleX(0); transform-origin: left; }

@media (max-width: 1180px) {
  .art-cols { grid-template-columns: minmax(0, 1fr) 380px; gap: clamp(40px, 5vw, 80px); }
}
@media (max-width: 1024px) {
  .art-cols { grid-template-columns: 1fr; gap: clamp(44px, 6vw, 64px); }
}
@media (max-width: 900px) {
  .art-hero-in { grid-template-columns: 1fr; gap: 22px; }
}
/* The site header is two rows on phones (see .bar), 97px tall. */
@media (max-width: 760px) {
  .art { --bar-h: 97px; }
}
@media (max-width: 680px) {
  .art-rb-title { max-width: 100%; font-size: 12.5px; }
  .art-rb-in { grid-template-columns: auto minmax(0, 1fr) auto; gap: 10px; padding: 0 6px 0 12px; }
  .art-rb-mark b { display: none; }
}
/* A phone held upright: the cover runs full-bleed and square-cornered, at the
   picture's own shape, and the gutters tighten to 20px. */
@media (orientation: portrait) and (max-width: 599px) {
  .art { --art-pad: 20px; }
  .art-cover { margin: 0 -20px; border-radius: 0; aspect-ratio: auto; box-shadow: none; }
  .art-cover img { height: auto; }
  .art-cols { padding-top: 12px; }
  .art-more a { grid-template-columns: 120px minmax(0, 1fr); }
}
@media (prefers-reduced-motion: reduce) {
  .art-readbar { transition: none; }
  .art-more-th img { transition: none; }
}
.post a:not(.btn) {
  color: var(--bronze);
  border-bottom: 1px solid rgba(12, 36, 84, .32);
  transition: border-color var(--fast) var(--ease);
}
.post a:not(.btn):hover { border-bottom-color: var(--bronze); }
.post blockquote {
  margin: 1.8em 0;
  padding-left: 20px;
  border-left: 2px solid var(--bronze);
  color: var(--ink-2);
  font-style: italic;
}
.post pre {
  overflow-x: auto;
  background: var(--bg-2);
  border: 1px solid var(--hair);
  border-radius: var(--r);
  padding: 16px 18px;
  font-size: .85rem;
  line-height: 1.6;
}
.post code { font-family: var(--mono); font-size: .89em; }
.post :not(pre) > code {
  background: var(--bg-2);
  padding: .13em .38em;
  border-radius: 3px;
}

/* =============================================================
   Forms
   ============================================================= */

.formbox {
  max-width: var(--measure);
  margin: clamp(48px, 7vw, 88px) auto 0;
  padding-top: clamp(28px, 4vw, 44px);
  border-top: 1px solid var(--hair);
}
.formbox h2 { font-size: 1.6rem; margin-bottom: 8px; }
.formbox p.sub { color: var(--ink-2); margin-bottom: 20px; font-size: .95rem; }

/* Rules, not boxes. The label is the placeholder, so each field is one line of
   type over one hairline — the MM Carz contact treatment. The visible label is
   gone but an aria-label is on every input, because a placeholder disappears
   the moment you type and a field a screen reader cannot name is not a field.
   No border-radius: a rounded box is exactly what this is not. */
.field { display: block; margin-bottom: 18px; }
.field span {
  display: block;
  font-size: .78rem;
  letter-spacing: var(--ls);
  color: var(--ink-3);
  margin-bottom: 6px;
}
.field input, .field textarea {
  width: 100%;
  font: inherit;
  color: var(--ink);
  background: none;
  border: 0;
  border-bottom: 1px solid var(--hair);
  border-radius: 0;
  padding: 10px 2px;
  transition: border-color var(--fast) var(--ease);
}
.field textarea { resize: vertical; min-height: 92px; }
.field input::placeholder, .field textarea::placeholder { color: var(--ink-3); opacity: 1; }
.field input:focus, .field textarea:focus {
  outline: none;
  border-bottom-color: var(--ink);
}
/* The rule is the only affordance the field has, so it has to survive an
   invalid value being pointed at. */
.field input:user-invalid, .field textarea:user-invalid { border-bottom-color: #A3242B; }
.row { display: flex; gap: 14px; flex-wrap: wrap; }
.row .field { flex: 1 1 210px; }

/* =============================================================
   Common Questions

   The MM Carz homepage FAQ: a native <details> accordion in two
   columns, hairline-divided, chevron turning on open. Native
   <details> rather than a hand-rolled panel because it opens with
   no JS, is reachable by the browser's own find-in-page (a closed
   custom accordion hides its text from Ctrl-F), and is keyboard-
   operable for free.
   ============================================================= */

.faq-head {
  text-align: center;
  max-width: 760px;
  margin: 0 auto clamp(34px, 5vw, 60px);
}
.faq-eyebrow {
  display: block;
  font-size: .74rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .22em;
  color: var(--ink-3);
  margin-bottom: clamp(12px, 1.8vw, 18px);
}
.faq-head h2 { margin: 0; }

.faq-list {
  display: grid;
  grid-template-columns: 1fr 1fr;
  column-gap: clamp(40px, 5vw, 88px);
  align-items: start;
}
/* The rule belongs to each column, not to the list: the two columns end at
   different heights once anything is open, and one border across the top of a
   grid that no longer has aligned rows reads as a stray line. */
.faq-col { border-top: 1px solid var(--hair); }
.faq-item { border-bottom: 1px solid var(--hair); }

.faq-q {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: clamp(18px, 3vw, 36px);
  padding: clamp(18px, 2.4vw, 26px) 0;
  font-size: 1rem;
  font-weight: 500;
  color: var(--ink);
  transition: opacity var(--fast) var(--ease);
}
/* Safari still paints its own triangle without this, and it sits outside the
   flex row rather than in it. */
.faq-q::-webkit-details-marker { display: none; }
.faq-q::marker { content: ""; }
.faq-q:hover { opacity: .62; }
/* :focus-visible, not :focus-within — a mouse click focuses the <summary>
   too, and that used to draw the same ring as a keyboard tab (owner,
   2026-09-28: "remove the blue border once clicked on a common question").
   Keyboard users still get it; a click no longer does. */
.faq-q:focus-visible { outline: 2px solid var(--spark); outline-offset: 3px; }

/* Chevron drawn from two borders rather than shipped as an icon: one element,
   no request, and it turns rather than swapping. */
.faq-ic { position: relative; flex: 0 0 15px; width: 15px; height: 15px; }
.faq-ic::before {
  content: "";
  position: absolute;
  left: 50%; top: 42%;
  width: 8px; height: 8px;
  border-right: 1.5px solid var(--ink-2);
  border-bottom: 1.5px solid var(--ink-2);
  transform: translate(-50%, -50%) rotate(45deg);
  transition: transform var(--mid) var(--ease), top var(--mid) var(--ease);
}
.faq-item[open] .faq-ic::before { top: 58%; transform: translate(-50%, -50%) rotate(-135deg); }

.faq-a { color: var(--ink-2); padding: 0 0 clamp(20px, 2.8vw, 28px); max-width: 62ch; }
.faq-a p { margin: 0; font-size: .95rem; line-height: 1.72; }
/* Seventy-four language names in a row of body copy reads as a wall. Set a
   size down and let it breathe so it scans as a list rather than a sentence
   somebody has to finish. */
.faq-langs { display: block; margin: .5em 0; font-size: .89rem; color: var(--ink-3); }

@media (max-width: 820px) {
  .faq-list { grid-template-columns: 1fr; column-gap: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .faq-ic::before { transition: none; }
}

/* =============================================================
   Book a call — two columns, calendar, time picker, segmented pills
   ============================================================= */

/* [hidden] is only a UA rule of `display: none`, and ANY class rule that sets
   display beats it on specificity. These set display (or may), so hiding them
   by attribute silently does nothing unless it is said again here. This is not
   cosmetic: the booking column ships hidden and is revealed by book.js, and the
   platform picker has to stay gone during a phone call. */
.bk-col[hidden], .bk-seg[hidden], .field[hidden] { display: none; }

/* Wider than the --measure every other form box keeps: this one holds two
   columns with an "or" between them. The section's wrap is let out a little
   for it, so the gap can grow without taking the room out of the calendar.
   The heading and its line centre over both. */
#contact > .wrap { max-width: 1240px; }
.bk-box { max-width: 1120px; }
.bk-head {
  text-align: center;
  max-width: 40rem;
  margin: 0 auto clamp(30px, 4.5vw, 52px);
}
/* p.sub, not .sub: .formbox p.sub carries an element and outranks a bare class. */
.bk-head p.sub { margin-bottom: 0; }

/* Pick a time | or | Send a message, one row. minmax(0, 1fr) rather than 1fr so
   the calendar's min-content cannot push its column past half. --bk-gap sits
   either side of the "or"; --bk-rule is where .bk-col-head draws its hairline
   (its line box plus its padding), so the divider can carry that line across. */
.bk-cols {
  --bk-gap: clamp(28px, 3.6vw, 60px);
  --bk-rule: calc(.9rem * 1.2 + 14px);   /* = .bk-col-head font-size x line-height + padding-bottom */
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  column-gap: var(--bk-gap);
  row-gap: 0;
  align-items: start;
}

/* The "or": a badge sitting on the column heads' hairline, carrying that line
   across the gap, with a thread down the middle that fades out well before the
   forms end. Decorative and aria-hidden, since each form already has its own
   name. */
.bk-or {
  --or: 40px;
  position: relative;
  align-self: stretch;
  display: flex;
  flex-direction: column;
  align-items: center;
  width: var(--or);
  padding-top: calc(var(--bk-rule) + .5px - var(--or) / 2);
}
/* The line is cut in the middle (a transparent stretch either side of the
   word) rather than hidden behind a patch of page colour, so it also works on
   the tinted section background. 24px = half the 40px badge plus a little air. */
.bk-or::before {
  content: "";
  position: absolute;
  top: var(--bk-rule);
  left: calc(-1 * var(--bk-gap));
  right: calc(-1 * var(--bk-gap));
  height: 1px;
  background: linear-gradient(to right,
    var(--hair) calc(50% - 24px), transparent calc(50% - 24px),
    transparent calc(50% + 24px), var(--hair) calc(50% + 24px));
}
.bk-or::after {
  content: "";
  flex: 1 1 auto;
  width: 1px;
  margin-top: 12px;
  background: linear-gradient(to bottom, var(--hair-2), transparent 75%);
}
/* No ring and no patch (the owner took both off): just the word. The hairline
   running across the gap stops either side of it (see ::before) instead of
   striking through it. */
.bk-or span {
  position: relative;
  flex: 0 0 auto;
  width: var(--or);
  height: var(--or);
  display: grid;
  place-items: center;
  font: italic 400 1.35rem/1 var(--display);
  color: var(--ink-3);
  padding-bottom: 3px;   /* optical: an italic lowercase "or" sits low in its box */
}

/* No JS means no booking column: the message form alone, at the width one form
   reads at, rather than stretched across the room left for two — and nothing to
   say "or" to. */
.bk-cols:has(> .bk-col[hidden]) {
  grid-template-columns: minmax(0, 1fr);
  max-width: var(--measure);
  margin-inline: auto;
}
.bk-cols:has(> .bk-col[hidden]) > .bk-or { display: none; }

/* Stacked, the "or" turns on its side: a rule, the word, a rule. */
@media (max-width: 900px) {
  .bk-cols { grid-template-columns: minmax(0, 1fr); max-width: var(--measure); margin-inline: auto; }
  .bk-or {
    flex-direction: row;
    gap: 16px;
    width: auto;
    align-self: auto;
    padding: 0;
    margin: clamp(44px, 10vw, 64px) 0;
  }
  .bk-or::before { position: static; flex: 1 1 auto; background: var(--hair); }
  .bk-or::after { width: auto; height: 1px; margin: 0; background: var(--hair); }
}

/* Each column lays its insides out against its OWN width. Whether the calendar
   fits beside the time picker depends on the column, and the column is a
   different share of the window either side of 900px. */
.bk-col { container-type: inline-size; }

/* The Common Questions eyebrow, ruled underneath the way its columns are. */
/* Sentence case at the owner's request, not the uppercase eyebrow it started as,
   so it is set a little larger and without the tracking capitals need. Its size,
   line-height and padding feed --bk-rule on .bk-cols: change them together. */
.bk-col-head {
  font: 500 .9rem/1.2 var(--sans);
  letter-spacing: .01em;
  color: var(--ink-2);
  text-align: center;
  margin: 0 0 22px;
  padding-bottom: 14px;
  border-bottom: 1px solid var(--hair);
}

.bk-col > button[type="submit"] { display: flex; width: fit-content; margin: 6px auto 0; }
.bk-col .form-msg { text-align: center; }

.bk-lede {
  color: var(--ink-3);
  font-size: .88rem;
  line-height: 1.6;
  text-align: center;
  margin: 0 0 20px;
}

.bk-grid {
  display: grid;
  /* The time track never drops under 190px: below that "Choose a time" wraps
     inside the field. The calendar gives up the width instead, down to the
     440px column where the two stack. */
  grid-template-columns: minmax(0, 250px) minmax(190px, 1fr);
  gap: clamp(18px, 3vw, 34px);
  margin-bottom: 24px;
}

.bk-calhead {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
}
.bk-month { font-size: .9rem; letter-spacing: var(--ls); }
.bk-nav {
  width: 28px; height: 28px;
  display: grid; place-items: center;
  background: none;
  border: 1px solid var(--hair);
  border-radius: 50%;
  color: var(--ink-2);
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
  transition: border-color var(--fast) var(--ease), color var(--fast) var(--ease);
}
.bk-nav:hover:not(:disabled) { border-color: var(--ink); color: var(--ink); }
.bk-nav:disabled { opacity: .3; cursor: default; }

.bk-dow, .bk-days { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
.bk-dow span {
  text-align: center;
  font-size: .68rem;
  letter-spacing: var(--ls);
  color: var(--ink-3);
  padding-bottom: 5px;
}
.bk-day {
  aspect-ratio: 1;
  display: grid;
  place-items: center;
  background: none;
  border: 0;
  border-radius: 50%;
  font: inherit;
  font-size: .82rem;
  color: var(--ink);
  cursor: pointer;
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}
.bk-day:hover:not(:disabled):not(.on) { background: var(--bg-2); }
.bk-day:disabled { color: var(--ink-3); opacity: .32; cursor: default; }
.bk-day.on { background: var(--navy); color: var(--on-ink); }

.bk-times-head {
  font-size: .78rem;
  letter-spacing: var(--ls);
  color: var(--ink-3);
  margin: 0 0 10px;
}
/* The times are a real <select>: the phone's own wheel on a phone, keyboard-
   operable for free, one control in place of thirteen buttons. The closed
   control is drawn to sit with the round days and pills around it rather than
   as a bare rule: a soft white field with a clock and a chevron that says which
   state it is in — dashed until a day is picked, grey while it asks, navy once
   it has a time. The states come off :has() on the checked option, so no JS
   has to keep a class in step. */
.bk-select { position: relative; display: block; margin-bottom: 10px; color: var(--ink-3); }
.bk-select select {
  width: 100%;
  font: 500 1rem/1.25 var(--sans);   /* 16px: under that, iOS Safari zooms the page on tap */
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  background: #fff;
  border: 1px solid var(--hair-2);
  border-radius: 12px;
  padding: 11px 36px 11px 40px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  box-shadow: 0 1px 2px rgba(12, 36, 84, .05);
  -webkit-appearance: none;
  appearance: none;
  cursor: pointer;
  transition: border-color var(--fast) var(--ease), box-shadow var(--fast) var(--ease),
              background var(--fast) var(--ease), color var(--fast) var(--ease);
}
.bk-select select:hover:not(:disabled) {
  border-color: var(--ink-3);
  box-shadow: 0 8px 20px -12px rgba(12, 36, 84, .35);
}
/* A transparent outline rather than none: forced-colors mode (Windows contrast
   themes) repaints it in a system colour, so focus never vanishes there. The
   visible ring is an outline, not the border, because the chosen state below
   is a :has() rule that outranks a plain :focus-visible border and hid it. */
.bk-select select:focus { outline: 2px solid transparent; outline-offset: 2px; }
.bk-select select:focus-visible { outline: 2px solid var(--spark); outline-offset: 2px; }
.bk-select:has(option[value=""]:checked) select { color: var(--ink-3); font-weight: 400; }
.bk-select:has(option:checked:not([value=""])) { color: var(--navy); }
.bk-select:has(option:checked:not([value=""])) select { border-color: var(--navy); color: var(--navy); }
.bk-select select:disabled {
  background: transparent;
  border-style: dashed;
  box-shadow: none;
  cursor: not-allowed;
}
/* The clock is one stroked SVG used as a mask, so it simply takes the label's
   colour — grey while asking, navy once chosen — with no second asset. */
.bk-select::before {
  content: "";
  position: absolute;
  left: 15px; top: 50%;
  width: 17px; height: 17px;
  transform: translateY(-50%);
  background: currentColor;
  -webkit-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='9'/><path d='M12 7.5V12l3 2'/></svg>") center / contain no-repeat;
  mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='9'/><path d='M12 7.5V12l3 2'/></svg>") center / contain no-repeat;
  pointer-events: none;
  z-index: 1;
}
.bk-select::after {
  content: "";
  position: absolute;
  right: 17px; top: 50%;
  width: 7px; height: 7px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: translateY(-70%) rotate(45deg);
  transition: transform var(--mid) var(--ease);
  pointer-events: none;
  z-index: 1;
}
.bk-select:has(select:disabled)::before,
.bk-select:has(select:disabled)::after { opacity: .45; }

/* Where the browser lets a select be styled all the way down (customizable
   select: Chromium now, WebKit arriving) AND the visitor has a mouse or
   trackpad, the list gets the same treatment: a floating card of time chips,
   three to a row, the chosen one filled navy like a chosen day. Touch devices
   keep the platform's own list on purpose, because a phone's wheel beats a
   card of small chips; browsers without customizable select keep theirs too.
   book.js makes the arrow keys follow the grid (the native listbox only steps
   through it in a line) and renders the placeholder as a disabled option, so
   it can be neither focused nor clicked to wipe a chosen time. Gated on
   @supports because base-select changes how the control lays itself out. */
@supports (appearance: base-select) { @media (hover: hover) and (pointer: fine) {
  .bk-select select,
  .bk-select select::picker(select) { appearance: base-select; }
  .bk-select select::picker-icon { display: none; }
  .bk-select select:open { border-color: var(--navy); box-shadow: 0 0 0 4px rgba(12, 36, 84, .08); }
  .bk-select:has(select:open)::after { transform: translateY(-25%) rotate(-135deg); }
  .bk-select select::picker(select) {
    margin-block: 6px;
    padding: 8px;
    min-width: max(anchor-size(width), 268px);
    max-height: min(330px, 60vh);
    border: 1px solid var(--hair);
    border-radius: 16px;
    background: #fff;
    box-shadow: 0 24px 48px -20px rgba(12, 36, 84, .35), 0 2px 8px rgba(12, 36, 84, .06);
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 4px;
  }
  .bk-select option {
    justify-content: center;
    padding: 10px 4px;
    border-radius: 10px;
    font: 500 .88rem/1 var(--sans);
    font-variant-numeric: tabular-nums;
    color: var(--ink);
    white-space: nowrap;
    cursor: pointer;
    transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
  }
  .bk-select option::checkmark { display: none; }
  .bk-select option:hover { background: var(--bg-2); }
  .bk-select option:checked { background: var(--navy); color: var(--on-ink); }
  /* The placeholder becomes the card's caption, across the top of the chips.
     It is a disabled option (book.js), so it takes no focus and no click. */
  .bk-select option[value=""] {
    grid-column: 1 / -1;
    justify-content: flex-start;
    padding: 6px 8px 10px;
    font: 600 .68rem/1.2 var(--sans);
    text-transform: uppercase;
    letter-spacing: .18em;
    color: var(--ink-3);
    background: none;
    cursor: default;
  }
  /* Last, so neither the chosen fill nor the caption reset can hide it. */
  .bk-select option:focus-visible { outline: 2px solid var(--spark); outline-offset: -2px; }
} }
.bk-tz { margin: 0; font-size: .74rem; line-height: 1.5; color: var(--ink-3); }

/* Segmented radio pills. The input is the state and the span is the surface —
   :checked + span keeps it working with no JS, which is how the MM Carz
   contact-method toggle is built. */
.bk-seg {
  border: 0; margin: 0 0 18px; padding: 0;
  display: flex; flex-wrap: wrap; gap: 8px;
  align-items: center; justify-content: center;
  text-align: center;
}
.bk-seg legend {
  float: left;
  width: 100%;
  font-size: .78rem;
  letter-spacing: var(--ls);
  color: var(--ink-3);
  margin-bottom: 9px;
  padding: 0;
  text-align: center;
}
/* A zero-height full-width flex item: it ends the row wherever it sits, so the
   five platforms break 3 + 2 instead of wherever the container happens to run
   out. Both rows still centre, which is the point — letting them wrap naturally
   left the last one stranded on its own. */
.bk-break { flex-basis: 100%; height: 0; margin: 0; }
.bk-pill input {
  position: absolute;
  opacity: 0;
  width: 1px; height: 1px;
}
.bk-pill span {
  display: inline-block;
  padding: 8px 16px;
  border: 1px solid var(--hair);
  border-radius: 999px;
  font-size: .84rem;
  color: var(--ink-2);
  cursor: pointer;
  transition: border-color var(--fast) var(--ease), background var(--fast) var(--ease),
              color var(--fast) var(--ease);
}
.bk-pill span:hover { border-color: var(--hair-2); }
.bk-pill input:checked + span { background: var(--navy); border-color: var(--navy); color: var(--on-ink); }

/* Platform pills carry the real marks, because "Teams" as a word is one more
   thing to read and the logo is recognised before it is read. The mark sits in
   its own brand colour while the pill is plain, and goes white once the pill
   fills with navy — a coloured logo on a navy ground is where these usually
   fall down. Kept small and monochrome-friendly: single-path marks, no
   gradients, so the swap is one property. */
.bk-pill-logo span { display: inline-flex; align-items: center; gap: 8px; }
.bk-pill-logo svg { width: 15px; height: 15px; flex: 0 0 auto; fill: var(--logo); transition: fill var(--fast) var(--ease); }
.bk-pill-logo input:checked + span svg { fill: currentColor; }
/* Zoom's mark is a wordmark, not a symbol. At icon size it is unreadable and it
   repeats the label beside it, so it gets the room to be the label — and the
   word stays in the DOM, visually hidden, so the control still announces
   itself. */
.bk-pill-mark svg { width: 46px; height: 22px; }
.hp-vis { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.bk-pill input:focus-visible + span { outline: 2px solid var(--spark); outline-offset: 2px; }

/* Against the column (.bk-col is the container), not the window. Stacked, the
   calendar is capped: a full-width month at 540px draws 77px day circles. */
@container (max-width: 440px) {
  .bk-grid { grid-template-columns: minmax(0, 1fr); }
  .bk-cal, .bk-times { width: 100%; max-width: 320px; margin-inline: auto; }
}
/* Browsers older than container queries (iOS 15 and earlier) would keep the
   calendar and the dropdown side by side in a phone-width column and squeeze
   the dropdown to a sliver, so they split on the window instead. */
@supports not (container-type: inline-size) {
  @media (max-width: 620px) {
    .bk-grid { grid-template-columns: minmax(0, 1fr); }
    .bk-cal, .bk-times { width: 100%; max-width: 320px; margin-inline: auto; }
  }
}

.form-msg { margin: 12px 0 0; font-size: .9rem; min-height: 1.2em; }
.form-msg.ok  { color: #2B6A45; }
.form-msg.bad { color: #A3242B; }

/* Off-screen, not display:none — some bots skip hidden fields, and one
   that is technically rendered catches more of them. */
.hp {
  position: absolute !important;
  left: -9999px !important;
  width: 1px; height: 1px; opacity: 0;
}

/* --- Book a call as a popup ---
   Every page but the homepage carries the booking form in a <dialog> (page() in
   build.py), and the header CTA opens it in place (book.js) instead of sending
   the visitor to /#contact. It is the SAME markup as the homepage's section, so
   the .bk-* rules above do all the work inside it. */
.bk-dialog {
  width: min(1120px, calc(100vw - 32px));
  max-width: none;
  max-height: calc(100dvh - 32px);
  margin: auto;
  padding: clamp(30px, 4vw, 52px) clamp(20px, 4vw, 56px) clamp(26px, 3.4vw, 44px);
  border: 0;
  border-radius: 16px;
  background: var(--bg);
  color: var(--ink);
  box-shadow: 0 40px 90px -24px rgba(12, 36, 84, .4), 0 0 0 1px var(--hair);
  overflow-y: auto;
  overscroll-behavior: contain;
}
.bk-dialog::backdrop {
  background: rgba(12, 36, 84, .45);
  -webkit-backdrop-filter: blur(3px);
  backdrop-filter: blur(3px);
}
.bk-dialog[open] { animation: bk-pop .24s var(--ease) both; }
.bk-dialog[open]::backdrop { animation: bk-fade .24s var(--ease) both; }
@keyframes bk-pop { from { opacity: 0; transform: translateY(14px) scale(.985); } }
@keyframes bk-fade { from { opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
  .bk-dialog[open], .bk-dialog[open]::backdrop { animation: none; }
}
/* The page behind stops scrolling. The gutter is kept so the sticky header does
   not shift sideways when the scrollbar goes. */
html:has(.bk-dialog[open]) { overflow: hidden; scrollbar-gutter: stable; }

/* Sticky and floated, so it stays in the corner while a phone-height dialog
   scrolls, and the negative margins pull it up into the padding. */
.bk-dialog-x {
  position: sticky;
  top: 0;
  float: right;
  z-index: 2;
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  margin: -16px -12px -22px 0;
  padding: 0;
  border: 1px solid var(--hair);
  border-radius: 50%;
  background: var(--bg);
  color: var(--ink-2);
  cursor: pointer;
  transition: border-color var(--fast) var(--ease), color var(--fast) var(--ease);
}
.bk-dialog-x:hover { border-color: var(--ink); color: var(--ink); }
.bk-dialog-x:focus-visible { outline: 2px solid var(--spark); outline-offset: 2px; }
.bk-dialog-x svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; }

/* The homepage's own touches on this form live under .experience-home, so the
   dialog repeats them: the heading face, the navy button, the light picker. */
.bk-dialog .bk-head { margin-bottom: clamp(24px, 3vw, 38px); }
.bk-dialog .bk-head h2 { font: 500 clamp(2rem, 3.4vw, 2.6rem)/1.13 var(--sans); letter-spacing: -.055em; }
.bk-dialog .bk-head p.sub { max-width: 58ch; margin: 0 auto; text-wrap: balance; line-height: 1.8; }
.bk-dialog .bk-col-head { color: var(--navy); font-weight: 500; }
.bk-dialog .btn-primary { color: var(--on-ink); background: var(--navy); border-color: var(--navy); box-shadow: 0 8px 30px -14px rgba(12, 36, 84, .35); }
.bk-dialog .btn-primary:hover { background: var(--navy-2); border-color: var(--navy-2); }
.bk-dialog .btn-primary::before { background: rgba(255, 255, 255, .2); }
.bk-dialog .bk-select select { background-color: var(--surface); color-scheme: light; }
.bk-dialog .bk-select select::picker(select) { background: var(--surface); border-color: var(--hair-2); color: var(--ink); }
/* Inside a dialog the time picker's card stays painted while the select is
   closed (a stray "Choose a time" chip floating over the field). Only the
   open select gets its card. */
.bk-dialog .bk-select select:not(:open)::picker(select) { display: none; }

/* --- the toggle: Pick a time | Send a message ---
   One form at a time instead of two side by side. data-tab on the dialog says
   which; book.js flips it. The column heads and the "or" only existed to tell
   two side-by-side forms apart, so they go. */
.bk-tabs {
  display: flex;
  width: fit-content;
  margin: 0 auto clamp(22px, 2.6vw, 32px);
  padding: 4px;
  gap: 2px;
  border: 1px solid var(--hair);
  border-radius: 100px;
  background: var(--bg-2);
}
.bk-tabs[hidden] { display: none; }
.bk-tab {
  padding: 11px 22px;
  border: 0;
  border-radius: 100px;
  background: transparent;
  color: var(--ink-2);
  font: 500 .86rem/1 var(--sans);
  cursor: pointer;
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}
.bk-tab:hover { color: var(--ink); }
.bk-tab[aria-pressed="true"] { background: var(--navy); color: var(--on-ink); }
.bk-tab:focus-visible { outline: 2px solid var(--spark); outline-offset: 2px; }

.bk-dialog[data-tab="cal"] #bkMsg,
.bk-dialog[data-tab="msg"] #bkCal { display: none; }
.bk-dialog .bk-or, .bk-dialog .bk-col-head { display: none; }
/* Send a message needs no headline: the toggle above it already says where you
   are, and the blurb is about booking a call. */
.bk-dialog[data-tab="msg"] .bk-head { display: none; }
.bk-dialog .bk-cols { display: block; max-width: none; }
.bk-dialog #bkMsg { max-width: 560px; margin-inline: auto; }

/* Pick a time, wide: the calendar down the left, and the time, the way to talk
   and the rest of the form down the right. .bk-grid stops being a box so the
   calendar and the time picker (its two children) can go to different
   columns. Narrower than this it is one column, as before. */
@media (min-width: 821px) {
  .bk-dialog #bkCal {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    column-gap: clamp(32px, 5vw, 72px);
    align-items: start;
  }
  .bk-dialog #bkCal[hidden] { display: none; }
  .bk-dialog #bkCal > *, .bk-dialog #bkCal .bk-times { grid-column: 2; }
  .bk-dialog #bkCal > .bk-lede { grid-column: 1 / -1; }
  .bk-dialog #bkCal .bk-grid { display: contents; }
  .bk-dialog #bkCal .bk-times { margin-bottom: 24px; }
  .bk-dialog #bkCal .bk-cal {
    grid-column: 1;
    grid-row: 2 / span 20;
    justify-self: center;
    width: 100%;
    max-width: 380px;
  }
}

/* A thin scrollbar with no arrows and a small rounded thumb. The transparent
   border is what makes the thumb thin: the track stays wide enough to grab. */
@supports selector(::-webkit-scrollbar) {
  .bk-dialog::-webkit-scrollbar { width: 11px; }
  .bk-dialog::-webkit-scrollbar-button { display: none; width: 0; height: 0; }
  .bk-dialog::-webkit-scrollbar-track { background: transparent; margin-block: 22px; }
  .bk-dialog::-webkit-scrollbar-thumb {
    background: rgba(12, 36, 84, .22);
    background-clip: content-box;
    border: 3px solid transparent;
    border-radius: 4px;
  }
  .bk-dialog::-webkit-scrollbar-thumb:hover { background: rgba(12, 36, 84, .42); background-clip: content-box; }
}
@supports not selector(::-webkit-scrollbar) {
  .bk-dialog { scrollbar-width: thin; scrollbar-color: rgba(12, 36, 84, .25) transparent; }
}

/* =============================================================
   Footer
   ============================================================= */

/* Three tracks with equal outer fractions, so the mark sits on the centre of
   the VIEWPORT rather than on the centre of whatever space the socials and the
   contact details happen to leave over. minmax(0,1fr) rather than 1fr: a bare
   fr keeps a min-content floor, and the moment one side is wider than its
   share it pushes the track open and drags the mark off centre with it. */
.foot {
  /* No top rule. The generous margin above already separates it, and a
     hairline there is a line the eye has to cross to reach the contact
     details — which are the one thing in the footer worth reaching. */
  padding: 18px clamp(16px, 2.4vw, 30px);
  margin-top: clamp(40px, 7vw, 88px);
  color: var(--ink-3);
  font-size: .84rem;
}
.foot-in {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  gap: 18px;
}

.social { display: flex; gap: 2px; justify-self: start; flex-wrap: wrap; }
.social a {
  width: 30px; height: 30px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  color: var(--ink-3);
  transition: color var(--fast) var(--ease), background var(--fast) var(--ease),
              transform var(--fast) var(--ease);
}
.social svg {
  width: 18px; height: 18px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.social a:hover {
  color: var(--spark);
  background: rgba(72, 108, 252, .09);
  transform: translateY(-2px);
}

.foot-mark { justify-self: center; display: block; }
.foot-mark img {
  width: 34px; height: 34px;
  mix-blend-mode: multiply;
  opacity: .85;
  transition: opacity var(--fast) var(--ease), transform var(--slow) var(--ease);
}
.foot-mark:hover img { opacity: 1; transform: rotate(-12deg); }

.foot-contact {
  justify-self: end;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 2px;
  line-height: 1.5;
}
.foot-contact a {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.foot-contact svg {
  width: 14px; height: 14px;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: .7;
}
.foot a { transition: color var(--fast) var(--ease); }
.foot-contact a:hover { color: var(--ink); }

.foot-legal {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 6px;
  font-size: .78rem;
  opacity: .75;
}

@media (max-width: 680px) {
  /* Stacked and centred rather than three cramped tracks. */
  .foot-in { grid-template-columns: 1fr; justify-items: center; gap: 16px; text-align: center; }
  .social, .foot-mark, .foot-contact { justify-self: center; align-items: center; }
  .foot-legal { justify-content: center; flex-wrap: wrap; }
}

/* =============================================================
   Accessibility
   ============================================================= */

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

/* Off, not reduced. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .001ms !important;
    transition-duration: .001ms !important;
  }
  html.motion [data-rise] { opacity: 1 !important; transform: none !important; }
  .card3d { transform: none !important; }
}


/* =============================================================
   The mark

   The enso is a raster, so it is only ever drawn small and at a
   size where its own resolution is far above the display's. The
   wordmark stays live text in the display serif rather than part
   of the image: it stays crisp at any zoom, it is selectable, and
   it is what a screen reader announces.
   ============================================================= */

.wordmark {
  display: inline-flex;
  align-items: center;
  gap: 9px;
}
.wordmark img {
  width: 26px;
  height: 26px;
  flex: none;
  /* The artwork carries a white ground rather than transparency, so it is
     multiplied into the page. On this near-white paper that is invisible;
     on anything darker it would need the reversed file instead. */
  mix-blend-mode: multiply;
  transition: transform var(--slow) var(--ease);
}
.wordmark:hover img { transform: rotate(-12deg); }
.wordmark b {
  font-family: var(--display);
  font-weight: 400;
  font-size: 1.3rem;
  letter-spacing: .005em;
}

/* The mark again, large and quiet, as the about page's one image. */
.mark-lg {
  width: clamp(120px, 18vw, 190px);
  height: auto;
  mix-blend-mode: multiply;
  margin-bottom: 26px;
}

/* =============================================================
   Agentic layer

   The brief was "high tech". The restraint is deliberate: the mark
   is a brushed enso, and neon on top of that reads as costume.
   So the motion here is precise rather than loud, and each piece
   means something —

     the headline streams, because that is what an agent does
     the spark is alive, because it is the one bright thing in the mark
     numbers count, because they were measured rather than typed

   Everything below is additive and scoped to html.motion, so with
   JavaScript off the page is simply a static, complete document.
   ============================================================= */

/* --- header: away on the way down, back on the way up --- */

html.scrolled .bar {
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  -webkit-backdrop-filter: blur(18px) saturate(180%);
  backdrop-filter: blur(18px) saturate(180%);
}
.bar { will-change: transform; }
html.bar-hidden .bar { transform: translateY(-100%); }

/* --- scroll progress ---
   Two pixels. It answers "how much of this is left" without asking
   for any attention while it does it. */
.progress {
  position: fixed;
  top: 0; left: 0; right: 0;
  height: 1px;
  z-index: 60;
  background: linear-gradient(90deg, var(--navy), var(--spark));
  transform: scaleX(0);
  transform-origin: left;
  pointer-events: none;
}

/* --- the headline, streaming ---
   Words are wrapped by JS. Without JS the h1 is untouched plain text,
   which is also what a crawler and a link preview read. */
html.motion .stream > i {
  display: inline-block;
  opacity: 0;
  transform: translate3d(0, .38em, 0);
  animation: streamIn .5s var(--ease) forwards;
  animation-delay: calc(var(--w) * 45ms);
  font-style: inherit;
}
@keyframes streamIn { to { opacity: 1; transform: none; } }

/* The cursor that trails the last word while it types, then leaves. */
html.motion .stream::after {
  content: "";
  display: inline-block;
  width: .5ch;
  height: .9em;
  margin-left: .08em;
  background: var(--spark);
  vertical-align: -.08em;
  animation: caret .75s steps(1) 6, caretOut .1s 4.5s forwards;
}
@keyframes caret   { 50% { opacity: 0; } }
@keyframes caretOut { to { opacity: 0; width: 0; margin: 0; } }

/* --- headings further down, streaming on arrival ---
   Same word-splitting as the hero, but keyed to .in rather than to load:
   these are below the fold, and an animation that has already finished by
   the time you scroll to it may as well not exist. The element keeps
   data-rise so the observer still marks it, but the block-level rise is
   switched off — both at once reads as two animations fighting over one
   element. Without JS there is no .stream-rise class and no ::after, so
   the heading is plain centred text. */
html.motion .stream-rise[data-rise] { opacity: 1; transform: none; transition: none; }
html.motion .stream-rise > i {
  display: inline-block;
  opacity: 0;
  transform: translate3d(0, .4em, 0);
  font-style: inherit;
}
html.motion .stream-rise.in > i {
  animation: streamIn .62s var(--ease) forwards;
  animation-delay: calc(var(--w) * 70ms);
}
/* A hairline draws itself out once the last word has landed. --words is
   the count the splitter measured, so the wait tracks the actual heading
   instead of a guess that goes wrong the moment the copy changes. */
html.motion .stream-rise::after {
  content: "";
  display: block;
  width: 0;
  height: 1px;
  margin: clamp(14px, 1.8vw, 22px) auto 0;
  background: linear-gradient(90deg, transparent, var(--hair-2), transparent);
}
html.motion .stream-rise.in::after {
  animation: ruleDraw 820ms var(--ease) forwards;
  animation-delay: calc(var(--words, 3) * 70ms + 220ms);
}
@keyframes ruleDraw { to { width: min(340px, 46%); } }

/* --- the spark, alive ---
   A slow pulse on the one bright dot the mark already contains. */
.spark-dot {
  display: inline-block;
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--spark);
  margin-right: 9px;
  vertical-align: middle;
  position: relative;
}
html.motion .spark-dot::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: var(--spark);
  animation: ping 2.6s var(--ease) infinite;
}
@keyframes ping {
  0%   { transform: scale(1);   opacity: .55; }
  70%  { transform: scale(3.4); opacity: 0; }
  100% { transform: scale(3.4); opacity: 0; }
}

/* --- a faint field that follows the cursor ---
   Positioned from two custom properties written once per frame. It sits
   under everything and is nearly invisible; you notice it when it stops. */
.field-glow {
  position: fixed;
  top: 0; left: 0;
  width: 480px; height: 480px;
  margin: -240px 0 0 -240px;
  border-radius: 50%;
  pointer-events: none;
  z-index: 0;
  opacity: 0;
  background: radial-gradient(circle, rgba(72,108,252,.10), rgba(72,108,252,0) 65%);
  transform: translate3d(var(--mx, -999px), var(--my, -999px), 0);
  transition: opacity 600ms var(--ease);
}
html.motion .field-glow { opacity: 1; }
/* .bar is NOT in this list. It was, and that silently replaced its
   position: sticky with position: relative — a later rule quietly winning
   over an earlier one on the same property. The bar already sits at
   z-index 50, well above this glow, so it never needed lifting. */
main, .foot { position: relative; z-index: 1; }

/* --- magnetic buttons --- */
.btn { transition: color var(--mid) var(--ease), border-color var(--mid) var(--ease),
                   transform 220ms var(--ease); }

/* --- counting numbers hold their width so the layout cannot jump --- */
.stat b { font-variant-numeric: tabular-nums; }

@media (prefers-reduced-motion: reduce) {
  .bar { transition: none !important; }
  html.bar-hidden .bar { transform: none !important; }
  html.motion .stream > i { opacity: 1 !important; transform: none !important; animation: none !important; }
  html.motion .stream::after { display: none; }
  html.motion .stream-rise > i { opacity: 1 !important; transform: none !important; animation: none !important; }
  html.motion .stream-rise::after { display: none; }
  html.motion .spark-dot::after { animation: none; }
  .field-glow { display: none; }
  .progress { display: none; }
}


/* =============================================================
   Hero: copy on the left, the listening ring on the right

   The ring is an enso made of radial ticks whose lengths move
   like an audio level meter, with greetings cycling through it.
   The circle is the mark, the meter is voice, the scripts are the
   languages — three claims in one shape and almost no words.

   The canvas is aria-hidden and carries no information that is
   not also stated in text. It is decoration, and a screen reader
   should be told so rather than handed a blank graphic.
   ============================================================= */

.hero-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  align-items: center;
  gap: clamp(24px, 5vw, 64px);
}
.hero-copy h1 {
  font-size: clamp(2.6rem, 6.4vw, 5rem);
  line-height: 1.0;
  letter-spacing: -.025em;
  text-wrap: balance;
}
.hero-copy h1 em { font-style: italic; color: var(--navy); }
.hero-acts { display: flex; flex-wrap: wrap; gap: 12px; margin-top: clamp(26px, 3.4vw, 40px); }

.hero-ring {
  position: relative;
  /* An aspect ratio rather than a fixed height, so the ring scales with the
     column instead of being cropped at narrow widths. */
  aspect-ratio: 1 / 1;
  max-height: 62vh;
  justify-self: center;
  width: 100%;
}
#heroCanvas { width: 100%; height: 100%; display: block; }

@media (max-width: 820px) {
  /* Ring first on a phone: it is the thing worth seeing, and stacking the
     copy above it would push it below the fold entirely. */
  .hero-grid { grid-template-columns: 1fr; gap: 8px; }
  .hero-ring { order: -1; max-height: 40vh; }
  .hero-copy { text-align: center; }
  .hero-acts { justify-content: center; }
}


/* =============================================================
   The deck

   Slides are stacked in one grid cell and cross-faded, rather than
   laid side by side and scrolled. The copy column therefore keeps
   the height of its tallest slide and nothing below the hero moves
   as the deck advances — a layout that reflows every five seconds
   is worse than no slideshow at all.
   ============================================================= */

.hero-copy { display: grid; }
.hd-slide {
  grid-area: 1 / 1;                 /* all slides share one cell */
  opacity: 0;
  visibility: hidden;
  transform: translate3d(0, 14px, 0);
  transition: opacity 520ms var(--ease), transform 520ms var(--ease),
              visibility 0s linear 520ms;
  pointer-events: none;
}
.hd-slide.on {
  opacity: 1;
  visibility: visible;
  transform: none;
  transition-delay: 0s;
  pointer-events: auto;
}
.hd-slide .kicker { margin-bottom: 16px; }
.hd-slide h1 {
  font-size: clamp(2.4rem, 5.6vw, 4.4rem);
  line-height: 1.02;
  letter-spacing: -.025em;
  text-wrap: balance;
}
.hd-slide h1 em { font-style: italic; color: var(--navy); }
.hero-sub {
  margin-top: 20px;
  max-width: 40ch;
  color: var(--ink-2);
  font-size: 1.02rem;
  line-height: 1.7;
}

/* The actions sit BELOW the stacked slides, in their own row, so they never
   move as the deck changes. */
.hero-copy .hero-acts { grid-area: 2 / 1; margin-top: clamp(26px, 4vh, 44px); }

/* In flow, directly under the content — NOT pinned to the hero's bottom
   edge. The hero grows past 100svh whenever the copy and the ring run tall,
   and a row pinned to the bottom of a box taller than the screen is a row
   below the fold. Here it sits wherever the content ends, always visible. */
.hd-dots {
  position: static;
  margin-top: clamp(20px, 3.5vh, 36px);
  display: flex;
  justify-content: center;
  gap: 2px;
  pointer-events: none;             /* the row must not swallow clicks */
}
/* The BUTTON is the hit target; the visible dot is the ::after. Laying the
   target out rather than overlaying it is what stops each dot stealing its
   neighbour's clicks. */
.hd-dot {
  width: 26px; height: 26px;
  padding: 0; border: 0; background: none;
  display: grid; place-items: center;
  pointer-events: auto;
  cursor: pointer;
}
.hd-dot::after {
  content: "";
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--ink-3);
  opacity: .38;
  transition: opacity var(--fast) var(--ease), transform var(--fast) var(--ease),
              background var(--fast) var(--ease);
}
.hd-dot:hover::after { opacity: .7; }
.hd-dot.on::after { opacity: 1; background: var(--spark); transform: scale(1.5); }

.hero-ring { max-height: 66vh; }

@media (max-width: 820px) {
  .hero { min-height: auto; padding: calc(52px + 4vh) 0 12vh; }
  .hero-ring { order: -1; max-height: 34vh; }
  .hd-dots { margin-top: 22px; }
}

/* =============================================================
   Work frames

   Near-full-viewport rounded screens, edge to edge, each holding
   a full-page capture of a real, live product that pans slowly
   from top to bottom — the thing a video of the site would show,
   without the bytes or the encode, and with a link into the real
   thing, which a video cannot offer.

   The pan is a CSS transform driven by an IntersectionObserver
   class, so it costs nothing until the frame is on screen and
   nothing at all under prefers-reduced-motion.
   ============================================================= */

.work { padding: clamp(24px, 4vw, 48px) 0 0; }
.work-head { margin-bottom: clamp(18px, 3vw, 30px); text-align: center; }
.work-head h2 { font-size: clamp(2rem, 4.6vw, 3.4rem); max-width: 22ch; margin-inline: auto; text-wrap: balance; }
.work-head p  { margin-top: 12px; max-width: 48ch; margin-inline: auto; color: var(--ink-2); }

/* Above the one case study, before anything else: real logos for the models
   this is built with, the platforms it publishes to, and the software
   already running a client's business. Static by default — a plain wrapped
   row, so no JS and prefers-reduced-motion both still show all of them.
   html.motion (see motion.js) is what switches it into the full, drifting
   field toolfield.js animates. The stage sits outside .wrap and runs edge to
   edge; no border by design — the logos themselves mark its edges. */
.toolfield { padding: clamp(40px, 6vw, 74px) 0; }
.toolfield-stage {
  position: relative;
  display: flex; flex-wrap: wrap; align-items: center; justify-content: center;
  gap: clamp(22px, 4vw, 44px);
  margin-top: clamp(28px, 4vw, 46px);
  padding: 20px clamp(16px, 4vw, 64px);
}
.tf-logo {
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  text-align: center;
}
/* object-fit because the marks are not all square (AWS, Salesforce). */
.tf-logo img { width: 32px; height: 32px; flex: none; object-fit: contain; }
.tf-logo b { font-weight: 500; font-size: .74rem; color: var(--ink-3); white-space: nowrap; }

/* Clipped sideways only, so a tilted logo at the very edge can never add a
   horizontal scrollbar, while the top row can still drop in from above the
   stage without being cut off at its edge. */
.motion .toolfield-stage {
  height: clamp(420px, 50vw, 560px);
  gap: 0;
  padding: 0;
  overflow-x: clip;
}
/* toolfield.js measures these boxes to size its grid, so a long label
   wraps rather than widening every cell on the stage. */
/* The arrival is the MM Carz reviews drop-in, copied from its
   reviews-section.php: each logo waits 130px above its spot, already tilted,
   and falls into place with the same curve and timings, one after another
   (--d, set by toolfield.js). Once everything has landed the stage gets
   .tf-live and a swap is a quiet fade-and-grow in place instead, because a
   130px drop into a full field would pass over the logo above. */
.motion .tf-logo {
  position: absolute;
  top: var(--y, 50%); left: var(--x, 50%);
  width: max-content; max-width: 104px;
  --drop: -130px; --pop: 1;
  transform: translate(-50%, calc(-50% + var(--drop))) scale(calc(var(--s, 1) * var(--pop))) rotate(var(--r, 0deg));
  opacity: 0;
  transition: transform .85s cubic-bezier(.16,.86,.3,1.04), opacity .6s ease;
  transition-delay: var(--d, 0s);
  pointer-events: none;
  /* The slow wander around its own spot. `translate` is separate from
     `transform`, so it rides on top of the drop, scale and tilt; --dx/--dy
     are a slice of the spare room in the spot, set by toolfield.js. */
  animation: tf-drift var(--dur, 20s) ease-in-out var(--delay, 0s) infinite;
}
.motion .tf-logo.on { --drop: 0px; opacity: 1; }
.motion .toolfield-stage.tf-live .tf-logo {
  --drop: 0px; --pop: .6;
  transition: transform .9s cubic-bezier(.2,.8,.2,1), opacity .9s ease;
  transition-delay: 0s;
}
.motion .toolfield-stage.tf-live .tf-logo.on { --pop: 1; }
.motion .toolfield-stage.tf-paused .tf-logo { animation-play-state: paused; }
@keyframes tf-drift {
  0%, 100% { translate: 0 0; }
  25% { translate: var(--dx, 0px) calc(var(--dy, 0px) * -.6); }
  50% { translate: calc(var(--dx, 0px) * -.5) var(--dy, 0px); }
  75% { translate: calc(var(--dx, 0px) * -1) calc(var(--dy, 0px) * -.4); }
}
.motion .tf-logo b { white-space: normal; line-height: 1.25; }
.motion .tf-logo img { width: 48px; height: 48px; }
@media (max-width: 640px) {
  .motion .toolfield-stage { height: clamp(420px, 128vw, 520px); }
  .motion .tf-logo { max-width: 84px; }
  .motion .tf-logo img { width: 36px; height: 36px; }
}

.frames { display: grid; gap: clamp(18px, 2.6vw, 34px); }

.frame {
  position: relative;
  display: block;
  border-radius: clamp(14px, 2vw, 26px);
  overflow: hidden;                        /* the one place it is doing its actual job */
  background: var(--bg-2);
  border: 1px solid var(--hair);
  aspect-ratio: 16 / 10;
  max-height: 86svh;
  /* Makes 100cqh below mean THIS frame's height, so each capture pans by
     exactly the amount that hangs below its own frame, with no script
     measuring anything. */
  container-type: size;
  box-shadow: 0 30px 80px -50px rgba(12, 36, 84, .45);
  transition: box-shadow var(--mid) var(--ease), border-color var(--mid) var(--ease);
}
.frame:hover { border-color: var(--hair-2); box-shadow: 0 40px 100px -50px rgba(12, 36, 84, .55); }

/* The capture. Anchored to the top; the pan moves it up by however much of
   it hangs below the frame, which --overflow carries per image. */
/* `.frame > img`, a direct child only. Written as `.frame img` this also
   caught the logo inside the caption, pinned it to the top-left corner and
   stretched it to the frame's full width — 493px tall, over the video. */
.frame > img {
  position: absolute;
  top: 0; left: 0;
  width: 100%;
  height: auto;
  will-change: transform;
}
html.motion .frame.in > img {
  animation: pan var(--pan, 22s) cubic-bezier(.45, .05, .55, .95) infinite alternate;
}
@keyframes pan {
  0%   { transform: translate3d(0, 0, 0); }
  8%   { transform: translate3d(0, 0, 0); }               /* hold the top */
  92%  { transform: translate3d(0, calc(-100% + 100cqh), 0); }
  100% { transform: translate3d(0, calc(-100% + 100cqh), 0); }
}

/* Browser chrome along the top, so the capture reads as a screen and the
   top edge of the image does not fight the frame's own edge. */
.frame::before {
  content: "";
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 28px;
  background: color-mix(in srgb, var(--bg) 86%, transparent);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--hair);
  z-index: 2;
}
.frame::after {
  content: "";
  position: absolute;
  top: 10px; left: 14px;
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--ink-3);
  opacity: .5;
  box-shadow: 14px 0 0 var(--ink-3), 28px 0 0 var(--ink-3);
  z-index: 3;
}

/* Caption over the bottom edge: name, what it is, and the way in. */
.frame-cap {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  z-index: 2;
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 16px;
  padding: clamp(18px, 3vw, 30px);
  color: var(--on-ink);
  background: linear-gradient(180deg, rgba(12, 36, 84, 0), rgba(12, 36, 84, .82));
}
.frame-cap h3 {
  font-family: var(--display);
  font-weight: 300;
  font-size: clamp(1.5rem, 3vw, 2.4rem);
  line-height: 1.05;
  color: #fff;
}
.frame-cap p { margin-top: 6px; font-size: .92rem; color: rgba(255, 255, 255, .78); max-width: 46ch; }
.frame-cap .tags { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }
.frame-cap .tag {
  color: #fff;
  background: rgba(255, 255, 255, .12);
  border-color: rgba(255, 255, 255, .22);
}
.frame-open {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font: 500 .88rem/1 var(--sans);
  color: var(--navy);
  background: #fff;
  border-radius: 999px;
  padding: 12px 18px;
  transition: transform var(--fast) var(--ease), background var(--fast) var(--ease);
}
.frame:hover .frame-open { transform: translateX(4px); }
.frame-open svg { width: 14px; height: 14px; }


@media (max-width: 720px) {
  .frame { aspect-ratio: 4 / 5; }
  .frame-cap { flex-direction: column; align-items: flex-start; }
}
@media (prefers-reduced-motion: reduce) {
  html.motion .frame.in img { animation: none; }
}


/* =============================================================
   The video frame

   Full width and full viewport, like the frames on humaan.com,
   and holding the real hero video from the site it shows rather
   than a panned screenshot of it. Overrides the generic .frame
   rules above on purpose; they stay for any later image frames.
   ============================================================= */

.frames-bleed {
  max-width: none;
  padding: 0 clamp(10px, 1.6vw, 22px);
}
/* The frame opens out as it comes through the middle of the screen and
   closes again on the way out, the way humaan.com moves its media.
   --bleed is written per frame by motion.js: 0 at either end of that
   travel, 1 across the middle. Scoped to html.motion, so with no JS the
   frame keeps the fixed inset above and reads as deliberate rather than
   half-finished. 5vw is chosen, not arbitrary: it is the largest inset
   that leaves the heading clamped at its 3.4rem ceiling on a 1440 screen,
   so the mock type does not visibly breathe while you scroll. */
html.motion .frames-bleed {
  /* 0 at the open end: the frame goes properly edge to edge rather than
     stopping 22px short, which just looked like a margin nobody meant. */
  padding-inline: calc((1 - var(--bleed, 0)) * 7vw);
}
/* ...and the corners square off as it gets there. A 30px radius hard
   against the window edge reads as a rendering mistake, not a choice. */
html.motion .frames-bleed .frame.frame-video {
  border-radius: calc((1 - var(--bleed, 0)) * clamp(16px, 2.2vw, 30px));
}
.frame.frame-video {
  aspect-ratio: auto;
  height: 100svh;
  max-height: none;
  border-radius: clamp(16px, 2.2vw, 30px);
  background: #0B0F1A;
}
/* No fake browser chrome on a video: the recording IS the screen. */
.frame.frame-video::before,
.frame.frame-video::after { display: none; }

.frame-video video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.frame-logo {
  display: block;
  height: clamp(30px, 4.2vw, 60px);
  width: auto;
  margin: 2px 0 12px;
}
.frame-video .frame-cap {
  padding: clamp(22px, 4vw, 44px);
  /* Softened from .78 once .mock-ui::before began painting the real hero's own
     scrim underneath: stacked, .84 under .78 composites to 96.5% opaque and the
     footage disappears behind the caption. At .45 the composite floor is 91%,
     which lands just past the real hero's own .84 bottom rather than at black. */
  background: linear-gradient(180deg, rgba(12, 36, 84, 0), rgba(6, 12, 30, .45));
}
.frame-video .frame-cap p { max-width: 52ch; font-size: .98rem; }

/* The hero's dots, a touch lower than before. The extra drop is a relative
   offset, not more margin: .hero centres its column, so added margin splits
   in two — the dots sink half of it and the headline rises the other half. */
.hd-dots { margin-top: clamp(36px, 6.5vh, 66px); position: relative; top: clamp(48px, 8.5vh, 86px); }

@media (max-width: 720px) {
  .frame.frame-video { height: 78svh; }
}


/* --- the site's chrome over the video frame ---

   Rebuilt from the real page rather than from an impression of it. Every value
   below was read off mmcarz.com.au's own stylesheet and then confirmed against
   the rendered page at 1920x959, because the first pass guessed and got three
   things visibly wrong: the hero is DIMMED and this was playing the raw bright
   clip, the heading is Manrope 300 and this had it at 500, and the filter bar is
   a naked row of white labels while this had glass capsules.

   .frame declares container-type: size (above), so cq units and @container here
   resolve against the frame -- which is the thing being imitated. The viewport
   is not: the frame is narrower by the .frames-bleed padding, and on mobile it
   is only 78svh tall. Decorative, inert and aria-hidden throughout. */
.mock-ui {
  position: absolute; inset: 0;
  z-index: 1;
  color: #fff;
  pointer-events: none;
  font-family: var(--sans);
}
/* The hero's scrim, copied exactly from .mm-hero__media::after on the live site.
   Painted as .mock-ui's first child so the nav and the heading sit over it, and
   .frame-cap (z-index 2, from .frame-cap above) still sits over both. */
.mock-ui::before {
  content: "";
  position: absolute; inset: 0;
  background: linear-gradient(180deg, rgba(16, 18, 20, .70) 0%, rgba(16, 18, 20, .54) 42%, rgba(16, 18, 20, .84) 100%);
}

/* Header: 1fr auto 1fr, so the nav is centred against the bar rather than against
   whatever is left over beside the logo. Each child names its own column --
   without that, hiding the nav below drops it out of the grid and the icons
   auto-place into the middle track, stranding them mid-bar. */
.mock-nav {
  position: absolute; top: 0; left: 0; right: 0;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 24px;
  height: 58px;
  padding: 0 clamp(20px, 2.5cqw, 24px);
}
.mock-nav img { grid-column: 1; justify-self: start; height: 18px; width: auto; }
.mock-nav nav {
  grid-column: 2; justify-self: center;
  display: flex; align-items: center; gap: 30px;
  min-width: 0; overflow: hidden;
  font-size: 14px; font-weight: 400; letter-spacing: .01em; white-space: nowrap;
}
.mock-icons { grid-column: 3; justify-self: end; display: flex; align-items: center; gap: 14px; }
/* An 18px glyph inside 4px of padding, the way .mm-icon does it: that is what
   sets the last icon 28px off the edge instead of flush at the 24px gutter.
   content-box beats the global * { box-sizing: border-box }. */
.mock-icons svg { width: 18px; height: 18px; padding: 4px; box-sizing: content-box; stroke-width: 1.4; }

/* The hero block. A flex column for a reason: the real 14px + 36px gap between
   heading and filter bar only adds up because .mm-hero is flex. As adjacent
   blocks the two margins collapse to 36px and the geometry goes quietly wrong.
   With the same 80/140 padding this reproduces the real hero to the pixel --
   heading top at 42.6% of the frame -- at any frame height. */
.mock-hero {
  position: absolute; inset: 0;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  text-align: center;
  padding: 80px clamp(20px, 4cqw, 64px) 140px;
}
.mock-h {
  font-size: clamp(2rem, 4.4cqw, 3.4rem);
  font-weight: 300;
  letter-spacing: -.02em;
  line-height: 1.18;
  margin: 0 0 14px;
}
/* margin-bottom is negative on the real bar too. It is not an overlap hack here:
   it shortens the flex item's outer box, which is what lands the whole block
   where it lands. Drop it and everything rises 43px. line-height is pinned
   because the 53px pill height comes from the 1.65 body ratio, not from Manrope. */
.mock-filters {
  display: flex; flex-wrap: wrap; align-items: stretch;
  gap: 0;
  width: fit-content; max-width: 94%;
  margin: 36px auto -86px;
  line-height: 1.65;
  text-shadow: 0 1px 3px rgba(0, 0, 0, .45);
}
.mock-filters span {
  display: inline-flex; align-items: baseline; gap: 7px;
  white-space: nowrap;
  padding: 15px 22px;
  font-size: 14px; font-weight: 500; letter-spacing: .02em;
  color: rgba(255, 255, 255, .96);
  background: none; border: 0; border-radius: 0;
  -webkit-backdrop-filter: none; backdrop-filter: none;
}
/* The caret is drawn, not typed: an 8px box rotated 45deg showing two of its
   borders. The U+25BE stays in the markup exactly as the real page keeps it --
   font-size: 0 hides it, and it is what shows if the stylesheet never loads. */
.mock-filters i {
  display: inline-block; align-self: center;
  width: 8px; height: 8px;
  margin-left: 8px;
  font-style: normal; font-size: 0;
  border-right: 1.5px solid rgba(255, 255, 255, .9);
  border-bottom: 1.5px solid rgba(255, 255, 255, .9);
  transform: translateY(-2px) rotate(45deg);
  text-shadow: none;
  filter: drop-shadow(0 1px 1px rgba(0, 0, 0, .35));
}
/* The caption runs down the left now, because Michi sits in the bottom-right
   corner of the real page and the frame should not put a button where the
   assistant lives. The right padding is the FAB's 56px plus its 22px gutter. */
.frame-video .frame-cap { z-index: 2; justify-content: flex-start; }
.frame-video .frame-cap > div { max-width: 62ch; }
.frame-video .frame-open { margin-top: 18px; }

/* Michi's launcher, at its real size and colour: 56px, #033E7A, the conic
   aura behind it, and the three-dot bubble. From widget.css, not invented. */
.mock-fab {
  position: absolute;
  right: 22px; bottom: 22px;
  z-index: 3;
  width: 56px; height: 56px;
  border-radius: 50%;
  pointer-events: none;
  isolation: isolate;   /* keeps the aura behind the button, not behind the frame */
  box-shadow: 0 8px 24px rgba(3, 62, 122, .35);
  /* A LIT SPHERE, not a flat disc -- the first pass got this wrong and it
     showed. The real launcher runs a WebGL shader: cLight #0266b0 over
     cDark #033d7a with the light at normalize(-0.30, 0.40, 0.85) (upper
     left, toward the viewer), a drifting sky highlight #73b3f2, a red band
     #e80007, then rim-darkened by col *= 0.55 + 0.45z. Three stacked
     gradients land on the same picture without shipping a shader into a
     decorative mock; the values are read off the shader source, not eyed. */
}
/* The sphere gets its OWN layer, above the aura. On the live widget the
   aura is a z-index:-1 ::before and the orb is an opaque <canvas> painted
   after it, so only the 5px overhang of the aura ever shows. Painting the
   sphere as this element's background instead put it UNDER the pseudo --
   a negative z-index child still paints above its parent's background --
   and the blurred conic washed over the whole face. */
.mock-fab::after {
  content: "";
  position: absolute; inset: 0;
  z-index: 1;
  border-radius: 50%;
  background:
    /* the drifting red band, low and left */
    radial-gradient(56% 50% at 24% 78%, rgba(232, 0, 7, .34), rgba(232, 0, 7, 0) 62%),
    /* the sky-blue band across the upper half */
    radial-gradient(58% 54% at 38% 30%, rgba(115, 179, 242, .40), rgba(115, 179, 242, 0) 72%),
    /* the directional shadow: light is up-left-toward-viewer, so the far
       side is the lower right */
    radial-gradient(72% 68% at 76% 82%, rgba(2, 32, 66, .55), rgba(2, 32, 66, 0) 70%),
    /* and the sphere itself. Brightest at the CENTRE, not at the highlight:
       the shader rim term 0.55 + 0.45z peaks where z = 1. Sampling that
       arithmetic gives centre #0265ae, shadow #033e78, rim #022e5b -- it
       never approaches black. */
    radial-gradient(100% 100% at 45% 42%, #0268b4 0%, #0265ae 34%, #02579a 62%, #024378 86%, #022e5b 100%);
}
/* No aura ring. The live widget turns a blurred red/blue conic ring behind
   the orb; the owner asked for the button without it (2026-09-14), so the
   ::before that drew it is gone and only the orb and its shadow remain. */
/* The overlay shuffles slot-machine style: the current icon rolls out the
   top, the next rolls in from below. Read from widget.js this time rather
   than timed by eye -- setInterval 4000ms, and
   "transform .55s cubic-bezier(.4,0,.2,1), opacity .55s ease" between
   translate(-50%,50%) below, translate(-50%,-50%) centred and
   translate(-50%,-150%) gone. So an 8s cycle: 4s a side, .55s to move,
   which is 6.875% of the cycle. The two icons run the same cycle half a
   turn apart, and the instant reset happens while opacity is 0. */
.mock-fab-i {
  position: absolute; top: 50%; left: 50%;
  z-index: 2;   /* above the sphere layer */
  display: grid; place-items: center;
  transform: translate(-50%, -50%);
  animation: 8s cubic-bezier(.4, 0, .2, 1) infinite;
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, .35));
}
.mock-fab-i svg, .mock-fab-i canvas { display: block; }
.mock-fab-msg { animation-name: mock-fab-msg; }
.mock-fab-wave { animation-name: mock-fab-wave; }
.mock-fab-msg svg { width: 24px; height: 24px; }
.mock-fab-wave canvas { width: 50px; height: 26px; }   /* the real one is 50x26 inside the 56px disc */
@keyframes mock-fab-msg {
  0%      { opacity: 0; transform: translate(-50%, 50%); }
  6.875%  { opacity: 1; transform: translate(-50%, -50%); }
  50%     { opacity: 1; transform: translate(-50%, -50%); }
  56.875% { opacity: 0; transform: translate(-50%, -150%); }
  56.9%   { opacity: 0; transform: translate(-50%, 50%); }
  100%    { opacity: 0; transform: translate(-50%, 50%); }
}
@keyframes mock-fab-wave {
  0%      { opacity: 1; transform: translate(-50%, -50%); }
  6.875%  { opacity: 0; transform: translate(-50%, -150%); }
  6.9%    { opacity: 0; transform: translate(-50%, 50%); }
  50%     { opacity: 0; transform: translate(-50%, 50%); }
  56.875% { opacity: 1; transform: translate(-50%, -50%); }
  100%    { opacity: 1; transform: translate(-50%, -50%); }
}

/* The bubble's three dots light up one at a time -- widget.css:
   opacity .25 base, 1.5s ease-in-out, 0 / .25s / .5s apart. */
.mock-dot  { opacity: .25; animation: mock-typing 1.5s ease-in-out infinite; }
.mock-dot2 { animation-delay: .25s; }
.mock-dot3 { animation-delay: .5s; }
@keyframes mock-typing { 0%, 55%, 100% { opacity: .25; } 25% { opacity: 1; } }

@media (prefers-reduced-motion: reduce) {
  /* No shuffle, no typing, no waveform loop. The bubble is the resting state. */
  .mock-fab-i { animation: none; opacity: 0; }
  .mock-fab-msg { opacity: 1; transform: translate(-50%, -50%); }
  .mock-dot { animation: none; opacity: 1; }
}
@keyframes mock-aura { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .mock-fab::before { animation: none; } }

/* The real header drops its nav below 1200px of viewport, and the frame runs
   about 46px narrower than the window, so 1160px of frame is the same moment.
   Above it the nav needs 1016px and has the room; below it there is none --
   which is why the real site collapses the bar rather than shrinking the type. */
/* 🚨 These are @media, NOT @container, and that is deliberate. The frame
   now changes width as you scroll (--bleed above), so a container query
   would cross its own threshold mid-scroll and pop the nav in and out on
   the way past — measured: a 1280px window swings the frame across 1160.
   The viewport does not move, so viewport breakpoints hold still. These
   also happen to be the real site's own numbers: mmcarz.com.au collapses
   its header at 1200px and drops the filter summaries at 560px.
   Continuous cqw sizing stays — that just breathes, it cannot pop. */
@media (max-width: 1200px) {
  .mock-nav { height: 50px; }
  .mock-nav nav { display: none; }
}
/* Narrow frames are only 78svh tall while .frame-cap stacks to a column and
   grows to about a third of them, so centring in the whole frame walks the
   filter row into the caption (measured: 12.6px of clearance at a 600px frame).
   Below 720px the block is centred in the top 62% instead, which is
   proportional and so cannot drift as the caption rewraps. */
@media (max-width: 720px) {
  .mock-hero { bottom: 38%; padding-bottom: 24px; }
  .mock-filters span:nth-child(n+3) { display: none; }
}
/* A landscape phone gives a frame ~290px tall against a ~150px caption: there
   is no room for a filter row at any padding. The heading alone survives. */
@container (max-height: 560px) {
  .mock-filters { display: none; }
}
/* Below 560px the real bar hides every pill and swaps in the round ENGINE
   START button instead. The mock has no equivalent affordance, so it drops the
   row rather than inventing one -- which is also what keeps the heading clear
   of the caption, since the caption stacks to a column at this width and takes
   about 44% of a 78svh frame. */
@media (max-width: 560px) {
  .mock-filters { display: none; }
  .mock-icons svg { width: 15px; height: 15px; }
  .mock-fab { width: 44px; height: 44px; right: 14px; bottom: 14px; }
  /* Sized per icon, not with one `.mock-fab svg` rule: the waveform's viewBox
     is 50x26, so a square box letterboxes it and leaves the wave floating in
     a gap twice its height. */
  .mock-fab-msg svg { width: 20px; height: 20px; }
  .mock-fab-wave canvas { width: 39px; height: 20px; }
}


/* =============================================================
   The switchboard

   Three lanes under one status strip: a web chat, a spoken order,
   an inbound call. Each lane holds the SAME conversation twice —
   once in English, then again in Chinese — because the multilingual
   claim is worth more demonstrated than asserted.

   The hero at the top of this page is an abstract canvas wireframe;
   this is deliberately its opposite: real type, real words, real UI,
   in the DOM, where it can be selected, translated, searched and
   read aloud.

   THE MARKUP HOLDS THE FINISHED BOARD — both passes of all three
   conversations. Everything below that hides anything is scoped to
   `.sw.live`, a class channels.js adds to itself. Two reasons, and
   the second is the important one:

     1. No JS, or a failed JS, means no `live`, means nothing is
        hidden — the site's rule 1, unchanged. The static reader gets
        six conversations, stacked and labelled.
     2. The gate belongs to the file that does the REVEALING. If
        motion.js's `html.motion` were used instead, motion.js loading
        while channels.js 404s would leave every transcript at opacity
        0. One file hiding and a different file revealing is the exact
        failure the top of this stylesheet warns about; scoping it this
        way makes that failure impossible rather than merely unlikely.

   THE TWO PASSES CHANGE LAYOUT BETWEEN THE TWO STATES, on purpose:

     static  the passes STACK, so both are read, one after the other.
     live    they occupy ONE grid cell, so the lane is always as tall
             as its taller pass and switching language cannot move a
             single pixel of the page.

   Apart from that one deliberate swap, nothing here uses `display` or
   `height` to hide a message. Every box is its final size at first
   paint, so a line arriving can never reflow a lane or move the chip
   under it.

   And note what is NOT here: no keyframe is relied on for a resting
   shape. `animation-duration: .001ms !important` does not stop an
   animation, it completes it instantly and parks it on its last
   frame — so anything whose visibility or rest position came from a
   keyframe would be a coin toss under reduced motion.
   ============================================================= */

/* --- the head, centred ---
   Centred copy wants to be shorter than left-aligned copy: the eye has
   no fixed left edge to return to, so a centred paragraph that runs
   four lines reads as a wall. Both paragraphs here are two. */
.sec-head-mid { text-align: center; }
.sec-head-mid h2 { margin-inline: auto; }
.sec-head-mid p { margin-inline: auto; max-width: 52ch; }

/* The same hairline-gap grid the offer grid uses, inherited on purpose so the
   board sits in the same family as the rest of the page. */
.sw {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 1px;
  background: var(--hair);
  border: 1px solid var(--hair);
  border-radius: var(--r);
  overflow: hidden;
}

/* --- the status strip ---
   One agent, three doors, and the word that keeps it honest. Centred, because
   with the language rail gone there is no longer a left column and a right
   one — just a short line that belongs over the middle of the board.

   The languages used to be listed here too. They are not any more: the board
   below plays every conversation twice and says it out loud, and naming the
   same three languages above it was the caption on a picture that does not
   need one. */

.sw-core {
  grid-column: 1 / -1;
  position: relative;
  background: var(--bg);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 6px clamp(12px, 2vw, 26px);
  padding: 12px clamp(14px, 2vw, 22px);
  font-size: .76rem;
  letter-spacing: var(--ls);
  color: var(--ink-3);
}
.sw-who { display: inline-flex; align-items: center; color: var(--ink-2); }
.sw-rail { display: inline-flex; align-items: baseline; }
.sw-rail b { font-weight: 500; color: var(--ink-3); transition: color var(--mid) var(--ease); }
.sw-rail b.on { color: var(--ink); }
/* The separator belongs to the rail, not to the item, so it does not light. */
.sw-rail b + b::before { content: "\00B7"; margin: 0 .55em; color: var(--hair-2); }
.sw-demo { color: var(--ink-3); }

/* The rule under the strip is progress, not a pointer: it grows a step each time
   the board finishes a leg — English, Chinese, English again. A pointer would
   have to choose one lane, and all three run at once. */
.sw-travel {
  position: absolute;
  left: 0; right: 0; bottom: -1px;
  height: 1px;
  background: var(--spark);
  transform: scaleX(var(--done, 0));
  transform-origin: left;
  transition: transform 620ms var(--ease), opacity var(--mid) var(--ease);
  opacity: 0;
  pointer-events: none;
}
.sw.live .sw-travel { opacity: 1; }

/* --- a lane --- */

.sw-lane {
  --sw-pad: clamp(14px, 1.9vw, 24px);
  position: relative;
  background: var(--bg);
  padding: var(--sw-pad);
  display: flex;
  flex-direction: column;
  gap: 14px;
  transition: background var(--mid) var(--ease);
}

/* Once JS drives the board only one pass shows at a time, so its "Hear it" can
   sit up on the lane's own header row (owner's request, 2026-09-14) instead of
   on a row of its own above the bubbles. The pass head keeps its place in the
   markup (channels.js finds each button inside its pass) and is only pinned to
   the lane's top-right: the same --sw-pad as the lane's padding, and the same
   30px as the header row, which is why both carry that height. Out of flow, it
   stops pushing the bubbles down. The faded pass is opacity 0, pointer-events
   none and inert, so its button, pinned to the same spot, is neither seen, hit
   nor tabbed to. In the stacked state (no JS, or reduced motion) both passes
   are on the page at once, so each keeps its button in its own head row. */
.sw.js .sw-head { min-height: 30px; }
.sw.js .sw-lane:has(.sw-play:not([hidden])) .sw-head { padding-right: 96px; }
.sw.js .sw-pass-head {
  position: absolute;
  top: var(--sw-pad);
  right: var(--sw-pad);
  height: 30px;
  min-height: 0;
}
/* The live lane is LIFTED, never the others dimmed. Dimming two of three reads
   as "these are disabled", which is the opposite of the claim. */
.sw.live .sw-lane.live { background: var(--surface); }
.sw.live .sw-lane.live::before {
  content: "";
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 2px;
  background: var(--spark);
}

.sw-head { display: flex; align-items: center; flex-wrap: wrap; gap: 4px 9px; }
.sw-ico { width: 15px; height: 15px; flex: none; color: var(--navy); }
.sw-ico svg { width: 100%; height: 100%; display: block; }
.sw-head h3 {
  font-family: var(--sans);
  font-weight: 600;
  font-size: .92rem;
  letter-spacing: -.005em;
}
.sw-meta { font-size: .72rem; letter-spacing: var(--ls); color: var(--ink-3); }
/* The trade's mark (a tooth, a rice bowl, scissors) rides INSIDE the meta, so it
   wraps with its words rather than being stranded at the end of the title row
   on a narrow lane. It takes the meta's grey through currentColor: the navy
   channel icon stays the lane's headline and this stays its caption. */
.sw-meta:has(.sw-trade) { display: inline-flex; align-items: center; gap: 5px; }
.sw-trade { width: 13px; height: 13px; flex: none; }
.sw-trade svg { width: 100%; height: 100%; display: block; }
/* Set by channels.js when any one lane's meta would wrap: then all three take
   a line of their own, so the three lanes' first messages stay level. */
.sw.js.meta-wrap .sw-meta { flex-basis: 100%; }

/* --- the two passes --- */

.sw-thread { display: flex; flex-direction: column; gap: 22px; }
/* Once JS is driving, the two passes share one grid cell: the lane is then
   permanently as tall as its taller pass, and switching language — the one
   moment in this section where the content genuinely changes size — costs
   nothing.

   This is `js`, NOT `live`. `live` is the animation, and it is dropped the
   moment the animation ends; if the overlay went with it, the board would
   double in height on its last frame, which is the largest layout shift the
   page could possibly make and it would land exactly where the reader is
   looking. `js` is added once and never removed, so what the animation leaves
   behind is a board the reader can then switch languages on by hand. */
.sw.js .sw-thread { display: grid; align-items: start; }
.sw.js .sw-pass { grid-area: 1 / 1; }

/* padding: 0 is not cosmetic. This was a <section> until it inherited the
   stylesheet's own `section { padding: clamp(56px, 9vw, 118px) 0 }` and carried
   118px of dead space above and below itself in all six places. It is a <div>
   now, and this line is here so it stays harmless if anyone changes it back. */
.sw-pass { display: flex; flex-direction: column; gap: 10px; padding: 0; }
/* Only in the static state, where they really are two blocks one after the
   other and the reader needs to see the join. */
.sw:not(.js) .sw-pass + .sw-pass {
  border-top: 1px dashed var(--hair);
  padding-top: 20px;
}
.sw.js .sw-pass { opacity: 0; pointer-events: none; transition: opacity 420ms var(--ease); }
.sw.js .sw-pass.on { opacity: 1; pointer-events: auto; }

.sw-pass-head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px 12px;
  min-height: 26px;
}
.sw-pass-lang {
  font-size: .7rem;
  letter-spacing: var(--ls);
  text-transform: uppercase;
  color: var(--ink-3);
}

/* --- the play button ---
   Shipped `hidden` and revealed by channels.js, for the same reason the
   "open the live assistant" button is: a control that does nothing is worse
   than no control, and with no JS there is no audio player to drive. */
.sw-play {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-left: auto;
  padding: 4px 10px 4px 8px;
  border: 1px solid var(--hair-2);
  border-radius: 100px;
  background: var(--surface);
  color: var(--ink-2);
  font: inherit;
  font-size: .72rem;
  font-weight: 600;
  letter-spacing: var(--ls);
  cursor: pointer;
  transition: color var(--fast) var(--ease), border-color var(--fast) var(--ease);
}
.sw-play[hidden] { display: none; }
.sw-play:hover { color: var(--ink); border-color: var(--navy); }
.sw-play svg { width: 11px; height: 11px; fill: currentColor; flex: none; }
.sw-play.playing { color: var(--spark); border-color: var(--spark); }
/* Playing, the triangle becomes a square. Drawn rather than swapped for a
   second icon, so the button holds its width and does not jog the row. */
.sw-play.playing svg { display: none; }
.sw-play.playing::before {
  content: "";
  width: 9px; height: 9px;
  border-radius: 1px;
  background: currentColor;
  flex: none;
}

/* --- the conversation --- */

.sw-turns {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.sw-msg { position: relative; max-width: 92%; }
.sw-in  { align-self: flex-start; }   /* the customer */
.sw-out { align-self: flex-end; }     /* the agent — this page's protagonist */

.sw-say {
  display: block;
  font-size: .88rem;
  line-height: 1.6;
  padding: 8px 12px;
  color: var(--ink);
  transition: box-shadow var(--mid) var(--ease);
}
.sw-in  .sw-say { background: var(--surface); border: 1px solid var(--hair);
                  border-radius: var(--r) var(--r) var(--r) 1px; }
.sw-out .sw-say { background: rgba(12, 36, 84, .07); border: 1px solid transparent;
                  border-radius: var(--r) var(--r) 1px var(--r); }

/* The line currently being SPOKEN by the audio. Not a colour change — the
   text is already at full contrast and dimming the others to make this one
   stand out would make five lines worse to make one better. A spark edge,
   and nothing else moves. */
.sw-msg.speaking .sw-say { box-shadow: inset 3px 0 0 var(--spark); }
.sw-out.speaking .sw-say { box-shadow: inset -3px 0 0 var(--spark); }

/* The gloss is the most valuable line in this section. It is what turns "we
   speak Cantonese" from a claim into evidence: an English-reading buyer can
   judge whether the answer was any good instead of admiring the characters.
   Set in the display serif so it reads as a translator's note rather than as
   another message. */
.sw-gloss {
  display: block;
  margin: 6px 2px 0;
  font-family: var(--display);
  font-style: italic;
  font-weight: 400;
  font-size: .95rem;
  line-height: 1.5;
  color: var(--ink-3);
}

/* Alignment is the only visual cue for who is speaking, and alignment is
   invisible to a screen reader. Scoped rather than added as a global utility,
   because this file has no utility classes and this is not the reason to
   start. */
.sw-role {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ATTRIBUTE selectors, not :lang(). :lang() matches on the INHERITED
   language, so it would also match every <i> channels.js wraps a line into,
   and a relative font-size would compound once per nesting level.
   [lang="..."] matches only the element that carries the attribute.

   Neither webfont carries CJK, so these lines fall to the OS face — which
   sets noticeably smaller at the same px and does not want Manrope's
   tracking. A little size back and the letter-spacing off, or the Cantonese
   reads as a footnote beside the English. Traditional and Simplified get
   different stacks on purpose: they are different readerships, and the wrong
   face is the tell. */
.chan [lang="yue-Hant"] {
  font-family: "PingFang HK", "Noto Sans HK", "Microsoft JhengHei", "Hiragino Sans", var(--sans);
  letter-spacing: 0;
}
.chan [lang="zh-Hans"] {
  font-family: "PingFang SC", "Noto Sans SC", "Microsoft YaHei", "Hiragino Sans GB", var(--sans);
  letter-spacing: 0;
}
/* Arabic joins its letters, and any letter-spacing pulls them apart into
   unjoined forms, so it is zeroed here as for the Han faces. Manrope has no
   Arabic, so the stack names the platform Arabic faces first. Vietnamese and
   Greek need no rule: Manrope's own subsets cover both. */
.chan [lang="ar"] {
  font-family: "Segoe UI", "Geeza Pro", "SF Arabic", "Noto Sans Arabic", Tahoma, var(--sans);
  letter-spacing: 0;
}
/* And the size is set where its base is known. A bare `font-size: 1.04em` on
   a selector that OUTRANKS .sw-say does not add 4% to .sw-say's .88rem — it
   replaces it, and 1.04em then resolves against the inherited body size,
   setting the CJK at 17px beside 14px of English. On the strip the em form is
   right, because there the parent IS the intended base. */
/* The 4% is for the Han faces only, which set small beside Latin at the same
   size. Vietnamese and Greek are Manrope, the same face as the English, so
   they take the English size; Arabic's joined script gets a little more. */
.sw-say:is([lang="yue-Hant"], [lang="zh-Hans"]) { font-size: calc(.88rem * 1.04); }
.sw-say[lang="ar"] { font-size: calc(.88rem * 1.08); }
.sw-rail b[lang] { font-size: 1.04em; }

/* A typing indicator is not content — it is a claim that a message is coming,
   which is false in a static document. So it is the one thing in this section
   the stylesheet hides on its own, and it is decoration (aria-hidden, no
   text), the same licence .field-glow and .progress take. */
.sw-dots { display: none; gap: 4px; padding: 8px 12px; }
/* Positioned OVER the bubble it stands in for, whose box is already reserved
   at opacity 0 — so switching it on moves nothing. Toggling `display` in flow
   instead cost ~34px of reflow four times a run, which moved the outcome
   chips and every section below the board. */
.sw.live .sw-dots.on { display: flex; position: absolute; top: 0; right: 0; }
.sw-dots i { width: 4px; height: 4px; border-radius: 50%; background: var(--ink-3); opacity: .3; }
.sw.live .sw-dots.on i { animation: swDot 1.05s var(--ease) infinite; }
.sw.live .sw-dots.on i:nth-child(2) { animation-delay: .15s; }
.sw.live .sw-dots.on i:nth-child(3) { animation-delay: .3s; }
@keyframes swDot {
  0%, 100% { opacity: .28; transform: translateY(0); }
  40%      { opacity: .85; transform: translateY(-2px); }
}

/* Three different KINDS of ending — a booking, an order, a handover — which is
   what stops the third lane reading as the first lane again. */
.sw-chip { margin-top: 4px; }
.sw-chip .tag { display: inline-block; white-space: nowrap; }

/* --- the foot: one instrument per channel ---
   margin-top:auto is what makes three cells read as one machine rather than
   three cards: whatever the transcripts do, the three instruments land on one
   baseline. */
.sw-foot { margin-top: auto; padding-top: 4px; }

/* web chat: a composer. Inert by construction — a span cannot take focus, so
   there is no field here that can swallow a message somebody types into it. */
.sw-type {
  display: flex;
  align-items: center;
  gap: 2px;
  min-height: 36px;
  padding: 8px 11px;
  border: 1px solid var(--hair);
  border-radius: var(--r);
  background: var(--surface);
  font-size: .84rem;
  line-height: 1.35;
  color: var(--ink-3);
}
.sw-typed { color: var(--ink); }
.sw.live .sw-type.typing .sw-ph { display: none; }
.sw-caret {
  display: inline-block;
  width: 1px; height: 1em;
  margin-left: 2px;
  background: var(--spark);
  vertical-align: -.1em;
}
.sw.live .sw-lane.live .sw-caret { animation: swCaret 1.05s steps(1) infinite; }
@keyframes swCaret { 50% { opacity: 0; } }

/* voice: a level meter. phone: a handset. Both sit in the same slot as the
   composer, so the three lanes rhyme without being the same picture. */
.sw-call { display: flex; align-items: center; gap: 10px; min-height: 36px;
           font-size: .74rem; letter-spacing: var(--ls); color: var(--ink-3); }

.sw-wave { --amp: 1.1; display: flex; align-items: flex-end; gap: 3px; height: 24px; flex: 1 1 auto; }
.sw-wave.on { --amp: 1.55; }
/* The rest height is a plain declaration, not the last frame of a keyframe.
   Under reduced motion an animation is COMPLETED, not stopped, so anything
   whose resting shape came from a keyframe would be a coin toss. --b is
   authored per bar in the markup and never exceeds .58, so --b * --amp cannot
   exceed 1 and no bar can overflow its own box. */
.sw-wave i {
  flex: 1 1 auto;
  min-width: 2px;
  height: 100%;
  border-radius: 1px;
  background: var(--navy);
  opacity: .34;
  transform: scaleY(var(--b, .3));
  transform-origin: bottom;
}
.sw.live .sw-lane.live .sw-wave i,
.sw-lane.sounding .sw-wave i {
  animation: swWave 880ms var(--ease) infinite;
  animation-delay: calc(var(--n, 0) * 55ms);
}
/* The meter also runs while the RECORDING is playing, which is the one time
   on this page a level meter is telling the literal truth. */
.sw-lane.sounding .sw-wave { --amp: 1.55; }
@keyframes swWave {
  0%, 100% { transform: scaleY(var(--b, .3)); }
  45%      { transform: scaleY(calc(var(--b, .3) * var(--amp, 1.1))); }
}

.sw-ring {
  position: relative;
  flex: none;
  width: 26px; height: 26px;
  display: grid;
  place-items: center;
  border: 1px solid var(--hair);
  border-radius: 50%;
  color: var(--navy);
}
.sw-ring svg { width: 13px; height: 13px; display: block; }
/* Navy, not spark. The spark has exactly three jobs in this section — the
   agent's dot, the progress rule and the line being spoken — and a spark used
   everywhere stops being one. */
.sw.live .sw-ring.ringing::after {
  content: "";
  position: absolute;
  inset: -1px;
  border: 1px solid var(--navy);
  border-radius: 50%;
  animation: swRing 1.1s var(--ease) infinite;
}
@keyframes swRing {
  0%   { transform: scale(1);    opacity: .5; }
  100% { transform: scale(1.85); opacity: 0; }
}
.sw-num { font-variant-numeric: tabular-nums; }

/* --- what the script reveals ---
   The transition lives only on `.on`, so hiding at setup is instantaneous and
   there is no fade-out flash before the first line arrives.

   The BUBBLE is hidden, not the <li> around it: the typing indicator is a
   child of that <li> and has to stay visible while the message it stands in
   for is still hidden. */
.sw.live .sw-msg .sw-say { opacity: 0; transform: translate3d(0, 8px, 0); }
.sw.live .sw-msg.on .sw-say {
  opacity: 1;
  transform: none;
  transition: opacity 340ms var(--ease), transform 340ms var(--ease);
}
.sw.live .sw-chip { opacity: 0; transform: translate3d(0, 6px, 0); }
.sw.live .sw-chip.on {
  opacity: 1;
  transform: none;
  transition: opacity 420ms var(--ease), transform 420ms var(--ease);
}

/* Words are wrapped by channels.js, and unwrapped again when it finishes.
   display and font-style are set UNSCOPED on purpose: the <i> elements outlive
   `live` for a moment, and an <i> without these renders italic and re-breaks
   the line. */
.sw-say > i { display: inline-block; font-style: inherit; }
.sw.live .sw-say > i { opacity: 0; transform: translate3d(0, .3em, 0); }
.sw.live .sw-msg.on .sw-say > i {
  animation: swWord .5s var(--ease) forwards;
  animation-delay: calc(var(--w, 0) * 40ms);
}
@keyframes swWord { to { opacity: 1; transform: none; } }

/* The gloss arrives after the line it translates, never with it: a translation
   follows the speech. --g is that line's own length, written by the script. */
.sw.live .sw-gloss { opacity: 0; }
.sw.live .sw-msg.on .sw-gloss {
  animation: swWord .4s var(--ease) forwards;
  animation-delay: var(--g, 300ms);
}

/* --- under the board --- */

.sw-acts {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 8px clamp(14px, 2.4vw, 30px);
  margin-top: 14px;
}
/* .btn is display:inline-flex, which BEATS the [hidden] attribute's UA
   display:none. Without these the buttons ship visible and dead on any browser
   where the thing they drive failed to load. */
.sw-try[hidden] { display: none; }

/* --- the language toggle, and what it does not list ---
   Two buttons for the two languages the board actually plays, and one line of
   plain text for the rest. A toggle with fourteen entries would be a language
   picker for a demonstration that only has two recordings — the sentence is
   honest about which two are on screen and which are merely true. Both ship
   `hidden`: without JS the two passes are already stacked on the page, and a
   control to switch between two things you can both see does nothing. */
.sw-langsel-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 8px clamp(12px, 2vw, 22px);
  margin-top: 20px;
}
/* One pill per ROUND, each naming its three languages, so they are separate
   pills with a gap rather than one segmented control: three long labels wrap
   onto a second line on a phone, and a segmented pill cannot wrap. */
.sw-langsel {
  display: inline-flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 6px;
  flex: 0 1 auto;
}
.sw-langsel[hidden] { display: none; }
.sw-langsel button {
  padding: 6px 14px;
  border: 1px solid var(--hair-2);
  border-radius: 100px;
  background: none;
  font: inherit;
  font-size: .74rem;
  font-weight: 600;
  letter-spacing: var(--ls);
  color: var(--ink-3);
  cursor: pointer;
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}
.sw-langsel button:hover { color: var(--ink); }
.sw-langsel button[aria-pressed="true"] { background: var(--ink); color: var(--on-ink); }

/* The board only demonstrates six languages; the web chat widget itself speaks
   fourteen (Kikuemon's own LANGS). Plain text, not more pills — the board isn't
   staging a demo of these, so they get no button and no round. */
.sw-langsel-more { margin-top: 10px; text-align: center; font-size: .74rem; line-height: 1.7; color: var(--ink-3); }
.sw-langsel-note {
  font-size: .8rem;
  line-height: 1.5;
  color: var(--ink-3);
  max-width: 54ch;
}

/* --- narrow ---
   The lanes stack. The claim does not depend on seeing three columns: the
   status strip names all three channels and all three languages in one line at
   the top, and each lane plays when it is the lane you are looking at
   (channels.js observes them one by one, not the board as a whole). */
@media (max-width: 820px) {
  .sw { grid-template-columns: 1fr; }
  .sw-core { gap: 6px 14px; padding: 12px 14px; font-size: .74rem; }
  .sw-demo { margin-left: 0; }
  .sw-msg { max-width: 100%; }
}
@media (max-width: 720px) {
  /* 360px viewport: .wrap leaves 320px, the lane's own padding leaves 292px.
     At .9rem a bubble is roughly 30 English characters or 15 Han characters a
     line — a real chat-widget measure rather than a squeezed one. */
  .sw-say { font-size: .9rem; }
  .sw-say:is([lang="yue-Hant"], [lang="zh-Hans"]) { font-size: calc(.9rem * 1.04); }
  .sw-say[lang="ar"] { font-size: calc(.9rem * 1.08); }
  .sw-gloss { font-size: .96rem; }
  .sw-head h3 { font-size: .9rem; }
}

/* Off, not reduced. channels.js returns before it adds `live`, so almost
   nothing above applies at all. This block is belt and braces for the two
   animations that are not behind that gate, and for anything a later edit
   forgets to scope. The play button is NOT disabled here: pressing it is a
   choice, and someone who has asked not to be shown motion has not asked not
   to be allowed to listen. */
@media (prefers-reduced-motion: reduce) {
  .sw-wave i, .sw-dots i, .sw-ring::after, .sw-caret { animation: none !important; }
  .sw-msg, .sw-say, .sw-chip, .sw-gloss, .sw-say > i, .sw-pass {
    opacity: 1 !important;
    transform: none !important;
    animation: none !important;
  }
  .sw-travel { display: none; }
}


/* =============================================================
   Services

   A full-width index of eight icon tiles, then eight rows that
   alternate copy and a live wireframe drawing. Replaces the old
   Services block wholesale — .svc, ul.spec, p.note, .offer > a and
   the [id] rule all move here.

   ⚠️ MOVE THIS BLOCK TO THE TRUE END OF THE FILE. The old one is no
   longer last: "The switchboard" was appended after it. Nothing
   here shares a selector with the switchboard so the move is free,
   but it is not optional: `.offer-icon .num` is (0,2,0), exactly
   tied with `.offer .num`, and wins on source order alone.
   ============================================================= */

:root {
  /* 1520, not "whatever the window is". The copy track is capped at
     --svc-copy whatever happens, so every pixel past this goes to the drawing
     alone — and past about 1520 the drawing out-scales the words beside it
     and the row stops being two columns and becomes a picture with a caption.
     It is also wider than a 1440 laptop, so on a laptop this page IS edge to
     edge and only a large desktop ever meets the cap. */
  --maxw-wide: 1520px;
  /* The copy track. Fluid, not fixed, and that is the whole measure control:
     a flat 32rem holds a 46ch line beautifully at 1520 and then sits there at
     1000px while the canvas beside it shrinks to 348px and the row goes
     visibly lopsided. Clamped against vw the track gives width back as the
     page loses it, so the copy travels between about 40ch (at the 22rem floor)
     and 46ch (at the 32rem ceiling, where the max-widths below finally bite)
     and the drawing keeps the larger half at every width in between. */
  --svc-copy: clamp(22rem, 34vw, 32rem);
}
.wrap-wide { max-width: var(--maxw-wide); }

/* The bar is sticky at 52px and the page scrolls smoothly, so a jump from the
   index landed the heading it targeted BEHIND the bar. The extra 28px is not
   decoration — a heading flush against the bar reads as clipped. Unscoped,
   because /#contact from the header CTA has exactly the same bug. */
[id] { scroll-margin-top: calc(52px + 28px); }

/* --- the page header ---
   DIRECT children only, which is what lets this box go without a class: every
   other .wrap-wide here is a .svc-grid whose only children are the two
   columns, so `.wrap-wide > p` can match nothing else. It also out-weighs
   .sec-head's own rules if that class turns out to be on the box too. */
/* The page title is one word, centred, with nothing under it but the index.
   A max-width and text-wrap:balance are for a title that might wrap; this one
   cannot, so both are pointless here and margin:auto is what does the work.

   Small and quiet on purpose. A single word set at display size is a banner
   announcing what the reader can already see in the nav; at this size it is a
   label on the page, and the eight tiles under it become the first real thing.

   `font-weight: 300` is already the lightest Cormorant Garamond this site
   loads, so the extra lightness is carried by COLOUR — ink-2 rather than full
   ink — and by the tracking. Asking for a weight below 300 would silently
   synthesise one and thicken the letterforms on some browsers instead.

   The padding is the "further down from the header" part: the bar is 52px and
   transparent at rest, so a title sitting at the top of the flow reads as
   though it is inside the bar rather than under it. */
.svc-title {
  font-size: clamp(1.35rem, 2.1vw, 1.75rem);
  font-weight: 300;
  letter-spacing: .02em;
  color: var(--ink-2);
  text-align: center;
  margin: 0 auto;
  /* Reduced 2026-09-28: at 12 tiles (3 rows) the old clamp(74px,11vw,152px)
     pushed the index low enough that its third row was cropped by the
     viewport on an ordinary laptop on load (measured 71px cropped at
     1440x900) instead of sitting cleanly below the fold. See service_tiles()
     in build.py for the other half of that day's fix: the tiles lost
     `data-rise` entirely, for a reason this padding value can't fix on its
     own. Still comfortably clear of the 52px bar at the low end. */
  padding-top: clamp(64px, 7.5vw, 110px);
}
.wrap-wide > .offer { margin-top: clamp(24px, 3.5vw, 40px); }

/* --- the closing call to action ---
   The paragraphs that used to sit here are gone; what is left is the ask, on
   its own, centred under the last row. Centred because it is not part of the
   two-column rhythm above it — it is the end of the page, and a left-aligned
   pair of buttons under eight alternating rows reads as a ninth row that lost
   its drawing. */
.svc-cta {
  text-align: center;
  padding: clamp(46px, 7vw, 92px) 0 clamp(20px, 3vw, 40px);
}
.svc-cta .btn { margin: 0 6px; }

/* --- the index ---
   Eight divides by four and by two and by nothing else useful. auto-fit across
   1392px lays five tiles then three, and the three empty cells are not empty:
   this grid's gutters ARE its background, so a missing tile is a rectangle of
   hairline. Hence counted columns rather than fitted ones. */
.offer-icon { grid-template-columns: repeat(4, minmax(0, 1fr)); }

/* .offer's cell rules are typed to `> article`, and by the same argument a
   cell with no background of its own is a hole rather than a tile. An index
   has to be links, so links have to be cells. */
.offer > a {
  background: var(--bg);
  padding: clamp(22px, 2.4vw, 30px);
  transition: background var(--mid) var(--ease);
}
.offer > a:hover { background: var(--surface); }

/* Explicit placement, no wrappers added to the markup: icon and numeral share
   row 1, heading and clause span both columns, and row 3 is the 1fr that
   absorbs the slack — which is what lands every arrow on one baseline however
   the clauses wrap. (.sw-foot buys that with margin-top:auto; same idea, rows
   named instead.) */
.offer-icon > a {
  display: grid;
  grid-template-columns: 22px minmax(0, 1fr);
  grid-template-rows: auto auto 1fr auto;
  column-gap: 12px;
}

/* 22px and hairline strokes: the icon is a diagram of the service, not a
   badge, so it is drawn in the same line the page divides things with. */
.svc-ico {
  grid-area: 1 / 1;
  width: 22px; height: 22px;
  color: var(--ink-3);
  transition: color var(--mid) var(--ease), transform var(--mid) var(--ease);
}
.svc-ico svg {
  width: 100%; height: 100%; display: block;
  fill: none; stroke: currentColor;
  stroke-width: 1.4; stroke-linecap: round; stroke-linejoin: round;
}

/* The numeral was 2.4rem of display serif. Beside a 22px icon that is two
   pictures in one corner and they fight — the eye cannot tell which one the
   tile is about. The icon wins the picture; the numeral becomes a folio: same
   serif, a third of the size, ink-3, parked in the opposite corner where it
   does its one remaining job, matching the "01 —" kicker of the section it
   links to. Tabular, so the eight of them line up down the grid. */
.offer-icon .num {
  grid-area: 1 / 2;
  justify-self: end;
  align-self: center;
  margin: 0;
  font-size: .82rem;
  letter-spacing: var(--ls);
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}
.offer-icon h3 { grid-column: 1 / -1; grid-row: 2; margin: 16px 0 8px; }
.offer-icon p  { grid-column: 1 / -1; grid-row: 3; margin: 0; }
.offer-icon .arrow { grid-column: 1 / -1; grid-row: 4; justify-self: start; margin: 14px 0 0; }

/* The arrow moves rather than merely tints, for the same reason it does
   everywhere else on the site: movement reads as a response, a fade as a delay. */
.offer > a .arrow {
  display: inline-block;
  color: var(--ink-3);
  transition: transform var(--mid) var(--ease), color var(--mid) var(--ease);
}
.offer > a:hover .arrow { transform: translateX(5px); color: var(--spark); }

/* Navy on the icon, spark on the arrow — NOT spark on both. One tile with two
   bright marks spends the page's single accent twice, and of the two only the
   arrow carries information ("this is a link"). The 2px lift is the shift
   .social a:hover already uses; the site has one hover. */
.offer-icon > a:hover .svc-ico { color: var(--navy); transform: translateY(-2px); }

/* --- the eight rows ---
   A bare `section` carries clamp(56px,9vw,118px) top AND bottom, and eight of
   those stacked is eight pages badly joined.

   There is no rule between rows any more (the owner asked for the lines out),
   so whitespace is the ONLY thing separating one service from the next and it
   has to be enough to read as a break rather than a paragraph gap. Hence more
   padding here than the hairline version needed, not less. */
.svc-row { padding: clamp(52px, 7vw, 104px) 0; }

/* Named areas, not `order` and never direction:rtl. rtl flips punctuation —
   an English full stop lands on the wrong end of the sentence — and `order`
   leaves the tab sequence to luck the moment the columns swap. Areas move the
   two boxes and touch nothing inside either.
   minmax(0, …) on both tracks: a bare fr keeps a min-content floor, so one
   long word, or a canvas backing store wider than its box, pushes the track
   open and the row overflows the page. */
.svc-grid {
  display: grid;
  grid-template-columns: minmax(0, var(--svc-copy)) minmax(0, 1fr);
  grid-template-areas: "copy viz";
  align-items: center;
  gap: clamp(28px, 4vw, 64px);
}
.svc-copy { grid-area: copy; min-width: 0; }
.svc-viz  { grid-area: viz;  min-width: 0; }

/* The flip: tracks AND areas, because the narrow track belongs to the copy and
   has to travel with it. `nth-of-type`, not `nth-child` — the page header is a
   div sibling, and nth-child would count it and flip the wrong four. */
.svc-row:nth-of-type(even) .svc-grid {
  grid-template-columns: minmax(0, 1fr) minmax(0, var(--svc-copy));
  grid-template-areas: "viz copy";
}

/* --- the copy column ---
   OUTSIDE .post, so it inherits none of that column's paragraph rhythm; with
   the global `p { margin: 0 }` underneath, every margin below is the rhythm
   rather than a correction to one. The ch caps are the CEILING on the measure
   and --svc-copy is the floor: between them a line here is never longer than
   46 characters and never much shorter than 40, at any window width. */
.svc-copy .kicker { margin: 0 0 14px; }
.svc-copy h2 {
  font-family: var(--display);
  font-weight: 300;
  font-size: clamp(1.7rem, 3.2vw, 2.5rem);
  letter-spacing: -.015em;
  line-height: 1.06;
  margin: 0;
  max-width: 18ch;
  text-wrap: balance;
}
.svc-copy h2 em { font-style: italic; color: var(--navy); }
.svc-copy p { max-width: 46ch; color: var(--ink-2); }
.svc-copy .lede { margin: 16px 0 0; font-size: 1.06rem; line-height: 1.7; }

/* "What you get" is scanned, not read. Discs and a hanging indent make it look
   like prose that has been chopped up, so the marker is the page's own hairline
   stood on its end. The `ul.spec` / `p.note` type qualifiers are gone: they
   existed only to out-weigh `.post ul`, and this column is not in .post any
   more — a specificity fight that has simply ended. */
.svc-copy .spec { list-style: none; margin: clamp(20px, 2.6vw, 28px) 0 0; padding: 0; }
.svc-copy .spec li {
  border-left: 1px solid var(--hair-2);
  padding: 1px 0 1px 14px;
  margin: 0 0 11px;
  max-width: 46ch;
  font-size: .94rem;
  line-height: 1.65;
  color: var(--ink-2);
}
.svc-copy .spec li:last-child { margin-bottom: 0; }
.svc-copy .spec strong { color: var(--ink); font-weight: 600; }

/* The caveat — the thing that would weaken the lede if it were inside it.
   ink-3 at .84rem is the weight .post .meta already carries: legible, never
   competing with the claim it qualifies. */
.svc-copy .note {
  margin: clamp(16px, 2.2vw, 24px) 0 0;
  max-width: 50ch;
  font-size: .84rem;
  line-height: 1.6;
  color: var(--ink-3);
}

/* --- the drawing ---
   No frame, no ground, no grid: the wireframe sits directly on the page and
   the row reads as one thing rather than as a column of words beside a picture
   of something else.

   That costs the designed empty state, and the trade is deliberate. This canvas
   IS empty with JS off, before the script boots, and on every frame under
   reduced motion — framed, that emptiness looked like a picture that failed to
   load, which is the worse failure. Unframed it is simply space, and space is
   what a page has plenty of.

   The ratio stays, because it is the contract with the script: it is the reason
   nothing moves when the canvas boots. Deliberately NO max-height — a height cap
   silently beats aspect-ratio and hands the script a box of a shape it was never
   told about. --maxw-wide is what stops this growing taller than a screen. */
.svc-viz {
  position: relative;
  aspect-ratio: 4 / 3;
}
.svc-viz canvas { width: 100%; height: 100%; display: block; }

@media (max-width: 1080px) {
  /* Four tiles here is a ~250px column holding a heading and a clause, under
     the 260px floor .offer itself already treats as the minimum. */
  .offer-icon { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 900px) {
  /* One column, and the drawing goes AFTER the words. The hero does the
     opposite — its ring goes first — because there the picture IS the claim;
     here the sentence is the claim and the canvas illustrates it. BOTH
     templates are restated: the even-row rule is the longer selector and would
     otherwise keep its two-area template down to a phone, stranding four
     canvases above their own headings. */
  .svc-grid,
  .svc-row:nth-of-type(even) .svc-grid {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "copy" "viz";
    gap: clamp(22px, 5vw, 34px);
  }
  .svc-row { padding: 34px 0; }
}

@media (max-width: 560px) {
  /* At 375px --pad is at its 20px floor: 335px of copy, a 335 x 251 drawing.
     The icon keeps its 22px — shrinking the smallest thing on the page saves
     8px and costs the tile the only picture it has. */
  .offer-icon { grid-template-columns: minmax(0, 1fr); }
  .offer-icon h3 { margin-top: 14px; }
  .svc-copy .note, .svc-copy .spec li { max-width: none; }
}

/* Off, not reduced. The global rule at the top of the file already collapses
   every duration to .001ms; naming them again is the belt and braces the
   switchboard block keeps, so a later edit that re-declares one of these
   `transition` shorthands cannot quietly reintroduce it. */
@media (prefers-reduced-motion: reduce) {
  .offer > a,
  .offer > a .arrow,
  .svc-ico { transition: none !important; }
}
