/* =============================================================
   Snap Zap Media — site styles

   tokens.css is loaded FIRST and owns every colour, the type
   scale, the buttons, .wrap and the sticky masthead. Nothing in
   this file declares a hex value. Where a translucent tint is
   needed it is derived from a token with color-mix(), so the
   palette still lives in exactly one place.

   Surfaces FLOAT. Ruled by Robbie 2026-09-15, replacing the flat,
   bordered, shadowless surfaces this file carried until
   2026-09-16. Every button and every card is rounded, filled with
   the gradient tokens.css derives from the surface colours, and
   lifted by a soft accent glow beneath it. The four values that
   do it — --radius-card, --fill-surface, --glow-card and
   --ease-lift — all live in tokens.css, so the whole family
   changes from one place.

   Two things here are deliberately NOT cards: the .visual image
   wells, which keep their faded edges and must never read as
   framed boxes, and the .disclose "Learn more" toggles, which
   keep the quiet hairline look they already had.
   ============================================================= */

*{ box-sizing:border-box }

/* No horizontal scrolling, at any width.
   On body ONLY. Setting overflow-x on <html> makes it a scroll container
   and silently kills position:sticky on the masthead — the same class of
   ancestor-overflow bug the hero script's own comment warns about. */
body{ overflow-x:hidden }
html{ scroll-behavior:auto }

img{ max-width:100%; height:auto }

/* ---------- the floating card ----------
   The shared look every card and highlighted block wears, kept in
   one place so "consistent everywhere" is a fact rather than an
   intention. Anything that opts in gets the same rounded corner,
   the same gradient between two surface tokens and the same accent
   glow beneath it; hover raises it 2px.

   It is applied through a selector list rather than a class so no
   markup has to change and nothing can be added to a page without
   it. .visual and .disclose-summary are not in the list, on
   purpose.                                                        */
.card,
.approval,
.preview-pair > div,
.onboarding-price,
.testimonial,
.piece,
.todo{
  border-radius:var(--radius-card);
  background:var(--fill-surface);
  box-shadow:var(--glow-card);
  transition:box-shadow .22s var(--ease-lift), transform .22s var(--ease-lift);
}
.card:hover,
.approval:hover,
.preview-pair > div:hover,
.onboarding-price:hover,
.testimonial:hover,
.piece:hover,
.todo:hover{
  transform:translateY(-2px);
  box-shadow:var(--glow-hover);
}

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

/* ---------- skip link ---------- */
.skip{
  position:absolute; left:-9999px; top:0; z-index:100;
  background:var(--surface); color:var(--ink);
  padding:.75rem 1.1rem; border:1px solid var(--surface-alt);
  border-radius:var(--radius); text-decoration:none;
}
.skip:focus{ left:1rem; top:1rem }

/* ---------- masthead ----------
   tokens.css makes .site-head sticky. DESIGN-SYSTEM §8 says the
   header is NOT sticky on mobile, so the single 880px breakpoint
   below releases it. The booking CTA is never sticky anywhere. */
/* Direct child only — the mobile menu below has its own .wrap and must not
   inherit the masthead's flex row, which would shrink the links to content. */
.site-head > .wrap{
  display:flex; align-items:center; justify-content:space-between;
  gap:2rem; padding-block:.85rem;
}
.brand{
  display:flex; align-items:center; gap:.6rem;
  font-family:var(--display); font-weight:800; font-size:var(--step--1);
  letter-spacing:.18em; text-transform:uppercase; color:var(--ink);
  text-decoration:none; white-space:nowrap;
}
.brand-mark{ width:30px; height:30px; display:block; border-radius:5px; flex:none }
.brand span{ color:var(--accent) }

/* ---------- the masthead links are BUTTONS ----------
   Ruled by Robbie 2026-09-17, approved on a rendered sample
   2026-09-16. Every one of the six parent-page links wears the
   footer's Facebook/Instagram look — .btn.btn-secondary inside
   .foot-social — so the masthead, the footer and the page
   buttons are visibly one family.

   The style is not re-declared here. Each link carries
   .btn.btn-secondary in the markup and takes the whole look from
   tokens.css: --body at 600, --radius 10px, 1px --hairline-2
   border, the --fill-ghost gradient, --glow beneath, and the 2px
   rise on hover. This block only does the two things the
   masthead needs on top of that.

   1 · TIGHTER PADDING, so all six fit on one row. 8px 14px
   against the .95rem 1.6rem of a page button — the one
   deliberate difference from the footer's pair, and the reason
   the row still closes at 1366 wide. The label still sets the
   width; nothing here is a fixed size.

   2 · THE CURRENT PAGE takes a soft pink edge and a little more
   glow. It keeps aria-current="page", which is what the rule
   selects on, so the marker is the same fact the screen reader
   already reads rather than a second one kept in step by hand.

   These links are NAVIGATIONAL and are secondary buttons, never
   primary: PROJECT-RULES §3 keeps one conversion ask per page
   and SITE-CHROME §1 keeps Book Discovery Call out of the
   navigation. Neither changes here — no rim, no primary fill,
   and no booking tab. */
.site-nav{ display:flex; flex-wrap:wrap; align-items:center; gap:.5rem }
.site-nav .btn{ font-size:var(--step--1); padding:8px 14px }

.site-nav .btn[aria-current="page"],
.mobile-nav .btn[aria-current="page"]{
  border-color:color-mix(in srgb, var(--accent) 65%, transparent);
  box-shadow:var(--glow-hover);
}
/* .btn-secondary:hover dulls the border to --accent-deep, which
   would take the pink edge off the page you are on at the moment
   you point at it. On the current page the edge brightens
   instead. */
.site-nav .btn[aria-current="page"]:hover,
.site-nav .btn[aria-current="page"]:focus-visible,
.mobile-nav .btn[aria-current="page"]:hover,
.mobile-nav .btn[aria-current="page"]:focus-visible{ border-color:var(--accent) }

/* ---------- mobile menu ---------- */
.nav-toggle{
  display:none; background:var(--fill-ghost); color:var(--ink);
  border:1px solid var(--hairline-2); border-radius:var(--radius);
  padding:.72rem .78rem; cursor:pointer; line-height:0;
  box-shadow:var(--glow);
  transition:border-color .18s ease, color .18s ease,
             box-shadow .22s var(--ease-lift), transform .22s var(--ease-lift);
}
.nav-toggle:hover,
.nav-toggle:focus-visible{ border-color:var(--accent-deep); color:var(--accent);
  box-shadow:var(--glow-hover) }
.nav-toggle:active{ transform:translateY(1px) scale(.98) }
.nav-toggle svg{ width:22px; height:22px; display:block; fill:none;
  stroke:currentColor; stroke-width:1.6; stroke-linecap:square }
.nav-toggle .bar-x{ display:none }
.nav-toggle[aria-expanded="true"] .bar-x{ display:block }
.nav-toggle[aria-expanded="true"] .bar-lines{ display:none }

.mobile-nav{ display:none; border-top:1px solid var(--hairline); background:var(--ground) }
.mobile-nav[data-open="true"]{ display:block }
/* THE COLLAPSED MENU BEHAVES EXACTLY AS IT DID — the hamburger
   opens and closes it, a tap on an item closes it, so does a tap
   outside or Escape. Only the look of the items changed with the
   masthead above: the hairline-separated rows are now the same
   secondary buttons, stacked.

   They keep the FULL button padding rather than the row's tighter
   8px 14px. There are no six-across to fit on a phone, and the
   full padding is what keeps each one a comfortable tap target.
   justify-items:start leaves every button the width of its own
   label, which is DESIGN-SYSTEM §5: the size is set by the label
   and the padding, never by a fixed width. */
.mobile-nav ul{ list-style:none; margin:0; padding:.9rem 0 1.25rem;
  display:grid; gap:.6rem; justify-items:start }

@media (max-width:880px){
  /* NOT sticky on mobile — DESIGN-SYSTEM §8 — but still `relative`
     rather than `static`, so its z-index:50 keeps working. The
     homepage hero's pinned frame is absolutely positioned and would
     otherwise paint straight over the logo and the hamburger at the
     top of the page, where the two overlap by the height of this
     bar. `relative` with no offset leaves it exactly where the flow
     puts it and scrolls it away exactly as before. */
  .site-head{ position:relative }
  .site-nav{ display:none }
  .nav-toggle{ display:inline-flex }
}

/* ---------- page rhythm ---------- */
.section{ padding-block:clamp(3.25rem,7vw,6rem); border-top:1px solid var(--hairline) }
.section:first-of-type{ border-top:0 }
/* h3 is named alongside h2 for the sake of any section that opens on
   one. NO PAGE DOES TODAY: Snap Zap Integration on /packages was the
   only h3 that was ever a direct child of .section, and on 2026-09-17
   it became an h2 inside the split's text column, where
   .wwc-split h2 sets its margin instead. The h3 is left standing in
   the selector because a section is entitled to open on one. */
.section > h2,
.section > h3{ margin:0 0 .9rem }
.page-head{ padding-block:clamp(2.75rem,6vw,4.5rem) clamp(1.5rem,3vw,2.25rem) }
.page-head h1{ margin:0 }
.lede{ font-size:var(--step-1); line-height:1.5; margin:1.1rem 0 0 }
.section-intro{ margin:0 0 2.25rem }

/* A section label reads as part of the heading it sits above, so it
   takes the heading colour. Ruled by Robbie 2026-09-15, replacing the
   --ink-faint tokens.css still declares for .label alongside .small;
   this is the override, and only the colour changes — same --step--1
   size, same .22em tracking, same uppercase, and still 400, never
   bold. .small keeps --ink-faint.

   The two labels that are NOT section labels keep their accent, and
   both already say so further down this file at a higher specificity:
   .approval .label on /packages and .todo .label on the build
   placeholders. */
.label{ display:block; margin:0 0 .75rem; color:var(--ink) }

/* ---------- sequential steps (How It Works, Getting Started) ---------- */
.steps-list{ list-style:none; margin:2.25rem 0 0; padding:0; display:grid; gap:1px;
  background:var(--hairline) }
.steps-list > li{ background:var(--ground); padding:1.6rem 0 }
.steps-list h3{ margin:0 0 .55rem }
.steps-list p{ margin:0 0 .8rem }
.steps-list p:last-child{ margin-bottom:0 }

/* ---------- two-column inclusions table ---------- */
.inclusions{ width:100%; border-collapse:collapse; margin-top:1.5rem }
.inclusions th,
.inclusions td{ text-align:left; vertical-align:top; padding:1.15rem 1.25rem;
  border-top:1px solid var(--hairline) }
.inclusions tr:nth-child(odd) th,
.inclusions tr:nth-child(odd) td{ background:var(--surface) }
.inclusions th{ font-family:var(--display); font-weight:700; font-size:var(--step-0);
  letter-spacing:-.015em; color:var(--ink); width:34%; white-space:normal }
.inclusions td{ color:var(--ink-muted) }
.inclusions td p{ margin:0; max-width:none }
@media (max-width:700px){
  .inclusions th,.inclusions td{ display:block; width:auto }
  .inclusions td{ border-top:0; padding-top:0 }
}

