/* ===================================================================
   Daniel Ro AI — the parts bin.

   Ported from wimn.co's home.html (BRAND AMBASSADOR AGENT/src/gabi/web/
   templates/site/home.html), which is where every one of these components
   was built and proven. The structure and the class names are deliberately
   identical so a section can be moved across with a find/replace on the
   colour names and nothing else.

   The one real difference: wimn.co is light-only and hardcodes four hex
   values. This site ships light AND dark, so every colour is a CSS variable
   and every component reads the variable. Light is the default.

   Colours come from brand/tokens.json (LOCKED 2026-07-15). Do not invent one.
   =================================================================== */

/* ── Tokens ─────────────────────────────────────────────────────────
   Stored as space-separated RGB channels, not hex, so Tailwind's opacity
   modifiers keep working: text-ink/75, bg-accent/20, border-edge/40.
   That is what lets her markup port over untouched.                  */

:root {
  /* TELLS THE BROWSER WHICH THEME ITS OWN CHROME IS IN — scrollbars, the
     form controls on /contact, autofill, the flash before first paint.
     It has to be stated now that the OS preference is ignored: without
     it, a visitor on a dark Mac reads the light page through dark
     scrollbars and dark-rendered inputs, which is the one part of the
     page the stylesheet does not own. */
  color-scheme: light;

  --page:        244 245 250;   /* #f4f5fa  page background          */
  --card:        255 255 255;   /* #ffffff  card on the page         */
  --ink:          15  23  42;   /* #0f172a  body text                */
  --ink-soft:     63  76  99;   /* #3f4c63  secondary text           */
  --muted:       100 116 139;   /* #64748b  meta, captions           */
  --edge:        221 227 238;   /* #dde3ee  hairline borders         */
  --accent:      109  40 217;   /* #6d28d9  accent AS TEXT on light  */
  --accent-solid:139  92 246;   /* #8b5cf6  fills, rings, glows      */
  /* VIOLET AS *SMALL* TEXT. --accent is the right violet for a headline
     word, but on the dark page it resolves to #8b5cf6, which measures
     3.97:1 and misses the bar at 12-14px. This is the same violet one
     step brighter for small text: accent_text_light on the light page,
     violet_block_text_dark on the dark one. Both are locked tokens.  */
  --violet-text: 109  40 217;   /* #6d28d9  on a light page          */
  --indigo:       99 102 241;   /* #6366f1                            */
  --ember:        99 102 241;   /* #6366f1  eyebrows                  */
  --on-deep:     241 245 249;   /* #f1f5f9  text on a deep card      */

  /* The surfaces set, approved 2026-08-16, tokens.json colors.surfaces.
     These are warm violet-blacks, NOT slate, and that is the whole point of
     them — the site was reading cold and over-indigo without them. */
  --deep:         29  25  56;   /* #1D1938  ink night, the dark bands */
  --deep-end:     29  29  40;   /* #1D1D28  where the gradient lands  */
  --panel:        30  37  58;   /* #1E253A  card fill on a dark band  */
  --panel-edge:   52  62  92;   /* #343E5C  hairline, never heavy     */
  --well:         11  15  28;   /* #0B0F1C  inset wells, darkest tone */
  --text-dim:    176 186 208;   /* #B0BAD0  body on a dark surface    */
  --text-faint:  128 138 160;   /* #808AA0  meta on a dark surface    */
  --feature:      30  37  58;   /* the featured card: Panel on a light page */

  /* The amber set, tokens.json colors.semantic. It is the only warm colour
     Daniel owns and nothing used it, which is why the page read violet from
     top to bottom. It is deliberately NOT theme-aware: the one card it is on
     is a fixed dark violet in both themes. */
  --amber:       245 158  11;   /* #F59E0B  the accent itself         */
  --amber-text:  253 230 138;   /* #FDE68A  amber as readable text    */
  /* AMBER AS TEXT ON A THEME-AWARE SURFACE. #F59E0B is a dark-surface
     value: on the light page it measures 2.2 against the background and is
     unreadable. tokens.json carries #92400E as amber_label_light for
     exactly this, and the dark override below puts the bright one back. */
  --amber-label: 146  64  14;   /* #92400E  on a light page           */

  --grad: linear-gradient(135deg, #8b5cf6, #6366f1);
  /* The final CTA card. Deep violet falling to ink night, so violet stops
     being the field and the amber has something to break. */
  --grad-cta: linear-gradient(150deg, #2E1065 0%, #1D1938 52%, #1D1D28 100%);
  /* THE SAME COLOURS, STRAIGHT DOWN, for the full-bleed band at the bottom
     of the homepage. It has to be 180deg and not 150: a diagonal gradient
     on a full-width block is a DIFFERENT colour at every point along its
     bottom edge, so the flat footer beneath it can only ever match at one
     x and shows a seam everywhere else. Measured before this was fixed: 34
     at the left edge, 1 at the right. Vertical means the bottom edge is one
     colour, #1D1D28, which is exactly what the footer is. */
  --grad-cta-band: linear-gradient(180deg, #2E1065 0%, #1D1938 55%, #1D1D28 100%);
  --shadow-key: 15 23 42;       /* what shadows are tinted with      */
}

/* Dark mode IS the surfaces palette. The page is ink night, cards are panel,
   hairlines are panel edge, and the darkest band is the well. Slate is gone
   from here entirely — that cold grey-blue is what made it feel like every
   other AI site. */
:root[data-theme="dark"] {
  color-scheme: dark;

  --amber-label:245 158  11;   /* #F59E0B on a dark page */
  --violet-text:233 213 255;   /* #e9d5ff on a dark page */
  --page:         29  25  56;   /* #1D1938  ink night */
  --card:         30  37  58;   /* #1E253A  panel     */
  --deep:         11  15  28;   /* #0B0F1C  well      */
  --deep-end:     11  15  28;
  --panel:        30  37  58;
  --panel-edge:   52  62  92;
  --feature:      11  15  28;   /* Well — a step BELOW the other cards here */
  --ink:         241 245 249;   /* #f1f5f9 */
  --ink-soft:    176 186 208;   /* #B0BAD0  text dim   */
  --muted:       128 138 160;   /* #808AA0  text faint */
  --edge:         52  62  92;   /* #343E5C  panel edge */
  --accent:      139  92 246;
  --accent-solid:139  92 246;
  --on-deep:     241 245 249;
  --shadow-key:   0   0   0;
}

/* THE OS PREFERENCE IS NOT CONSULTED. Daniel's call, 2026-09-21: every
   visitor starts on light, whatever their machine is set to, and dark is
   something they choose here. There were five prefers-color-scheme blocks
   and each one only shadowed the :root[data-theme="dark"] rule above it,
   so dark lost nothing when they came out — it is reached by the toggle
   and by a saved choice, and by nothing else. Put one back and a visitor
   on a dark Mac lands on the dark site again. */

/* ── Type ───────────────────────────────────────────────────────────
   Archivo 800 is the headline font and BOTH stances are brand voices
   (tokens.json fonts.headline): italic is the voice, upright is the
   structure. Gabby is italic-only; this is the line between the two
   brands, so never collapse them. Body is Switzer, from Fontshare.   */

html { scroll-behavior: smooth; }

body {
  font-family: 'Switzer', -apple-system, BlinkMacSystemFont, sans-serif;
  background: rgb(var(--page));
  color: rgb(var(--ink));
}

/* .display = the voice. Hero lines, punch lines, big statements. */
.display {
  font-family: 'Archivo', sans-serif;
  font-style: italic;
  font-weight: 800;
  letter-spacing: -0.025em;
  text-wrap: balance;
}

/* .structure = section headers and UI. Same weight, upright. */
.structure {
  font-family: 'Archivo', sans-serif;
  font-style: normal;
  font-weight: 800;
  letter-spacing: -0.02em;
  text-wrap: balance;
}

/* Dark text on a light ground optically spreads at heavy display weights,
   so light mode tracks tighter. tokens.json fonts.tracking. */
:root[data-theme="dark"] .display,
:root[data-theme="dark"] .structure { letter-spacing: -0.01em; }

/* Archivo's width axis. Condensed and Extra Condensed are the two Daniel
   added to the kit — they belong on eyebrows, chips, the marquee and
   tickers, where upright 800 at full width gets too wide to sit quietly. */
.condensed     { font-family: 'Archivo', sans-serif; font-stretch: 75%; }
.xcondensed    { font-family: 'Archivo', sans-serif; font-stretch: 62%; }

.metric-num {
  font-family: 'Archivo', sans-serif;
  font-style: italic;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.03em;
}

/* Eyebrows: indigo, uppercase, tiny, bold, wide-tracked. They sit above a
   heading and say what section you are in. Nearly every section has one. */
.eyebrow {
  font-family: 'Archivo', sans-serif;
  font-stretch: 75%;
  font-weight: 800;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: rgb(var(--ember));
}

/* ── Motion / reveal ───────────────────────────────────────────────── */

.reveal {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity .6s cubic-bezier(.16,1,.3,1), transform .6s cubic-bezier(.16,1,.3,1);
}
.reveal.in { opacity: 1; transform: none; }

/* Headline slides up from behind a mask. Hero and section H2s. */
.mask { overflow: hidden; padding-bottom: .12em; }
.mask > span { display: inline-block; transform: translateY(115%); transition: transform .8s cubic-bezier(.16,1,.3,1); }
.mask.in > span { transform: none; }

/* Typewriter caret for the rotating headline. Feed it real sentences. */
.tw-caret { display: inline-block; width: .06em; background: currentColor; margin-left: .04em; animation: caret 1s steps(1) infinite; }
@keyframes caret { 50% { opacity: 0; } }
/* The rotating line is as tall as its tallest phrase, at every width.

   A hidden copy of every phrase is stacked into one grid cell with the live
   one. A cell is as tall as its tallest child, so the height is the worst
   case and it never moves while the caret types or deletes. The browser
   redoes it on resize and after the webfont swaps.

   This replaced a min-height, which could not hold: the tallest state depends
   on where each phrase wraps, so it is a different number at every width and
   a different one again whenever the copy changes. It was out by 123px at
   1024px, which threw the whole page down while you were reading it. */
.typeword-box { display: grid; }
.typeword-box > * { grid-area: 1 / 1; }
.typeword-ghost { visibility: hidden; pointer-events: none; user-select: none; }

/* ── The offer rows arrive in pairs ─────────────────────────────────
   Outcome 1 and action 1 come in together from their own sides, then
   pair 2, then pair 3. The point is to teach in two seconds that the
   columns are pairs read ACROSS, which is the whole argument of the
   section, and then to stop — the page already has the Gap cards, the
   FAQ and the CTA marquee moving, and a fourth permanent animation
   between them would read as a slot machine.

   No JavaScript. The card already carries .reveal, and that observer
   adds .in once and unobserves, so "plays once when you reach it" is
   free. The delay per pair is set inline from the loop index, so both
   columns share a number and cannot drift.

   The marks inherit their row's delay and scale up from 0.7 on an
   overshoot curve while the row itself slides on a settle curve. Same
   start, different motion, which is what stops five rows reading as one
   sliding block. */
.offer-row {
  opacity: 0;
  filter: blur(7px);
  transition: opacity .55s cubic-bezier(.16,1,.3,1),
              transform .55s cubic-bezier(.16,1,.3,1),
              filter .55s cubic-bezier(.16,1,.3,1);
}
.offer-row-l { transform: translateX(-18px); }
.offer-row-r { transform: translateX(18px); }
.reveal.in .offer-row { opacity: 1; transform: none; filter: blur(0); }

.offer-mark {
  transform: scale(.7);
  opacity: 0;
  transition: transform .45s cubic-bezier(.34,1.56,.64,1), opacity .35s;
  transition-delay: inherit;
}
.reveal.in .offer-mark { transform: scale(1); opacity: 1; }


/* The glow follows the cursor, and its colour follows the legend.

   Two layers, not one that recolours: a gradient's colour cannot be
   transitioned, but opacity can, so amber hands over to violet across the
   midline instead of snapping. Position tracks instantly, which is what a
   cursor light should do; the handover is what needs easing.

   --gx/--gy are written by site.js as percentages. */
.offer-card { --gx: 50%; --gy: 0%; }
.offer-glow {
  position: absolute; inset: 0; pointer-events: none;
  opacity: 0; transition: opacity .5s cubic-bezier(.16,1,.3,1);
}
.offer-glow-a { background: radial-gradient(440px circle at var(--gx) var(--gy), rgb(var(--amber) / .3), transparent 62%); }
.offer-glow-v { background: radial-gradient(440px circle at var(--gx) var(--gy), rgb(var(--accent-solid) / .32), transparent 62%); }
.offer-card.glow-l .offer-glow-a { opacity: 1; }
.offer-card.glow-r .offer-glow-v { opacity: 1; }

/* The spine draws down while the pairs land, so the divider stitches the
   two columns together instead of just sitting between them. Scaled from
   the top, and faded at both ends so it does not read as a hard rule. */
.offer-spine {
  background: linear-gradient(180deg, transparent, rgb(var(--accent-solid) / .45) 18%, rgb(var(--amber) / .28) 82%, transparent);
  transform: scaleY(0);
  transform-origin: top;
  transition: transform .95s cubic-bezier(.16,1,.3,1) .12s;
}
.reveal.in .offer-spine { transform: scaleY(1); }

@media (prefers-reduced-motion: reduce) {
  .offer-row, .offer-mark { opacity: 1; transform: none; filter: none; transition: none; }
  .offer-spine { transform: none; transition: none; }
  .offer-glow { display: none; }
}


/* ── The six service drawings draw themselves ───────────────────────
   Each animation IS the sentence its card is already making: the room
   fills, the shoot fans into a stack, the playhead runs the timeline, the
   waveform rises, the system wires itself together, the line climbs. Six
   different motions for six different kinds of thing, which is the same
   argument that got the six identical icon tiles thrown out.

   THE STATIC DRAWING IS THE FINISHED STATE. Every keyframe set declares
   only `from`, so its `to` is whatever the element already is, and the
   section renders correctly with the class never arriving — no JS, an old
   browser, reduced motion. Nothing here can leave a card half-drawn.

   IT PLAYS ONCE PER ARRIVAL, then stops. The page already has the Gap
   cards, the FAQ and the CTA marquee moving; six drawings looping between
   them is the slot machine the offer rows were written to avoid. site.js
   re-arms it on hover, so the card answers when you point at it and is
   still otherwise.

   --viz-delay is the card's own stagger, set inline from the loop index, so
   the grid cascades left to right and both layouts share one number.

   transform-box: fill-box is why scaleX/scaleY work per shape here. An SVG
   child's default transform-origin is the origin of the whole viewBox, not
   the shape, so scaling a bar about "center" without it throws it off the
   canvas instead of growing it in place. */

/* 1 · Seats. Dots pop in, left column to right, a row at a time. */
@keyframes svc-seat { from { transform: scale(0); opacity: 0; } }
.viz-go .v-seat {
  transform-box: fill-box; transform-origin: center;
  animation: svc-seat .4s cubic-bezier(.34,1.56,.64,1) both;
}
.viz-go .v-seat-on { animation-duration: .52s; }

/* 2 · Stack. The two behind start underneath the front card and slide out
   of it, so one session visibly becomes three pieces. The rotations live
   here, not on a transform attribute — a CSS transform overrides the
   attribute outright, so a half-CSS version snapped them flat. */
.v-stack-a { transform-box: view-box; transform-origin: 33px 34px; transform: rotate(-9deg); }
.v-stack-b { transform-box: view-box; transform-origin: 47px 32px; transform: rotate(-3deg); }
.v-stack-c { transform-box: fill-box; transform-origin: center; }
@keyframes svc-stack-a { from { transform: translate(28px, -4px) rotate(0deg); opacity: 0; } }
@keyframes svc-stack-b { from { transform: translate(14px, -2px) rotate(0deg); opacity: 0; } }
@keyframes svc-stack-c { from { transform: scale(.86); opacity: 0; } }
.viz-go .v-stack-a { animation: svc-stack-a .55s cubic-bezier(.16,1,.3,1) both; }
.viz-go .v-stack-b { animation: svc-stack-b .55s cubic-bezier(.16,1,.3,1) both; }
.viz-go .v-stack-c { animation: svc-stack-c .45s cubic-bezier(.34,1.56,.64,1) both; }

/* 3 · Timeline. Clips grow from their left edge, then the playhead runs
   the whole strip and parks where it was drawn. */
@keyframes svc-clip { from { transform: scaleX(0); opacity: 0; } }
@keyframes svc-play { from { transform: translateX(-66px); opacity: 0; } }
.viz-go .v-clip {
  transform-box: fill-box; transform-origin: left center;
  animation: svc-clip .42s cubic-bezier(.16,1,.3,1) both;
}
.viz-go .v-play {
  transform-box: view-box;
  animation: svc-play .95s cubic-bezier(.33,0,.15,1) both;
}

/* 4 · Waveform. Bars rise out of their own centre line in a ripple, which
   is what an audio meter does and what a flat fade-in does not. */
@keyframes svc-bar { from { transform: scaleY(0); opacity: 0; } }
.viz-go .v-bar {
  transform-box: fill-box; transform-origin: center;
  animation: svc-bar .46s cubic-bezier(.34,1.56,.64,1) both;
}

/* 5 · Nodes. Left node, wire, centre node, wire, right node — in that
   order, because the claim is that it gets BUILT once and then runs. The
   wires draw with pathLength="1", so one dash length covers any path
   without measuring it. The pip is the thing still running at the end. */
@keyframes svc-wire { from { stroke-dashoffset: 1; } }
@keyframes svc-node { from { transform: scale(0); opacity: 0; } }
@keyframes svc-pip  { 0% { transform: scale(0); opacity: 0; } 62% { transform: scale(1.55); opacity: 1; } 100% { opacity: .9; } }
.viz-go .v-wire { stroke-dasharray: 1; animation: svc-wire .3s linear both; }
.viz-go .v-node {
  transform-box: fill-box; transform-origin: center;
  animation: svc-node .4s cubic-bezier(.34,1.56,.64,1) both;
}
.viz-go .v-pip {
  transform-box: fill-box; transform-origin: center;
  animation: svc-pip .6s cubic-bezier(.16,1,.3,1) both;
}

/* 6 · Chart. The line draws, the area wipes in BEHIND the line head, the
   last point lands on an overshoot.

   The wipe is the point. Fading the whole fill up on a delay was the first
   version and it showed the finished shape while the line was still two
   thirds of the way along, which hands over the ending before the line
   gets there. inset() is measured against the polyline's own bounding box
   (border-box resolves to fill-box on an SVG child), so 100% is exactly
   its width with nothing to keep in sync by hand. Same duration and curve
   as the line, and the points are evenly spaced in x, so the fill edge
   sits under the line head the whole way. */
@keyframes svc-line { from { stroke-dashoffset: 1; } }
@keyframes svc-area { from { clip-path: inset(0 100% 0 0); } }
@keyframes svc-tip  { from { transform: scale(0); opacity: 0; } }
.viz-go .v-line { stroke-dasharray: 1; animation: svc-line .85s cubic-bezier(.33,0,.15,1) both; }
.viz-go .v-area { clip-path: inset(0 0 0 0); animation: svc-area .85s cubic-bezier(.33,0,.15,1) both; }
.viz-go .v-tip {
  transform-box: fill-box; transform-origin: center;
  animation: svc-tip .42s cubic-bezier(.34,1.56,.64,1) both;
}

@media (prefers-reduced-motion: reduce) {
  .viz-go .v-seat, .viz-go .v-stack-a, .viz-go .v-stack-b, .viz-go .v-stack-c,
  .viz-go .v-clip, .viz-go .v-play, .viz-go .v-bar, .viz-go .v-wire,
  .viz-go .v-node, .viz-go .v-pip, .viz-go .v-line, .viz-go .v-area,
  .viz-go .v-tip { animation: none; }
}


/* ── The contact form's honeypot ────────────────────────────────────
   A field a person never sees and a form-filling bot fills in.

   NOT display:none and NOT type="hidden". Both are the first things a bot
   skips, which turns the trap into decoration. This moves it out of the
   viewport and takes it out of the layout and the tab order while leaving
   it a real, visible-to-the-DOM text input. aria-hidden and tabindex="-1"
   are on the markup so a screen reader and a keyboard never reach it. */
.hp-field {
  position: absolute !important;
  left: -9999px;
  top: auto;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* The contact links. The border picks up the accent on hover the way the
   service cards do, so they read as the thing to press. */
.social-row:hover { border-color: rgb(var(--accent-solid) / .5); }

/* ── The bento tiles on /contact ────────────────────────────────────
   Seven tiles telling one story: three facts, then what moves first and
   when, then leads → bookings → sales along the bottom.

   Same contract as the service drawings above — the STATIC state is the
   FINISHED state, every keyframe declares only `from`, site.js adds
   .viz-go on arrival. Read that block before changing anything here.

   PAIRED ELEMENTS MATCH, and copy alone cannot hold it. A label that sits
   on one line at 1440 wraps at 1024 and back at 860, so there is no set of
   strings that is one line at every width. Both rows are subgrids: every
   card's parts land on the same rows as its neighbours' whatever the copy
   does. A min-height reserve is the wrong tool — it leaves a hole under
   whichever card is shortest. */

@supports (grid-template-rows: subgrid) {
  @media (min-width: 640px) {
    /* Row 1 goes three abreast at sm; row 3 only at lg, so its subgrid is
       declared in its own query below. */
    /* Row 1 — number, then label. Two rows. */
    .bento-facts { grid-template-rows: auto auto; }
    .bento-facts > .bento-fact { display: grid; grid-template-rows: subgrid; grid-row: span 2; gap: .375rem; }

    /* Row 3 — eyebrow, heading, body, drawing. Four rows, and the drawing
       row is the one that stretches, so every drawing sits on the same
       baseline however tall the copy above it ran. */
    .bento-seq { grid-template-rows: auto; }
    .bento-seq > .bento-tile { display: flex; flex-direction: column; gap: .5rem; }
    .bento-seq > .bento-tile > .svc-viz { margin-top: auto; }
  }
}

@supports (grid-template-rows: subgrid) {
  @media (min-width: 1024px) {
    /* Row 3 — eyebrow, heading, body, drawing. Four rows, and the drawing
       row is the one that stretches, so every drawing sits on the same
       baseline however tall the copy above it ran. */
    .bento-seq { grid-template-rows: auto auto auto 1fr; }
    .bento-seq > .bento-tile { display: grid; grid-template-rows: subgrid; grid-row: span 4; gap: .5rem; }
  }
}

/* Without subgrid: a flex column with the drawing pushed to the bottom.
   The cards are still the same height because the grid stretches them, so
   the only thing that moves is where the copy sits inside. */
@supports not (grid-template-rows: subgrid) {
  .bento-seq > .bento-tile { display: flex; flex-direction: column; gap: .5rem; }
  .bento-seq > .bento-tile > .svc-viz { margin-top: auto; }
  .bento-facts > .bento-fact { display: flex; flex-direction: column; gap: .375rem; }
}

/* The tile itself lifts on hover the way the service cards do, and its
   border picks up its own stop on the violet-to-indigo walk. */
.bento-tile { transition: transform .3s cubic-bezier(.4,0,.2,1), border-color .3s, box-shadow .3s; }
.bento-tile:hover { transform: translateY(-4px); border-color: rgb(var(--card-tone) / .55); }

/* 1 · The three facts. Their numbers count up from zero — site.js does the
   counting; all this does is stop the digits shifting the label while they
   run. tnum holds every digit at the same width, so 7 and 400 occupy the
   same space and nothing under them moves. */
.countup { font-variant-numeric: tabular-nums; }

/* 2 · Months. Each bar grows out of its own baseline, so three months read
   as a climb rather than three boxes fading in. The labels come up with
   their bars rather than scaling, which would blur them. */
@keyframes svc-month  { from { transform: scaleY(0); opacity: 0; } }
@keyframes svc-montht { from { transform: translateY(6px); opacity: 0; } }
.viz-go .v-month {
  transform-box: fill-box; transform-origin: bottom;
  animation: svc-month .5s cubic-bezier(.34,1.56,.64,1) both;
}
.viz-go .v-month-t {
  transform-box: fill-box; transform-origin: center;
  animation: svc-montht .45s cubic-bezier(.16,1,.3,1) both;
}

/* 3 · Bubbles. Two arrive from the left, then the reply comes back from
   the right, then it is typing. They come in from the side they belong to,
   which is what makes it read as a conversation and not as three shapes. */
@keyframes svc-bub-l { from { transform: translateX(-14px) scale(.9); opacity: 0; } }
@keyframes svc-bub-r { from { transform: translateX(14px) scale(.9); opacity: 0; } }
@keyframes svc-bub-d { from { transform: scale(0); opacity: 0; } }
.viz-go .v-bub { transform-box: fill-box; transform-origin: center; }
.viz-go .v-bub-l { animation: svc-bub-l .42s cubic-bezier(.34,1.56,.64,1) both; }
.viz-go .v-bub-r { animation: svc-bub-r .42s cubic-bezier(.34,1.56,.64,1) both; }
.viz-go .v-bub-dot {
  transform-box: fill-box; transform-origin: center;
  animation: svc-bub-d .3s cubic-bezier(.34,1.56,.64,1) both;
}

/* 4 · Calendar. The empty days lay themselves out first and the booked
   ones land after, one at a time — the delay split is in the template, not
   here, because it is per-cell. Booked days pop; empty ones just appear. */
@keyframes svc-day    { from { transform: scale(.7); opacity: 0; } }
.viz-go .v-day {
  transform-box: fill-box; transform-origin: center;
  animation: svc-day .34s cubic-bezier(.16,1,.3,1) both;
}
.viz-go .v-day-on { animation-duration: .46s; animation-timing-function: cubic-bezier(.34,1.56,.64,1); }

/* 5 · Funnel. Views, leads, booked, sales — each step lands, then the wire
   draws to the next one. The order is the claim, so it has to be a
   sequence and not four things appearing together. */
@keyframes svc-step   { from { transform: scale(.82); opacity: 0; } }
@keyframes svc-stepw  { from { stroke-dashoffset: 1; } }
.viz-go .v-step, .viz-go .v-step-t {
  transform-box: fill-box; transform-origin: center;
  animation: svc-step .4s cubic-bezier(.34,1.56,.64,1) both;
}
.viz-go .v-step-wire { stroke-dasharray: 1; animation: svc-stepw .2s linear both; }

@media (prefers-reduced-motion: reduce) {
  .viz-go .v-month, .viz-go .v-month-t, .viz-go .v-bub, .viz-go .v-bub-l,
  .viz-go .v-bub-r, .viz-go .v-bub-dot, .viz-go .v-day, .viz-go .v-day-on,
  .viz-go .v-step, .viz-go .v-step-t, .viz-go .v-step-wire { animation: none; }
  .bento-tile:hover { transform: none; }
}

/* Card titles reserve two lines, so every card's body starts on the same
   row as its neighbours'.

   Copy alone cannot fix this. A title that sits on one line at 1440 wraps
   at 1024 and back at 860, because each width has its own wrap point, so
   there is no set of strings that is one line at every size. Measured:
   the workshop and process grids both matched at 1440 and 1280 and both
   broke at 1024.

   The grid already stretches cards to a common height, so the only thing
   that was actually misaligning was where the body started. Reserving the
   title box fixes that at every width at once. Where every title happens
   to be one line the reserve is uniform, so the row still reads level —
   just with a little more air. */
/* The workshop cards share their rows with each other, so the title row is
   as tall as the tallest title and no taller. That is the whole difference
   from a min-height reserve, which is tall whether anything needs it or not
   and leaves a hole under a one-line title.

   Falls back safely: without subgrid the cards are ordinary blocks and the
   row behaves exactly as it did before. */
@supports (grid-template-rows: subgrid) {
  /* .ws-card was the workshop point cards on the homepage. That section is
     a video band now and the cards live only on /workshop, so the rule is
     gone with them. Left as a note because the technique below is the same
     one and this is where to look for it. */

  /* The service cards, same idea, four rows: visual, name, description,
     "Learn more". Two of the six names wrap at 1024 — "AI Systems and
     Automation" and "Strategy, Ads and Consulting" — and they cannot be
     shortened, because the service names have to match the Google Business
     Profile exactly. Sharing rows is what keeps the row level anyway, with
     no reserve and no dead space where a name happens to fit on one line. */
  .svc-grid > .svc-card { display: grid; grid-template-rows: subgrid; grid-row: span 4; }
}

/* Without subgrid the cards fall back to the column they were before. */
@supports not (grid-template-rows: subgrid) {
  .svc-card { display: flex; flex-direction: column; }
  .svc-card > span:last-child { margin-top: auto; }
}

/* ── Cards ─────────────────────────────────────────────────────────── */

.liftcard { transition: transform .3s cubic-bezier(.4,0,.2,1), box-shadow .3s; }
.liftcard:hover { transform: translateY(-6px); box-shadow: 0 18px 40px -18px rgba(var(--shadow-key) / .35); }

/* Cursor-following glow. Reads best on the deep cards. */
.spotlight { position: relative; overflow: hidden; }
.spotlight::before {
  content: ''; position: absolute; inset: 0; pointer-events: none; opacity: 0; transition: opacity .3s;
  background: radial-gradient(220px circle at var(--mx,50%) var(--my,50%), rgba(139,92,246,.35), transparent 65%);
}
.spotlight:hover::before { opacity: 1; }

/* The Gap cards walk a highlight down the stack, one at a time, so the
   section reads as three beats rather than a wall of three paragraphs.
   `is-live` is the same treatment as a hover deliberately — it is the card
   saying "you could be pointing at me", not a second visual language.

   Desktop only. Below 768px these are tap-to-expand accordions and a
   highlight moving on its own would fight the thing the finger is doing.

   This has to sit after `.reveal.in`, which is also two classes and also
   sets transform: whichever comes last wins, and the lift has to. */
@media (min-width: 768px) {
  .truth-card.is-live {
    transform: translateY(-6px);
    border-color: rgb(var(--live-tone, var(--accent-solid)) / .5);
    box-shadow: 0 18px 40px -16px rgb(var(--live-tone, var(--accent-solid)) / .45);
  }
  .truth-card.is-live::before { opacity: 1; }
}

/* ── SECTION RHYTHM, AND HOW SECTIONS SEPARATE ──────────────────────
   The page was 48px, 64px and 80px of padding with no system behind
   which got which, plus exactly two border-t rules — on the process and
   podcast sections and nowhere else. Two dividers on a twelve-section
   page read as random because they were: a line under one section says
   nothing about where any other one begins.

   Both lines are gone. The separation is now two things doing one job:

     .sec        one gap, everywhere, on the light passage
     .sec-mark   a short gradient rule opening every section

   A mark is a POSITIVE cue — this is where a section starts — where a
   divider is a negative one that cuts the page into pieces. Repeated at
   every heading it carries the rhythm without a single line between
   sections, which is what Daniel picked on 2026-09-20.

   .sec-band is the dark full-bleed punctuation: The Gap, the workshop,
   the podcast. It gets more inside it than a light section does, because
   a colour change needs room to read as a band and not as a stripe.

   .sec-after-band is the section that FOLLOWS one. Coming off a
   full-bleed colour you need more air than a step between two sections
   on the same ground — Who I Am opening 64px under the end of The Gap
   was the tightest transition on the page. */

.sec        { padding-top: 3.5rem; padding-bottom: 3.5rem; }
.sec-band   { padding-top: 4rem;   padding-bottom: 4rem; }

@media (min-width: 768px) {
  .sec      { padding-top: 5.5rem; padding-bottom: 5.5rem; }
  .sec-band { padding-top: 6.5rem; padding-bottom: 6.5rem; }
  .sec-after-band { padding-top: 7.5rem; }
}

.sec-mark {
  width: 44px;
  height: 3px;
  border-radius: 99px;
  background: var(--grad);
  margin-bottom: 1.15rem;
}

/* ── EVERY MONTH — panels, not cards ────────────────────────────────
   Five tinted panels butted together inside one rounded block. No card
   borders anywhere: the site already has a bento on /contact and a
   second set of white cards with borders would read as the same
   component twice. The 2px seams are the grid gap showing through the
   block's own background, which is why the block sets a colour and the
   panels each set their own.

   THE SHAPE IS THE POINT. 2, 1, 1 across the top and 1, 3 under it.
   Five equal tiles is a grid; this is a composition.

   The tints walk violet to indigo across the five, two locked stops and
   the steps between. No new colour. */

.mgrid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 2px;
  border-radius: 1.25rem;
  overflow: hidden;
  background: rgb(var(--edge));
}
.mgrid > .span-1 { grid-column: span 1; }
.mgrid > .span-2 { grid-column: span 2; }
.mgrid > .span-3 { grid-column: span 3; }
.mgrid > .span-4 { grid-column: span 4; }

/* TOP-ALIGNED, NOT CENTRED. Centring each panel's content floated the
   overlines to different heights across a row — panel 1's overline fits
   one line and panel 2's takes two, so every panel started somewhere
   different and the row read as ragged. Anchored to the top they all
   begin on the same line. */
.mpanel {
  background: rgb(var(--accent-solid) / .055);
  padding: 1.4rem 1.6rem;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  min-height: 9.5rem;
}
.mpanel:nth-child(2) { background: rgb(var(--accent-solid) / .045); }
.mpanel:nth-child(3) { background: rgb(var(--indigo) / .045); }
.mpanel:nth-child(4) { background: rgb(var(--indigo) / .055); }

/* Fixed dark in both themes, like the other dark surfaces on this site,
   so nothing on it may use a theme-aware colour. */
.mpanel-dark { background: #1D1938; }

.mpanel-small {
  font-family: Archivo, system-ui, sans-serif;
  font-weight: 800;
  font-size: 10px;
  letter-spacing: .2em;
  text-transform: uppercase;
  line-height: 1.5;
  color: rgb(var(--accent-solid));
  margin-bottom: .55rem;

  /* TWO LINES RESERVED, so every outcome in a row starts on the same
     baseline whatever its overline did. The overlines are genuinely
     different lengths — "I edit everything we need" against "I shoot
     your content or use what you already have" — and no wording fixes
     that without losing what the copy says. The reserve is above the
     outcome and identical in every panel, so it reads as spacing rather
     than as a hole. */
  min-height: 3em;
}
.mpanel-large {
  font-family: Archivo, system-ui, sans-serif;
  font-weight: 800;
  font-size: 1.18rem;
  line-height: 1.2;
  letter-spacing: -.025em;
  color: rgb(var(--ink));
}
.mgrid > .span-2 .mpanel-large,
.mgrid > .span-3 .mpanel-large { font-size: 1.65rem; line-height: 1.14; letter-spacing: -.03em; }

.mpanel-dark .mpanel-small { color: #A78BFA; }
.mpanel-dark .mpanel-large { color: #F1F5F9; }

/* ── The scroll-in ──────────────────────────────────────────────────
   The overline lands before the outcome it belongs to, which is the
   order you read them in. Both hang off .reveal.in — the observer the
   whole site already uses — and --mb-delay is the panel's own stagger,
   set inline from the loop index so the two lines inside cannot drift
   away from the panel they are in.

   The copy is in the markup at full strength; this only ever runs
   forwards. Nothing here can leave a panel blank. */
.mpanel-small, .mpanel-large {
  opacity: 0;
  transform: translateY(9px);
  transition: opacity .5s cubic-bezier(.16,1,.3,1), transform .5s cubic-bezier(.16,1,.3,1);
}
.mpanel.reveal.in .mpanel-small {
  opacity: 1; transform: none;
  transition-delay: calc(var(--mb-delay, 0s) + .06s);
}
.mpanel.reveal.in .mpanel-large {
  opacity: 1; transform: none;
  transition-delay: calc(var(--mb-delay, 0s) + .2s);
}

@media (max-width: 767px) {
  /* Stacked. The outcome stays dominant — shrinking it to fit is the one
     thing the brief ruled out. */
  .mgrid { grid-template-columns: 1fr; }
  .mgrid > .span-1, .mgrid > .span-2,
  .mgrid > .span-3, .mgrid > .span-4 { grid-column: span 1; }
  .mpanel { min-height: 0; padding: 1.25rem 1.35rem; }
  .mpanel-large,
  .mgrid > .span-2 .mpanel-large,
  .mgrid > .span-3 .mpanel-large { font-size: 1.32rem; line-height: 1.18; }
}

@media (prefers-reduced-motion: reduce) {
  .mpanel-small, .mpanel-large { opacity: 1; transform: none; transition: none; }
}

/* ── YOUR NEXT STEPS — the rail ─────────────────────────────────────
   A hairline with three nodes on it. The line fills as far as the live
   step and stays hollow past it; the live node is filled with a ring
   around it; the two you are not on drop back.

   THE STATIC STATE IS ALL THREE AT FULL STRENGTH, rail fully drawn.
   site.js adds .rail-on and everything below it starts applying, so with
   no JavaScript, or under reduced motion, this is three readable steps
   joined by a line. Nothing here can leave a step unreadable.

   NOTHING REFLOWS WHEN THE STEP CHANGES. Every step is set at the same
   type size and the ones you are not on are pulled back with transform:
   scale, which does not affect layout. Sizing the live step up would
   re-wrap its body, change the column height and shove the rest of the
   page down every few seconds — the same trap the rotating hero line
   has, and the reason .typeword-box exists. Differentiate with colour,
   never with size, is the standing rule and this is the case it is for:
   the apparent size change is painted, not laid out.

   THE SEGMENT LIVES INSIDE ITS COLUMN rather than being one bar measured
   across the grid. It spans its own column and reaches into the gap to
   the next node, so "filled up to here" is only which segments carry the
   gradient, and it stays right at every width with no calc() against the
   column count or the gap. */

.rail-step { position: relative; padding-top: 3.25rem; }

.rail-node {
  position: absolute; top: 0; left: 0;
  width: 22px; height: 22px; border-radius: 99px;
  box-sizing: border-box;
  border: 2px solid rgb(var(--accent-solid));
  background: rgb(var(--page));
  transition: background-color .6s cubic-bezier(.16,1,.3,1),
              border-color .6s, box-shadow .6s cubic-bezier(.16,1,.3,1);
}
.rail-node::after {
  content: ''; position: absolute; inset: 4px;
  border-radius: 99px; background: rgb(var(--accent-solid));
  transition: background-color .6s, opacity .6s;
}

/* THE HALO. A soft radial glow behind the live node that breathes, so
   the node reads as where the rail is RESTING rather than as one dot
   that happens to be filled. A static ring says "this one"; a breathing
   one says "this one, now, and it is about to move on".

   It is a separate ::before rather than an animated box-shadow: a
   box-shadow animation repaints the node's own box every frame, and the
   node carries the border and fill transitions that run when the step
   changes. Kept apart, the halo can loop on its own without fighting
   them. ::after was already the inner dot.

   inset: -13px and a transparent stop at 70% is what makes it a glow and
   not a second ring — a hard edge here would read as a third circle. */
.rail-node::before {
  content: ''; position: absolute; inset: -13px;
  border-radius: 99px;
  background: radial-gradient(circle, rgb(var(--accent-solid) / .5), rgb(var(--accent-solid) / 0) 70%);
  opacity: 0;
  transition: opacity .6s cubic-bezier(.16,1,.3,1);
  pointer-events: none;
}

/* Reaches from this node into the gap and up to the next one. */
.rail-line {
  position: absolute; top: 10px; left: 22px; right: -2.5rem;
  height: 2px; border-radius: 2px;
  background: var(--grad);
  transition: background-color .6s, opacity .6s;
}

.rail-num {
  font-style: italic;
  font-size: 15px;
  letter-spacing: -.02em;
  color: rgb(var(--accent-solid));
  margin-bottom: .45rem;
  transition: opacity .6s;
}
.rail-body { font-size: 15px; }

.rail-copy {
  transform-origin: top left;
  transition: transform .6s cubic-bezier(.16,1,.3,1), opacity .6s cubic-bezier(.16,1,.3,1);
}

/* Live only once the script is driving it. */
.rail.rail-on .rail-step .rail-copy { opacity: .36; transform: scale(.95); }
.rail.rail-on .rail-step.is-live .rail-copy { opacity: 1; transform: none; }

/* Ahead of the live step: hollow node. */
.rail.rail-on .rail-step.is-ahead .rail-node { border-color: rgb(var(--edge)); }
.rail.rail-on .rail-step.is-ahead .rail-node::after { opacity: 0; }

/* THE SEGMENT IS HOW FAR YOU HAVE COME, so it is hollow by default and
   fills only behind the live node. Tying it to the node's own state
   instead left the segment LEAVING the live step filled, which drew the
   line past the point it is meant to stop at. */
.rail.rail-on .rail-line { background: rgb(var(--edge)); }
.rail.rail-on .rail-step.line-done .rail-line { background: var(--grad); }

/* The live node: filled, ringed, and glowing. */
.rail.rail-on .rail-step.is-live .rail-node {
  background: rgb(var(--accent-solid));
  box-shadow: 0 0 0 6px rgb(var(--accent-solid) / .15);
}
.rail.rail-on .rail-step.is-live .rail-node::after { background: #ffffff; }
.rail.rail-on .rail-step.is-live .rail-node::before {
  opacity: 1;
  animation: rail-halo 2.6s ease-in-out infinite;
}

/* Slow, and it never fully fades. A halo that blinks out reads as a
   status light; one that breathes between .55 and 1 reads as something
   resting there. 2.6s is long enough that it is felt rather than
   watched, and the node only holds for 4.2s anyway. */
@keyframes rail-halo {
  0%, 100% { transform: scale(.82); opacity: .55; }
  50%      { transform: scale(1.12); opacity: 1; }
}

@media (max-width: 639px) {
  /* Stacked, the rail runs DOWN the left instead of across. */
  .rail-step { padding-top: 0; padding-left: 2.5rem; }
  .rail-line { top: 22px; left: 10px; right: auto; bottom: -2.5rem; width: 2px; height: auto; }
  .rail-num { margin-top: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .rail.rail-on .rail-step .rail-copy { opacity: 1; transform: none; }
  .rail.rail-on .rail-step .rail-node { border-color: rgb(var(--accent-solid)); box-shadow: none; }
  .rail.rail-on .rail-step .rail-node::after { opacity: 1; }
  .rail.rail-on .rail-line { background: var(--grad); }
  /* The halo goes entirely: a looping pulse is the one thing someone who
     asked the browser to stop animating is asking to be rid of. */
  .rail.rail-on .rail-step.is-live .rail-node::before { animation: none; opacity: 0; }
}

/* ── The Gap cards are a coverflow ─────────────────────────────────
   Three cards, one of them the point. The other two sit behind it,
   smaller, dimmer and slightly out of focus, so the eye is given one
   message at a time instead of three paragraphs at once.

   THE STATIC STATE IS THE THREE-CARD STACK. site.js adds .cover-on and
   everything here starts applying; with no JavaScript, or with reduced
   motion, the section is exactly the stack it has always been, all three
   readable and nothing moving. Showing only the front card as the
   fallback would hide two thirds of the copy from anyone who asked the
   browser to stop animating things, which is the one group that cannot
   go and look for it.

   HEIGHT COMES FROM THE TALLEST CARD, not from a number. All three sit
   in the same grid cell and a cell is as tall as its tallest child, so
   the container reserves the worst case at every width and the browser
   redoes it on resize. A min-height cannot hold this: the three bodies
   are different lengths and wrap at different points, so the tallest of
   them is a different number at 1440 than at 1024. Same mechanism as
   .typeword-box, and it is here for the same reason.

   DEPTH IS translateZ, NOT JUST scale. The back cards are pushed away
   from the camera and the perspective does the shrinking, which is why
   they read as behind rather than merely smaller. The rotateX is four
   degrees. It is deliberately not enough to see as a rotation — it is
   enough to stop the stack looking like flat rectangles, and any more
   starts to spin. */

#truth-cycle.cover-on {
  display: grid;
  perspective: 1400px;
  padding: 3rem 0;
}
/* space-y-3 puts a margin between children. In one grid cell that margin
   offsets the card instead of separating it from anything. */
#truth-cycle.cover-on > * + * { margin-top: 0; }

#truth-cycle.cover-on > .truth-card {
  grid-area: 1 / 1;
  transform-style: preserve-3d;
  transition: transform .7s cubic-bezier(.16,1,.3,1),
              opacity .7s cubic-bezier(.16,1,.3,1),
              filter .7s cubic-bezier(.16,1,.3,1),
              border-color .7s, box-shadow .7s;

  /* THE CARD HAS TO BE OPAQUE, and this is the whole thing that makes a
     coverflow read as one. Stacked, the card is bg-white/[0.04] over the
     band — four per cent — so the front card does not occlude anything
     and all three bodies print through each other into an unreadable
     smear. Nothing else was wrong with the geometry; you simply could
     not see which card was in front.

     NO NEW COLOUR. This is the band's own --deep with the same 4% white
     veil the card already carried, composited instead of blended, so a
     card that is not moving looks exactly as it always did. */
  background-color: rgb(var(--deep));
  background-image: linear-gradient(rgba(255,255,255,.04), rgba(255,255,255,.04));
}

#truth-cycle.cover-on > .truth-card.cf-active {
  transform: translate3d(0, 0, 0);
  opacity: 1;
  filter: none;
  z-index: 3;
}
#truth-cycle.cover-on > .truth-card.cf-prev {
  transform: translate3d(0, -2.4rem, -150px) rotateX(4deg) scale(.98);
  opacity: .44;
  /* Depth of field. Barely there, and it is what stops the back cards
     competing for the eye once they are still. */
  filter: blur(.6px);
  z-index: 1;
}
#truth-cycle.cover-on > .truth-card.cf-next {
  transform: translate3d(0, 2.4rem, -150px) rotateX(-4deg) scale(.98);
  opacity: .44;
  filter: blur(.6px);
  z-index: 1;
}

/* A back card is the way to the message on it. The front one is already
   in front, so it keeps the default cursor. */
#truth-cycle.cover-on > .truth-card.cf-prev,
#truth-cycle.cover-on > .truth-card.cf-next { cursor: pointer; }
#truth-cycle.cover-on > .truth-card.cf-active { cursor: default; }

/* liftcard's hover lift and is-live's are both translateY, and either one
   landing on a card mid-flight throws it out of the stack. The coverflow
   owns the transform while it is on. */
#truth-cycle.cover-on > .truth-card:hover { box-shadow: none; }
#truth-cycle.cover-on > .truth-card.cf-active.is-live {
  border-color: rgb(var(--live-tone, var(--accent-solid)) / .55);
  box-shadow: 0 26px 60px -20px rgb(var(--live-tone, var(--accent-solid)) / .55);
}

@media (max-width: 767px) {
  /* Shallower, because the column is narrower and the same offsets read
     as the cards falling out of it. */
  #truth-cycle.cover-on { perspective: 900px; padding: 2rem 0; }
  #truth-cycle.cover-on > .truth-card.cf-prev {
    transform: translate3d(0, -1.6rem, -110px) rotateX(4deg) scale(.98);
  }
  #truth-cycle.cover-on > .truth-card.cf-next {
    transform: translate3d(0, 1.6rem, -110px) rotateX(-4deg) scale(.98);
  }
  /* The accordion is off while this is on: one card at a time and
     tap-to-expand cannot both be true, and the front card shows its body
     in full. Tapping a back card brings it forward instead. */
  #truth-cycle.cover-on .truth-body { max-height: none; opacity: 1; overflow: visible; }
  #truth-cycle.cover-on .truth-chev { display: none; }
}