/* ---------- tick list ----------
   The eight Snap Zap Integration items, shown on the homepage as a
   compact list rather than the two-column table look. Only the look
   changed: every name and description is the one the table carried.

   The homepage was .inclusions' only user, so that block above is now
   unused. It is left in place rather than deleted: PROJECT-RULES §7
   is verify-before-delete, and removing it is Robbie's call, not a
   side effect of this change.

   Two columns per item — the tick, then the name with its description
   beneath it. The name and the note are both placed in column 2 so
   the note lines up under the name rather than under the tick.

   The tick is the accent spent as a glyph, the way .matrix .yes is:
   small, sharp-capped and decorative, so it is hidden from assistive
   technology and the list is named by the caption the table used. */
.tick-list{ list-style:none; margin:1.5rem 0 0; padding:0; display:grid; gap:1.05rem }
.tick-list > li{ display:grid; grid-template-columns:auto minmax(0,1fr); column-gap:.7rem }
.tick-list .tick{ grid-column:1; grid-row:1; width:.8em; height:.8em; margin-top:.45em;
  display:block; fill:none; stroke:var(--accent); stroke-width:2; stroke-linecap:square }
.tick-list p{ grid-column:2; margin:0 }
.tick-list .tick-name{ font-family:var(--display); font-weight:700; font-size:var(--step-0);
  letter-spacing:-.015em; color:var(--ink) }
.tick-list .tick-name + p{ margin-top:.25rem }

/* ---------- package cards ----------
   Ruled by Robbie 2026-09-17, approved on a rendered sample
   2026-09-16. Each card now carries its own price and its own
   feature list, so the separate comparison table and the separate
   Monthly Investment list are gone from the page — their data
   lives here, one card at a time, and not one word of it changed.

   SIDE BY SIDE, and not a design variable. Three equal columns on
   desktop AND tablet, all the way down to 761px. They stack in one
   column on phones, 760px and below, and nowhere else. This is the
   one breakpoint on the site that is NOT the shared 880px: at 880
   the three columns still read, and Robbie ruled them never
   stacked above phone width.

   The three prices sit on one baseline because every card reserves
   the same room above them, whether or not it has anything to put
   there: .rec-row holds the height of the Recommended pill on all
   three, and .card-sub reserves two lines on desktop. Nothing is
   positioned to a number — reserve the space and the baselines
   fall into line on their own at any width.                       */
.cards{ display:grid; gap:1.25rem; margin-top:2rem;
  grid-template-columns:repeat(3,minmax(0,1fr)) }
@media (max-width:760px){ .cards{ grid-template-columns:1fr } }
.card{ border:1px solid var(--surface-alt);
  padding:1.9rem 1.6rem; position:relative }
/* The body sits above the two layers the Liquid Metal rim injects
   into the Recommended card — an absolutely positioned sibling
   paints over ordinary in-flow content whatever its z-index, so
   the content is given one of its own. It is in the markup on all
   three cards so they stay identical; on the two without a rim it
   does nothing at all. */
.card-body{ position:relative; z-index:2 }
.card h2,.card h3{ margin:0 }
.card .card-sub{ color:var(--accent); margin:.35rem 0 1rem; font-family:var(--display);
  font-weight:500; font-size:var(--step-0) }
.card p{ margin:0 }

/* Two lines of tagline, reserved on desktop and tablet whether the
   tagline needs them or not. One of the three runs to two lines in
   a narrow column and the other two do not; without this the price
   beneath it would sit a line lower than its neighbours. On phones
   the cards are stacked, nothing has to line up with anything, and
   the reserve comes off. */
@media (min-width:761px){
  .card .card-sub{ min-height:calc(2 * 1.6 * var(--step-0)) }
}

/* The Recommended row. On Signature it holds the pill; on Presence
   and Authority it is empty and holds only the height, so all
   three cards start their name at the same place. */
.rec-row{ display:flex; justify-content:center;
  min-height:calc(1.4 * var(--step--1) + .6rem); margin-bottom:1.1rem }
/* The flag is a badge on the card, not a card of its own: it keeps
   the flat accent fill that makes it read as a label, and takes
   only the rounded corner. From 2026-09-17 it sits centred at the
   top INSIDE the card rather than straddling its top edge. */
.rec-flag{ background:var(--accent); color:var(--ground); border-radius:var(--radius);
  font-family:var(--body); font-weight:600; font-size:var(--step--1);
  letter-spacing:.16em; text-transform:uppercase; padding:.3rem .6rem; line-height:1.4 }

/* ---------- the price: THE DOMINANT ELEMENT OF THE CARD ----------
   Corrected 2026-09-17 against the approved sample, which a first
   build had rendered too small and in pink. The figure is the
   display face at 800 in the HEADING colour and it is the largest
   thing in the card — larger than the card's own name. "/month"
   stays small beside it, at body size in the body colour, sitting
   on the figure's baseline.

   THE SUPERSEDED VERSION — the --step-2 figure in --accent, which
   was the treatment the Monthly Investment list carried before it
   came off the page — is deleted rather than struck through, per
   PROJECT-RULES §8. ★ THE PRICE IS NOT PINK.

   Its size is its own clamp() rather than a step off the shared
   scale: --step-hero is the H1's and nothing else may wear it, and
   --step-2 is where this build went wrong. The three values are
   the approved sample's, transcribed.

   --ink-muted rather than --ink-faint on "/month": faint on a card
   surface is 3.8:1 and under AA.

   The price and the Features label are both written against .card
   because .card p{margin:0} above outranks a lone class on a <p>.
   margin-top 1.4rem collapses with the tagline's 1rem to exactly
   1.4rem, which is the space the sample has above the figure.

   flex-wrap:wrap is a GUARD, not a layout. A flex line does not
   wrap by default, so without it a figure too wide for its column
   pushes "/month" straight out through the card's padding and over
   the card's edge — which is what a first build of this correction
   did between 761 and 878px. With it, the worst case is "/month"
   on its own line and nothing ever spills. Measured: it stays on
   the figure's line down to 822px, so it never wraps at any width
   the cards are checked at. */
.card .card-price{ display:flex; flex-wrap:wrap; align-items:baseline;
  gap:.4rem; margin:1.4rem 0 1rem }
.card .label{ margin:0 0 .85rem }
.card-price .figure{ font-family:var(--display); font-weight:800;
  font-size:clamp(2.4rem, 1.9rem + 1.6vw, 3.2rem);
  line-height:1; letter-spacing:-.03em; color:var(--ink);
  font-variant-numeric:tabular-nums }
.card-price .per{ font-size:var(--step-0); color:var(--ink-muted) }

.card-rule{ border:0; border-top:1px solid var(--hairline); margin:1.4rem 0 1.15rem }

/* One row per line, in the order the comparison table had them.
   Included takes a small circular tick — an accent ring with a
   light tick inside it; not included is dimmed and takes a dash.
   Both marks are decorative and hidden from assistive technology,
   which reads the sr-only word beside them instead, exactly as the
   table's ✓ and — did. */
.feature-list{ list-style:none; margin:0; padding:0; display:grid; gap:.7rem }
.feature-list li{ display:grid; grid-template-columns:1rem minmax(0,1fr);
  column-gap:.7rem; align-items:start; font-size:var(--step-0) }
.feature-list .feat-in{ color:var(--ink) }
.feature-list .feat-out{ color:var(--ink-muted) }   /* --ink-faint on a card is 4.37:1, under AA */
.feature-list strong{ font-weight:700; color:var(--ink) }

.feat-mark{ width:1rem; height:1rem; margin-top:.2em; display:block }
.feat-mark circle{ fill:none; stroke:var(--accent); stroke-width:1 }
.feat-mark path{ fill:none; stroke:var(--ink); stroke-width:1.7;
  stroke-linecap:square; stroke-linejoin:miter }
.feat-dash{ display:block; width:1rem; text-align:center; line-height:1.6 }

/* ---------- the Recommended card ----------
   Three things, all ruled by Robbie 2026-09-17 on the rendered
   sample of 2026-09-16.

   1 · THE MOVING LIQUID METAL RIM, the same one every Book
   Discovery Call button carries and the first thing on the site
   other than that button to carry it. /assets/js/metal.js mounts
   it, at a slower speed, and sets data-metal="on" when it has. The
   rim is the ONLY thing at stake: nothing below applies until the
   shader is up, so under prefers-reduced-motion, without WebGL,
   with JavaScript off or with the CDN unreachable this is the
   accent-bordered card it has always been. The rim never moves
   under reduced motion because it never mounts.

   2 · A slightly stronger glow, --glow-recommended.

   3 · 14px higher than the two beside it, on desktop and tablet
   only. A transform, so it moves without moving anything else —
   the grid row, and therefore the three cards' heights, are
   untouched. The hover lift has to be restated because the
   translate would otherwise replace it.                           */
.card[data-recommended="true"]{ border-color:var(--accent);
  box-shadow:var(--glow-recommended) }
.card[data-recommended="true"]:hover{ box-shadow:var(--glow-hover) }

@media (min-width:761px){
  .card[data-recommended="true"]{ transform:translateY(-14px) }
  .card[data-recommended="true"]:hover{ transform:translateY(-16px) }
}

/* ---------- room for the price in the narrow three-column band ----------
   THE ONE THING THE BIG PRICE NEEDED, and it is a layout
   accommodation, not a change to the price: every value of the
   price treatment above is the approved one, at every width.

   Between 761 and about 1020px the cards are at their narrowest —
   three columns out of 92vw — and the widest figure, $3,399, plus
   the gap plus "/month" comes to more than the column has to give.
   Measured: at 880 the row had 203px of room and wanted 204, so it
   sat a third of a pixel over its own padding, and below 878 it
   spilled outright. A third of a pixel is not a pass: an 880px
   WINDOW with a classic scrollbar is a ~865px viewport, which is
   well inside that.

   Trimming the side padding in that band only gives the row about
   14px of real headroom at 880 and leaves every other width, and
   every other card property, exactly as it was. */
@media (min-width:761px) and (max-width:1023px){
  .card{ padding-inline:1.15rem }
}

/* Mounted. The face and the edge both come off the card itself —
   the shader layer paints the border box and the fill layer paints
   everything 2px inside it, which is the rim, built exactly as the
   booking button's is. */
.card[data-metal="on"]{ background:transparent; border-color:transparent }
.metal-rim{
  position:absolute; inset:-1px; z-index:0;
  border-radius:var(--radius-card); overflow:hidden;
  pointer-events:none;
}
.metal-rim-fill{
  position:absolute; inset:1px; z-index:1;
  border-radius:calc(var(--radius-card) - 2px);
  background:var(--fill-surface);
  pointer-events:none;
}

/* ---------- Presence and Authority ----------
   The still 1px polished chrome edge the homepage's Visual
   Concepts card wears, same --chrome-edge gradient, same values.
   Ruled by Robbie 2026-09-17 — a new ruling, which is what that
   card's own record requires of anything wanting this edge, not a
   reuse of it.

   Built the same way: the face paints to the PADDING box and the
   chrome to the BORDER box, so the gradient fills the 1px ring and
   the card's own face is untouched beneath it. The border width
   stays 1px, so nothing moves or resizes.

   ★ STATIC. No motion of any kind — that is the whole difference
   between this edge and the rim on the card between them. */
.card[data-chrome="true"]{
  border:1px solid transparent;
  border-radius:var(--radius-card);
  background:
    var(--fill-surface) padding-box,
    var(--chrome-edge)  border-box;
}

/* ---------- feature matrix ----------
   UNUSED from 2026-09-17: the comparison table came off /packages
   when the cards took its data. The rules are left standing rather
   than deleted — PROJECT-RULES §7 is verify-before-delete, and
   removing them is Robbie's call, not a side effect of this
   change. Same treatment .inclusions and .site-foot ul already
   have. */
.matrix{ width:100%; border-collapse:collapse; margin-top:1.5rem; font-size:var(--step-0) }
.matrix th,.matrix td{ text-align:left; padding:.95rem 1rem; border-top:1px solid var(--hairline) }
.matrix thead th{ font-family:var(--display); font-weight:700; color:var(--ink);
  border-top:0; border-bottom:1px solid var(--hairline-2) }
.matrix thead th:not(:first-child),
.matrix tbody td{ text-align:center }
.matrix tbody th{ font-weight:400; color:var(--ink-muted); font-family:var(--body) }
.matrix tbody tr:nth-child(odd) th,
.matrix tbody tr:nth-child(odd) td{ background:var(--surface) }
.matrix .yes{ color:var(--accent) }
.matrix .no{ color:var(--ink-muted) }   /* --ink-faint on --surface is 4.37:1, under AA */
@media (max-width:700px){
  .matrix thead{ position:absolute; width:1px; height:1px; overflow:hidden; clip-path:inset(50%) }
  .matrix tbody tr{ display:block; border:1px solid var(--hairline); margin-bottom:-1px; padding:.35rem 0 }
  .matrix tbody th,.matrix tbody td{ display:flex; justify-content:space-between; gap:1rem;
    border-top:0; background:transparent!important; text-align:left!important; padding:.5rem 1rem }
  .matrix tbody th{ font-family:var(--display); font-weight:700; color:var(--ink);
    border-bottom:1px solid var(--hairline); margin-bottom:.25rem }
  .matrix tbody td::before{ content:attr(data-col); color:var(--ink-faint);
    font-size:var(--step--1); letter-spacing:.16em; text-transform:uppercase }
}

/* ---------- the pre-post approval block ----------
   Given real visual weight: it is the strongest line on the site. */
.approval{ margin-top:2rem; border:1px solid var(--accent);
  padding:1.9rem 1.75rem;
  position:relative; overflow:hidden }
.approval::before{ content:""; position:absolute; inset:0;
  background:radial-gradient(120% 140% at 0% 0%,
    color-mix(in srgb, var(--accent) 16%, transparent), transparent 62%);
  pointer-events:none }
.approval .label{ color:var(--accent); position:relative }
.approval p{ margin:0; color:var(--ink); font-size:var(--step-1); line-height:1.5;
  max-width:60ch; position:relative }

/* ---------- pricing ----------
   .pricing is UNUSED from 2026-09-17: the Monthly Investment list
   came off /packages when the three prices moved into the cards.
   Left standing rather than deleted, for the same reason .matrix
   above is. .onboarding-price is NOT unused — the Snap Zap
   Integration one-time price is still on the page, wording
   unchanged, in its old place beneath the Integration copy. */
.pricing{ list-style:none; margin:1.25rem 0 0; padding:0; display:grid; gap:1px;
  background:var(--hairline); border-block:1px solid var(--hairline) }
.pricing li{ background:var(--ground); display:flex; flex-wrap:wrap; gap:.5rem 1rem;
  align-items:baseline; justify-content:space-between; padding:1.05rem .25rem }
.pricing .plan{ font-family:var(--display); font-weight:700; font-size:var(--step-2);
  letter-spacing:-.025em; color:var(--ink) }
.pricing .figure{ font-family:var(--display); font-weight:700; font-size:var(--step-2);
  letter-spacing:-.025em; color:var(--accent); font-variant-numeric:tabular-nums }
.onboarding-price{ margin-top:1.5rem; padding:1.25rem 1.4rem;
  border:1px solid var(--surface-alt);
  font-family:var(--display); font-weight:700;
  font-size:var(--step-2); letter-spacing:-.025em; color:var(--ink) }
.onboarding-price strong{ font-weight:700 }

/* ---------- CTA rows ---------- */
.cta-row{ display:flex; flex-wrap:wrap; gap:.9rem 1rem; align-items:center;
  margin-top:2.25rem }
/* A row that closes a section stands well clear of it. One sitting
   inside a step — Getting Started step 1, which IS "Book a Discovery
   Call" — belongs to the line above it and is pulled in to read that
   way. Nothing about the button itself changes. */
.steps-list .cta-row{ margin-top:1.25rem }

/* ---------- inline links in prose ----------
   :not(.btn) is the fix, and it is the whole fix. This rule used to
   read .prose a, which outranks .btn-primary on specificity — so
   pkg-cta on the Packages preview and lt-cta in Let's Talk, both
   inside .prose sections, rendered as pink underlined text instead
   of buttons. A button inside running copy is still a button; only
   genuine inline links take the accent and the underline. */
.prose a:not(.btn){ color:var(--accent); text-decoration:underline; text-underline-offset:.22em;
  text-decoration-thickness:1px }
.prose a:not(.btn):hover,.prose a:not(.btn):focus-visible{ color:var(--ink) }
.arrow-link{ display:inline-block; margin-top:1rem; color:var(--accent);
  text-decoration:none; font-weight:500;
  border-bottom:1px solid color-mix(in srgb, var(--accent) 45%, transparent);
  padding-bottom:.15rem; transition:color .18s ease, border-color .18s ease }
.arrow-link:hover,.arrow-link:focus-visible{ color:var(--ink); border-bottom-color:var(--ink) }

/* ---------- preview blocks (homepage) ---------- */
.preview-pair{ display:grid; gap:1.25rem; margin-top:2.25rem;
  grid-template-columns:repeat(2,minmax(0,1fr)) }
@media (max-width:880px){ .preview-pair{ grid-template-columns:1fr } }
.preview-pair > div{ border:1px solid var(--hairline);
  padding:1.75rem 1.6rem }
.preview-pair h3{ margin:0 0 .5rem }
.preview-pair p{ margin:0 }

/* ---------- "1 Hairline" — the one feature card ----------
   Ruled by Robbie 2026-09-16, from the sample of that name: the
   Visual Concepts card, on the HOMEPAGE only, inside What We
   Create. It is the single feature card on the site — the whole
   treatment, the brighter --glow-feature included.

   Its CHROME EDGE alone travels, and only where Robbie has ruled
   it by name: the hero's navigational secondary (2026-09-16), the
   Presence and Authority cards on /packages (2026-09-17) and the
   contact form card on /contact (2026-09-17). Each was a new
   ruling, which is what this card's record requires.
   That supersedes the "nothing else takes this treatment — not the
   package cards" this comment carried until 2026-09-17; the
   superseded wording is deleted rather than struck through, per
   PROJECT-RULES §8. The Recommended card takes the moving rim
   instead, and /what-we-create is still untouched, including the
   same work shown there in full.

   The class sits on the element rather than the rule being written
   against .preview-pair > div, so it cannot leak: a second preview
   card added later stays an ordinary floating card.

   WHAT DOES NOT CHANGE: the dark purple gradient face, the
   padding, the heading, the line beneath it and the arrow link.
   Every one of them is inherited from the rules above and is not
   named here.

   WHAT CHANGES: two things.

   1 · The 1px border becomes a 1px polished chrome edge, all
   round. The face is painted to the PADDING box and the chrome to
   the BORDER box, so the gradient fills the 1px ring and the
   purple face is untouched beneath it. Outer radius 16px, and
   because CSS insets a padding-box corner by the border width, the
   inner face lands on 15px on its own — the two radii Robbie
   specified, with no second element to keep in step. This is the
   "equivalent border technique" of his ruling; it renders the same
   as the 1px-padding wrapper he described, without adding a
   wrapper to the markup.

   The border width stays 1px, so the card does not move or resize
   by a single pixel.

   2 · The ordinary card glow becomes the approved --glow-feature.

   ★ NO MOTION OF ANY KIND. No animation, no shader, no shimmer, no
   glints. The chrome is a fixed gradient and stays exactly as
   painted. The only thing that moves is the same gentle 2px hover
   lift every other card has, inherited from the shared rule and
   deliberately not overridden.

   ★ The glow is HELD at the approved value on hover too. The
   shared rule swaps in --glow-hover, which is a smaller, dimmer
   glow than this card's own — inheriting it would make the feature
   card dim as you touched it. Robbie approved one glow for this
   card, so it keeps that one and only the lift moves.

   Specificity: (0,2,1) here against (0,1,1) on the shared rules,
   and (0,3,1) on :hover against (0,2,1), so both win outright
   rather than by source order. */
.preview-pair > div.feature-card{
  border:1px solid transparent;
  border-radius:16px;
  background:
    var(--fill-surface) padding-box,
    var(--chrome-edge)  border-box;
  box-shadow:var(--glow-feature);
}
.preview-pair > div.feature-card:hover{
  box-shadow:var(--glow-feature);
}

/* ---------- split section: text beside a visual ----------
   One idea, few words and a lot of dark space around them. Text
   left, the visual well right, the two centred against each other.

   Mobile drops to one column and slots the visual between the
   supporting line and the link, which is what .split-text stops
   generating a box for: its children become items of .split so
   the order can be set across both halves. If display:contents
   is ever unsupported the visual simply falls to the end of the
   column — still readable, still no overflow. */
.split{
  display:grid;
  grid-template-columns:minmax(0,1fr) minmax(0,1fr);
  align-items:center;
  column-gap:clamp(2.5rem,6vw,5.5rem);
}
.split h2{ margin:0; text-wrap:balance }
.split .lede{ max-width:38ch }
.split .split-link{ margin:1.6rem 0 0 }

/* Opened, the text column outgrows the visual. Centring it against a
   column three times its height would drag the picture down the page
   as the detail opens, so the row aligns to the top for as long as it
   is open. Where :has() is unsupported the row simply stays centred —
   still two columns, still nothing overlapping. */
.split:has(.disclose[open]){ align-items:start }

@media (max-width:880px){
  .split{ display:flex; flex-direction:column; align-items:stretch }
  .split-text{ display:contents }
  .split .label{ order:1 }
  .split h2{ order:2 }
  .split .lede{ order:3 }
  .split .visual{ order:4; margin-block:clamp(2rem,7vw,3rem) }
  .split .split-link{ order:5 }
  .split .disclose{ order:5 }
  .split .cta-row{ order:6 }
}

/* ---------- .split, mirrored ----------
   One modifier, worn by every other section down the page. How It
   Works, Packages and Getting Started carry their visual on the
   right; What We Create, Snap Zap Integration and Let's Talk take
   the opposite side, so the six alternate instead of repeating.

   Row and column are both named because the DOM order is unchanged —
   text first, then the well, exactly as its two neighbours — and
   auto-placement would otherwise push the well onto a second row.
   Grid placement means nothing to the flex column above, so the
   mobile stack is the shared one, untouched.

   .preview-pair is the only thing in this column the shared order
   list does not name. Left unnamed it takes order:0 and floats to
   the top of the mobile stack, so it is given the place it reads
   in: last, beneath the link. Both rules are scoped to the
   modifier; no other section sees either of them. */
.split-mirror > .split-text{ grid-column:2; grid-row:1 }
.split-mirror > .visual{ grid-column:1; grid-row:1 }

/* One block per row. It went in because half a column was too narrow
   for the pair's two-up grid — the longer heading broke across four
   lines. From 2026-09-15 the pair holds a single card, and the rule
   is what makes that card fill the column instead of standing in half
   of it beside an empty slot. Still needed; still scoped here. */
.split-mirror .preview-pair{ grid-template-columns:minmax(0,1fr) }

@media (max-width:880px){
  .split-mirror .preview-pair{ order:6 }
}