/* BELOW 1024 THE BACK CARDS PEEK AS FRAMES, NOT AS WORDS.

   The strip of a back card that shows past the front one is only empty if
   that card happens to have padding at the point it is cut. On a narrow
   column the bodies run to three and four lines and fill the card, so what
   peeked out was half a sentence from a card you are not reading — it read
   as text bleeding through a bug, not as depth.

   Hiding the contents leaves the frame, which is the thing that has to
   carry "there are more of these". The copy is still in the DOM and one
   tap brings the whole card forward. Above 1024 the column is wide enough
   that the bodies stop short of the cut and the faint text is part of the
   effect, so it stays. */
@media (max-width: 1023px) {
  #truth-cycle.cover-on > .truth-card.cf-prev > *,
  #truth-cycle.cover-on > .truth-card.cf-next > * {
    opacity: 0;
    transition: opacity .45s cubic-bezier(.16,1,.3,1);
  }
  #truth-cycle.cover-on > .truth-card.cf-active > * {
    opacity: 1;
    transition: opacity .45s cubic-bezier(.16,1,.3,1) .12s;
  }
}

/* Specialty card hover shadows — override liftcard's generic one. */
.spec-indigo:hover { box-shadow: 0 18px 40px -14px rgba(99,102,241,.38) !important; }
.spec-violet:hover { box-shadow: 0 18px 40px -14px rgba(139,92,246,.48) !important; }
.spec-deep:hover   { box-shadow: 0 20px 50px -14px rgba(139,92,246,.55) !important; }