/* ---------- a split whose text column is running copy ----------
   Let's Talk carries no short description and no "Learn more". The
   hook is followed by three body lines, then the CTA, then the pair
   that closes the homepage — and that pair stays last at every
   width. The shared mobile order list names none of those three
   blocks, and anything it does not name takes order:0 and floats
   above the section label: the same trap .preview-pair fell into.

   Two wrappers, and they do nothing else. On desktop they are
   ordinary blocks inside the text column; on mobile they are the
   flex items that hold places 3 and 7 in the stack, around the
   well at 4 and the CTA row at 6. */
@media (max-width:880px){
  .split-body{ order:3 }
  .split-tail{ order:7 }
}

/* The closing pair is a separate beat from the booking button, not the
   bottom of it. Ruled by Robbie 2026-09-15: the two were reading as one
   group. The break is the footer's own padding rhythm, already in this
   file, so it stays proportionate — 4rem at desktop width, 2.5rem at
   390px, against the 1.5rem it had.

   The heading's own margin is zeroed so the gap is this one value in
   both layout modes: on desktop the wrapper's margin collapses with
   it, on mobile the wrapper is a flex item and it would not. */
.split-tail{ margin-top:clamp(2.5rem,5vw,4rem) }
.split-tail > h3{ margin-top:0 }

/* ---------- the visual well ----------
   A place for a picture, not a panel: no border, no background
   plate, no card, no visible edge anywhere. The accent is spent
   as light — a low radial glow that dissolves into --ground.

   The radial mask is the point of the class. It fades the edges
   of whatever sits inside it, so an <img> dropped in later fades
   into the page instead of showing a square edge. currentColor
   paints the mask because only its alpha is read; it keeps the
   rule free of a hard-coded colour. */
.visual{
  position:relative;
  width:min(100%,26rem);
  aspect-ratio:4/5;
  margin-inline:auto;
  background:radial-gradient(58% 48% at 50% 50%,
    color-mix(in srgb, var(--accent) 20%, transparent), transparent 72%);
  -webkit-mask-image:radial-gradient(64% 64% at 50% 50%,
    currentColor 40%, transparent 100%);
  mask-image:radial-gradient(64% 64% at 50% 50%,
    currentColor 40%, transparent 100%);
}
.visual > img{ display:block; width:100%; height:100%; object-fit:cover }

/* ---------- disclosure: a control that opens its detail in place ----------
   A PATTERN, not a How It Works component — which is why nothing here
   is named after a section. It now carries How It Works, Snap Zap
   Integration and Getting Started. Let's Talk was the fourth candidate
   and is not one: ruled 2026-09-15, the closing section keeps every
   word on show.

   Native <details>/<summary> and not one line of JavaScript: the
   detail is in the HTML whether it is open or shut, so a search
   engine reads it either way, the keyboard operates it for free, and
   a visitor with JavaScript off still gets the whole section.

   The label never changes. State is carried by the chevron alone —
   motion with a job, per DESIGN-SYSTEM §6, and no second word to
   read. Styled as the section's other quiet controls are: accent
   text on a hairline, no button, so it never reads as a second ask. */
.disclose{ margin:1.6rem 0 0 }

.disclose-summary{
  display:inline-flex; align-items:center; gap:.5rem;
  cursor:pointer; list-style:none;
  color:var(--accent); font-weight:500;
  border-bottom:1px solid color-mix(in srgb, var(--accent) 45%, transparent);
  padding-bottom:.15rem;
  transition:color .18s ease, border-color .18s ease;
}
.disclose-summary::-webkit-details-marker{ display:none }   /* Safari */
.disclose-summary:hover,
.disclose-summary:focus-visible{ color:var(--ink); border-bottom-color:var(--ink) }
/* tokens.css gives :focus-visible its outline to a and button. A
   summary is neither, so the visible focus state is named here. */
.disclose-summary:focus-visible{ outline:2px solid var(--accent); outline-offset:3px }

.disclose-chevron{
  width:.85em; height:.85em; flex:none; display:block;
  fill:none; stroke:currentColor; stroke-width:1.8; stroke-linecap:square;
  transition:transform .18s ease;
}
.disclose[open] .disclose-chevron{ transform:rotate(180deg) }

/* The detail opens beneath the control, inside whichever column the
   control sits in — never over the top of anything beside it. */
.disclose-panel{ margin-top:1.75rem }
/* .split caps its own lede at a 38ch hook measure. The panel is a
   column of running copy and reads at the ordinary one. */
.disclose-panel .lede{ max-width:70ch }

/* The chevron is the only thing on the page that moves on a click.
   The blanket reduced-motion block at the foot of this file already
   stops it; it is named again here so the component keeps its own
   guarantee wherever it is lifted to. */
@media (prefers-reduced-motion:reduce){
  .disclose-chevron{ transition:none }
}

/* ---------- portfolio ---------- */
.grid-work{ display:grid; gap:1.25rem; margin-top:1.75rem;
  grid-template-columns:repeat(auto-fill,minmax(min(260px,100%),1fr)) }
.piece{ position:relative; border:1px solid var(--hairline);
  aspect-ratio:4/5; display:grid; place-items:center }
.spec-flag{ position:absolute; top:.75rem; left:.75rem; z-index:2;
  background:var(--ground); color:var(--accent);
  border:1px solid var(--accent); border-radius:var(--radius);
  font-size:var(--step--1); letter-spacing:.18em; text-transform:uppercase;
  padding:.28rem .55rem; line-height:1.4 }

/* ---------- testimonial ---------- */
.testimonial{ margin:2rem 0 0; border-left:1px solid var(--accent);
  padding:2rem clamp(1.4rem,3vw,2.5rem) }
.testimonial p{ color:var(--ink-muted); max-width:68ch }
.testimonial p:first-child{ margin-top:0 }
.testimonial footer{ margin-top:1.6rem; padding-top:1.1rem;
  border-top:1px solid var(--hairline); color:var(--ink); font-size:var(--step-0) }
.testimonial footer strong{ font-family:var(--display); font-weight:700 }

/* ---------- /what-we-create: the plated layout ----------
   Robbie, approved on a rendered sample 2026-09-17, option A.
   Recorded in CONTENT.md §2.

   The goal is one line long: no wall of text on any screen, and
   least of all on a phone. Every block of words is balanced by an
   image space, and the whole of the Dr Rose testimonial sits one
   tap deeper instead of standing on the page.

   From 2026-09-17 the split and the image space are worn by
   /packages too, below its three cards: Robbie approved a rendered
   sample of the same plated treatment there, and a second copy of
   these rules under a pkg- name would be the same layout maintained
   twice. The names keep their wwc- prefix because renaming them
   would rewrite /what-we-create to no visible end. The frame-grid
   and testimonial rules further down are still worn by
   /what-we-create alone. Recorded in CONTENT.md §2 and §3.

   No rule in this block changed when /packages started using it;
   everything that page needs on top of it is page-scoped under
   .pkg-plate, at the end of this file.

   Why it is not the homepage's .split: that layout collapses at
   880px, the width the navigation collapses at. The sample Robbie
   approved holds the two columns at 880 wide, so this one collapses
   at 760px instead — the same idea, a different number, and
   page-scoped. DESIGN-SYSTEM.md §8 still names 880 as the site's
   single real breakpoint; see the warning in CONTENT.md §2.      */
.wwc-split{
  display:grid;
  grid-template-columns:minmax(0,1fr) minmax(0,1fr);
  align-items:center;
  column-gap:clamp(2.5rem,6vw,5.5rem);
}
/* The heading lives inside the text column now, so .section > h2
   no longer reaches it and its margin is set here. */
.wwc-split h1,
.wwc-split h2{ margin:0; text-wrap:balance }

/* A short measure is what makes the column read as a plate of text
   beside a picture rather than a block of it. .todo is left alone —
   the build placeholder keeps its own 64ch. */
.wwc-split-text > p{ max-width:34ch }

/* The intro's first sentence takes the heading colour and leads
   into the second, which is ordinary body copy. Not a word changes;
   only the weight of the ink. */
.wwc-lead{ color:var(--ink) }

/* The intro is still the page head, so it keeps .page-head's top
   padding and takes a section's generous bottom, because an image
   space now stands beside it and the two need the same room. */
.wwc-intro{ padding-bottom:clamp(3.25rem,7vw,6rem) }

/* Mirrored: the image space on the LEFT, the text on the right, so
   the page alternates rather than repeats.

   Row and column are both named because the DOM order is unchanged
   — text first, then the image space — and auto-placement would
   otherwise push the well onto a second row. Grid placement means
   nothing once the section is a block below 761px, so the phone
   stack is text first with no order list to keep in step. */
.wwc-split-mirror > .wwc-split-text{ grid-column:2; grid-row:1 }
.wwc-split-mirror > .visual{ grid-column:1; grid-row:1 }

@media (max-width:760px){
  .wwc-split{ display:block }
  .wwc-split-text > p{ max-width:none }
  .wwc-split > .visual{ margin-top:clamp(2rem,7vw,3rem) }
}

/* ---------- /what-we-create: the frame grids ----------
   Robbie, 2026-09-17, matching the approved sample: equal columns
   filling the content width, and NEVER an empty cell at the end of a
   row. That was the defect — .grid-work's own auto-fill rule fitted
   four columns at 1240px wide and three frames left the fourth
   standing empty. That rule is untouched above and still serves any
   grid that wants to reflow; these two modifiers set the count
   explicitly instead.

   The frames themselves are unchanged: same .piece styling, same 4:5,
   same floating face. Only how many there are and how they are
   arranged.                                                        */

/* A ROW of frames: three equal columns on desktop and tablet, two on
   phones. It carried the six-frame Visual Concepts grid from earlier on
   2026-09-17 under the name .wwc-frames-6; the revised sample replaced
   that one grid of six with three rows of three, so the rule is renamed
   to what it actually does and the column values are unchanged. Nothing
   on `main` is touched by the rename — the class was introduced in this
   same change.

   Three frames in two columns does leave the second row half empty on a
   phone. That is what “two columns on phones” means with three frames,
   and it is Robbie's ruling on the revised sample: at 390px three across
   would be 106px and one across would be a 448px frame per scroll. */
.grid-work.wwc-frames-row{ grid-template-columns:repeat(3,minmax(0,1fr)) }
@media (max-width:760px){
  .grid-work.wwc-frames-row{ grid-template-columns:repeat(2,minmax(0,1fr)) }
}

/* THREE frames, and unlike the six they sit inside HALF a section,
   beside the image space — which is where Robbie put the client-work
   container on 2026-09-17 and where it stays.

   Three across, so the row always fills: two columns would leave a
   hole in the second row and that is the very defect this change
   removes. Sized by the column, so they are 179px wide at 1440 and
   112px at 880 — thumbnails, deliberately smaller than the six spec
   frames that have the whole content width to themselves.

   One column on phones, where the section has stacked and the column
   is the full width: three frames at 359px, three rows, still no hole.
   Three across at that width would be 106px, and a 4:5 frame that
   small is not a portfolio frame.                                   */
.grid-work.wwc-frames-3{ grid-template-columns:repeat(3,minmax(0,1fr)) }
@media (max-width:760px){
  .grid-work.wwc-frames-3{ grid-template-columns:minmax(0,1fr) }
}

/* A column of stacked frames grows far taller than the picture beside
   it, and centring the two would float the image space in the middle
   of a long empty gutter. The row aligns to the top while a frame grid
   is in it — the same thing .split does for an open disclosure on the
   homepage. Where :has() is unsupported the row simply stays centred:
   still two columns, still nothing overlapping. */
.wwc-split:has(.grid-work){ align-items:start }


/* ---------- /what-we-create: the nine-piece portfolio rows ----------
   Robbie, approved on a rendered sample 2026-09-17. Three collapsible
   rows of three frames — the same three subjects shown in each of the
   three formats — replacing the single grid of six.

   Native <details>/<summary> again, exactly as the “Learn more” control
   works: no JavaScript, the keyboard operates it for free, every frame
   is in the HTML whether a row is open or shut, and the browser owns
   aria-expanded so it cannot go stale. “Image posts” carries `open` in
   the markup, so it is open on arrival with nothing to run.

   A hairline above every row and one below the last, so the three read
   as a list rather than three loose blocks.                          */
.wwc-rows{ margin-top:2.25rem }
.wwc-row{ border-top:1px solid var(--hairline) }
.wwc-row:last-of-type{ border-bottom:1px solid var(--hairline) }

.wwc-row-head{
  display:flex; align-items:center; justify-content:space-between;
  gap:1rem; padding-block:1.35rem;
  cursor:pointer; list-style:none;
}
.wwc-row-head::-webkit-details-marker{ display:none }   /* Safari */
/* tokens.css gives :focus-visible its outline to a and button. A summary
   is neither, so the visible focus state is named here — the same line
   .disclose-summary needs and for the same reason. */
.wwc-row-head:focus-visible{ outline:2px solid var(--accent); outline-offset:3px }

.wwc-row-title{
  font-family:var(--display); font-weight:700; color:var(--ink);
  font-size:var(--step-2); letter-spacing:-.025em;
}
/* The bracketed note sits inside the title, so it is on the same line as
   “Reels” and a screen reader reads the two together — which is the
   point: it must never sound like a format that is ready to buy. */
.wwc-row-note{
  font-family:var(--body); font-weight:500; font-size:var(--step-0);
  letter-spacing:normal; color:var(--ink-muted);
}

/* The square toggle. Same 1px --hairline-2 border, same --fill-ghost
   wash and same --glow beneath as the masthead and footer buttons, at
   the shared 10px --radius — it reads as one of the site's controls
   rather than a new kind of thing. It is decorative: the summary is the
   control and the row title is its name, so the square is aria-hidden
   and the plus or minus is the state, never a second label. */
.wwc-row-toggle{
  flex:none; width:34px; height:34px; display:grid; place-items:center;
  border:1px solid var(--hairline-2); border-radius:var(--radius);
  background:var(--fill-ghost); box-shadow:var(--glow);
  color:var(--ink);
  transition:border-color .18s ease, color .18s ease,
             box-shadow .22s var(--ease-lift), transform .22s var(--ease-lift);
}
.wwc-row-head:hover .wwc-row-toggle,
.wwc-row-head:focus-visible .wwc-row-toggle{
  border-color:var(--accent-deep); color:var(--accent);
  box-shadow:var(--glow-hover); transform:translateY(-2px);
}
.wwc-row-toggle svg{
  width:14px; height:14px; display:block;
  fill:none; stroke:currentColor; stroke-width:1.6; stroke-linecap:round;
}
/* A plus closed, a minus open: the upright bar is the only thing that
   changes, so there is one shape and one state, not two icons. */
.wwc-row[open] .wwc-row-bar-v{ display:none }

.wwc-row-panel{ padding-bottom:2rem }
/* .grid-work brings its own 1.75rem top margin, which is the gap between
   the row header and its frames. Nothing else is needed here. */

/* ---------- the format marks on a frame ----------
   Two small devices that say what KIND of piece a frame is, without a
   word of copy on the page: four slide dots for a carousel and a play
   button for a reel. Both are decorative and both are aria-hidden — the
   row title above already names the format.                          */

/* Four dots near the bottom, the first one brighter, as a carousel on a
   phone shows its position. No new colour: --ink and --ink mixed down. */
.wwc-dots{
  position:absolute; bottom:1rem; left:50%; transform:translateX(-50%);
  display:flex; gap:.4rem;
}
.wwc-dots > span{
  width:6px; height:6px; border-radius:50%;
  background:color-mix(in srgb, var(--ink) 28%, transparent);
}
.wwc-dots > span:first-child{ background:var(--ink) }

/* A circular play button, centred by .piece's own place-items. Same
   border and wash as the row toggle, so the page has one control
   language. It is a MARK, not a control — there is nothing to play
   until the real reel drops in, and DESIGN-SYSTEM.md §9 keeps reels out
   of the grid as autoplaying video in any case. */
.wwc-play{
  width:clamp(2.5rem,4.5vw,3.25rem); aspect-ratio:1;
  display:grid; place-items:center;
  border:1px solid var(--hairline-2); border-radius:50%;
  background:var(--fill-ghost); box-shadow:var(--glow);
  color:var(--ink);
}
.wwc-play svg{ width:45%; height:45%; display:block; fill:currentColor;
  margin-left:6% }

/* ---------- /what-we-create: the testimonial, option A ----------
   Her own line, large and centred, with the whole testimonial one
   tap beneath it.

   Type on the ground: no plate, no border, no fill and no glow
   behind the words. DESIGN-SYSTEM.md §1 and §4 — the accent is
   spent as light, under an element, and never behind text. The
   .testimonial block this page used until 2026-09-17 is untouched
   in this file and is archived, not deleted; see CONTENT.md §2. */
.wwc-voice{ text-align:center }
.wwc-quote{ margin:0 }

/* The opening quote mark is punctuation drawn large, so it is drawn
   here and not written into the markup: nothing in the copy layer
   of that page moves to get it, and the mark is explicitly nothing
   to a screen reader. The plain declaration comes first as the
   fallback — a browser without the alternative-text syntax keeps
   it and reads the mark as the quote mark it is. */
.wwc-quote::before{
  content:"\201C";
  content:"\201C" / "";
  display:block;
  font-family:var(--display); font-weight:700;
  font-size:clamp(3.5rem,2rem + 6vw,6rem); line-height:.7;
  color:var(--accent);
  margin-bottom:clamp(.75rem,2vw,1.4rem);
}

/* The pull line. Two sentences lifted out of the testimonial
   verbatim, set in the display face at the size Robbie approved.
   It is a real blockquote, never hidden and never relabelled, so a
   screen reader reads it as the quotation it is. The measure is
   what breaks it over two lines instead of one long one. */
.wwc-pull{ margin:0 }
/* The line breaks at its own full stop from 2026-09-17. Each sentence
   is its own block, rather than a <br>, for one reason: text-wrap
   balances PER BLOCK, so a sentence that will not fit a phone's width
   splits evenly instead of dropping a one-word orphan on the last line.
   The space between the two sentences stays in the markup, so the text
   a screen reader reads and a visitor copies is still the one sentence
   pair from her testimonial.

   The 26ch cap came off with it: the break is explicit now, so a
   measure could only add a THIRD line. */
.wwc-pull p{
  margin:0; margin-inline:auto; max-width:none;
  font-family:var(--display); font-weight:700; color:var(--ink);
  font-size:clamp(1.7rem, 1.2rem + 2vw, 2.8rem); line-height:1.15;
  letter-spacing:-.025em; text-wrap:balance;
}

/* Her attribution, as it read before: the name in the display face
   at 700. It sits ONCE, above the control, so it is on show whether
   or not the panel is open. */
.wwc-pull-line{ display:block; text-wrap:balance }

.wwc-attrib{ margin-top:1.6rem; color:var(--ink); font-size:var(--step-0) }
.wwc-attrib strong{ font-family:var(--display); font-weight:700 }

/* .disclose-summary is inline-flex, so the section's own centring
   is what places the control. Nothing about the control changes. */
.wwc-disclose{ margin-top:1.75rem }

/* The full testimonial: left-aligned inside the centred section, at
   an ordinary reading measure. max-width:none on the paragraphs
   hands the measure to this container, so 720px is the measure
   rather than the 70ch tokens.css gives every p. */
.wwc-full{ max-width:720px; margin-inline:auto; text-align:left }
.wwc-full blockquote{ margin:0 }
.wwc-full p{ max-width:none }
.wwc-full p:first-child{ margin-top:0 }

/* ---------- founder wrap (/about) ----------
   Ruled 2026-09-14: small portrait, beside the text, never above it.
   Its job is trust, not presence. */
.founder{ margin-top:2.5rem }
.founder-wrap{ display:flex; align-items:flex-start; gap:clamp(1.5rem,3vw,2.5rem) }
.founder-photo{ flex:none; width:200px; position:relative }
.founder-photo::after{ content:""; position:absolute; inset:-22%;
  background:radial-gradient(50% 50% at 50% 50%,
    color-mix(in srgb, var(--accent) 22%, transparent), transparent 70%);
  z-index:0; pointer-events:none }
.founder-photo img{ position:relative; z-index:1; display:block; width:100%; height:auto;
  border:1px solid var(--accent); border-radius:12px }
.founder-statement{ margin:0; font-size:var(--step-1); line-height:1.55; color:var(--ink-muted) }
.founder-statement strong{ color:var(--ink); font-family:var(--display); font-weight:700 }
.founder-after{ margin-top:2.25rem }
.founder-after .lead-in{ margin:0 0 .75rem }
.closing{ font-family:var(--display); font-weight:700; font-size:var(--step-2);
  letter-spacing:-.025em; color:var(--ink); margin:0; max-width:32ch; text-wrap:balance }
@media (max-width:880px){
  .founder-wrap{ flex-direction:column; align-items:center; text-align:center }
  .founder-statement{ text-align:left }
}

/* ---------- privacy ---------- */
.legal h2{ margin:2.25rem 0 .6rem; font-size:var(--step-2) }
.legal > :first-child{ margin-top:0 }
.legal p{ margin:0 }

/* ---------- build placeholders ----------
   Deliberately visible. Each one marks something that does not
   exist yet. None of them ships to production as-is. */
.todo{ border:1px dashed var(--accent);
  padding:1.75rem 1.6rem; margin-top:1.75rem }
.todo .label{ color:var(--accent) }
.todo p{ margin:0; max-width:64ch }
.todo p + p{ margin-top:.6rem }

/* ---------- footer ----------
   Ruled by Robbie 2026-09-15: the NAVIGATION column came off whole,
   heading and all six links. The masthead carries those six on every
   page and the footer was repeating them. SOCIAL stays, with its
   heading; its two links are now small secondary buttons side by
   side, so the footer belongs to the same button family as the rest
   of the site; and "More to come" sits beside them as plain text —
   never a link, never a button. The copyright line and the Privacy
   Policy link are untouched, and the footer is still the only route
   to /privacy.

   Every page that has a footer gets this one. /404 has no footer and
   still has none — confirmed by Robbie 2026-09-16. */
.site-foot{ border-top:1px solid var(--hairline);
  padding-block:clamp(2.5rem,5vw,4rem) }
.foot-grid{ display:flex; flex-wrap:wrap; gap:2rem clamp(3rem,9vw,7rem) }
.site-foot h2{ font-family:var(--body); font-weight:600; font-size:var(--step--1);
  letter-spacing:.22em; text-transform:uppercase; color:var(--ink-faint);
  margin:0 0 .9rem }
/* Nothing in the footer is a list any more. The rule is left standing
   rather than deleted — PROJECT-RULES §7 is verify-before-delete, and
   removing it is Robbie's call, not a side effect of this change. It
   is the same treatment .inclusions got above. */