/* ── The video card ────────────────────────────────────────────────── */

/* The bloom behind the player.

   Inset on both axes and then blurred wider than that inset, so the colour
   spills out past the card's edges instead of stopping at them — that spill
   is the whole effect. A box-shadow cannot do it: a shadow follows the
   card's own rectangle, so it reads as a drop shadow tinted violet rather
   than as light coming off the screen.

   It is the locked gradient blurred, not a new colour, and it sits under a
   z-10 card so it never touches the poster or the controls. */
.vglow {
  position: absolute; inset: -16px; z-index: 0;
  border-radius: 2.5rem;
  background: var(--grad);
  filter: blur(56px);
  opacity: .56;
  pointer-events: none;
  transition: opacity .6s cubic-bezier(.16,1,.3,1), filter .6s cubic-bezier(.16,1,.3,1);
}
.vwrap:hover .vglow { opacity: .81; filter: blur(66px); }

/* A tighter, brighter pool low and centre. One even halo reads as a printed
   rectangle of colour; light actually pools, so a second smaller source
   under the player is what makes it look lit rather than painted. */
.vglow-core {
  position: absolute; left: 12%; right: 12%; bottom: -26px; height: 62%; z-index: 0;
  border-radius: 999px;
  background: radial-gradient(closest-side, rgb(var(--accent-solid) / .85), transparent);
  filter: blur(42px);
  opacity: .5;
  pointer-events: none;
  transition: opacity .6s cubic-bezier(.16,1,.3,1);
}
.vwrap:hover .vglow-core { opacity: .72; }

/* The sound pill. Amber on the card's only control, because a black pill on
   a dark video frame is the one thing nobody sees, and the dark text is the
   card's own ground colour rather than black. */