.site-foot ul{ list-style:none; margin:0; padding:0; display:grid; gap:.55rem }
/* :not(.btn), for the same reason .prose a needs it: this rule
   outranks .btn-secondary, and without it the two social buttons
   would take the footer's link colour and size instead of the
   button family's. */
.site-foot a:not(.btn){ color:var(--ink-muted); text-decoration:none; font-size:var(--step-0) }
.site-foot a:not(.btn):hover,
.site-foot a:not(.btn):focus-visible{ color:var(--accent) }

/* Facebook and Instagram side by side, with the note beside them.
   It wraps rather than overflows at 390px. */
.foot-social{ display:flex; flex-wrap:wrap; align-items:center; gap:.7rem .85rem }
.foot-social .btn{ font-size:var(--step--1); padding:.62rem 1.1rem }
/* Plain text. Not a link, not a button, and never styled as either. */
.foot-more{ margin:0; color:var(--ink-faint); font-size:var(--step--1) }

.foot-legal{ margin-top:2.5rem; padding-top:1.25rem; border-top:1px solid var(--hairline);
  display:flex; flex-wrap:wrap; gap:.6rem 1.5rem; align-items:baseline;
  justify-content:space-between }
.foot-legal p{ margin:0; color:var(--ink-faint); font-size:var(--step--1) }

/* ---------- 404 ----------
   nf-link is the only link on the page: no nav, no footer. Confirmed
   by Robbie 2026-09-16 when the footer change was being applied
   everywhere else: the footer belongs on every page that already has
   one, and this page does not have one. Its only change in this
   build is that "Back to the homepage" takes the new button styling,
   which it gets from tokens.css with nothing needed here. */
.nf{ min-height:100svh; display:grid; align-content:center; padding-block:3rem }
.nf .brand-mark{ width:38px; height:38px; margin-bottom:2rem }
.nf h1{ margin:0 0 1rem }
.nf p{ margin:0 0 2rem; font-size:var(--step-1) }

/* ---------- Liquid Metal — the booking button, and one card ----------
   Ruled by Robbie 2026-09-15, option B. Every [data-book] button on
   every page gets a thin moving rim of metallic light round its
   edge. No other BUTTON carries it: the rim marks the single
   conversion ask, and on two buttons it would mark nothing.

   From 2026-09-17 one card carries it too — Signature, the
   Recommended card on /packages, ruled by Robbie and approved on a
   rendered sample 2026-09-16, spun slower than the button's. That
   replaces the "and nothing else" this block carried until then;
   the superseded wording is deleted rather than struck through,
   per PROJECT-RULES §8. Its own rules are up with the package
   cards; what follows is the button's.

   Rebuilt from the "Liquid Metal Button" on 21st.dev by johuniq,
   which is React and @paper-design/shaders. This site is plain HTML,
   so /assets/js/metal.js loads the same shader as an ES module from
   jsDelivr, deferred, and mounts it by hand. No React, no build step.

   THE FALLBACK IS THE DEFAULT. Nothing below applies until the
   script has mounted a shader successfully and set data-metal="on"
   on the button. Until then — and for good, under
   prefers-reduced-motion, without WebGL, with JavaScript off, or if
   the CDN never answers — the button is the ordinary floating
   primary button from tokens.css, with its label, its Calendly link
   and its new tab. The rim is the only thing at stake.

   Three layers inside the <a>, injected by the script:
     .metal-shader  the canvas, at full button size
     .metal-fill    the palette gradient, inset so a 2px rim shows
     .metal-label   the label, above both, at the heading colour     */

.btn[data-metal="on"]{
  background:transparent;
  border-color:transparent;
  box-shadow:var(--glow);
  transition:box-shadow .3s var(--ease-lift), transform .3s var(--ease-lift);
}
.btn[data-metal="on"]:hover,
.btn[data-metal="on"]:focus-visible{
  background:transparent; border-color:transparent;
  box-shadow:var(--glow-hover);
}

/* Full button size. inset:-1px reaches back over the 1px transparent
   border the fallback button keeps, so the metal covers the whole
   box and the button does not change size when the shader mounts. */
.metal-shader{
  position:absolute; inset:-1px; z-index:0;
  border-radius:var(--radius); overflow:hidden;
  pointer-events:none;
}

/* Inset 1px from the padding box — 2px from the outer edge — so
   exactly 2px of metal shows as a rim. The fill is the palette
   gradient from the floating family, never the reference's grey. */
.metal-fill{
  position:absolute; inset:1px; z-index:1;
  border-radius:calc(var(--radius) - 2px);
  background:var(--fill-primary);
  pointer-events:none;
  transition:box-shadow .3s var(--ease-lift);
}
/* Pressed: the button drops 1px and shrinks to .98 through the
   shared .btn:active rule; the fill takes the inner shadow, so the
   surface reads as pushed in rather than merely moved. */
.btn[data-metal="on"]:active .metal-fill{
  box-shadow:inset 0 2px 7px color-mix(in srgb, var(--ground) 85%, transparent);
}

/* Scoped to data-metal="on": if a mount fails after the label has
   been wrapped, the orphan span must change nothing about the plain
   button, whose label is #FFFFFF from tokens.css. */
.btn[data-metal="on"] .metal-label{ position:relative; z-index:3; color:var(--ink) }

/* A soft light spreading from the point of the click, over .6s. */
.metal-ripple{
  position:absolute; inset:1px; z-index:2;
  border-radius:calc(var(--radius) - 2px); overflow:hidden;
  pointer-events:none;
}
.metal-ripple > span{
  position:absolute; left:0; top:0; width:var(--rd); height:var(--rd);
  margin-left:calc(var(--rx) - var(--rd) / 2);
  margin-top:calc(var(--ry) - var(--rd) / 2);
  border-radius:50%;
  background:radial-gradient(circle closest-side,
    color-mix(in srgb, var(--ink) 55%, transparent) 0%,
    color-mix(in srgb, var(--accent) 30%, transparent) 45%,
    transparent 72%);
  animation:metal-ripple .6s var(--ease-lift) forwards;
}
@keyframes metal-ripple{
  from{ opacity:.9; transform:scale(0) }
  to  { opacity:0;  transform:scale(1) }
}

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

  /* metal.js checks this query and never mounts a shader under it, so
     nothing here should ever have anything to hide. It is named again
     as a second lock: if a shader is somehow mounted, the rim, the
     ripple and the hover lift are all off and the button is the plain
     floating primary again. The Recommended card's rim is locked the
     same way — the moving layer goes, the face fills the card, and
     the accent border it falls back to is put back by hand, because
     .card[data-metal="on"] took it off. */
  .metal-shader,.metal-ripple,.metal-rim{ display:none }
  .metal-fill{ inset:0; border-radius:var(--radius) }
  .metal-rim-fill{ display:none }
  .card[data-metal="on"]{ background:var(--fill-surface);
    border-color:var(--accent) }
  .btn:hover{ transform:none }
}


/* ---------- /packages: the plated layout below the cards ----------
   Robbie, approved on a rendered sample 2026-09-17. The three package
   cards and everything above them are untouched; what follows them —
   Pre-post approval, Snap Zap Integration and its one-time price — is
   now two two-column splits, each block of words balanced by an image
   space, the way /what-we-create already reads.

   NOT ONE WORD OF COPY CHANGED. Two sentence pairs are each set as
   two paragraphs, broken at their own full stop, and the one-time
   price is restyled. That is all.

   The layout itself is NOT redeclared here. .wwc-split,
   .wwc-split-mirror, .wwc-split-text and .visual are reused exactly as
   /what-we-create wrote them — including the page-scoped 761px column
   break and the 760px stack — so the two pages cannot drift apart.
   Everything below is only what /packages needs on top of that, and
   every rule is scoped to .pkg-plate or to a pkg- class, so nothing
   here can reach /what-we-create or the homepage.                  */

/* The approved sample sets the text columns a shade wider than the
   34ch /what-we-create holds: about 36ch. A measure, not a width —
   the column is still half the grid.

   A measure is for RUNNING COPY — it exists so a line of prose does
   not run too long to track back to. The one-time price is not prose:
   it is one short line whose three parts sit on one baseline, and a
   measure applied to it breaks "one time" onto a line of its own. So
   the price is released from it on the next line.

   That exception has to out-rank .wwc-split-text > p, which is where
   the 34ch measure this page inherits actually comes from — one class
   and one element, so a bare .pkg-onetime (one class) loses to it and
   the price stays capped. Naming the parent as well settles it. */
.pkg-plate .wwc-split-text > p{ max-width:36ch }
.pkg-plate .wwc-split-text > .pkg-onetime{ max-width:none }

/* The first sentence of each pair takes the heading colour and leads
   into the second, which is ordinary body copy. Not a word changes;
   only the weight of the ink. The same device as .wwc-lead, named
   separately so /packages can never be restyled by a change made for
   /what-we-create's intro. */
.pkg-lead{ color:var(--ink) }

/* ---------- /packages: the Snap Zap Integration heading ----------
   The approved sample sets it at SECTION-HEADING size, not at the size
   the three card names take. Without this it renders at --step-2, the
   site's shared h2 size, which caps at 24px and reads as a card name
   sitting loose on the page rather than as the heading of the last
   section of the page.

   Scoped to the one heading that wears the class. The three card names
   are h2s too and they are NOT touched: they stay at --step-2, exactly
   as the cards ruling set them. */
.pkg-section-heading{
  font-family:var(--display); font-weight:700;
  font-size:clamp(1.6rem, 1.3rem + 1.2vw, 2.2rem);
  line-height:1.1; letter-spacing:-.02em; color:var(--ink);
}

/* Pre-post approval keeps its accent label. Everything else about the
   block is gone: the 1px accent border, the radial wash inside it and
   the floating-card treatment .approval carries in the list at the top
   of this file. The line gets its weight from the room around it and
   the picture beside it now. .approval itself is left standing,
   unused, for the same reason .matrix and .pricing are.

   This is the third label on the site to take the accent rather than
   the heading colour, alongside .approval .label and .todo .label. */
.pkg-accent-label{ color:var(--accent) }

/* ---------- /packages: the Snap Zap Integration one-time price ----------
   Every word of the line is kept, in its order: "Snap Zap Integration
   — $1,999 one time". Only the weight of the ink changes, so it reads
   like the three card prices above it rather than like the bordered
   plate it was.

   The values are .card-price's, deliberately: the display face at 800
   in the heading colour at clamp(2.4rem, 1.9rem + 1.6vw, 3.2rem), the
   small words at --step-0 in the body colour on the figure's baseline.
   They are written out rather than shared with .card-price because
   that rule is scoped .card .card-price and this price is not in a
   card — and because the two are free to diverge.

   ALL THREE PARTS SIT ON ONE LINE, on one baseline, which is what the
   approved sample shows: the small label, the large figure, then
   "one time". An earlier build of this change put the label on a line
   of its own above the figure; that is superseded and is not restored.

   flex-wrap is a GUARD, not a layout, the same one .card-price
   carries: it only comes into play when the column is too narrow to
   hold the line, which is a phone. Nothing ever spills out of the
   column. The em dash is tied to "Integration" by a non-breaking space
   in the markup, so a column that does wrap breaks after the dash and
   a line can never open with one.                                  */
.pkg-onetime{
  display:flex; flex-wrap:wrap; align-items:baseline;
  gap:.4rem; margin:1.6rem 0 0;
}
.pkg-onetime .lead-words{ font-size:var(--step-0); color:var(--ink-muted) }
.pkg-onetime .figure{ font-family:var(--display); font-weight:800;
  font-size:clamp(2.4rem, 1.9rem + 1.6vw, 3.2rem);
  line-height:1; letter-spacing:-.03em; color:var(--ink);
  font-variant-numeric:tabular-nums }
.pkg-onetime .per{ font-size:var(--step-0); color:var(--ink-muted) }

/* The one conversion ask on the page stands centred on its own beneath
   both splits, so it reads as the page's button and not as the
   Integration column's. .cta-row is otherwise unchanged — same gap,
   same 2.25rem above it, same Liquid Metal rim on the button. */
.pkg-cta-centre{ justify-content:center }

/* ---------- /packages: phones ----------
   The splits stack text first — that is .wwc-split's own rule and it
   is not repeated here. Two things are this page's:

   The 36ch measure comes off, because the column is the full width of
   the page and a 36ch line in the middle of it would read as a stray
   indent. /what-we-create does this for its own 34ch; the rule above
   outranks that one, so it has to be undone here as well.

   The image spaces go from 4:5 to 4:3. A portrait well under a stacked
   column of text is a lot of empty page to scroll past on a 390px
   screen; landscape keeps the picture in view without pushing the
   button below it out of reach. Only .pkg-plate's wells change —
   /what-we-create's stay 4:5.                                      */
@media (max-width:760px){
  .pkg-plate .wwc-split-text > p{ max-width:none }
  .pkg-plate > .visual{ aspect-ratio:4/3 }
}

/* ---------- /about: the plated layout ----------
   Robbie, approved on a rendered sample 2026-09-17. Recorded in
   CONTENT.md §5.

   The third page to take the treatment, and the goal has not moved:
   every block of words balanced by room or by a picture, and no wall
   of text on a phone. Beat 1 is a split with an image space beside
   it, beat 2 opens on one large statement and runs as three short
   columns, the founder beat keeps its magazine layout and the closing
   stands centred on its own.

   THE LAYOUT ITSELF IS NOT REDECLARED HERE. .wwc-split,
   .wwc-split-text and .visual are reused exactly as /what-we-create
   wrote them — including the page-scoped 761px column break and the
   760px stack — so the three plated pages cannot drift apart.
   Everything below is only what /about needs on top of that, and
   every rule is scoped to an about- class, so nothing here can reach
   another page.

   FOUR RULES ARE LEFT STANDING AND UNUSED, for the same reason
   .approval, .matrix and .pricing are: .founder, .founder-after,
   .founder-after .lead-in and .closing. Nothing was deleted from this
   file. The magazine layout itself — .founder-wrap, .founder-photo
   and .founder-statement — is untouched and still worn by the page;
   only .founder's 2.5rem top margin is gone, because the founder is
   its own section now and the section's padding does that job. */

/* Beat 1 is still the page head, so it keeps .page-head's top padding
   and takes a section's generous bottom, because an image space now
   stands beside it. The value is /what-we-create's and the rule is
   deliberately NOT shared with .wwc-intro: /about must not be
   re-spaced by a change made for that page's intro. The same reason
   .pkg-lead is not .wwc-lead. */
.about-intro{ padding-bottom:clamp(3.25rem,7vw,6rem) }

/* Beat 1 reads heading colour, body colour, heading colour, so the
   sentence that says why Snap Zap exists closes the column as firmly
   as the opening pair starts it. Not a word changes; only the weight
   of the ink. Named for this page, like .pkg-lead, so the device can
   never be restyled from another page's intro. */
.about-lead{ color:var(--ink) }

/* ---------- /about: the beat 2 statement ----------
   The approved sample sets it well above the site's shared h2, which
   caps at 24px and would read here as a subheading rather than as the
   line the page turns on. The short measure is what keeps it to two
   or three lines at every width instead of one long ribbon at 1440. */
.about-statement{
  font-family:var(--display); font-weight:700;
  font-size:clamp(1.7rem, 1.2rem + 2vw, 2.8rem);
  line-height:1.1; letter-spacing:-.02em; color:var(--ink);
  max-width:22ch; text-wrap:balance;
}

/* ---------- /about: beat 2's three columns ----------
   Three short columns rather than four stacked paragraphs, each
   opened by a thin accent rule so the three read as one set under the
   statement. Body size, not lead size: they are the detail beneath
   the statement, never a second statement.

   Three equal columns, set explicitly and never by auto-fill: a grid
   that fitted four would leave the fourth cell standing empty at
   1440, which is the very defect the frame grids on /what-we-create
   were corrected for on 2026-09-17. */
.about-cols{
  display:grid; grid-template-columns:repeat(3,minmax(0,1fr));
  column-gap:clamp(1.5rem,3vw,2.5rem);
  row-gap:clamp(1.75rem,4vw,2.5rem);
  margin-top:clamp(2.25rem,4vw,3rem);
}
.about-col{
  margin:0; padding-top:clamp(.9rem,1.8vw,1.25rem);
  border-top:1px solid var(--accent);
}

/* ---------- /about: the closing ----------
   Centred, with the lead-in the quiet half of the beat and the three
   sentences the loud one, a line each. Every word is the word that
   was already on the page.

   The three lines are large enough that the 70ch measure tokens.css
   puts on p never binds, but the auto side margins are set anyway so
   a short line stays centred in the page instead of sitting at the
   left edge of a 70ch box. */
.about-close{ text-align:center }
.about-lead-in{ margin:0 auto clamp(1rem,2vw,1.5rem) }
.about-line{
  font-family:var(--display); font-weight:700;
  font-size:clamp(1.5rem, 1.1rem + 1.8vw, 2.5rem);
  line-height:1.15; letter-spacing:-.025em; color:var(--ink);
  margin:0 auto; text-wrap:balance;
}
.about-line + .about-line{ margin-top:.35em }

/* ---------- /about: phones ----------
   The split stacks text first — that is .wwc-split's own rule and it
   is not repeated here. One thing is this page's: the three columns
   become one, because 390px divided by three is not a column of
   running copy. The accent rule stays above each paragraph, so the
   set still reads as a set on the way down.

   The image space keeps 4:5 here, where /packages went to 4:3. This
   page has one well, not two, and nothing sits below it that has to
   stay within reach. */
@media (max-width:760px){
  .about-cols{ grid-template-columns:minmax(0,1fr) }
}


/* ---------- /contact: the plated layout and the form card ----------
   Robbie, approved on a rendered sample 2026-09-17. Recorded in
   CONTENT.md §4.

   The fourth page to take the plated treatment, after
   /what-we-create, /packages and /about, and the goal has not moved:
   every block of words balanced by something, and no wall of text on
   a phone. Here the words are balanced by the form itself — the one
   page on the site where the thing beside the text is the thing the
   visitor came to use, so there is no image space and none is
   wanted.

   NOT ONE WORD OF THE PAGE'S OWN COPY CHANGED. The H1, the H2 and
   the body line are the three that were already on the page, in the
   same order. The one copy change on this page is a FORM FIELD
   LABEL, which is not on the page at all yet — it lives in
   CONTENT.md §4 and INTEGRATIONS.md §2, waiting for the Tally form
   to be built to it.

   THE LAYOUT IS REUSED, NOT RECREATED. .wwc-split and
   .wwc-split-text are worn exactly as /what-we-create wrote them,
   including the page-scoped 761px column break and the 760px stack,
   so the four plated pages cannot drift apart. Everything below is
   only what /contact needs on top of them, and every rule is scoped
   to a contact- class, so nothing here can reach another page.

   The masthead and the footer are untouched.                      */

/* Two columns from 761px, TOP-ALIGNED — .wwc-split centres its two
   halves against each other, which is right when a column of words
   faces a picture of a fixed ratio. It is wrong here: the card grows
   with the form that will fill it, and a centred card would drift
   down the page as it did, leaving the H1 stranded. Named with
   .wwc-split so it out-ranks that rule outright rather than by
   source order.

   The bottom padding is the page head's own, replaced with a much
   larger one: the card's glow reaches about 45px below it and the
   pooled light below that again, and a section that ended where the
   card ends would clip both. Same reason, same override. */
.wwc-split.contact-plate{
  align-items:start;
  padding-bottom:clamp(5.5rem,10vw,9rem);
}

/* The approved sample sets the text column at about 36ch, the
   measure /packages holds rather than /what-we-create's 34ch. A
   measure, not a width — the column is still half the grid. Named
   parent and child, so it out-ranks .wwc-split-text > p, which is
   where the 34ch this page would otherwise inherit comes from. */
.contact-plate .wwc-split-text > p{ max-width:36ch }

/* .wwc-split zeroes the margin on both headings, which is right for
   the pages that carry one. This column carries two, so the H2 needs
   its gap back. The body line below it takes .lede's own 1.1rem and
   is not named here. */
.contact-plate .wwc-split-text > h2{ margin-top:clamp(.6rem,1.4vw,.9rem) }

/* ---------- /contact: the form card ----------
   A floating card holding the form area, built from three things and
   a fourth that sits beneath it:

   1 · THE STILL 1px POLISHED CHROME EDGE the homepage's Visual
   Concepts card wears — same --chrome-edge gradient, same values,
   built the same way. The face paints to the PADDING box and the
   chrome to the BORDER box, so the gradient fills the 1px ring and
   the face is untouched beneath it. Outer radius 16px; CSS insets a
   padding-box corner by the border width, so the inner face lands on
   15px on its own, with no second element to keep in step.

   ★ STATIC. No motion of any kind — no animation, no shader, no
   shimmer, no glints. The chrome is a fixed gradient and stays
   exactly as painted. This is Robbie's ruling of 2026-09-17 and the
   FIFTH thing on the site to wear the edge; the record of all five
   is in tokens.css. It must never read as the moving Liquid Metal
   rim on Book Discovery Call, which is the one thing on this site
   allowed to move like that.

   2 · THE DARK PURPLE FACE, --fill-contact-card, the approved
   gradient transcribed in tokens.css.

   3 · THE PINK GLOW BENEATH, --glow-contact-card, two layers, also
   in tokens.css.

   4 · THE POOLED LIGHT, below. That one is painted here, because it
   is an element rather than a shadow.

   NO HOVER LIFT. Every other card on the site rises 2px when you
   touch it, because every other card is a thing you might click.
   This one is a container for a form: the visitor is going to be
   inside it, typing, not pointing at it. A card that jumped as the
   cursor crossed it would be a distraction at exactly the wrong
   moment. It is not in the floating family's selector list at the
   top of this file, so there is nothing to override — the stillness
   is the default and this comment is why it was left that way. */
.contact-card{
  position:relative;
  border:1px solid transparent;
  border-radius:var(--radius-card);
  background:
    var(--fill-contact-card) padding-box,
    var(--chrome-edge)       border-box;
  box-shadow:var(--glow-contact-card);
  padding:clamp(1.6rem,3vw,2.4rem);
}

/* The soft pink light pooling just below the card's bottom edge. A
   painted element rather than a third shadow layer, because a
   box-shadow cannot be blurred independently of its own spread and
   this reads as light on the page rather than as the card's own
   shadow.

   It starts exactly at the card's bottom border edge — top:100% —
   so it never washes over the face; the blur bleeds a little way up
   behind the card, which is where the negative z-index puts it. The
   card sets no z-index of its own, so it creates no stacking context
   and the pool stays above the page ground rather than disappearing
   behind it.

   INSET INSIDE THE CARD'S OWN WIDTH, 10% each side, so it can never
   widen the page at any viewport. A filter's blur is ink overflow
   and adds nothing to the scrollable area, so the element's own box
   is the whole of its footprint — and that box is narrower than the
   card. The section's extra bottom padding above covers it
   vertically.

   The accent, through color-mix(), as everything on this site is. */