.vsound {
  background: rgb(var(--amber));
  color: #1D1D28;
  box-shadow: 0 8px 22px -8px rgb(var(--amber) / .7);
}
.vsound svg { color: #1D1D28; }

@media (prefers-reduced-motion: reduce) {
  .vglow, .vglow-core { transition: none; }
}

.play-ring { transition: transform .3s cubic-bezier(.4,0,.2,1), background .3s; }
.vcard:hover .play-ring { transform: scale(1.08); }
.vcard:hover img { transform: scale(1.03); }
.vcard img { transition: transform .5s cubic-bezier(.16,1,.3,1); }

/* ── Buttons ───────────────────────────────────────────────────────── */

/* The hero button, with a sheen that sweeps across it. The single best
   button on wimn.co — it earns the primary action and nothing else. */
.cta-primary { position: relative; overflow: hidden; }
.cta-primary::before {
  content: ''; position: absolute; inset: 0; border-radius: inherit;
  background: linear-gradient(110deg,
      transparent 30%,
      rgba(255,255,255,.28) 47%,
      rgba(255,255,255,.42) 50%,
      rgba(255,255,255,.20) 53%,
      transparent 68%);
  transform: translateX(-150%);
  animation: cta-sheen 3.5s ease-in-out infinite;
}
@keyframes cta-sheen {
  0%   { transform: translateX(-150%); opacity: 0; }
  12%  { opacity: 1; }
  88%  { opacity: 1; }
  100% { transform: translateX(250%); opacity: 0; }
}

/* Primary fill is the locked violet→indigo gradient, white text, 8px. */
.btn-grad {
  background: var(--grad);
  color: #fff;
  border-radius: 8px;
  font-weight: 600;
  touch-action: manipulation;
}
.btn-ghost {
  background: transparent;
  border: 1px solid rgb(var(--edge));
  color: rgb(var(--ink));
  border-radius: 8px;
  font-weight: 600;
  transition: border-color .25s, background-color .25s;
}
.btn-ghost:hover { border-color: rgb(var(--accent-solid) / .6); background: rgb(var(--accent-solid) / .07); }

/* Button drifts toward the cursor. Pure delight, costs nothing.
   No transition here on purpose — JS drives it (instant follow, smooth return). */
.magnetic { will-change: transform; }

/* ── Social proof: the infinite testimonial wall ───────────────────── */

.twall-mask {
  -webkit-mask-image: linear-gradient(180deg, transparent, #000 14%, #000 86%, transparent);
          mask-image: linear-gradient(180deg, transparent, #000 14%, #000 86%, transparent);
}
.tcol-track { display: flex; flex-direction: column; gap: 1rem; animation: scrollUp 42s linear infinite; }
.tcol-track.rev  { animation-direction: reverse; }
.tcol-track.slow { animation-duration: 56s; }
.twall:hover .tcol-track { animation-play-state: paused; }
@keyframes scrollUp { to { transform: translateY(-50%); } }

/* The tilted review card that straightens on hover. */
.tcard { transition: transform .35s cubic-bezier(.16,1,.3,1), box-shadow .35s; }
.tcard:hover {
  transform: rotate(0deg) scale(1.02) !important;
  box-shadow: 0 20px 50px -15px rgba(var(--shadow-key) / .25);
  z-index: 10; position: relative;
}
mark.hl      { background: rgba(139,92,246,.22); border-radius: 3px; padding: 0 3px; color: inherit; }
mark.hl-dark { background: rgba(139,92,246,.32); border-radius: 3px; padding: 0 3px; color: #fff; }
.tcard-avatar { transition: box-shadow .25s; }
.tcard:hover .tcard-avatar { box-shadow: 0 0 0 3px rgba(139,92,246,.5); }

/* Desktop gets the 3 vertical columns, mobile a horizontal marquee. */
@media (min-width: 768px) { .twall-mobile { display: none; } }
.twall-mobile {
  overflow: hidden; white-space: nowrap;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
}
.mob-review-track { display: inline-flex; gap: 1rem; padding-right: 1rem; animation: marquee 30s linear infinite; }
.twall-mobile:hover .mob-review-track { animation-play-state: paused; }
.mob-review-track .tcard { flex-shrink: 0; width: 260px; white-space: normal; transform: none !important; }

/* ── The marquee ───────────────────────────────────────────────────── */

.marquee {
  overflow: hidden; white-space: nowrap;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}
/* align-items: center is load-bearing. The track is a flex row and the
   default is stretch, so the mark — which has a fixed height — pinned to the
   TOP of the line box and sat about 6px above the middle of the type. */
.marquee-track { display: inline-flex; align-items: center; gap: 2.5rem; padding-right: 2.5rem; animation: marquee 30s linear infinite; }
/* Each item is a flex child, and a flex child will happily wrap its own text
   even inside a nowrap parent. Gabby's phrases are short enough that it never
   showed; Daniel's are three and four words, so without this the ticker comes
   out as a stack of broken columns. */
.marquee-track > span { white-space: nowrap; flex: 0 0 auto; }
.marquee:hover .marquee-track { animation-play-state: paused; }
@keyframes marquee { to { transform: translateX(-50%); } }

/* The vertical one, in the final CTA. Same mechanism turned 90 degrees: the
   list is rendered twice and the track travels exactly one copy, so the loop
   has no seam.

   The reference build for this did the edge fade with a requestAnimationFrame
   loop measuring every item's distance from the centre on every frame, and
   then again with two stacked vignette divs. The mask below is the same
   result in one line, with no JavaScript and nothing running per frame.

   NO gap on the track. Height would then be 2N items + (2N-1) gaps and -50%
   lands half a gap out, which shows as a stutter once per loop. The spacing
   lives on the items instead, so the track is exactly 2N equal boxes. The
   horizontal one solves the same problem with a trailing padding. */
.vmarquee {
  overflow: hidden;
  -webkit-mask-image: linear-gradient(180deg, transparent, #000 20%, #000 80%, transparent);
          mask-image: linear-gradient(180deg, transparent, #000 20%, #000 80%, transparent);
}
.vmarquee-track { display: flex; flex-direction: column; animation: vmarquee 26s linear infinite; }
/* Archivo's width axis, pulled in just enough that the longest name —
   "Gyms and fitness facilities" — stays on one line in the column. Wrapping
   one item out of eight breaks the rhythm of the scroll, and dropping the
   type size to fix it would have cost the whole list its presence. Same
   italic 800 stance as the heading, only narrower. */
.vmarquee-track > * { font-stretch: 85%; white-space: nowrap; }
.vmarquee:hover .vmarquee-track { animation-play-state: paused; }
@keyframes vmarquee { to { transform: translateY(-50%); } }

@media (prefers-reduced-motion: reduce) {
  .vmarquee-track { animation: none; }
  /* Without the travel the mask would fade the top of a static list for no
     reason, so show it plainly and let the column clip. */
  .vmarquee { -webkit-mask-image: none; mask-image: none; }
}

/* ── Structure / decoration ────────────────────────────────────────── */

.navlink { position: relative; }
.navlink::after {
  content: ''; position: absolute; left: 0; bottom: -3px; height: 2px; width: 0;
  background: rgb(var(--accent-solid)); transition: width .25s;
}
.navlink:hover::after, .navlink.active::after { width: 100%; }

/* Dashed connector for the journey timeline. */
.jline { background: repeating-linear-gradient(90deg, rgba(139,92,246,.42) 0, rgba(139,92,246,.42) 8px, transparent 8px, transparent 18px); }
/* Opaque so the dashed line sits cleanly behind it. */
.jicon {
  background-color: rgb(var(--accent-solid) / .14);
  transition: background-color .25s, box-shadow .25s, transform .25s cubic-bezier(.34,1.56,.64,1);
}
.jicon svg { transition: transform .4s cubic-bezier(.34,1.56,.64,1); }
.jicon:hover { background-color: #8b5cf6; box-shadow: 0 8px 20px rgba(139,92,246,.45); transform: scale(1.08); }
.jicon:hover svg { color: #fff; }
/* Each icon moves like the thing it represents. */
.jicon-search:hover svg { transform: rotate(-15deg) scale(1.12); }
.jicon-plan:hover svg   { transform: rotate(6deg) scale(1.1); }
.jicon-chat:hover svg   { animation: jwobble .5s ease-in-out; }
.jicon-bolt:hover svg   { animation: jzap .45s ease-in-out; }
@keyframes jwobble { 0%,100%{transform:rotate(0)} 25%{transform:rotate(-12deg)} 75%{transform:rotate(12deg)} }
@keyframes jzap    { 0%,100%{transform:scale(1)} 30%{transform:scale(1.3) rotate(-8deg)} 60%{transform:scale(.95) rotate(6deg)} }

/* Floating rings behind the final CTA. Intentional, not blobs. */
@keyframes floatA { 0%,100%{transform:translate(0,0) scale(1)} 50%{transform:translate(30px,-36px) scale(1.08)} }
@keyframes floatB { 0%,100%{transform:translate(0,0) scale(1)} 50%{transform:translate(-26px,28px) scale(0.92)} }
.circle-a { animation: floatA 5s ease-in-out infinite; }
.circle-b { animation: floatB 7s ease-in-out infinite; }

.soc-icon { transition: transform .2s cubic-bezier(.34,1.56,.64,1), background-color .2s, box-shadow .2s; }
.soc-icon:hover { transform: scale(1.15) translateY(-2px); background: rgba(139,92,246,.45) !important; box-shadow: 0 4px 16px rgba(139,92,246,.35); }
.soc-icon:hover svg { color: #fff !important; }

/* ── The endcap ─────────────────────────────────────────────────────
   ONE block, ONE background, holding the closing section and the footer.

   The bottom of the homepage was two stacked backgrounds that happened to
   meet at a matching colour: the CTA had a gradient, the footer had a flat
   fill beneath it. Matched is not the same as single, and it read as a card
   with a footer glued under it.

   Now the gradient belongs to the wrapper and runs the whole height, from
   the top of the CTA to the bottom of the page. The glows sit on it too, so
   they light behind the footer as well.

   It starts HARD. No fade in from the page colour — that wash was mine and
   Daniel did not ask for it. The Gap band and the workshop band both begin
   on a hard edge, and this is the same language.

   .endcap alone (every other page) is the flat ink-night footer. */
.endcap { background: #1D1D28; position: relative; overflow: hidden; }
.endcap-tall { background: var(--grad-cta-band); }
.endcap-tall::before,
.endcap-tall::after { content: ''; position: absolute; inset: 0; pointer-events: none; }
.endcap-tall::before { background: radial-gradient(1100px circle at 72% 30%, rgb(var(--accent-solid) / .26), transparent 62%); }
.endcap-tall::after  { background: radial-gradient(760px circle at 3% 4%, rgb(var(--amber) / .15), transparent 62%); }
.endcap > * { position: relative; z-index: 1; }

/* Footer links. Fixed, because the footer band is dark in both themes and
   text-muted would have inverted against a surface that does not. */
.foot-link { color: rgba(255,255,255,.55); }
.foot-link:hover { color: #C4B5FD; }

/* ── FAQ ───────────────────────────────────────────────────────────── */

/* The answer opens on grid-template-rows, 0fr to 1fr.

   This is the one way to animate a box to its own content height without
   knowing that height. max-height cannot: you have to pick a number, and a
   number that clears the longest answer makes every shorter one ease out
   most of its duration against nothing, so they all close at different
   apparent speeds. The inner element carries overflow:hidden — the grid
   child is what gets clipped, not the track. */
.faq-a { display: grid; grid-template-rows: 0fr; transition: grid-template-rows .42s cubic-bezier(.16,1,.3,1); }
.faq-item.is-open .faq-a { grid-template-rows: 1fr; }
.faq-a-inner { overflow: hidden; opacity: 0; transition: opacity .3s .06s; }
.faq-item.is-open .faq-a-inner { opacity: 1; }

.faq-q { background: none; border: 0; cursor: pointer; font: inherit; color: inherit; }
.faq-q:focus-visible { outline: 2px solid rgb(var(--accent-solid)); outline-offset: -2px; border-radius: 1rem; }
.faq-num { color: rgb(var(--accent-solid) / .45); transition: color .3s; }
.chev { transition: transform .35s cubic-bezier(.16,1,.3,1); }

/* The open row is the only lit one, so a walk down the list reads as one
   thing moving rather than six cards flickering. */
.faq-item { transition: border-color .35s, background-color .35s; }
.faq-item.is-open { border-color: rgb(var(--accent-solid) / .45); background: rgb(var(--accent-solid) / .05); }
.faq-item.is-open .faq-num { color: rgb(var(--accent-solid)); }
.faq-item.is-open .chev { transform: rotate(45deg); }
.faq-item:not(.is-open):hover { border-color: rgb(var(--accent-solid) / .3); }

/* The dwell bar. Zero height until the walk is actually running, so a
   clicked-open row shows nothing. */
.faq-timer { height: 2px; background: rgb(var(--accent-solid) / .12); opacity: 0; transition: opacity .25s; }
.faq-timer > span { display: block; height: 100%; width: 0; background: rgb(var(--accent-solid)); }
.faq-item.is-timing .faq-timer { opacity: 1; }
.faq-item.is-timing .faq-timer > span { animation: faq-dwell var(--faq-dwell, 5.5s) linear forwards; }
@keyframes faq-dwell { to { width: 100%; } }

/* ── Podcast tabs ──────────────────────────────────────────────────── */

.pod-tab { opacity: .45; border-bottom: 2px solid transparent; padding-bottom: .25rem; transition: opacity .2s, border-color .2s; }
.pod-tab.pod-tab-act { opacity: 1; border-bottom-color: rgb(var(--accent-solid)); color: rgb(var(--ink)); }
.pod-panel { display: none; }
.pod-panel.pod-active { display: flex; flex-direction: column; }

/* ── Theme toggle ──────────────────────────────────────────────────── */

.theme-toggle { transition: background-color .25s, border-color .25s, transform .2s cubic-bezier(.34,1.56,.64,1); }
.theme-toggle:hover { transform: scale(1.06); border-color: rgb(var(--accent-solid) / .55); }
.theme-toggle .ico-dark  { display: none; }
:root[data-theme="dark"] .theme-toggle .ico-dark  { display: block; }
:root[data-theme="dark"] .theme-toggle .ico-light { display: none; }

/* ── The "After we start" bento, as a carousel on a phone ───────────
   Daniel, 2026-09-21: "What I do" is a carousel on mobile and this should
   be too — five tall cards in a column is a lot of scrolling for
   something people are already scrolling past.

   SAME TRACK AS THE SPECIALTIES ROW, and the same dots, because they are
   the same gesture and a visitor should not have to learn it twice.

   display:contents ON THE TWO ROW WRAPPERS is the whole trick. They hold
   the lg grid the desktop bento is built from — the wide dark tile
   spanning two of three columns is the shape of the thing — so lifting
   the tiles out of them would mean maintaining two markup trees for one
   bento. contents removes the wrappers from the box tree here and leaves
   their five tiles as direct columns of the track. Their margins go with
   them, which is why mb-3 does not need unsetting.

   THE CARDS ARE EQUAL HEIGHT because a flex row stretches them, and that
   is the right answer for a carousel: a card that changes height as you
   swipe moves the whole page under your thumb. */
@media (max-width: 767px) {
  .bento-carousel {
    display: flex;
    overflow-x: scroll;
    scroll-snap-type: x mandatory;
    scroll-behavior: smooth;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    gap: .75rem;
    padding-bottom: .25rem;
  }
  .bento-carousel::-webkit-scrollbar { display: none; }
  .bento-carousel > div { display: contents; }
  .bento-carousel .bento-tile {
    flex: 0 0 82vw;
    scroll-snap-align: start;
  }
}

/* ── The mobile menu panel ──────────────────────────────────────────
   The site's six nav links were `hidden md:flex`, so a phone had no
   navigation at all beyond the bottom bar. This is where they live now.

   SLIDES FROM THE RIGHT because that is the side the thumb is on and the
   button that opens it is in the bottom bar. transform, not `right`, so
   it composites instead of laying out on every frame.

   IT SHIPS CLOSED AND JS OPENS IT. That is the opposite of the rule for
   the animated SECTIONS on this site, and deliberately: a section that
   ships open degrades to readable copy, but a menu that ships open
   degrades to a panel sitting over the page with no way to shut it.
   translateX(100%) is off-canvas, not display:none, so it is still in the
   accessibility tree and still animates.                               */
.mmenu {
  position: fixed;
  top: 0; right: 0; bottom: 0;
  z-index: 60;                      /* over Luna (50) and the bar (40) */
  width: min(80vw, 320px);
  display: flex;
  flex-direction: column;
  background: rgb(var(--card));
  border-left: 1px solid rgb(var(--edge));
  box-shadow: -18px 0 50px -20px rgba(var(--shadow-key) / .45);
  transform: translateX(100%);
  transition: transform .32s cubic-bezier(.16, 1, .3, 1);
  padding-bottom: env(safe-area-inset-bottom, 0px);
  overscroll-behavior: contain;
  overflow-y: auto;
}
.mmenu.is-open { transform: translateX(0); }

/* The scrim is what makes the panel read as ON TOP rather than beside.
   visibility, not display, so the fade actually runs both ways. */
.mmenu-scrim {
  position: fixed;
  inset: 0;
  z-index: 55;
  background: rgba(0, 0, 0, .45);
  opacity: 0;
  visibility: hidden;
  transition: opacity .3s, visibility .3s;
}
.mmenu-scrim.is-open { opacity: 1; visibility: visible; }

.mmenu-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 1.1rem 1.25rem;
  border-bottom: 1px solid rgb(var(--edge));
}
.mmenu-close {
  width: 44px; height: 44px;
  display: flex; align-items: center; justify-content: center;
  margin-right: -.5rem;             /* optical: the X sits on the gutter */
  border-radius: 9999px;
  color: rgb(var(--ink-soft));
}
.mmenu-close:active { background: rgb(var(--ink) / .06); }

.mmenu-links { display: flex; flex-direction: column; padding: .5rem 0; }
.mmenu-link {
  display: flex;
  align-items: center;
  min-height: 52px;                 /* comfortably past the 44px minimum */
  padding: 0 1.25rem;
  font-size: 1rem;
  font-weight: 600;
  color: rgb(var(--ink));
  border-left: 3px solid transparent;
  transition: background-color .2s, color .2s, border-color .2s;
}
.mmenu-link:active { background: rgb(var(--accent-solid) / .08); }

/* THE FOCUS RING IS OURS, NOT THE BROWSER'S. Opening the panel moves
   focus to the first link so a keyboard or switch user is not left behind
   on the button, and Chrome draws its default blue box for that — a
   colour that is not in tokens.json, on the first row of the menu, every
   time it opens. focus-visible rather than focus, so a finger does not
   summon a ring a mouse user would never see. */
.mmenu-link:focus-visible,
.mmenu-close:focus-visible {
  outline: 2px solid rgb(var(--accent-solid));
  outline-offset: -2px;
}

/* WHERE AM I, not just where can I go. Someone opening this two pages
   deep is asking both. The rule is on the left edge rather than a filled
   row so it reads as a marker and not as a pressed button. */
.mmenu-link.is-here {
  color: rgb(var(--accent));
  border-left-color: rgb(var(--accent-solid));
  background: rgb(var(--accent-solid) / .06);
}

.mmenu-foot {
  margin-top: auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 1rem 1.25rem;
  border-top: 1px solid rgb(var(--edge));
}

/* The panel animates; holding still is honoured. */
@media (prefers-reduced-motion: reduce) {
  .mmenu, .mmenu-scrim { transition: none; }
}

/* Nothing scrolls behind an open panel. Set on <html> by the toggle. */
.mmenu-lock { overflow: hidden; }

/* ── 44px TAP TARGETS WITHOUT REDRAWING ANYTHING ────────────────────
   The audit at 390px found 30 targets under the iOS minimum on /workshop
   and 18 on the homepage: 32px social icons, 26px podcast tabs, text
   links 20px tall. Small targets on a phone are missed taps, and a missed
   tap on "Contact" or a social icon is the visitor deciding the site is
   fiddly rather than trying again.

   THE HIT AREA GROWS, THE ARTWORK DOES NOT. Every one of these was sized
   deliberately — the footer icons are a 40px row Daniel approved, the
   podcast tabs are type with an underline, the jump link is a link and
   not a button on purpose. Making them physically 44px would redraw all
   of it. A centred, invisible ::after is 44px of touch over artwork that
   is unchanged, which is the whole point.

   Mobile only. A mouse does not need 44px and desktop should not grow
   invisible boxes that could swallow a click meant for a neighbour.

   None of these selectors had an ::after of their own — checked, not
   assumed, because silently replacing one would break whatever it drew. */
@media (max-width: 767px) {
  .soc-icon,
  .course-teach-soc,
  .course-teach-contact,
  .course-enrol-teams,
  .jumplink,
  .pod-tab,
  .foot-link,
  .course-teams-cta,
  .course-gate-cta,
  .nav-wordmark,
  .course-updates button,
  .magnetic { position: relative; }

  .soc-icon::after,
  .course-teach-soc::after,
  .course-teach-contact::after,
  .course-enrol-teams::after,
  .jumplink::after,
  .pod-tab::after,
  .foot-link::after,
  .course-teams-cta::after,
  .course-gate-cta::after,
  .nav-wordmark::after,
  .course-updates button::after,
  .magnetic::after {
    content: '';
    position: absolute;
    left: 50%;
    top: 50%;
    width: 100%;
    height: 100%;
    min-width: 44px;
    min-height: 44px;
    transform: translate(-50%, -50%);
  }
}

/* ── The mobile bottom bar, and the home indicator ──────────────────
   Daniel, 2026-09-21: the labels crop on the iPhone. They did. The bar was
   38px tall sitting flush on bottom:0, and iOS draws the home indicator over
   the bottom ~34px of the viewport, so the system was painting on top of the
   labels. Nothing was cropping them in CSS — nothing had reserved the space.

   env() is the only thing that knows how big that area is: it is 34px on a
   notched phone, 21px in landscape and 0 on a device with a home button and
   in every desktop browser, so a hardcoded number is wrong on most of them.
   The fallback in env(x, 0px) is what keeps it honest on browsers that do
   not support it.

   BODY'S PADDING IS COMPUTED FROM THE SAME VALUE, so the bar can grow and
   still never cover the end of the page. It was a flat pb-16 (64px) against
   a 38px bar, which happened to leave enough slack; against a 44px bar plus
   34px of inset it would not have. */
.mobile-bar { padding-bottom: env(safe-area-inset-bottom, 0px); }

/* ── One ask per corner, on /workshop, on a phone ───────────────────
   Luna's bubble and the bar's "Reserve my seat" were both in the
   bottom-right of the page whose whole job is to sell a seat, and the
   bubble also sat on the body copy of the dark band. Daniel's call: the
   seat wins there. See the note on body in base.html for the scope —
   /workshop only, and only where there is a bar to compete with. */
@media (max-width: 767px) {
  .luna-off-mobile #luna-bubble,
  .luna-off-mobile #luna-whisper,
  .luna-off-mobile #luna-panel { display: none; }
}

/* THE SEAT SLOT IS A BUTTON, NOT A FOURTH TAB. Daniel: he wants the seat
   ask to be more obvious and was not sure how. It was set in the accent
   colour and nothing else, which made it the fourth item in a row of
   navigation rather than the thing the page is for. The gradient is the
   same one every primary button on the site uses, so it reads as the ask
   without introducing anything new.

   Inset rather than full-bleed: the pill keeps the 44px target and the
   grid keeps its four equal columns, so the other three do not move. */
.mobile-bar-cta {
  background: var(--grad);
  color: #fff;
  border-radius: 9999px;
  margin: .3rem .35rem;
  box-shadow: 0 6px 16px -6px rgba(139, 92, 246, .7);
}

/* THE FOURTH SLOT IS THE ASK, AND IT IS ALWAYS THE LONGEST LABEL —
   "Reserve my seat" on /workshop, "Send me a text" everywhere else,
   against "Top" and "Blog". Four equal columns gave it exactly as much
   room as the shortest word on the bar, and measured at 320 that is 73px
   of text in a 65px pill, so the label ran out through the rounded end.
   At 360 it was 78 in 75. It read fine at 390, which is why it needed
   measuring at the widths below it rather than looking at one.

   Weighting the last column is what fixes it, not a smaller label: the
   three tabs are short words with room to give, and at 320 they still get
   72px against about 58px for "Workshop".

   On .mobile-bar > div, not the utility, because Tailwind's .grid-cols-4
   is a single class and its sheet is injected after ours. An element in
   the selector wins the tie. */
/* FOUR EQUAL COLUMNS, so the four labels sit on an even rhythm. Daniel
   spotted that they did not: measured at 390 the text centres ran 84,
   84, then 103px, because the last column had been widened to 1.45fr to
   stop "Reserve my seat" running out through the end of its pill. That
   fixed the overflow and broke the spacing.

   Equal columns are back and the pill is made to fit instead — a tighter
   inset and one step down in size below 375, where the cell is under
   90px. Measured at 320, 360, 390 and 430: even centres at all four, no
   overflow at any. */
.mobile-bar > div { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.mobile-bar-cta { margin: .3rem .25rem; }
@media (max-width: 374px) {
  .mobile-bar .mobile-bar-link { font-size: 9.5px; }
  .mobile-bar-cta { margin: .3rem .15rem; }
}

@media (max-width: 767px) {
  body { padding-bottom: calc(44px + env(safe-area-inset-bottom, 0px) + 0.75rem); }
}

/* ── Mobile: collapse the card bodies into accordions ──────────────── */

@media (max-width: 767px) {
  .truth-body { max-height: 0; overflow: hidden; opacity: 0; transition: max-height .35s cubic-bezier(.16,1,.3,1), opacity .25s; }
  .truth-card.mob-open .truth-body { max-height: 12rem; opacity: 1; }
  .truth-chev { display: inline-flex; transition: transform .3s; color: rgb(var(--accent-solid) / .6); font-style: normal; flex-shrink: 0; }
  .truth-card.mob-open .truth-chev { transform: rotate(90deg); }

  .step-body { max-height: 0; overflow: hidden; opacity: 0; transition: max-height .35s cubic-bezier(.16,1,.3,1), opacity .25s; }
  .step-card.mob-open .step-body { max-height: 14rem; opacity: 1; }
  .step-chev { display: inline-flex; transition: transform .3s; color: rgb(var(--accent-solid) / .65); font-style: normal; font-size: .65rem; flex-shrink: 0; margin-left: .5rem; }
  .step-card.mob-open .step-chev { transform: rotate(90deg); }

  /* Specialties become a snap carousel. */
  .spec-track {
    display: flex; overflow-x: scroll; scroll-snap-type: x mandatory; scroll-behavior: smooth;
    -webkit-overflow-scrolling: touch; scrollbar-width: none; gap: .75rem; padding-bottom: .25rem;
  }
  .spec-track::-webkit-scrollbar { display: none; }
  .spec-track > a { flex: 0 0 82vw; scroll-snap-align: start; }
  .spec-dot { width: 6px; height: 6px; border-radius: 9999px; background: rgb(var(--ink) / .15); transition: background .25s, width .25s; display: inline-block; }
  .spec-dot.active { background: rgb(var(--accent-solid)); width: 18px; }
}

/* ── Luna ──────────────────────────────────────────────────────────── */

#luna-whisper { transition: opacity .4s cubic-bezier(.16,1,.3,1), transform .4s cubic-bezier(.16,1,.3,1); }
#luna-whisper.whisper-hidden { opacity: 0; transform: translateY(6px) scale(.96); pointer-events: none; }
@keyframes luna-ping { 0% { transform: scale(1); opacity: .6; } 100% { transform: scale(2.6); opacity: 0; } }
#luna-ring { position: absolute; inset: 0; border-radius: 50%; background: #8b5cf6; opacity: 0; pointer-events: none; }
#luna-ring.pinging { animation: luna-ping 1.5s ease-out forwards; }
.luna-hidden { opacity: 0; transform: translateY(10px) scale(.97); pointer-events: none; transition: opacity .3s, transform .3s; }

/* ══ THE WORKSHOP PAGE ═════════════════════════════════════════════════
   /workshop has to read as somewhere else on the site without inventing a
   second design system. Nothing below adds a colour: everything is the
   locked violet, the locked indigo, or a walk between the two.

   Three things carry the difference, and they are the three sections the
   homepage has no equivalent of: the chatbot/agent split on the dark
   band, the modules on a spine instead of in a grid, and the private-
   workshop band. Everything else on the page is the shared parts bin. */


/* ── The chatbot / agent split ───────────────────────────────────────
   PAIRED ELEMENTS MATCH, and on this one the copy cannot be trusted to
   hold it. "You ask, it answers" is one line at 1440 and could be two at
   900 while its opposite number is still one, and then the two lists run
   out of step down the whole column.

   So the columns are ONE subgrid. Every row — label, sub, each point's
   title, each point's body, the closing line — is a row of the parent,
   and both columns place their parts on the same rows. Nine rows, in
   order, and a card's part cannot land anywhere else however its text
   wraps. A min-height per point would leave a hole under the shorter one.

   The two sides are told apart by COLOUR and nothing else: the same type
   sizes, the same three points, the same structure. One is white on the
   dark band, the other is violet. */
.wsplit { row-gap: .3rem; }

@supports (grid-template-rows: subgrid) {
  @media (min-width: 768px) {
    /* label · sub · (title · body) x3 · closing = 9 */
    .wsplit { grid-template-rows: repeat(9, auto); }
    .wsplit > .wsplit-col { display: grid; grid-template-rows: subgrid; grid-row: span 9; }
    /* dt and dd have to be the grid's own children to land on its rows. */
    .wsplit-pts { display: contents; }
  }
}

/* Without subgrid the columns are still the same height, because the grid
   stretches them. Only the internal rows are free to drift, which is the
   old behaviour and is no worse than it was. */
@supports not (grid-template-rows: subgrid) {
  .wsplit-col { display: flex; flex-direction: column; }
  .wsplit-pts { margin: 0; }
}

/* The gaps between the parts are margins, not row gaps, because one row
   gap cannot be tight between a title and its body and loose between one
   point and the next. Both columns carry the same margins on the same
   rows, so the two sides stay level. */
.wsplit-sub  { margin-bottom: 1.15rem; }
.wsplit-pts dt { margin-top: 1.05rem; }
.wsplit-pts dt:first-of-type { margin-top: 0; }
.wsplit-pts dd { margin: 0; }
.wsplit-stat { margin-top: 1.5rem; padding-top: 1.15rem; border-top: 1px solid; }


/* ── A fact, and the hairline before it ───────────────────────
   Used by the hero's key/value strip and by the proof figures under it,
   so the two read as one system.

   THE RULE BELOW EXISTS. A wrapped row must not start with a divider
   hanging off its left edge, and `:first-child` cannot see that — the
   second item is still the second child once it has wrapped onto a new
   line. There is no selector for "first on this line", so the padding is
   in the markup (`{% if not loop.first %}`) and this only draws the rule,
   inset from the text by that same padding. Keep them in step. */
.fact { position: relative; }
.fact[class*="pl-"]::before {
  content: '';
  position: absolute;
  left: 0; top: 0.15em; bottom: 0.15em;
  width: 1px;
  background: rgb(var(--edge));
}

/* ── The hero rail ──────────────────────────────────────────────────
   /workshop's hero closes on ONE line under both columns: the session,
   who is running it, and what he has done. Option B off a canvas of
   four, Daniel 2026-09-20.

   The grouping is carried by the DIVIDERS, not by guessing at gaps: a
   short hairline between items inside a group (that is .fact's own
   ::before, the same one the strip has always used) and a full-height
   rule between the groups. Three things, one row, one baseline band.

   IT STACKS BELOW 1180, NOT BELOW 1024. The row needs about 1045px of
   content to hold all eight items without wrapping, and the content
   line is 976px at 1024 and 1104 only from 1152 up. Stacked, the
   full-height rules turn into full-width hairlines and the three groups
   read down the page as the same three things. Measured at 390, 640,
   768, 1024, 1100, 1180, 1280, 1440 and 1920, both themes. */
.hrail {
  margin-top: 2.5rem;
  padding-top: 1.5rem;
  border-top: 1px solid rgb(var(--edge));
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
}
.hrail-group { display: flex; align-items: flex-start; gap: 1.25rem; flex-wrap: wrap; }
.hrail-sep { height: 1px; background: rgb(var(--edge)); }

/* ── Below the row, each group is a GRID and not a wrap ──────────────
   Daniel on a phone, 2026-09-21: the workshop hero is a disaster. This
   was the second half of it, after the nav.

   flex-wrap put four facts on a 3 + 1 and three figures on a 2 + 1, so
   SPOTS and "10+ yrs" each sat alone on a second row — and because the
   divider is a ::before on every item but the FIRST IN THE DOM, the
   orphan carried a hairline with it that now had nothing to its left. A
   stray rule hanging in the margin is what made it read as broken rather
   than merely stacked.

   A grid is the fix, not a tighter gap: four facts in two columns and
   three figures in three are both exact, so there is no orphan to
   position at any width.

   AND THE HAIRLINE GOES BY COLUMN, NOT BY DOM ORDER. In a grid the item
   that starts a row is a first item whatever its index is, so the rule is
   driven off nth-child matching the column count. That is the house rule
   for every strip on this site — dividers BETWEEN things, never boxing
   them — held in a layout that wraps. */
@media (max-width: 1023px) {
  .hrail-facts,
  .hrail-figs { display: grid; gap: 1.15rem 0; align-items: start; }
  .hrail-facts { grid-template-columns: 1fr 1fr; }
  .hrail-figs  { grid-template-columns: repeat(3, 1fr); }

  /* Column 1 of any row starts it: no rule, no indent. */
  .hrail-facts > .fact:nth-child(2n+1)::before,
  .hrail-figs  > .fact:nth-child(3n+1)::before { content: none; }
  .hrail-facts > .fact:nth-child(2n+1),
  .hrail-figs  > .fact:nth-child(3n+1) { padding-left: 0; }

  /* Every other column carries the rule that separates it from the one
     before. More specific than Tailwind's .pl-5, which is what beats the
     CDN sheet being injected after ours. */
  .hrail-facts > .fact:nth-child(2n),
  .hrail-figs  > .fact:not(:nth-child(3n+1)) { padding-left: .9rem; }

  /* THE THREE FIGURES SHARE A THIRD OF A PHONE, and their labels have to
     be allowed to wrap. Measured before this was written: with nowrap the
     three labels are 94 + 145 + 137 = 376px of content inside a 257px
     cell at 320, and the third one ran to x=400 on a 390px screen. It did
     not LOOK broken, because body's overflow-x:hidden was clipping it —
     which is the worst version of the bug, since the row reads fine and
     the last label is simply cut.

     minmax(0, 1fr) is what makes the columns actually equal. A plain 1fr
     is minmax(auto, 1fr), and auto is the content's own minimum, so
     nowrap content pushes the track wider than its share and the grid
     silently stops being three equal columns.

     THEY ALL RESERVE THE TALLEST LABEL. Wrapping alone gives three
     different line counts, which is the same orphan problem wearing a
     different hat — and it is the note Daniel makes most. The reserve is
     on the label, measured into the real column, so the three numbers sit
     on one baseline whatever the copy does. */
  .hrail-figs { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  /* TWO CLASSES DELIBERATELY. Tailwind's .whitespace-nowrap is on this
     element in the markup and ties on specificity, and the CDN sheet is
     injected after ours, so a single class would lose the tie and the
     label would keep its nowrap. Same trap as .block over .typeword-box. */
  .hrail-figs .hrail-fig-label {
    font-size: 10.5px;
    line-height: 1.3;
    white-space: normal;
    min-height: calc(10.5px * 1.3 * 2);
  }
}

/* 320 ONLY, AND IT IS THE NUMBER THAT BREAKS, NOT THE LABEL. At 320 a
   third of the rail is 71px of usable column and "10+ yrs" is about 75px
   at 22px italic, so the FIGURE wrapped to two lines and made its card
   77px against the other two at 55. The labels were fine — that was
   measured before this rule was written, which is the only reason it is
   one line of CSS in the right place rather than a smaller font
   everywhere.

   Left at 22px from 360 up, because that is the size Daniel picked off
   the canvas and it fits from there on: 90px of column against 75px of
   number. */
@media (max-width: 359px) {
  .hrail-figs .metric-num { font-size: 1.125rem; white-space: nowrap; }
}

/* THE ROW STARTS AT 1024, TIGHT, AND LOOSENS AT 1180. Eight items of
   content is about 745px; everything else in the row is divider space,
   and the content line is 976px at 1024 against 1104 from 1152 up. One
   set of gaps cannot serve both: roomy enough to read at 1104 overflows
   at 976, and tight enough for 976 looks cramped at 1104. So the gaps
   step with the measure rather than the row collapsing into a 235px
   stack across the whole 1024-1179 band. */
@media (min-width: 1024px) {
  .hrail {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: 1.25rem;
  }
  .hrail-group { flex-wrap: nowrap; gap: .875rem; }
  .hrail-sep { width: 1px; height: auto; align-self: stretch; }
  .hrail-who { text-align: center; }
  .hrail .fact[class*="pl-"] { padding-left: .875rem; }
}

@media (min-width: 1180px) {
  .hrail { gap: 1.75rem; }
  .hrail-group { gap: 1.25rem; }
  .hrail .fact[class*="pl-"] { padding-left: 1.25rem; }
}

/* The jump link under the hero CTA. It is a link, not a button: the
   button is the only thing on this page asking for money. */
.jumplink { text-decoration: none; transition: color .2s; }
.jumplink:hover { color: rgb(var(--accent)); }
.jumplink svg { transition: transform .25s cubic-bezier(.16,1,.3,1); }
.jumplink:hover svg { transform: translateY(2px); }
@media (prefers-reduced-motion: reduce) { .jumplink svg { transition: none; } }


/* The hero's three-across bullet row lived here. Daniel struck the row
   on 2026-09-20 and the rule went with the markup rather than being left
   behind to be wondered about later. What it did, in case it comes back:
   a border-left on every item but the first, at lg and up, so the three
   were divided by hairlines BETWEEN them and never boxed. That is the
   house rule for every strip on this site — the fact strip under the
   video and the three figures under the button both follow it. */

}


/* ── The roster ────────────────────────────────────────
   Six hires a business owner keeps putting off. NOT six cards: a row of
   bordered boxes is what the old page reached for every time, and it
   turns a list into a pattern the eye skims instead of reads. A roster
   reads DOWN — role, what it costs you now, what takes it over — and a
   hairline between rows is the only division it needs.

   ONE SUBGRID ACROSS ALL SIX ROWS at md and up, so the three columns
   line up down the whole list however any one row's copy wraps. Paired
   elements match, and these are paired six ways.

   The "takes it over" column is the one being argued for, so it carries
   the violet rule and the full-strength ink. "Costs you now" is the same
   size in the quieter ink — differentiated by colour and weight, never
   by size. */
.roster { border-top: 1px solid rgb(var(--edge)); }

.roster-row {
  display: grid;
  gap: .75rem;
  padding: 1.6rem 0;
  border-bottom: 1px solid rgb(var(--edge));
}

@media (min-width: 768px) {
  .roster-row {
    grid-template-columns: minmax(0, 15rem) minmax(0, 1fr) minmax(0, 1.25fr);
    gap: 2rem;
    align-items: start;
    padding: 1.75rem 0;
  }
}

/* The row lifts the way every other list on this site does, and the rule
   under it warms toward the accent so the whole row reads as one object
   rather than three cells that happen to be adjacent. */
.roster-row { transition: border-color .3s; }
.roster-row:hover { border-bottom-color: rgb(var(--accent-solid) / .45); }

.roster-next { padding-left: .9rem; border-left: 2px solid rgb(var(--accent-solid)); }
@media (min-width: 768px) { .roster-next { padding-left: 1.15rem; } }


/* ── The four modules, on a spine ────────────────────────────────────
   Everywhere else on this site a set of four things is a grid of cards.
   That is exactly why the modules are not one: the page needs a section
   that could not be mistaken for the homepage, and this is the one worth
   spending it on.

   It is also the honest shape. The modules are an ORDER — ChatGPT, then
   visuals, then agents, then Claude Code, each one leaning on the one
   before it. Four cards in a 2x2 say they are four interchangeable
   options you could take in any order. A spine says 1, then 2, then 3,
   then 4, which is what the workshop actually is.

   NOTHING HERE RESERVES OR ANIMATES A HEIGHT. A module is as tall as its
   copy at that width. The only motion is the rail scaling from the top,
   which is a transform and moves nothing, and it is a segment PER MODULE
   reaching into the gap below it rather than one bar measured down the
   list — the same reason the process rail on the homepage is built per
   column. No calc() against row heights, correct at every width. */
.wmod { position: relative; }

@media (min-width: 640px) {
  .wmod { display: grid; grid-template-columns: 4.75rem 1fr; column-gap: 1.5rem; }

  .wmod::before {
    content: '';
    position: absolute;
    left: 1.45rem;          /* under the numeral, not centred on the column */
    top: 3.35rem;
    bottom: -0.35rem;       /* reaches into the gap, so the rail is unbroken */
    width: 2px;
    border-radius: 99px;
    background: linear-gradient(180deg, rgb(var(--accent-solid) / .5), rgb(var(--indigo) / .12));
    transform: scaleY(0);
    transform-origin: top;
    transition: transform .85s cubic-bezier(.16,1,.3,1) .1s;
  }
  /* The rail belongs to the module above the gap, so the last one has none. */
  .wmod:last-child::before { content: none; }
  .wmod.in::before { transform: scaleY(1); }
}

/* The numeral is the node. A rounded tile with a glyph in it is the thing
   that got stripped off every other section of this site, so the marker
   here is type: the module number at display weight, violet, sitting on
   the rail it starts. */
.wmod-num {
  font-size: 2rem;
  line-height: 1;
  letter-spacing: -0.02em;
  color: rgb(var(--accent-solid));
}


/* ── The private-workshop band ───────────────────────────────────────
   Sits directly under Register and has to read as a second, quieter door
   rather than a competing CTA. So it is the violet wash the audience
   band uses, not the gradient card — there is exactly one gradient CTA
   per page and the final one owns it here. */
.wprivate {
  background: linear-gradient(135deg, rgb(var(--accent-solid) / .09), rgb(var(--indigo) / .05));
  border: 1px solid rgb(var(--accent-solid) / .22);
}
.wprivate-chip {
  border: 1px solid rgb(var(--accent-solid) / .28);
  background: rgb(var(--accent-solid) / .07);
}


/* ── Reduced motion ────────────────────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .reveal, .mask > span { opacity: 1; transform: none; transition: none; }
  /* The module rail is drawn, not drawing. scaleY(0) with the transition
     off would leave the spine missing instead of still. */
  .wmod::before { transform: none; transition: none; }
  .marquee-track, .tcol-track, .tw-caret, .circle-a, .circle-b, .mob-review-track { animation: none; }
  .cta-primary::before { animation: none; }
  /* The FAQ still opens and closes, it just does it instantly and the walk
     does not run at all — site.js checks the same query. */
  .faq-a, .faq-a-inner, .faq-item, .chev { transition: none; }
  .faq-item.is-timing .faq-timer > span { animation: none; }
}

/* ── Prose ─────────────────────────────────────────────────────────────
   Long-form body copy: blog posts and the legal pages. Tailwind resets
   headings and lists to nothing, so anything rendered from stored HTML
   (the privacy policy, the terms) needs these or it comes out as one
   undifferentiated block.                                             */

.prose-dr { color: rgb(var(--ink-soft)); line-height: 1.75; }
.prose-dr p { margin-bottom: 1.15rem; }
.prose-dr h2 {
  font-family: 'Archivo', sans-serif; font-weight: 800; letter-spacing: -0.02em;
  color: rgb(var(--ink)); font-size: 1.5rem; line-height: 1.25;
  margin-top: 2.5rem; margin-bottom: 0.85rem; text-wrap: balance;
}
.prose-dr h3 {
  font-family: 'Archivo', sans-serif; font-weight: 800;
  color: rgb(var(--ink)); font-size: 1.1rem; line-height: 1.3;
  margin-top: 1.75rem; margin-bottom: 0.5rem;
}
.prose-dr ul, .prose-dr ol { margin: 0 0 1.15rem 0; padding-left: 1.35rem; }
.prose-dr ul { list-style: disc; }
.prose-dr ol { list-style: decimal; }
.prose-dr li { margin-bottom: 0.5rem; padding-left: 0.25rem; }
.prose-dr li::marker { color: rgb(var(--accent-solid)); }
.prose-dr strong { color: rgb(var(--ink)); font-weight: 600; }
.prose-dr a { color: rgb(var(--accent)); text-decoration: underline; text-underline-offset: 2px; }
.prose-dr a:hover { opacity: .75; }
.prose-dr hr { border: 0; border-top: 1px solid rgb(var(--edge)); margin: 2.5rem 0; }
.prose-dr blockquote {
  border-left: 3px solid rgb(var(--accent-solid));
  padding-left: 1.1rem; margin: 1.5rem 0; font-style: italic;
}

/* ── Buttons that sit ON the gradient ──────────────────────────────────
   The final-CTA card is the violet gradient in BOTH themes, so anything
   inside it must not use theme-aware colours. `bg-white text-ink` looked
   right in light mode and turned into white-on-white in dark, because
   --ink flips and the card behind it does not. These two are fixed.    */

.btn-on-grad {
  background: #ffffff;
  color: #0f172a;          /* fixed, never var(--ink) */
  font-weight: 700;
  box-shadow: 0 10px 30px -10px rgba(0,0,0,.25);
  transition: transform .2s cubic-bezier(.34,1.56,.64,1);
}
.btn-on-grad:hover { transform: scale(1.03); }

.btn-on-grad-ghost {
  background: transparent;
  color: #ffffff;
  border: 2px solid rgba(255,255,255,.4);
  font-weight: 700;
  transition: background-color .25s;
}
.btn-on-grad-ghost:hover { background: rgba(255,255,255,.12); }

/* Buttons on the amber CTA card. Same rule as the two above and for the same
   reason: that card is a fixed dark violet in BOTH themes, so nothing on it
   may use a theme-aware colour or it inverts against a surface that did not.

   The dark text on the amber fill is #1D1D28, Ink night end, not black. */
.btn-amber {
  background: rgb(var(--amber));
  color: #1D1D28;          /* fixed, never var(--ink) */
  font-weight: 700;
  box-shadow: 0 14px 34px -12px rgb(var(--amber) / .55);
  transition: transform .2s cubic-bezier(.34,1.56,.64,1);
}
.btn-amber:hover { transform: scale(1.03); }

.btn-amber-ghost {
  background: transparent;
  color: rgb(var(--amber-text));
  border: 1px solid rgb(var(--amber) / .45);
  font-weight: 600;
  transition: background-color .25s, border-color .25s;
}
.btn-amber-ghost:hover { background: rgb(var(--amber) / .1); border-color: rgb(var(--amber) / .8); }

/* ── The scroller mark ─────────────────────────────────────────────────
   The four-pointed sparkle between phrases in the marquee, filled with
   the brand gradient instead of a flat violet.

   It is a CSS mask, not an inline <svg>, for one reason: the marquee
   duplicates its whole set of items to make the loop seamless, so an SVG
   with a <defs> gradient would be stamped out dozens of times per page
   with a repeated id. A mask needs no defs, is defined once here, and
   comes along automatically on danielro.photos, which copies this file.

   Size is set by font-size so it scales with the ticker: the marquee is
   text-2xl on mobile and text-4xl on desktop and the mark follows.     */

.mark {
  display: inline-block;
  width: 0.75em;
  height: 0.75em;
  /* The phrases are uppercase, so the type's optical centre sits above the
     middle of the line box — the line box reserves descender space that
     capitals never use. Dead-centring the mark therefore reads slightly
     low. This lifts it back onto the cap's centre. */
  margin-top: -0.07em;
  background: var(--grad);
  /* Full strength on purpose. The phrases around it sit at 15% so the mark
     is the only saturated thing in the ticker, which is what makes it read
     as a brand mark instead of punctuation. */
  -webkit-mask: var(--mark-sparkle) center / contain no-repeat;
          mask: var(--mark-sparkle) center / contain no-repeat;
}

:root {
  --mark-sparkle: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M12 0C12.5 6.5 17.5 11.5 24 12C17.5 12.5 12.5 17.5 12 24C11.5 17.5 6.5 12.5 0 12C6.5 11.5 11.5 6.5 12 0Z'/></svg>");
}


/* ── The page gradient, DARK ONLY ──────────────────────────────────────
   tokens.json calls these "page top" and "page bottom, where the background
   gradient lands". Both stops are dark values, so this is a dark-mode
   treatment. Applying it in light mode faded the page to near-black by the
   footer, which is exactly what it did on the first attempt.             */

:root[data-theme="dark"] body {
  background-image: linear-gradient(180deg, #1D1938, #1D1D28);
  background-attachment: fixed;
}

/* ── Tag colours as text ───────────────────────────────────────────────
   The layout-tag colours are bright by design — they were picked to sit on
   video, against black. Teal and mint at full strength on a white card are
   unreadable as text.

   So a tag colour used as TEXT is mixed toward the page's own ink: on light
   that darkens it, on dark it lifts it, and either way it stays the same
   hue rather than becoming a colour Daniel never approved. The icon tiles
   and the hover borders still use the pure value, because those sit on a
   tint of themselves where the bright version is correct.               */

.tone-fg {
  color: rgb(var(--card-tone));                                   /* fallback */
  /* 50/50 is what puts the lightest tag colour (teal) over 4.5:1 as 14px
     text on a white card. Measured, not guessed. */
  color: color-mix(in srgb, rgb(var(--card-tone)) 50%, rgb(var(--ink)));
}
.tone-icon {
  color: rgb(var(--card-tone));
  /* Icons are graphics, not text, so 3:1 is the bar and they can stay
     brighter than the label under them. */
  color: color-mix(in srgb, rgb(var(--card-tone)) 60%, rgb(var(--ink)));
}
/* The featured card is Panel in BOTH themes, so mixing toward --ink is
   wrong there: in light mode --ink is near-black and it would darken violet
   against an already dark card. It mixes toward white instead, which is
   what takes the link from 3.6:1 to comfortably over the line. */
.is-featured .tone-fg {
  color: rgb(var(--card-tone));
  color: color-mix(in srgb, rgb(var(--card-tone)) 68%, #ffffff);
}
.is-featured .tone-icon {
  color: rgb(var(--card-tone));
  color: color-mix(in srgb, rgb(var(--card-tone)) 80%, #ffffff);
}

/* ── Accent text that sits ON a dark surface ───────────────────────────
   --accent is two different colours by design: the deep violet #6d28d9 on
   light, the bright #8b5cf6 on dark, because the bright one is unreadable
   on white. That is correct for text on the page.

   It is wrong for text inside a dark card while the page is in LIGHT mode,
   where --accent is the deep violet and the card behind it is near-black.
   That combination measured 2.37:1. `.accent-d` is the accent for anything
   on a dark surface: always the bright value, in both themes.         */

.accent-d { color: #8b5cf6; }


/* ═══════════════════════════════════════════════════════════════════════
   /course — THE COURSE PAGE
   ═══════════════════════════════════════════════════════════════════════

   A second workshop page, built 2026-09-20 off a reference page Daniel
   picked: maven.com/futurefactors/build-ai-agent-team-with-claude. The
   brief was to recreate its structure, section order and layout as closely
   as possible in his colours. /workshop is untouched.

   WHY THIS IS ITS OWN BLOCK AND NOT SPREAD THROUGH THE PARTS BIN. The
   reference page has a different skeleton to the rest of this site: a
   narrow content column with a sticky buy card beside it, running the
   whole length of the page. Nothing above reuses that, and the components
   inside it are shaped by it. Anything here that turns out to be general
   should move up into the bin rather than be copied.

   NO NEW COLOUR. Every value below is a token or a walk between two of
   them. The two fixed-dark surfaces — the hero panel and the private band
   — use fixed hex, because a theme-aware colour on a surface that does not
   invert is how white-on-white happened once already.                   */

/* ── The measure ────────────────────────────────────────────────────
   ONE content line with every other page: 1104px at x=168 on a 1440
   viewport. The padding is INSIDE the max-width, never on the section,
   which is what put three sections 24px proud on each side.          */
.course-shell {
  max-width: 1152px;
  margin-left: auto;
  margin-right: auto;
  padding-left: 1.5rem;
  padding-right: 1.5rem;
}

/* ── HERO ───────────────────────────────────────────────────────────
   Copy left, a coloured panel bleeding off the right edge of the
   screen, a gradient rule across the full width underneath. Same
   skeleton as the reference; video on the panel instead of photos. */
/* 7rem clears the fixed nav, which is the same figure every other page
   pads by (pt-28 / sm:pt-32). It was 2.75rem and the badge pill sat behind
   the nav pill. */
.course-hero { position: relative; padding-top: 7rem; }
.course-hero-grid { display: grid; gap: 2.5rem; align-items: center; }
@media (min-width: 1024px) {
  .course-hero { padding-top: 8rem; }
  .course-hero-grid { grid-template-columns: minmax(0, 1fr) minmax(0, 1.02fr); gap: 3rem; }
}

.course-hero-copy { padding-bottom: 2.5rem; }
@media (min-width: 1024px) { .course-hero-copy { padding-bottom: 3.25rem; } }

.course-pill {
  display: inline-block;
  font-family: 'Archivo', sans-serif;
  font-stretch: 75%;
  font-weight: 800;
  font-size: 11px;
  letter-spacing: .18em;
  text-transform: uppercase;
  padding: .4rem .7rem;
  border-radius: 999px;
  color: rgb(var(--violet-text));
  background: rgb(var(--accent-solid) / .13);
}

.course-h1 {
  margin-top: 1.1rem;
  font-size: clamp(2.05rem, 4.4vw, 3.35rem);
  line-height: 1.04;
  color: rgb(var(--ink));
}

/* ── The star row ───────────────────────────────────────────────────
   Amber, and --amber-label not --amber: #F59E0B is a dark-surface
   value and measures 2.2 on the light page. The label variable is the
   theme-aware pair and it is set in BOTH dark blocks.               */
.course-rating { display: flex; align-items: center; gap: .18rem; color: rgb(var(--amber-label)); }
.course-rating svg { width: 15px; height: 15px; flex: 0 0 auto; }
.course-rating-num { margin-left: .45rem; font-size: 13px; font-weight: 600; color: rgb(var(--ink)); }
.course-rating-count { font-size: 13px; color: rgb(var(--muted)); }
.course-h1 + .course-rating { margin-top: 1.1rem; }
.course-rating-sm svg { width: 13px; height: 13px; }
.course-rating-bare { gap: .1rem; }
.course-rating-lead { margin-bottom: .55rem; }

.course-hero-sub {
  margin-top: 1.1rem;
  font-size: 1.0625rem;
  line-height: 1.55;
  color: rgb(var(--ink-soft));
}

.course-hero-teacher { margin-top: 1.35rem; display: flex; flex-wrap: wrap; align-items: baseline; gap: .55rem; }
.course-teacher-name { font-weight: 600; color: rgb(var(--ink)); }
.course-teacher-role { font-size: 14px; color: rgb(var(--muted)); }

.course-hero-btn { display: inline-block; margin-top: 1.6rem; padding: .7rem 1.25rem; font-size: 15px; }

/* THE PANEL BLEEDS TO THE RIGHT EDGE OF THE VIEWPORT.
   The negative margin is the shell's own gutter: half the leftover width
   plus the 24px padding, floored at 24px so it still reaches the edge
   below 1152px. A percentage cannot do this — percentage margins resolve
   against the grid area, not the page.

   100vw INCLUDES A CLASSIC SCROLLBAR and the layout width does not, so on
   a platform that reserves 15px for one the panel runs about 8px past the
   right edge. `overflow-x: hidden` on <body> clips it, and what gets
   clipped is 8px of flat violet, not content — the video card is centred
   inside its own padding. On macOS, where the scrollbar is an overlay,
   the two agree and the panel lands exactly on the edge. */
.course-hero-panel {
  position: relative;
  overflow: hidden;
  background: #2E1065;                      /* violet_block_bg_dark, fixed */
  border-radius: 20px 0 0 20px;
  padding: 1.75rem;
  margin-right: calc(-1 * max(1.5rem, (100vw - 1152px) / 2 + 1.5rem));
  min-height: 300px;
  display: flex;
  align-items: center;
}
@media (max-width: 1023px) {
  /* Stacked: bleed BOTH edges rather than sit as a rounded card, so it
     still reads as the band it is on a phone. */
  .course-hero-panel {
    margin-left: calc(-1 * max(1.5rem, (100vw - 1152px) / 2 + 1.5rem));
    border-radius: 0;
    min-height: 0;
    padding: 1.25rem;
  }
}

/* The stripes the reference page has behind its photos. Two locked stops
   and nothing else: violet_block_bg_dark, and the accent at low alpha. */
.course-hero-stripes {
  position: absolute;
  inset: 0;
  background: repeating-linear-gradient(90deg,
    rgba(139, 92, 246, .26) 0px, rgba(139, 92, 246, .26) 2px,
    transparent 2px, transparent 13px);
  -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 55%, transparent 100%);
          mask-image: linear-gradient(180deg, #000 0%, #000 55%, transparent 100%);
}
.course-hero-video { position: relative; z-index: 1; width: 100%; }

.course-hero-rule { height: 4px; background: var(--grad); }

/* ── The two columns ────────────────────────────────────────────────
   The buy card on the LEFT, every section stacked on the right. The
   reference page has it on the right; Daniel asked for left.        */
.course-body { padding-top: 2.75rem; padding-bottom: 2.75rem; display: grid; gap: 2.5rem; }
@media (min-width: 1024px) {
  .course-body {
    grid-template-columns: 336px minmax(0, 1fr);
    column-gap: 3.5rem;
    align-items: start;
    padding-top: 3.5rem;
    padding-bottom: 4rem;
  }
  .course-rail { position: sticky; top: 5.5rem; }

  /* THE CARD ON THE RIGHT, which is where /workshop puts it (Daniel
     2026-09-21) and where the reference page has it. /course keeps it on
     the left, which is where he first approved it.

     ORDER, NOT MARKUP. The card stays FIRST IN SOURCE ORDER on both
     pages, because below 1024 the grid is one column and the price and
     the button have to come before the reading. Swapping the source to
     move it would put the buy card underneath everything on a phone. */
  .course-body-right { grid-template-columns: minmax(0, 1fr) 336px; }
  .course-body-right > .course-rail { order: 2; }
  .course-body-right > .course-col  { order: 1; }
}

/* ── The enrolment card ─────────────────────────────────────────────
   Price, button and email all above the fold, which is the part
   Daniel named. Nothing in it is typed — the date, the time and the
   price come from content/workshop.json → session.                  */
.course-enrol {
  background: rgb(var(--card));
  border: 1px solid rgb(var(--edge));
  border-radius: 16px;
  padding: 1.35rem;
  box-shadow: 0 12px 34px -22px rgba(var(--shadow-key) / .55);
}
.course-enrol-top { display: flex; align-items: center; justify-content: space-between; gap: .75rem; }
.course-price { display: flex; align-items: baseline; gap: .35rem; }
.course-price .structure { font-size: 1.7rem; color: rgb(var(--ink)); }
.course-price-tbc { font-size: 1.15rem !important; }
.course-price-cur { font-size: 12px; font-weight: 600; color: rgb(var(--muted)); }

.course-enrol-label { display: block; font-size: 10px; letter-spacing: .2em; color: rgb(var(--muted)); }
.course-enrol-meta { margin-top: 1.1rem; }
.course-enrol-date { margin-top: .4rem; font-weight: 600; color: rgb(var(--ink)); }
.course-enrol-time { font-size: 14px; color: rgb(var(--ink-soft)); }

.course-enrol-cta {
  display: block;
  margin-top: 1.1rem;
  text-align: center;
  padding: .8rem 1rem;
  font-size: 15px;
}
.course-enrol-seats { margin-top: .6rem; text-align: center; font-size: 12px; color: rgb(var(--muted)); }

.course-enrol-updates { margin-top: 1.25rem; padding-top: 1.1rem; border-top: 1px solid rgb(var(--edge)); }
.course-updates { margin-top: .5rem; display: flex; gap: .5rem; }
.course-updates input[type="email"] {
  flex: 1 1 auto;
  min-width: 0;
  background: rgb(var(--page));
  border: 1px solid rgb(var(--edge));
  border-radius: 8px;
  padding: .55rem .7rem;
  font-size: 14px;
  color: rgb(var(--ink));
}
.course-updates input[type="email"]::placeholder { color: rgb(var(--muted)); }
.course-updates input[type="email"]:focus { outline: none; border-color: rgb(var(--accent-solid) / .7); }
.course-updates button {
  flex: 0 0 auto;
  width: 40px;
  border-radius: 8px;
  background: var(--grad);
  color: #fff;
  display: grid;
  place-items: center;
}
.course-updates button svg { width: 17px; height: 17px; }
.course-updates-msg { margin-top: .45rem; font-size: 12px; color: rgb(var(--violet-text)); min-height: 0; }
.course-enrol-fine { margin-top: .55rem; font-size: 11px; line-height: 1.45; color: rgb(var(--muted)); }

.course-enrol-teams {
  display: block;
  margin-top: 1.1rem;
  text-align: center;
  font-size: 13px;
  font-weight: 600;
  color: rgb(var(--violet-text));
}
.course-enrol-teams:hover { text-decoration: underline; }

.course-enrol-guarantee {
  margin-top: 1.1rem;
  padding: .7rem .8rem;
  border-radius: 10px;
  background: rgb(var(--accent-solid) / .07);
  display: flex;
  align-items: center;
  gap: .55rem;
  font-size: 12px;
  color: rgb(var(--ink-soft));
}
.course-enrol-guarantee svg { width: 18px; height: 18px; flex: 0 0 auto; color: rgb(var(--violet-text)); }

/* ── The updates dialog ────────────────────────────────────────────
   Daniel wants the name as well as the address, and a name field beside
   an email field inside a 336px sticky card is two cramped inputs and a
   button. So the inline field opens this.

   A REAL <dialog>, so Escape, the backdrop, the focus trap and the inert
   page behind it are the browser's job and not hand-rolled JavaScript.
   It is lifted to the top layer when it opens, which is why it can live
   next to the field it belongs to rather than at the end of the body. */
.course-modal {
  width: min(26rem, calc(100vw - 2rem));
  padding: 0;
  border: 1px solid rgb(var(--edge));
  border-radius: 16px;
  background: rgb(var(--card));
  color: rgb(var(--ink));
  box-shadow: 0 30px 70px -24px rgba(var(--shadow-key) / .6);
}
.course-modal::backdrop { background: rgba(11, 15, 28, .55); backdrop-filter: blur(2px); }
.course-modal-form { position: relative; padding: 1.5rem; }

.course-modal-x {
  position: absolute;
  top: .75rem; right: .75rem;
  width: 30px; height: 30px;
  border-radius: 999px;
  display: grid; place-items: center;
  color: rgb(var(--muted));
  transition: background-color .2s, color .2s;
}
.course-modal-x:hover { background: rgb(var(--ink) / .06); color: rgb(var(--ink)); }
.course-modal-x svg { width: 16px; height: 16px; }

.course-modal-title { font-size: 1.25rem; padding-right: 2rem; }
.course-modal-body { margin-top: .4rem; font-size: 14px; line-height: 1.55; color: rgb(var(--ink-soft)); }

/* A REAL LABEL ON EACH FIELD, not a placeholder doing the job. A
   placeholder is gone the moment somebody types, which is exactly when
   they look up to check which box they are in. */
.course-modal-lab {
  display: block;
  margin-top: 1rem;
  margin-bottom: .3rem;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: rgb(var(--muted));
}
.course-modal-in {
  width: 100%;
  background: rgb(var(--page));
  border: 1px solid rgb(var(--edge));
  border-radius: 8px;
  padding: .65rem .8rem;
  /* 16px, not 14. Anything under it and iOS zooms the page on focus. */
  font-size: 16px;
  color: rgb(var(--ink));
}
.course-modal-in::placeholder { color: rgb(var(--muted)); }
.course-modal-in:focus { outline: none; border-color: rgb(var(--accent-solid) / .7); }

.course-modal-go {
  width: 100%;
  margin-top: 1.1rem;
  padding: .75rem 1rem;
  font-size: 15px;
  text-align: center;
}
.course-modal-go:disabled { opacity: .6; cursor: default; }
.course-modal-msg { margin-top: .5rem; font-size: 12px; min-height: 1em; color: rgb(var(--violet-text)); }
.course-modal-fine { margin-top: .3rem; font-size: 11px; line-height: 1.45; color: rgb(var(--muted)); }

@media (prefers-reduced-motion: reduce) { .course-modal-x { transition: none; } }

/* ── Section furniture ──────────────────────────────────────────────
   Air between sections, no rules and no marks. The reference page
   separates on whitespace alone and that is the whole look.         */
.course-col > .course-sec + .course-sec { margin-top: 3.75rem; }
.course-h2 { font-size: clamp(1.45rem, 2.6vw, 1.85rem); line-height: 1.16; color: rgb(var(--ink)); }
.course-lead { margin-top: .9rem; font-weight: 600; color: rgb(var(--ink)); }
.course-p { margin-top: .9rem; max-width: 68ch; line-height: 1.62; color: rgb(var(--ink-soft)); }
.course-meta-line { margin-top: .6rem; font-size: 13px; color: rgb(var(--muted)); }
.course-h2 + .course-p, .course-h2 + .course-acc, .course-h2 + .course-who,
.course-h2 + .course-prereq, .course-h2 + .course-inc, .course-h2 + .course-revs,
.course-h2 + .course-sched, .course-h2 + .course-tests, .course-h2 + .course-teach { margin-top: 1.1rem; }

/* The six hires, as the reference page lists its four officers: the role
   in the body weight, what it takes over after it. Not cards — a row of
   bordered boxes turns a list into a pattern the eye skims. */
.course-roster { margin-top: .5rem; display: grid; gap: .55rem; }
.course-roster li { padding-left: 1.1rem; position: relative; line-height: 1.55; color: rgb(var(--ink-soft)); max-width: 68ch; }
.course-roster li::before {
  content: '';
  position: absolute;
  left: 0; top: .62em;
  width: 5px; height: 5px;
  border-radius: 999px;
  background: rgb(var(--accent-solid));
}
.course-roster-who { font-weight: 600; color: rgb(var(--ink)); }
.course-roster-intro { font-weight: 600; color: rgb(var(--ink)); }
.course-intro-close { color: rgb(var(--ink-soft)); }

/* ── Accordions: "What you'll learn" and the FAQ ────────────────────
   0fr → 1fr on grid-template-rows, the same mechanism the homepage FAQ
   uses. It is the one way to animate a box to its own content height
   without knowing that height.

   THEY SHIP OPEN. Every item is `is-open` in the markup and site.js
   closes all but the first. With no JS, an old browser, or reduced
   motion, the page reads as four plain lists — no word is hidden
   behind a control that is not working.                             */
.course-acc { display: grid; gap: .7rem; }
.course-acc-item {
  border: 1px solid rgb(var(--edge));
  border-radius: 12px;
  background: rgb(var(--card));
  overflow: hidden;
  transition: border-color .25s;
}
.course-acc-item:hover { border-color: rgb(var(--accent-solid) / .45); }
.course-acc-head {
  width: 100%;
  display: flex;
  align-items: center;
  gap: .6rem;
  text-align: left;
  padding: .95rem 1.05rem;
  font-weight: 600;
  line-height: 1.4;
  color: rgb(var(--ink));
}
.course-acc-head > span { flex: 1 1 auto; }
.course-acc-chev { width: 17px; height: 17px; flex: 0 0 auto; color: rgb(var(--violet-text)); transition: transform .3s cubic-bezier(.16,1,.3,1); }
.course-acc-chev-end { order: 2; }
.course-acc-item:not(.is-open) .course-acc-chev { transform: rotate(-90deg); }
.course-acc-item:not(.is-open) .course-acc-chev-end { transform: none; }
.course-acc-item.is-open .course-acc-chev-end { transform: rotate(90deg); }

.course-acc-body { display: grid; grid-template-rows: 1fr; transition: grid-template-rows .38s cubic-bezier(.16,1,.3,1); }
.course-acc-item:not(.is-open) .course-acc-body { grid-template-rows: 0fr; }
.course-acc-body > * {
  overflow: hidden;
  min-height: 0;
  transition: padding .38s cubic-bezier(.16,1,.3,1);
}

/* THE PADDING HAS TO GO WHEN THE ROW DOES.

   A 0fr grid row takes the child's CONTENT height to zero, but the box
   is border-box and padding cannot shrink below itself, so the used
   height of a closed panel was its own padding — 16px of padding-bottom
   holding open a 16px window onto the first line of the answer. Every
   closed item on /workshop and /course sat under a clipped sentence.

   This is the cost of putting the padding on the grid child itself
   rather than on a wrapper inside it. The homepage's FAQ avoids it with
   an inner .faq-a-inner; this one zeroes the padding on the same curve
   as the row, which collapses to nothing and animates identically. */
.course-acc-item:not(.is-open) .course-acc-body > * {
  padding-top: 0;
  padding-bottom: 0;
}
.course-acc-body ul { padding: 0 1.05rem 1rem 2.3rem; display: grid; gap: .45rem; }
.course-acc-body p { padding: 0 1.05rem 1rem 1.05rem; line-height: 1.6; color: rgb(var(--ink-soft)); }
.course-acc-body li { position: relative; line-height: 1.55; color: rgb(var(--ink-soft)); }
.course-acc-body li::before {
  content: '';
  position: absolute;
  left: -1.05rem; top: .6em;
  width: 5px; height: 5px;
  border-radius: 999px;
  background: rgb(var(--accent-solid) / .55);
}
.course-acc-faq .course-acc-head { font-weight: 600; }

/* ── The instructor ─────────────────────────────────────────────────
   The reference page runs two people as two columns. One person, so
   one column with the photo and the contact button beside it.       */
.course-teach { display: grid; gap: 1.25rem; }
@media (min-width: 640px) { .course-teach { grid-template-columns: 116px minmax(0, 1fr); column-gap: 1.6rem; } }
/* 4:5 AND CROPPED FROM THE TOP, which is the crop the homepage hero
   already takes out of the same square 1100px file. A centre crop on a
   portrait puts the frame across his chin. */
.course-teach-photo {
  width: 116px;
  height: 145px;
  border-radius: 12px;
  object-fit: cover;
  object-position: top;
  display: block;
}
.course-teach-photo-fallback {
  background: linear-gradient(135deg, #2E1065, #1E1B4B);
  color: #E9D5FF;
  display: grid; place-items: center;
  font-family: 'Archivo', sans-serif; font-weight: 800; font-size: 1.6rem; letter-spacing: -.02em;
}

/* ALL SIX ACCOUNTS, in a 116px column: three across, two rows.
   3 x 32 + 2 x 6 = 108, inside 116 with room to spare. Theme-aware,
   because this card is on the page surface and not on a fixed-dark
   band — the footer's row is white-on-dark and cannot be reused here. */
.course-teach-socials {
  width: 116px;
  margin-top: .5rem;
  display: flex;
  flex-wrap: wrap;
  gap: .375rem;
}
.course-teach-soc {
  width: 32px; height: 32px;
  border-radius: 999px;
  border: 1px solid rgb(var(--edge));
  display: grid; place-items: center;
  color: rgb(var(--violet-text));
  transition: border-color .25s, background-color .25s, transform .2s cubic-bezier(.34,1.56,.64,1);
}
.course-teach-soc:hover {
  border-color: rgb(var(--accent-solid) / .6);
  background: rgb(var(--accent-solid) / .08);
  transform: translateY(-2px);
}
.course-teach-soc svg { width: 14px; height: 14px; }
@media (prefers-reduced-motion: reduce) { .course-teach-soc { transition: border-color .25s, background-color .25s; } }
.course-teach-contact {
  margin-top: .6rem;
  width: 116px;
  display: flex; align-items: center; justify-content: center; gap: .4rem;
  padding: .45rem;
  border: 1px solid rgb(var(--edge));
  border-radius: 8px;
  font-size: 12px; font-weight: 600;
  color: rgb(var(--ink));
  transition: border-color .25s, background-color .25s;
}
.course-teach-contact:hover { border-color: rgb(var(--accent-solid) / .6); background: rgb(var(--accent-solid) / .07); }
.course-teach-contact svg { width: 13px; height: 13px; }
.course-teach-name { font-size: 1.25rem; color: rgb(var(--ink)); }
.course-teach-role { margin-top: .15rem; font-size: 14px; color: rgb(var(--muted)); }
.course-teach-quote {
  margin-top: 1.1rem;
  padding-left: .9rem;
  border-left: 2px solid rgb(var(--accent-solid) / .55);
  font-style: italic;
  line-height: 1.6;
  color: rgb(var(--ink-soft));
  max-width: 68ch;
}

/* ── Who it's for: the collapsible, option C ────────────────────────
   The six audiences from workshop.json with their before and after.

   IT BORROWS THE ACCORDION WHOLESALE — .course-acc, .course-acc-item,
   .course-acc-head, .course-acc-body and the same data-acc handler — so
   this column has ONE open/close behaviour and not a second one that
   looks the same and works slightly differently. Everything below is
   only what the head and the body hold. */

/* Both spans in the head are flex:1 by default, which would give the
   numeral half the row. */
.course-acc-head > .course-who-num { flex: 0 0 auto; }
.course-who-num {
  font-family: 'Archivo', sans-serif;
  font-style: italic;
  font-weight: 800;
  font-size: 15px;
  font-variant-numeric: tabular-nums;
  color: rgb(var(--violet-text));
}
.course-who-role {
  font-family: 'Archivo', sans-serif;
  font-weight: 800;
  font-size: 1.125rem;
  letter-spacing: -0.02em;
}
:root[data-theme="dark"] .course-who-role { letter-spacing: -0.01em; }

/* BEFORE AND AFTER SIDE BY SIDE, which is what the pair is saying and
   what the 712px column finally has room for. Below 560px of column
   they stack and the arrow turns to point down. */
.course-who-split {
  padding: 0 1.3rem 1.3rem;
  display: grid;
  grid-template-columns: 1fr;
  gap: .6rem;
  align-items: center;
}
@media (min-width: 900px) {
  .course-who-split { grid-template-columns: 1fr 18px 1fr; column-gap: 1.1rem; }
}

.course-who-lab {
  font-size: 10.5px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .18em;
  margin-bottom: .25rem;
}

/* The BEFORE half is recessed: sunk into a tinted well, in the quieter
   ink. --ink is near-black on the light page and near-white on the dark
   one, so the same 4.5% veil darkens one card and lightens the other and
   reads as inset on both. */
.course-who-before {
  border-radius: 12px;
  padding: .85rem 1rem;
  background: rgb(var(--ink) / .045);
}
.course-who-before .course-who-lab { color: rgb(var(--muted)); }
.course-who-before p:last-child { font-size: 14px; line-height: 1.6; color: rgb(var(--ink-soft)); }

/* The AFTER half sits on the card, heavier, behind a violet rule. Four
   things carry the difference and none of them is a new colour — the two
   halves were both 14px inksoft under two tiny labels once, and you
   could read the pair and still not know which one was the win. */
.course-who-after {
  padding-left: 1rem;
  border-left: 2px solid rgb(var(--accent-solid));
}
.course-who-after .course-who-lab { color: rgb(var(--violet-text)); }
.course-who-after p:last-child { font-size: 14px; line-height: 1.6; font-weight: 500; color: rgb(var(--ink)); }

.course-who-arrow { display: flex; justify-content: center; color: rgb(var(--violet-text)); }
.course-who-arrow svg { width: 16px; height: 16px; transform: rotate(90deg); }
@media (min-width: 900px) { .course-who-arrow svg { transform: none; } }


/* ── Who it's for: three bordered cards, the same mark on each ──────
   PAIRED ELEMENTS MATCH. Equal-height rows come from the grid, so a
   card that wraps to one more line lifts all three rather than
   leaving two short boxes beside a tall one.                        */
.course-who { display: grid; gap: .8rem; }
/* THREE ACROSS ONLY WHERE THE MEASURE SURVIVES IT.

   PAIRED ELEMENTS MATCH, and for these three cards that is a fight the
   copy cannot win alone — below about 185px of measure there is no set of
   three strings that breaks to the same line count, because what fits the
   narrow case is too short to say anything in the wide one. So the layout
   gives way instead: the row stacks wherever the measure would drop under
   that, and three-across resumes when it is back.

   The two bands where it stacks are 720-767 (measure 173px) and 1024-1199,
   where the sticky card has already taken 336px and leaves each of three
   cards 144-176px. Everywhere it IS three across the measure is 189px or
   more and all three run the same number of lines.

   Measured at 390, 500, 640, 720, 768, 820, 900, 1000, 1120, 1200, 1280,
   1366, 1440 and 1600, in both themes. */
@media (min-width: 768px) and (max-width: 1023px) { .course-who { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (min-width: 1200px) { .course-who { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.course-who-card {
  border: 1px solid rgb(var(--edge));
  border-radius: 12px;
  background: rgb(var(--card));
  padding: 1.1rem;
}
.course-who-icon { width: 22px; height: 22px; color: rgb(var(--violet-text)); }
.course-who-card p { margin-top: 1.5rem; font-size: 14px; line-height: 1.55; color: rgb(var(--ink-soft)); }

/* ── Before you come: the gate, and the two quiet rows ─────────────
   Option B, Daniel 2026-09-21. The Claude requirement is the only one
   that costs money and the only one that stops anybody, so it is the
   only one that gets a surface.

   THE BLOCK IS FIXED DARK IN BOTH THEMES. It is violet_block_bg_dark
   with its own border and text colour, all three from tokens.json —
   the set that exists for exactly this, a violet panel that does not
   invert. Every value on it is a literal, because a theme-aware colour
   on a surface that does not turn over is how white-on-white shipped
   once already. */
.course-gate {
  border-radius: 16px;
  padding: 1.5rem;
  background: #2E1065;
  border: 1px solid #5b21b6;
}
.course-gate-lab {
  font-family: 'Archivo', sans-serif;
  font-stretch: 75%;
  font-weight: 800;
  font-size: 10.5px;
  letter-spacing: .2em;
  text-transform: uppercase;
  color: #e9d5ff;
  margin-bottom: .55rem;
}
.course-gate-title { font-size: 1.375rem; letter-spacing: -.01em; color: #fff; margin-bottom: .5rem; }
.course-gate-body { font-size: 15px; line-height: 1.6; color: rgba(255, 255, 255, .72); }
.course-gate-cta {
  margin-top: 1rem;
  display: inline-flex;
  align-items: center;
  gap: .45rem;
  font-size: 13px;
  font-weight: 600;
  color: #e9d5ff;
  border: 1px solid rgba(139, 92, 246, .55);
  border-radius: 8px;
  padding: .55rem .9rem;
  transition: border-color .25s, background-color .25s;
}
.course-gate-cta:hover { border-color: rgba(167, 139, 250, .9); background: rgba(139, 92, 246, .22); }
.course-gate-cta svg { width: 14px; height: 14px; transition: transform .25s cubic-bezier(.16,1,.3,1); }
.course-gate-cta:hover svg { transform: translateX(3px); }

/* The other two. Bordered rows on the page surface, so they read as the
   quieter pair without a second colour doing the work. */
.course-alsos { margin-top: .75rem; display: grid; gap: .625rem; }
.course-also {
  border: 1px solid rgb(var(--edge));
  border-radius: 12px;
  background: rgb(var(--card));
  padding: .875rem 1.125rem;
  display: grid;
  grid-template-columns: 20px minmax(0, 1fr);
  column-gap: .875rem;
}
.course-also-icon { color: rgb(var(--violet-text)); margin-top: 2px; }
.course-also-icon svg { width: 18px; height: 18px; }
.course-also-title { font-weight: 600; color: rgb(var(--ink)); }
.course-also-body { margin-top: .2rem; font-size: 14px; line-height: 1.55; color: rgb(var(--ink-soft)); }

@media (prefers-reduced-motion: reduce) { .course-gate-cta svg { transition: none; } }

/* ── Prerequisites: the dotted rail — REPLACED, kept for now ────────
   The rail this section used until 2026-09-21. Nothing renders it any
   more; it goes when it is clear nothing else wants it. */
.course-prereq { display: grid; gap: 1.1rem; }
.course-prereq-row { padding-left: 1.1rem; border-left: 2px dotted rgb(var(--accent-solid) / .45); }
.course-prereq-q { font-weight: 600; color: rgb(var(--ink)); }
.course-prereq-a { margin-top: .3rem; line-height: 1.6; color: rgb(var(--ink-soft)); max-width: 68ch; }

/* ── What's included ────────────────────────────────────────────────
   Icon, title, one line. No tile behind the icon.                   */
.course-inc { display: grid; gap: 1.1rem; }
.course-inc-row { display: grid; grid-template-columns: 26px minmax(0, 1fr); column-gap: .85rem; }
.course-inc-icon { color: rgb(var(--violet-text)); }
.course-inc-icon svg, .course-sched-icon svg { width: 22px; height: 22px; }
.course-inc-title { font-weight: 600; color: rgb(var(--ink)); }
.course-inc-body { margin-top: .2rem; font-size: 14px; line-height: 1.55; color: rgb(var(--ink-soft)); max-width: 66ch; }
.course-inc-guarantee {
  margin-top: 1.4rem;
  padding: 1rem 1.1rem;
  border-radius: 12px;
  background: rgb(var(--accent-solid) / .07);
  display: grid;
  grid-template-columns: 26px minmax(0, 1fr);
  column-gap: .85rem;
}
.course-inc-guarantee > svg { width: 22px; height: 22px; color: rgb(var(--violet-text)); }

/* ── The day ────────────────────────────────────────────────────────
   The reference page's week boxes. Theirs is seven weeks; this is one
   day, which is the one structural difference Daniel called out.    */
.course-syl { border: 1px solid rgb(var(--edge)); border-radius: 12px; overflow: hidden; background: rgb(var(--card)); }
.course-syl-head {
  display: flex; align-items: baseline; gap: .65rem; flex-wrap: wrap;
  padding: .8rem 1.05rem;
  border-bottom: 1px solid rgb(var(--edge));
  background: rgb(var(--accent-solid) / .05);
}
.course-syl-day { font-weight: 600; color: rgb(var(--ink)); }
.course-syl-time { font-size: 13px; color: rgb(var(--muted)); }
.course-syl-row {
  display: grid;
  grid-template-columns: 62px minmax(0, 1fr);
  column-gap: .85rem;
  padding: .85rem 1.05rem;
  border-bottom: 1px solid rgb(var(--edge));
}
.course-syl-row:last-child { border-bottom: 0; }
.course-syl-at { font-size: 12px; font-weight: 600; color: rgb(var(--muted)); padding-top: .15rem; font-variant-numeric: tabular-nums; }
.course-syl-title { font-weight: 600; line-height: 1.45; color: rgb(var(--ink)); }
.course-syl-num { color: rgb(var(--violet-text)); }
/* The hands-on chip. A row can carry a module number AND this one,
   because module 4 IS the second build. Filled rather than plain text so
   the two chips do not read as one long violet run-on. */
.course-syl-tag {
  display: inline-block;
  font-family: 'Archivo', sans-serif;
  font-stretch: 75%;
  font-weight: 800;
  font-size: 9.5px;
  letter-spacing: .14em;
  text-transform: uppercase;
  padding: .18rem .4rem;
  border-radius: 4px;
  vertical-align: 2px;
  color: rgb(var(--violet-text));
  background: rgb(var(--accent-solid) / .14);
}
.course-syl-note { margin-top: .2rem; font-size: 13px; color: rgb(var(--muted)); }
.course-syl-break .course-syl-title { font-weight: 400; color: rgb(var(--muted)); }

/* ── Reviews ────────────────────────────────────────────────────── */
.course-revs { display: grid; gap: .8rem; }
.course-rev { border: 1px solid rgb(var(--edge)); border-radius: 12px; background: rgb(var(--card)); padding: 1.05rem; }
.course-rev-top { display: flex; align-items: flex-start; gap: .7rem; }
.course-rev-av {
  flex: 0 0 auto;
  width: 34px; height: 34px;
  border-radius: 999px;
  display: grid; place-items: center;
  background: linear-gradient(135deg, #2E1065, #1E1B4B);
  color: #E9D5FF;
  font-family: 'Archivo', sans-serif; font-weight: 800; font-size: 13px;
}
.course-rev-id { flex: 1 1 auto; min-width: 0; }
.course-rev-name { font-size: 14px; font-weight: 600; color: rgb(var(--ink)); display: flex; align-items: center; gap: .45rem; flex-wrap: wrap; }
.course-rev-pill {
  font-family: 'Archivo', sans-serif; font-stretch: 75%; font-weight: 800;
  font-size: 9px; letter-spacing: .14em; text-transform: uppercase;
  padding: .18rem .4rem; border-radius: 4px;
  color: rgb(var(--violet-text)); background: rgb(var(--accent-solid) / .14);
}
.course-rev-role { font-size: 12px; color: rgb(var(--muted)); }
.course-rev-meta { flex: 0 0 auto; text-align: right; }
.course-rev-date { display: block; margin-top: .2rem; font-size: 11px; color: rgb(var(--muted)); }
.course-rev-video { margin-top: .9rem; }
.course-rev-quote { margin-top: .75rem; font-size: 14px; line-height: 1.6; color: rgb(var(--ink-soft)); }

/* ── Schedule tiles ─────────────────────────────────────────────── */
.course-sched { display: grid; gap: .8rem; }
/* stretch, not start: the big tile and the stack of two beside it are a
   paired set, and a 103px box next to a 134px column is the mismatch
   Daniel points at every time. */
@media (min-width: 720px) { .course-sched { grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr); align-items: stretch; } }
.course-sched-big { display: flex; flex-direction: column; }
.course-sched-big .course-sched-times { margin-top: auto; }
.course-sched-big, .course-sched-tile {
  border: 1px solid rgb(var(--edge));
  border-radius: 12px;
  background: rgb(var(--card));
  padding: 1.05rem;
}
.course-sched-head, .course-sched-tile { display: grid; grid-template-columns: 26px minmax(0, 1fr); column-gap: .85rem; }
.course-sched-icon { color: rgb(var(--violet-text)); }
.course-sched-amount { margin-top: .1rem; font-size: 14px; color: rgb(var(--ink-soft)); }
.course-sched-small { display: grid; gap: .8rem; }
.course-sched-times { margin-top: .9rem; padding: .7rem .8rem; border-radius: 10px; background: rgb(var(--accent-solid) / .06); }
.course-sched-time { display: flex; align-items: center; justify-content: space-between; gap: .75rem; font-size: 13px; color: rgb(var(--ink-soft)); }
.course-sched-clock { display: inline-flex; align-items: center; gap: .35rem; color: rgb(var(--muted)); }
.course-sched-clock svg { width: 14px; height: 14px; }

/* ── Testimonials: quote rows, not cards ────────────────────────────
   A different shape to the review cards above on purpose. Two proof
   blocks built the same way read as one block repeated.             */
.course-tests { display: grid; }
.course-test { padding: 1.15rem 0; border-bottom: 1px solid rgb(var(--edge)); }
.course-test:first-child { padding-top: .5rem; }
.course-test:last-child { border-bottom: 0; }
.course-test-quote { line-height: 1.6; color: rgb(var(--ink-soft)); max-width: 68ch; }
.course-test-mark { display: inline-block; vertical-align: -2px; margin-right: .3rem; color: rgb(var(--violet-text)); }
.course-test-mark svg { width: 16px; height: 16px; }
.course-test-who { margin-top: .75rem; display: flex; align-items: center; gap: .7rem; }

/* ── The private band ───────────────────────────────────────────────
   The reference page's "Maven for Teams". It PAINTS NOTHING: the
   .endcap wrapper in base.html carries one background across this
   section and the footer, so the bottom of the page is one block.

   FIXED DARK IN BOTH THEMES. Nothing in here may use a theme-aware
   colour — that surface does not invert, and bg-white/text-ink on a
   surface like this is how white-on-white shipped once.             */
.course-teams { padding-top: 4rem; padding-bottom: 3.5rem; }
.course-teams-h { font-size: clamp(1.6rem, 3vw, 2.1rem); line-height: 1.12; color: #F1F5F9; }
.course-teams-sub { margin-top: .75rem; max-width: 60ch; line-height: 1.6; color: rgba(241, 245, 249, .72); }
.course-teams-grid { margin-top: 1.75rem; display: grid; gap: .9rem; }
@media (min-width: 820px) { .course-teams-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.course-teams-card {
  background: rgba(255, 255, 255, .05);
  border: 1px solid rgba(139, 92, 246, .3);
  border-radius: 14px;
  padding: 1.2rem;
  display: flex;
  flex-direction: column;
}
.course-teams-title { font-weight: 600; color: #F1F5F9; }
.course-teams-lead { margin-top: .1rem; font-size: 13px; color: #E9D5FF; }
.course-teams-body { margin-top: .75rem; flex: 1 1 auto; font-size: 14px; line-height: 1.55; color: rgba(241, 245, 249, .72); }
.course-teams-cta {
  margin-top: 1.1rem;
  display: inline-flex; align-items: center; gap: .4rem;
  font-size: 13px; font-weight: 600;
  color: #E9D5FF;
}
.course-teams-cta svg { width: 15px; height: 15px; transition: transform .25s cubic-bezier(.16,1,.3,1); }
.course-teams-cta:hover svg { transform: translateX(3px); }

/* Reduced motion: the accordions still open and close, they just do it
   at once. Nothing here conveys meaning through movement. */
@media (prefers-reduced-motion: reduce) {
  .course-acc-body, .course-acc-chev, .course-teams-cta svg { transition: none; }
}