.contact-card::after{
  content:"";
  position:absolute; z-index:-1;
  left:14%; right:14%; top:100%;
  height:clamp(3.5rem,7vw,5.5rem);
  background:radial-gradient(50% 110% at 50% 0%,
    color-mix(in srgb, var(--accent) 34%, transparent) 0%,
    color-mix(in srgb, var(--accent) 16%, transparent) 38%,
    transparent 74%);
  filter:blur(34px);
  pointer-events:none;
}

/* ---------- /contact: the build placeholder, inside the card ----------
   The Tally embed target moves inside the card and STAYS VISIBLE
   until the form exists. Not a word of it changes and it is not
   replaced by a substitute form: INTEGRATIONS.md §2 records that the
   Tally account exists and the form does not, and PROJECT-RULES.md
   §1 is that a visible gap is safe and a quietly closed one is not.

   What changes is only what it takes to sit INSIDE a card rather
   than on the page. .todo is in the floating family's selector list
   at the top of this file, so on its own it brings a gradient face,
   a glow and a 2px hover lift — a second floating card inside the
   first, glowing against a face it should be sitting quietly on.
   Three things come off: the face, the glow and the lift. Its own
   top margin goes too, because it is the first thing in the card and
   the card's padding already does that job.

   ★ WHAT STAYS IS WHAT MAKES IT A PLACEHOLDER: the 1px DASHED accent
   border, the accent label and both paragraphs, all exactly as they
   are. It must still read as unfinished, because it is.

   Scoped to this card, so the placeholders on any other page are
   untouched. */
.contact-card .todo{
  margin-top:0;
  background:none;
  box-shadow:none;
}
.contact-card .todo:hover{
  transform:none;
  box-shadow:none;
}

/* ---------- /contact: phones ----------
   The split stacks TEXT FIRST with the card beneath it — that is
   .wwc-split's own rule, the DOM order is already text first, and
   neither is repeated here. One thing is this page's: the 36ch
   measure comes off, because the column is the full width of the
   page and a 36ch line in the middle of it would read as a stray
   indent. /packages and /what-we-create each do this for their own
   measure; the rule above out-ranks both, so it has to be undone
   here as well.

   The card gets its own gap from the text above it. Everything else
   — the chrome edge, the face, the glow and the pooled light — is
   unchanged at every width. */
@media (max-width:760px){
  .contact-plate .wwc-split-text > p{ max-width:none }
  .contact-plate > .contact-card{ margin-top:clamp(2rem,7vw,3rem) }
}


/* ---------- /contact: the form ----------
   Robbie, 2026-09-17. Tally is dropped and the form is ours. It sits
   INSIDE the floating card, which is unchanged by it: same chrome
   edge, same face, same glow, same pooled light, same padding. Only
   its contents changed.

   Every rule here is scoped to a cf- class or to .contact-card, so
   nothing can reach another page. NO SHARED RULE IS REDECLARED, and
   there is still no hex value in this file: every colour below is a
   token, because every colour Robbie specified turned out to be one
   already —

     border   rgba(246,244,248,.18)  is --hairline-2, exactly
     text     #F6F4F8               is --ink, exactly
     focus    #E052E0               is --accent, exactly
     labels   the body colour        is --ink-muted
     face     rgba(23,3,18,.45)      is --ground at 45%, through
                                     color-mix(), so it follows the
                                     token rather than freezing it

   The fields sit ON the card face rather than floating above it:
   they take no glow and no lift. Everything on this site that
   floats is a thing you press. A text field is a thing you type
   into, and one that rose to meet the cursor would be noise at
   exactly the wrong moment — the same reason the card itself takes
   no hover lift.                                                   */

.cf-form{ margin:0 }

/* A field is a label above its control, both full width. The label
   is the only text: there is NO placeholder text inside any field,
   so nothing disappears as soon as the visitor types. */
.cf-field{ margin:0 0 clamp(.9rem,2vw,1.2rem); max-width:none }
.cf-field > label{
  display:block; margin-bottom:.4rem;
  font-size:var(--step-0); color:var(--ink-muted);
}

/* One rule for both controls. width:100% with border-box is what
   keeps a field inside the card at every width — without the box
   sizing, the padding and the border would push it past the card's
   padding box and the page would scroll sideways on a phone.

   font:inherit because an input does not inherit the page's face or
   size on its own, and a form set in the browser's default font
   inside this card would look like a foreign object. */
.cf-field input,
.cf-field textarea{
  display:block; width:100%; box-sizing:border-box;
  font:inherit; color:var(--ink);
  background:color-mix(in srgb, var(--ground) 45%, transparent);
  border:1px solid var(--hairline-2);
  border-radius:var(--radius);
  padding:.75rem .9rem;
  transition:border-color .18s ease;
}

/* The message box is about five rows and stays vertical-only: a
   field that can be dragged wider than its column could push the
   card out of the page. */
.cf-field textarea{ resize:vertical; min-height:7.5rem; line-height:1.5 }

/* Hover and focus both answer on the border, so the field reacts
   without moving. The keyboard ring is the accent outline the rest
   of the site uses — tokens.css gives it to links and buttons only,
   and these are neither, so it is written out here to the same
   values rather than left to the browser's default. */
.cf-field input:hover,
.cf-field textarea:hover{ border-color:color-mix(in srgb, var(--accent) 45%, var(--hairline-2)) }
.cf-field input:focus-visible,
.cf-field textarea:focus-visible{
  outline:2px solid var(--accent); outline-offset:2px;
  border-color:var(--accent);
}
/* Firefox still gives :focus without :focus-visible on a text field
   in some versions; this keeps the ring identical there. */
.cf-field input:focus,
.cf-field textarea:focus{ border-color:var(--accent) }

/* Chromium paints a stored value with its own near-white fill,
   which would put a light slab inside a dark card.

   The usual fix is an inset box-shadow in the field's colour, but
   that means naming a colour here, and a composite of two tokens
   over a gradient is not a value this file is allowed to hold —
   PROJECT-RULES.md §6, no hex outside tokens.css.

   So the field simply keeps the background it already has: the
   autofill fill is applied as a transition, and a transition long
   enough never arrives. Nothing is painted over the face, no
   colour is named, and the text colour is the token the field
   uses anyway. */
.cf-field input:-webkit-autofill,
.cf-field input:-webkit-autofill:hover,
.cf-field input:-webkit-autofill:focus{
  -webkit-text-fill-color:var(--ink);
  transition:background-color 100000s ease 0s;
  caret-color:var(--ink);
}

/* ---------- /contact: the honeypot ----------
   Invisible to a sighted visitor, hidden from assistive technology
   by aria-hidden in the markup, and out of the tab order by
   tabindex. NOT display:none and NOT hidden — some bots skip a
   field they can see is unrendered, and the point is that they
   fill it.

   It is moved off-screen rather than sized to nothing so that a
   browser still treats it as a real, fillable input. */
.cf-hp{
  position:absolute; left:-9999px; top:auto;
  width:1px; height:1px; overflow:hidden;
  margin:0;
}

/* ---------- /contact: the submit ----------
   The STILL 1px chrome hairline the hero's "See What We Create"
   button wears — same --chrome-edge gradient, same 1px, same
   stillness. Robbie, 2026-09-17.

   ★ IT IS NOT THE LIQUID METAL RIM, which stays on Book Discovery
   Call and nothing else. That rim marks the single conversion ask;
   on two things it would mark nothing. DESIGN-SYSTEM.md §5.

   ★ NOT the feature card's border-box mechanism, and it cannot be,
   for the reason hero.css sets out for the same button: this face
   is --fill-ghost, a translucent wash, and chrome painted to the
   border box reads straight through it and turns the whole button
   silver. So the gradient is painted over the button and masked
   down to its own 1px rim, leaving the ghost face untouched.

   Behind @supports, so a browser without mask compositing keeps the
   ordinary .btn-secondary hairline rather than a silver slab. The
   border comes off and its 1px goes into the padding, so the button
   is exactly the size it was.

   The mechanism is hero.css's, written out rather than shared: that
   file styles one button by id, and this is a different page, a
   different element and a different file. A change made for the
   hero must not reach this button. */
.cf-submit{ cursor:pointer; margin-top:clamp(.3rem,1vw,.6rem) }

@supports ((-webkit-mask-composite: xor) or (mask-composite: exclude)){
  .cf-submit{
    border:0;
    padding:calc(.95rem + 1px) calc(1.6rem + 1px);
  }
  .cf-submit::before{
    content:''; position:absolute; inset:0; border-radius:inherit;
    padding:1px; background:var(--chrome-edge); pointer-events:none;
    -webkit-mask:linear-gradient(#fff 0 0) content-box,
                 linear-gradient(#fff 0 0);
    -webkit-mask-composite:xor;
            mask:linear-gradient(#fff 0 0) content-box,
                 linear-gradient(#fff 0 0);
            mask-composite:exclude;
  }
}

/* While a submission is in flight the button is disabled. It dims
   and stops answering hover, so the page says "not now" without a
   word of copy — the label never changes to "Sending...", because
   that is a customer-facing sentence and this page writes none that
   are not approved.

   The lift is cancelled explicitly: .btn:hover raises every button
   2px and a disabled control must not answer the cursor at all. */
.cf-submit:disabled{
  opacity:.55; cursor:default;
}
.cf-submit:disabled:hover{ transform:none; box-shadow:var(--glow) }

/* ---------- /contact: the two states ----------
   ★ APPROVED COPY — Robbie, 2026-09-17. Both lines are his exact
   words and both superseded the Tally specification's versions,
   which are deleted rather than struck through, per
   PROJECT-RULES.md §8. Recorded in CONTENT.md §4.

   These were build placeholders in the .todo style for part of
   2026-09-17, while the wording was outstanding. The copy landed the
   same day, so THE PLACEHOLDER STYLING IS GONE: neither block wears
   .todo, neither carries a dashed accent border or a label, and
   nothing on this page now reads as unfinished.

   Both are set in THE CARD'S BODY TYPE — --step-0, the page's body
   size, not the lede — and they differ only in the weight of the
   ink: the confirmation in the HEADING colour, because when the form
   has gone it is the only thing left on the card; the failure line
   in the BODY colour, because the loud thing on that screen should
   be the form the visitor is about to use again.

   NO ICON on either, and no exclamation mark in either line.

   The measure is released. tokens.css caps every p at 70ch, which
   never binds inside a card this width, but the auto margin would
   otherwise leave a short line sitting in a 70ch box rather than
   filling the card. */
.cf-state{ margin:0; max-width:none; font-size:var(--step-0) }
.cf-sent{ color:var(--ink) }
.cf-failed{ color:var(--ink-muted) }

/* The failure line sits above the form it is about to send the
   visitor back to, so it needs the gap. The confirmation replaces
   the form and needs none — the card's own padding is the room
   around it. */
.cf-failed:not([hidden]){ margin-bottom:clamp(1.2rem,3vw,1.6rem) }

/* A state is focused when it appears, so the keyboard lands on the
   outcome rather than staying where the button used to be. */
.cf-state:focus-visible{ outline:2px solid var(--accent); outline-offset:3px }
