/* ==========================================================================
   LUMORIA — site v3 stylesheet
   Palette V3 (warm luxury) · Coolvetica display · Helvetica Neue body
   Motion: fluid, blurry, motion-graphic entrances (house rule)
   ========================================================================== */

@font-face{font-family:"Coolvetica";src:url("/assets/fonts/Coolvetica-Rg.otf") format("opentype");font-weight:400;font-style:normal;font-display:swap}

:root{
  --paper:#FBF8F1; --surface:#FFFEFA; --sand:#F1E8D6; --sand-deep:#E7D9BF;
  --ink:#221A11; --ink-soft:#5C4F3D; --espresso:#4A2A15; --onyx:#15120D;
  --brass:#9A6B1C; --brass-bright:#CDA055; --brass-ink:#87590F;
  --line:rgba(34,26,17,.12); --line-2:rgba(34,26,17,.07); --line-dk:rgba(247,240,225,.14);
  --ok:#2F6B44; --ok-bg:rgba(47,107,68,.09);
  --err:#8C2F1E; --err-bg:rgba(140,47,30,.07);
  /* chrome — tokenised so the Black theme can restyle nav/menu without a fork */
  --nav-bg:rgba(251,248,241,.72);
  --nav-line:var(--line);
  --menu-bg:rgba(251,248,241,.94);
  --brand-ink:var(--espresso);
  --focus:var(--brass);
  --display:"Coolvetica","Familjen Grotesk",-apple-system,sans-serif;
  --body:-apple-system,"Helvetica Neue","Helvetica","Segoe UI",system-ui,sans-serif;

  /* ---- motion, one token set (ADR-048 · docs/10-interface-standard.md §6) ----
     Four durations, one job each; two curves. Before this the site carried nine
     entrance durations and had its easing hardcoded three times inside .reveal
     — so the site's own reveal did not use the site's own --ease. The character
     stays exactly as it was (fluid, blurry, motion-graphic); the WEIGHT halves.
     860ms is the ceiling for anything to be fully readable, everywhere. */
  --dur-1:120ms;   /* state   — hover, press, colour */
  --dur-2:200ms;   /* control — focus, toggle, tab */
  --dur-3:320ms;   /* surface — drawer, sheet, menu, rail */
  --dur-4:560ms;   /* entrance — the reveal */
  --ease:cubic-bezier(.33,.78,.27,1);      /* general */
  --ease-out:cubic-bezier(.16,1,.3,1);     /* entrance, and anything that settles */
  --spring:cubic-bezier(.34,1.4,.5,1);
  /* The entrance as tokens rather than 13 blur radii and 15 travel distances. */
  --rv-blur:10px;
  --rv-y:20px;
  --rv-scale:.985;
  --rv-stagger:50ms;   /* index capped at 6 => 300ms max chain */

  /* ---- controls: PUBLIC density (§1) ----
     52px tall, 14px radius, and 16px type. The 16px is not taste: iOS Safari
     zooms the viewport on focus of any control under 16px, which physically
     moves the page under the user's thumb mid-typing. This stylesheet shipped
     15.5px, so it had that bug live. 44px is the absolute floor for any tap
     target; these controls sit well above it. */
  --ctl-h:52px;
  --ctl-r:14px;
  --ctl-pad:14px 16px;
  --ctl-size:16px;
  --lbl-size:13px;

  --maxw:1200px;

  /* ---- spacing system ----
     Every gap on the site comes from here. Before this, spacing was ~40 magic
     numbers sprinkled across the stylesheet and inline `style=` attributes in
     the generator, so no two sections breathed the same way. */
  --s-1:6px; --s-2:12px; --s-3:18px; --s-4:26px;
  --s-5:34px; --s-6:44px; --s-7:56px; --s-8:72px;
  --gutter:34px;
  /* The chrome is 94px tall: 18px page padding + 58px pill + 18px. Anything
     that sticks, or that a #hash scroll lands on, has to clear it — three
     places used to hard-code 82/74/104px and two of them tucked under the nav. */
  --nav-h:94px;
  --band-y:clamp(76px,8vw,124px);
  --band-y-tight:clamp(52px,5.5vw,82px);
  --head-top:clamp(126px,13vw,168px);
  /* Default gap inside a .rgroup stack. */
  --stack:22px;
  color-scheme:light;
}

*{margin:0;padding:0;box-sizing:border-box}
/* Author display rules beat the UA [hidden] rule — restore it explicitly. */
[hidden]{display:none!important}
html{scroll-behavior:smooth;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;scroll-padding-top:calc(var(--nav-h) + 14px)}
/* Nothing may make the page wider than the phone. `clip` has to be on <html>
   as well as <body>: with <html> visible, body's overflow propagates up to the
   viewport and the body box itself computes back to `visible`, so a few px of
   overhang still counts toward the layout width — and a phone will happily
   pinch out to that width, sliding the fixed backdrops off the content and
   showing the page at a size it was never composed at. `clip` rather than
   `hidden` so no scroll container is created and the sticky nav still sticks. */
html,body{overflow-x:clip}
body{font-family:var(--body);background:var(--paper);color:var(--ink);line-height:1.5;letter-spacing:-.01em;
  -webkit-tap-highlight-color:rgba(154,107,28,.18)}
a{color:inherit;text-decoration:none}
img,svg{display:block;max-width:100%}
button,input,select,textarea{font:inherit;color:inherit}
button,a,label,summary,input[type=checkbox]{touch-action:manipulation}
::selection{background:var(--brass);color:var(--paper)}
:focus-visible{outline:2px solid var(--focus);outline-offset:3px;border-radius:4px}
[id]{scroll-margin-top:calc(var(--nav-h) + 14px)}

/* visually hidden, still announced */
.vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}
.hp{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}
.skip{position:fixed;top:10px;left:50%;z-index:60;transform:translate(-50%,-160%);background:var(--ink);color:var(--paper);
  padding:12px 22px;border-radius:999px;font-size:14px;font-weight:600;transition:transform var(--dur-2) var(--spring)}
.skip:focus-visible{transform:translate(-50%,0)}

body::before{content:"";position:fixed;inset:0;z-index:1;pointer-events:none;opacity:.03;mix-blend-mode:multiply;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E")}

.wrap{max-width:var(--maxw);margin:0 auto;padding:0 var(--gutter);position:relative;z-index:2}
.wrap-n{max-width:940px}

/* ---------- vertical rhythm ----------
   .rgroup is the stack. It used to be a JS-only hook for staggering the
   entrance, so on every plain block container — both columns of every
   .two-col, the Black tease, the service hero — an h2 and the paragraph under
   it both had margin:0 and rendered *touching*.

   Giving it a single gap fixed the collision but created a flatter problem: a
   96px headline, a paragraph, a list and a call-to-action were all separated by
   the same 22px, so nothing grouped and the eye couldn't tell what belonged to
   what. Space has to express relationship. Three tiers, applied by what the two
   neighbours actually are:

     bound   the thing and the thing it belongs to (a button and its
             risk-reversal note) — closer than the flow, so they read as one
     flow    the default: prose to prose, label to field           (--stack)
     break   a change of mode — prose into a list, a panel, a CTA cluster

   Space after a heading is proportional to the heading (em, not px): a 96px
   display line needs far more air beneath it than a 40px sub-head, and one
   fixed value can't serve both. The em also absorbs the display face's .96
   line-height, which lets descenders fall below the box. */
/* The column is stated, not inferred. A grid with no grid-template-columns
   leans on one implicit `auto` track plus the "stretch auto tracks" rule to
   reach full width — correct per spec, and the single most fragile thing this
   stylesheet relied on, because an engine that skips the stretch sizes that
   track to MAX-CONTENT instead and every paragraph in the stack becomes one
   long line running off the right edge. `minmax(0,1fr)` is a definite track:
   it fills, and the 0 floor stops a long unbroken string blowing it out. */
.rgroup{display:grid;grid-template-columns:minmax(0,1fr);align-content:start;gap:var(--stack)}
.rgroup-tight{--stack:var(--s-2)}
.rgroup-loose{--stack:var(--s-4)}

/* after a heading — scales with its own size */
.rgroup > :is(h1,h2,h3){margin-bottom:max(4px,.16em)}
/* into a list, a data block or a UI panel: a real break */
.rgroup > * + :is(.checklist,.svc-meta,.priceline,.addons,.finder,.region-note){margin-top:8px}
/* ...except straight after a heading, where the block IS the heading's content
   and the heading's own proportional space is already the break */
.rgroup > :is(h1,h2,h3) + :is(.checklist,.svc-meta,.priceline,.addons,.finder,.region-note){margin-top:0}
/* breadcrumbs and a flag pill are both page meta — they belong together above
   the title, not spaced like separate blocks */
.rgroup > .crumbs + *{margin-top:calc(-1 * var(--stack) + 14px)}
/* the CTA cluster is the biggest break in any stack — it must stand alone */
.rgroup > * + .hero-cta{margin-top:14px}
/* ...and the microcopy under a button is bound to that button, not floating
   between it and whatever comes next.

   18px, not the 12px the bound tier calls for, and the extra 6 is not a fudge:
   the primary button carries a large soft drop shadow that renders BELOW its
   border box. Margin measures from the border box, so a true 12px gap puts the
   text 12px from the button's edge and perhaps 4px from the bottom of its
   visible shadow — the eye reads the shadow as part of the button, so the pair
   looks cramped even though the number is right. Spacing is optical: measure to
   what the visitor can see, not to the box model. */
.rgroup > .hero-cta + :is(.cta-note,.region-inline),
.rgroup > .btn + .cta-note{margin-top:calc(-1 * var(--stack) + 18px)}

/* ---- a block, and the note that explains it ------------------------------
   A list, a table or a CTA cluster is a change of MODE, so the prose that
   follows one sits at the break tier — never at whatever margin it inherited.
   Left alone these collapse to nothing and the note butts straight onto the
   last row, which is what makes /partners/ read as unfinished in two separate
   places: the fineprint under the rules and the note under the earnings table
   both looked like a sixth row of the thing above them rather than a comment
   on it. Bound to the block above, broken from the section below. */
:is(.worth,.deflist,.checklist,.steps,.faq) + .fineprint,
:is(.worth,.deflist,.checklist,.steps,.faq) + .cta-note{margin-top:clamp(26px,2.6vw,34px)}
.fineprint{font-size:13.5px;line-height:1.6;color:var(--ink-soft);max-width:74ch;text-wrap:pretty}
.fineprint a{color:var(--brass-ink);font-weight:600}

/* Pills and inline affordances would otherwise be stretched by the grid. */
.rgroup > .svc-flag,.rgroup > .btn,.rgroup > .under-link,.rgroup > .lockup{justify-self:start}

/* ---------- type ---------- */
h1,h2,h3,h4{font-family:var(--display);font-weight:400;line-height:.96;letter-spacing:-.02em;text-wrap:balance}
.h-display{font-size:clamp(42px,7.4vw,96px)}
.h-section{font-size:clamp(34px,4.8vw,64px)}
.h-sub{font-size:clamp(26px,3.2vw,40px)}

/* ---- .h-duo — a headline that is two statements, not one -----------------
   "Send someone our way. Take 20% of what they spend." is a proposition and
   then its terms. Set at ONE size inside a 16ch measure it broke into four
   ragged lines and orphaned "our way." on its own — four lines of identical
   weight, so the eye got no help deciding which half was the offer.

   Two tiers instead: the proposition large enough to hold one line at desktop,
   the terms about 58% of it and a step softer in colour. The size drop is what
   makes the first line land; the colour drop is what stops the second line
   competing. Same tonal ladder as the home hero (ink → espresso → brass).

   `max-width:none` overrides the 16ch hero measure deliberately — 16ch is the
   right measure for one running headline and the wrong one for a lead line
   that is supposed to stay unbroken. `balance` still governs the wrap on a
   phone, where one line is not possible and an even 2+2 split is the goal. */
.h-duo{display:grid;gap:.1em;max-width:none}
.svc-hero h1.h-duo{max-width:none}
.h-duo .hl-a{display:block;font-size:clamp(34px,6.2vw,80px);line-height:.94;
  color:var(--ink);text-wrap:balance}
.h-duo .hl-b{display:block;font-size:clamp(24px,3.6vw,47px);line-height:1;
  color:var(--espresso);text-wrap:balance}
/* A lead is display-adjacent, not body prose. At 1.55 — the body ratio — a 21px
   lead set two or three lines deep opens up to 32.5px of leading and stops
   reading as one block under the headline: the lines float apart while the
   headline above them sits at .96, so the pair looks like two unrelated
   paragraphs rather than a statement and its qualifier. Larger type needs
   proportionally LESS leading, not the same. 1.45 keeps the block cohesive and
   still clears the descenders at 17px on a phone. */
.lead{font-size:clamp(17px,1.7vw,21px);color:var(--ink-soft);line-height:1.45;max-width:48ch;font-weight:400;text-wrap:pretty}
.brass{color:var(--brass)}
/* The accent has two values, one per ground: --brass reads as mud on onyx.
   Every other on-dark component already switches; the emphasis span did not,
   because until the standard band there was no brass word on a dark heading. */
.on-dark .brass{color:var(--brass-bright)}
em.plain{font-style:normal;color:var(--espresso)}

/* NOTE: there is deliberately no section-eyebrow class here.
   The dash-plus-tracked-caps kicker above every heading ("— HOW IT WORKS")
   is gone from the whole site and is not to be reintroduced. If a section
   needs framing, the heading does it. Same rule as the numbered-incrementer
   ban: labels that only exist to decorate a heading read as filler. */

/* A quiet label for the one place a value genuinely needs naming: the price
   in a buy box. Sentence case, no tracking, no rule — a caption, not a kicker. */
.lab{font-size:12.5px;font-weight:600;color:var(--ink-soft);letter-spacing:.01em}

/* ---------- buttons ---------- */
.btn{display:inline-flex;align-items:center;gap:13px;font-family:var(--body);font-weight:600;font-size:15px;letter-spacing:-.01em;
  padding:13px 14px 13px 24px;border-radius:999px;cursor:pointer;border:none;text-align:left;
  min-height:44px;
  transition:transform var(--dur-3) var(--spring),background var(--dur-2) var(--ease),color var(--dur-2) var(--ease),box-shadow var(--dur-2) var(--ease)}
/* Promoted only while it is actually being touched. It used to carry
   `will-change:transform` unconditionally, so every button on every page held a
   composited layer for the whole session. */
.btn:hover,.btn:active{will-change:transform}
.btn .ic-c{width:30px;height:30px;border-radius:999px;display:grid;place-items:center;flex:none;
  transition:transform var(--dur-3) var(--spring),background var(--dur-2) var(--ease)}
.btn .ic-c svg{width:14px;height:14px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.btn:active{transform:scale(.97)}
.btn-primary{background:var(--ink);color:var(--paper);box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 16px 36px -18px rgba(34,26,17,.55)}
.btn-primary .ic-c{background:rgba(255,255,255,.13)}
.btn-primary:hover{box-shadow:0 1px 0 rgba(255,255,255,.06) inset,0 22px 44px -18px rgba(34,26,17,.6)}
.btn-primary:hover .ic-c{transform:translate(3px,-2px);background:var(--brass-bright)}
.btn-primary:hover .ic-c svg{stroke:var(--onyx)}
.btn-brass{background:var(--brass);color:#fff;box-shadow:0 16px 36px -18px rgba(154,107,28,.9)}
.btn-brass .ic-c{background:rgba(255,255,255,.2)}
.btn-brass:hover .ic-c{transform:translate(3px,-2px);background:#fff}
.btn-brass:hover .ic-c svg{stroke:var(--brass)}
.btn-ghost{background:transparent;color:var(--ink);padding-left:20px;box-shadow:0 0 0 1px var(--line) inset}
.btn-ghost .ic-c{background:rgba(74,42,21,.07)}
.btn-ghost:hover{box-shadow:0 0 0 1px var(--brass) inset}
.btn-ghost:hover .ic-c{transform:translate(3px,-2px)}
.on-dark.btn-ghost{color:var(--paper);box-shadow:0 0 0 1px rgba(205,160,85,.45) inset}
.on-dark.btn-ghost .ic-c{background:rgba(255,255,255,.08)}
.on-dark.btn-ghost:hover{box-shadow:0 0 0 1px var(--brass-bright) inset}
.btn-sm{font-size:13.5px;padding:9px 10px 9px 18px}
.btn-sm .ic-c{width:25px;height:25px}
.btn[disabled]{opacity:.4;pointer-events:none}
.under-link{display:inline-flex;align-items:center;gap:7px;font-size:14px;font-weight:500;color:var(--ink-soft);transition:color var(--dur-2) var(--ease)}
.under-link svg{width:13px;height:13px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;transition:transform var(--dur-3) var(--spring)}
.under-link:hover{color:var(--brass-ink)}.under-link:hover svg{transform:translateX(3px)}
.on-dark .under-link{color:rgba(251,248,241,.72)}.on-dark .under-link:hover{color:var(--brass-bright)}

/* ---------- nav ---------- */
.nav{position:fixed;top:0;left:0;right:0;z-index:40;display:flex;justify-content:center;padding:18px 16px;pointer-events:none}
.nav-inner{pointer-events:auto;display:flex;align-items:center;gap:26px;padding:9px 9px 9px 22px;border-radius:999px;width:min(1020px,100%);
  background:var(--nav-bg);backdrop-filter:blur(20px) saturate(150%);-webkit-backdrop-filter:blur(20px) saturate(150%);
  box-shadow:0 0 0 1px var(--nav-line) inset,0 14px 40px -24px rgba(34,26,17,.45);transition:box-shadow var(--dur-3) var(--ease)}

/* The logo is always the vector — never live text — so it is identical on every
   machine and cannot reflow while the display font loads. */
.lockup{display:block;color:var(--brand-ink);line-height:0}
.lockup .lk{height:1em;width:calc(1em * 5.9603);display:block}
.brand{display:flex;align-items:center;margin-right:auto;font-size:19px;color:var(--brand-ink);
  transition:opacity var(--dur-2) var(--ease)}
.brand:hover{opacity:.72}
.lockup-lg{font-size:26px}
.lockup-inline{display:inline-block;vertical-align:baseline;font-size:.86em}

.nav-links{display:flex;align-items:center;gap:26px;font-size:14.5px;font-weight:500}
.nav-links a{position:relative;opacity:.74;transition:opacity var(--dur-3) var(--ease);white-space:nowrap}
/* scaleX, not `right`. Animating `right` relayouts the pseudo-element on every
   frame of every nav hover; a transform runs on the compositor. */
.nav-links a::after{content:"";position:absolute;left:0;right:0;bottom:-5px;height:1.5px;background:var(--brass);
  transform:scaleX(0);transform-origin:left;transition:transform var(--dur-3) var(--ease)}
.nav-links a:hover,.nav-links a[aria-current="page"]{opacity:1}
.nav-links a:hover::after,.nav-links a[aria-current="page"]::after{transform:scaleX(1)}
.nav .btn{font-size:14px;padding:10px 11px 10px 19px}
.nav .btn .ic-c{width:26px;height:26px}
.burger{display:none;border:none;background:none;padding:0}

/* visibility:hidden keeps the closed menu's links out of the tab order —
   opacity alone left six invisible links keyboard-reachable. */
.menu{position:fixed;inset:0;z-index:39;background:var(--menu-bg);backdrop-filter:blur(28px);-webkit-backdrop-filter:blur(28px);
  display:flex;flex-direction:column;justify-content:center;gap:2px;padding:0 32px;opacity:0;visibility:hidden;overscroll-behavior:contain;
  transition:opacity var(--dur-4) var(--ease),visibility 0s linear var(--dur-4);overflow-y:auto}
.menu.open{opacity:1;visibility:visible;transition:opacity var(--dur-4) var(--ease),visibility 0s}
.menu a{font-family:var(--display);font-size:clamp(32px,9vw,60px);color:var(--brand-ink);transform:translateY(40px);opacity:0;
  transition:transform var(--dur-4) var(--ease),opacity var(--dur-4) var(--ease),color var(--dur-2) var(--ease);line-height:1.12;padding:2px 0}
.menu a:hover{color:var(--brass-ink)}
.menu.open a{transform:none;opacity:1}
/* One --rv-stagger step each, capped at index 6 — the seventh link shares the
   sixth's delay rather than pushing the chain past 300ms. */
.menu.open a:nth-child(1){transition-delay:50ms}.menu.open a:nth-child(2){transition-delay:100ms}
.menu.open a:nth-child(3){transition-delay:150ms}.menu.open a:nth-child(4){transition-delay:200ms}
.menu.open a:nth-child(5){transition-delay:250ms}.menu.open a:nth-child(n+6){transition-delay:300ms}
.menu-black{display:flex;align-items:center;gap:14px;font-size:clamp(22px,6vw,34px)!important;color:var(--brass)!important;margin-top:18px;
  padding-top:22px!important;border-top:1px solid var(--line)}
.menu-black .jingle{width:15px;height:24px;flex:none}

/* ---------- sections ---------- */
section{position:relative}
.band{--band-pad:var(--band-y);padding:var(--band-pad) 0}
.band-tight{--band-pad:var(--band-y-tight)}
.band-sand{background:var(--sand)}
.band-ruled{border-top:1px solid var(--line)}
/* Two plain bands in a row paid for the gap twice — up to 250px of empty paper
   between the end of one section's copy and the next heading, with no change of
   surface to justify it. When the background doesn't change, one band's worth
   of space is the section break. A band that changes surface (dark, sand) or
   carries its own top rule keeps its full padding: there, the space is the
   transition. */
.band + .band{padding-top:0}
.band + .on-dark,.band + .band-sand,.band + .band-ruled,
.on-dark + .band,.band-sand + .band{padding-top:var(--band-pad)}

/* Sets --stack, not gap: the bound-pair rules subtract --stack to pull a note
   up under its button, so anything that changes the spacing has to change the
   variable or the two disagree. */
.band-head{display:grid;--stack:var(--s-3);gap:var(--stack);max-width:68ch;margin-bottom:clamp(34px,4vw,52px)}
/* Heads that introduce a table or a card row rather than a whole section. */
.band-head-tight{margin-bottom:clamp(20px,2.4vw,30px)}
.on-dark{background:var(--onyx);color:var(--paper)}
.on-dark h2,.on-dark h3{color:var(--paper)}
.on-dark .lead{color:rgba(251,248,241,.72)}
.rule{height:1px;background:var(--line);border:none}

/* ---------- hero ---------- */
.hero{position:relative;display:flex;align-items:center;padding:var(--head-top) 0 clamp(44px,6vw,70px);overflow:hidden}
.hero-glow{position:absolute;inset:0;z-index:0;pointer-events:none;
  background:radial-gradient(1000px 560px at 82% 4%,var(--sand) 0%,transparent 58%),radial-gradient(760px 520px at 6% 96%,rgba(231,217,191,.5),transparent 60%);
  animation:glowdrift 16s ease-in-out infinite}
/* No `will-change` here. It was on permanently, which pins a full-viewport
   composited layer in GPU memory for the life of the page for an animation the
   compositor already handles — one blurred ambient layer per surface, and never
   with a standing promotion hint. */
.hero .wrap{--stack:var(--s-4)}
.hero h1{max-width:16ch}
.hero-cta{display:flex;align-items:center;gap:14px;flex-wrap:wrap}
/* No wrap: the mark is a bullet for the line, and when it wrapped it left an
   orphan sparkle sitting alone above the text on every phone. */
.hero-proof{display:flex;align-items:flex-start;gap:10px;font-size:14px;color:var(--ink-soft)}
.hero-proof .jingle{width:11px;height:17px;color:var(--brass);flex:none;margin-top:2px}
.hero-proof b{color:var(--ink);font-weight:600}
/* z-index 0, below .wrap — at z-index 1 the lower mark sat on top of the
   finder card's corner. Decoration must never land on an input. */
.float-mark{position:absolute;color:var(--sand-deep);z-index:0;animation:bob 8s var(--ease) infinite}
.float-mark.a{top:15%;right:6%;width:58px;height:90px;color:var(--brass);opacity:.45}
.float-mark.b{bottom:22%;right:4%;width:26px;height:40px;opacity:.6;animation-delay:-2.5s}
@keyframes bob{0%,100%{transform:translateY(0) rotate(0)}50%{transform:translateY(-18px) rotate(5deg)}}
@keyframes glowdrift{0%,100%{transform:translate3d(0,0,0) scale(1)}50%{transform:translate3d(-2.2%,1.6%,0) scale(1.045)}}

/* ---------- instant service finder (hero-adjacent) ---------- */
.finder{position:relative;z-index:3;margin-top:8px;background:var(--surface);border-radius:26px;padding:10px;
  box-shadow:0 0 0 1px var(--line) inset,0 30px 70px -40px rgba(34,26,17,.45)}
/* 8px of right padding on the bar put the text hard against a rounded corner,
   so a placeholder one character too long ended in a guillotined glyph rather
   than reading as trailing-off text. The bar keeps a real right inset now. */
.finder-bar{display:flex;align-items:center;gap:12px;padding:6px 16px 6px 20px;border-radius:19px;background:var(--paper);box-shadow:0 0 0 1px var(--line-2) inset}
.finder-bar svg.mag{width:19px;height:19px;stroke:var(--ink-soft);fill:none;stroke-width:2;stroke-linecap:round;flex:none}
.finder-bar input{flex:1;min-width:0;border:none;background:none;outline:none;font-size:var(--ctl-size);
  line-height:1.25;min-height:var(--ctl-h);padding:14px 0;letter-spacing:-.01em;
  /* Belt to the JS braces: site.js swaps in the short placeholder when the
     long one will not fit, and this makes the cut soft if it ever does. */
  text-overflow:ellipsis}
/* ::placeholder was styled in exactly ONE rule site-wide (this one), so every
   other placeholder on the site rendered at the UA's grey. One value, applied
   to every control that has one. */
.finder-bar input::placeholder,.dir-search input::placeholder{color:var(--ink-soft);opacity:.62}
.finder-tags{display:flex;flex-wrap:wrap;gap:7px;padding:14px 10px 8px}
.ftag{font-size:13px;font-weight:500;color:var(--ink-soft);padding:8px 15px;border-radius:999px;background:var(--paper);cursor:pointer;border:none;
  min-height:44px;
  box-shadow:0 0 0 1px var(--line-2) inset;transition:box-shadow var(--dur-2) var(--ease),color var(--dur-2) var(--ease),background var(--dur-2) var(--ease)}
.ftag:hover{color:var(--brass-ink);box-shadow:0 0 0 1px rgba(154,107,28,.32) inset}
.finder-results{display:none;padding:6px 6px 8px}
.finder.has-results .finder-results{display:block}
.finder.has-results .finder-tags{display:none}
.fres{display:flex;align-items:center;gap:14px;padding:13px 14px;border-radius:15px;transition:background var(--dur-2) var(--ease)}
.fres:hover,.fres:focus-visible{background:var(--sand)}
.fres .fr-n{font-weight:600;font-size:15.5px}
.fres .fr-l{font-size:12.5px;color:var(--ink-soft)}
.fres .fr-p{margin-left:auto;font-size:14px;color:var(--brass-ink);font-weight:600;white-space:nowrap}
.fres-empty{padding:18px;font-size:15px;color:var(--ink-soft)}

/* ---------- proof strip ----------
   Client NAMES, set as a credit line. The previous version rendered each name
   as a wordmark in LUMORIA's own display face, which read as a row of client
   logos we were never given. Names are honest; fake logos are not. */
.proof-strip{padding:var(--s-5) 0;border-top:1px solid var(--line-2);border-bottom:1px solid var(--line-2)}
/* Sentence case, no tracking. As tracked caps this was an eyebrow in all but
   name, and the .72 opacity put 11px type at ~4.6:1. */
.strip-label{display:block;font-size:13.5px;color:var(--ink-soft);margin-bottom:var(--s-3);text-align:center;text-wrap:balance}
.credits{list-style:none;display:flex;flex-wrap:wrap;justify-content:center;gap:10px}
.credits li{display:flex;align-items:baseline;gap:9px;padding:9px 18px;border-radius:999px;background:var(--surface);
  box-shadow:0 0 0 1px var(--line-2) inset;transition:box-shadow var(--dur-3) var(--ease),transform var(--dur-3) var(--ease)}
.credits li:hover{box-shadow:0 0 0 1px rgba(154,107,28,.3) inset;transform:translateY(-2px)}
.credits b{font-size:14.5px;font-weight:600;letter-spacing:-.005em}
.credits span{font-size:13px;color:var(--ink-soft)}
.credits-note{margin:var(--s-3) auto 0;max-width:62ch;text-align:center;font-size:13px;color:var(--ink-soft);line-height:1.55;text-wrap:pretty}

/* ---------- lane rail (5 lanes) ---------- */
/* A bottom rule as well as a top one — with only a top rule the tab strip had
   a ragged open edge and read as an unfinished block. */
.rail{display:grid;grid-template-columns:repeat(5,1fr);border-top:1.5px solid var(--ink);border-bottom:1px solid var(--line);margin-bottom:var(--s-6)}
.rail-item{text-align:left;background:none;border:none;font-family:var(--body);cursor:pointer;padding:22px 20px 22px;display:grid;gap:7px;align-content:start;position:relative;border-left:1px solid var(--line);transition:background var(--dur-3) var(--ease)}
.rail-item:first-child{border-left:none}
/* Same conversion as the nav underline: transform, never `right`. */
.rail-item::before{content:"";position:absolute;top:-1.5px;left:0;right:0;height:1.5px;background:var(--brass);
  transform:scaleX(0);transform-origin:left;transition:transform var(--dur-3) var(--ease)}
.rail-item.is-active::before{transform:scaleX(1)}
.rail-item.is-active{background:linear-gradient(180deg,rgba(154,107,28,.07),transparent)}
/* The lane icon, above the name on its own line so five equal-weight columns
   read as a set at a glance. Brass, because it is the one accent and these are
   the only marks in the row. */
.rail-ic{display:block;line-height:0;margin-bottom:2px}
.rail-ic svg{width:22px;height:22px;color:var(--brass);stroke:currentColor;fill:none;
  stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round;
  transition:color var(--dur-3) var(--ease)}
.rail-item.is-active .rail-ic svg,.rail-item:hover .rail-ic svg{color:var(--espresso)}
.rail-k{font-family:var(--display);font-size:clamp(21px,2.1vw,29px);color:var(--ink);line-height:1;transition:color var(--dur-3) var(--ease)}
.rail-item.is-active .rail-k,.rail-item:hover .rail-k{color:var(--espresso)}
.rail-sub{font-size:13px;color:var(--ink-soft);line-height:1.35}
/* Pushed to the floor of the cell so the five counts sit on one line — "Grow"
   has a two-line description and its count used to hang 15px below the rest,
   leaving the bottom of the tab strip ragged. */
.rail-n{font-size:12px;color:var(--brass-ink);font-weight:600;margin-top:auto;padding-top:6px}

/* ---------- service cards ---------- */
/* Same shape as .tiers: single column is the base, extra tracks are opt-in
   above a width where the 288px minimum is guaranteed to fit (2 * 288 + 16). */
.svc-grid{display:grid;grid-template-columns:minmax(0,1fr);gap:16px}
@media (min-width:681px){.svc-grid{grid-template-columns:repeat(auto-fill,minmax(288px,1fr))}}
.svc{min-width:0;overflow-wrap:break-word;overflow-wrap:anywhere}
/* Column, not grid-with-align-content:start — so .svc-foot can take the slack
   and every price row in a grid of cards lands on the same baseline no matter
   how many lines the title above it wrapped to. */
.svc{position:relative;display:flex;flex-direction:column;gap:9px;background:var(--surface);border-radius:22px;padding:26px 26px 24px;
  box-shadow:0 0 0 1px var(--line) inset,0 1px 0 rgba(255,255,255,.6) inset;overflow:hidden;
  transition:transform var(--dur-3) var(--ease),box-shadow var(--dur-3) var(--ease)}
.svc::before{content:"";position:absolute;inset:0;border-radius:inherit;pointer-events:none;opacity:0;transition:opacity var(--dur-3) var(--ease);
  background:radial-gradient(420px circle at var(--px,50%) var(--py,50%),rgba(154,107,28,.10),transparent 62%)}
.svc:hover{transform:translateY(-4px);box-shadow:0 0 0 1px rgba(154,107,28,.32) inset,0 30px 60px -34px rgba(34,26,17,.42);z-index:2}
.svc:hover::before{opacity:1}
/* The per-card lane tag is gone: every grid of cards on the site is already
   inside a lane — a selected tab panel on the homepage, a lane-headed group in
   the directory — so it restated the heading directly above it in tracked caps
   on all 27 cards. Only a real flag (Best seller / Instant) earns the row now. */
.svc-top{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
/* Coolvetica, weight 400, zero tracking — and each of those three is a fix.
   This was 11px body-BOLD at letter-spacing:.06em. Positive tracking is what
   you reach for when 11px bold body type has no other way to look deliberate,
   and it is exactly what makes a badge read as stock SaaS chrome; it is the
   same treatment the eyebrow ban exists to kill, surviving inside a pill.
   The display face does not need the crutch, and it costs no room: measured,
   "Best seller" advances 61.7px at 14px Coolvetica against 62.6px at the old
   11px+.06em, so the pill keeps its footprint in every card grid while the
   glyphs get ~27% bigger and stop looking shouted.

   font-weight stays 400 because there IS no Coolvetica bold — the @font-face
   at the top of this file declares 400 only, so a 700 here would be
   SYNTHESISED: the browser smears the outline sideways and the pill renders in
   a fake bold that does not match the h1..h4 set in the same face 25px below
   it on the same card. Never put a weight on --display that the file has not
   loaded.

   letter-spacing:0, not the house -.02em: that value is tuned for the 40-124px
   display lines it was written for. At 14px the face's own sidebearings are
   already right, and the defect being fixed here is ADDED tracking — so the
   honest value is none, not a different amount.

   line-height is pinned to 1 so the pill's height is the padding plus the font
   size and nothing else, and the padding is asymmetric the OTHER way round to
   the obvious guess. Coolvetica's em box carries more descent than the strings
   in this pill ever use ("Best seller", "Instant" have no descender at all), so
   a symmetric pad leaves the cap band sitting high. Measured at 6/7: 7.59px
   above the cap against 10px below the baseline — 2.41px out. 7 top / 6 bottom
   moves it a pixel down and lands at 8.59 against 9.00, i.e. 0.41px out, which
   is under the rounding. Optical centring here means centring the CAP BAND
   (cap-top to baseline), not the font box; "Everything" still keeps 5.7px of
   clearance under its descenders. */
.svc-flag{font-family:var(--display);font-size:14px;font-weight:400;letter-spacing:0;line-height:1;
  color:#fff;background:var(--brass);padding:7px 12px 6px;border-radius:999px}
.svc-flag.alt{background:var(--espresso)}
/* The card title is display type at 25px; it needs its own air, not the same
   9px that separates the flag row from it. */
.svc h3{font-size:25px;letter-spacing:-.015em;line-height:1.02;margin-bottom:3px}
.svc p{font-size:14.8px;color:var(--ink-soft);line-height:1.5}
.svc-foot{display:flex;align-items:flex-end;justify-content:space-between;gap:12px;margin-top:auto;padding-top:15px;border-top:1px solid var(--line-2)}
.svc-price{font-size:13.5px;color:var(--ink-soft);line-height:1.3}
.svc-price b{font-family:var(--display);font-size:23px;color:var(--espresso);font-weight:400;display:block;letter-spacing:-.01em}
.svc-go{display:inline-flex;align-items:center;gap:7px;font-size:13.5px;font-weight:600;color:var(--brass-ink);white-space:nowrap}
.svc-go svg{width:14px;height:14px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;transition:transform var(--dur-3) var(--spring)}
.svc:hover .svc-go svg,.svc:focus-within .svc-go svg{transform:translateX(3px)}
/* The card is one link, but the anchor now wraps the heading rather than being
   an empty overlay — so the accessible name is the real text and the card body
   stays selectable. ::after does the full-card hit area. */
.svc-link::after{content:"";position:absolute;inset:0;z-index:3}
.svc:focus-within{box-shadow:0 0 0 1px rgba(154,107,28,.32) inset,0 30px 60px -34px rgba(34,26,17,.42)}
.svc-link:focus-visible{outline:none}
.svc:has(.svc-link:focus-visible){outline:2px solid var(--focus);outline-offset:3px}

/* ---------- directory page ---------- */
.dir-head{padding:var(--head-top) 0 var(--s-5)}
/* Was top:82px against a 94px-tall nav, so the filter bar parked 12px *under*
   the chrome. Pinned to the token now, with the same 14px breathing room the
   scroll anchors use. */
.dir-controls{position:sticky;top:calc(var(--nav-h) + 6px);z-index:20;padding:12px 0 16px;background:linear-gradient(var(--paper) 72%,transparent);margin-bottom:var(--s-4)}
.dir-search{display:flex;align-items:center;gap:12px;padding:4px 6px 4px 18px;border-radius:999px;background:var(--surface);box-shadow:0 0 0 1px var(--line) inset;max-width:520px}
/* Was 15.5px — under 16px, so focusing it zoomed the viewport on iOS. */
.dir-search input{flex:1;min-width:0;border:none;background:none;outline:none;font-size:var(--ctl-size);
  line-height:1.25;min-height:44px;padding:12px 0;
  /* Belt to the JS braces — site.js already swaps in a placeholder that fits;
     this makes the cut soft rather than a guillotined glyph if it ever does. */
  text-overflow:ellipsis}
.dir-search svg{width:18px;height:18px;stroke:var(--ink-soft);fill:none;stroke-width:2;stroke-linecap:round;flex:none}
.dir-clear{border:none;background:var(--sand);cursor:pointer;font-size:12.5px;font-weight:600;color:var(--ink-soft);padding:8px 14px;border-radius:999px;
  flex:none;min-height:44px;transition:background var(--dur-2) var(--ease),color var(--dur-2) var(--ease)}
.dir-clear:hover{background:var(--sand-deep);color:var(--ink)}
.dir-filters{display:flex;gap:8px;flex-wrap:wrap;margin-top:14px}
/* `transition:all` also animates layout properties nobody intended — name them. */
.fchip{font-size:13px;font-weight:600;color:var(--ink-soft);padding:9px 17px;border-radius:999px;background:var(--surface);cursor:pointer;border:none;
  min-height:44px;
  box-shadow:0 0 0 1px var(--line) inset;
  transition:background var(--dur-2) var(--ease),color var(--dur-2) var(--ease),box-shadow var(--dur-2) var(--ease)}
.fchip:hover{color:var(--brass-ink);box-shadow:0 0 0 1px rgba(154,107,28,.32) inset}
.fchip.on{background:var(--ink);color:var(--paper);box-shadow:none}
.dir-count{font-size:13.5px;color:var(--ink-soft);margin:4px 0 var(--s-3)}
.dir-empty{padding:var(--s-8) 0;text-align:center;color:var(--ink-soft);display:grid;gap:var(--s-3);justify-items:center}
.dir-empty h3{font-size:30px;color:var(--ink)}
.dir-group{margin-bottom:var(--s-7)}
.dir-group-head{display:flex;align-items:baseline;gap:14px;margin-bottom:var(--s-3);padding-bottom:14px;border-bottom:1.5px solid var(--ink);flex-wrap:wrap}
.dir-group-head h2{font-size:clamp(27px,3vw,38px)}
.dir-group-head span{font-size:14px;color:var(--ink-soft)}

/* ---------- service detail ---------- */
.svc-hero{padding:var(--head-top) 0 clamp(38px,5vw,58px);position:relative;overflow:hidden}
.svc-hero .hero-glow{animation:glowdrift 18s ease-in-out infinite}
/* Margins on these were hand-tuned one at a time (22 / 18 / 26 / 30px) and no
   two heroes matched. The .rgroup stack owns the spacing now. */
.crumbs{display:flex;align-items:center;gap:9px;font-size:13px;color:var(--ink-soft);flex-wrap:wrap}
.crumbs a:hover{color:var(--brass-ink)}
.crumbs .sep{opacity:.4}
.svc-hero h1{max-width:16ch}
.svc-hero .lead{max-width:56ch}
.svc-meta{display:flex;gap:10px;flex-wrap:wrap}
.pill{display:inline-flex;align-items:center;gap:8px;font-size:13px;font-weight:500;color:var(--ink-soft);padding:9px 16px;border-radius:999px;background:var(--surface);box-shadow:0 0 0 1px var(--line) inset}
.pill b{color:var(--ink);font-weight:600}
.pill .dot{width:6px;height:6px;border-radius:999px;background:var(--brass);flex:none}
.pill .dot.live{background:var(--ok);box-shadow:0 0 0 3px var(--ok-bg)}

.priceline{display:flex;align-items:flex-end;gap:16px;flex-wrap:wrap}
.priceline .big{font-family:var(--display);font-size:clamp(44px,6vw,72px);line-height:.94;color:var(--espresso);letter-spacing:-.02em}
.priceline .big .px{color:var(--brass)}
/* Two stacked facts, so they stack — this used to be one span with a <br> in it. */
.priceline .qual{display:grid;gap:2px;font-size:14.5px;color:var(--ink-soft);padding-bottom:8px;line-height:1.35}

.two-col{display:grid;grid-template-columns:1.35fr .95fr;gap:clamp(38px,4.4vw,56px);align-items:start}
/* 12px between items whose own line spacing is 24px meant a wrapped item ran
   into the next one — the four-point list read as one grey block. */
.checklist{display:grid;gap:16px}
.checklist li{list-style:none;display:flex;gap:13px;align-items:flex-start;font-size:16px;color:var(--ink-soft);line-height:1.5}
.checklist li svg{width:17px;height:17px;color:var(--brass);flex:none;margin-top:3px;stroke:currentColor;fill:none;stroke-width:2.3;stroke-linecap:round;stroke-linejoin:round}
.checklist li b{color:var(--ink);font-weight:600}
.on-dark .checklist li{color:rgba(251,248,241,.82)}
.on-dark .checklist li svg{color:var(--brass-bright)}

/* tiers */
/* ONE COLUMN IS THE BASE, and every extra column is opt-in. This used to be a
   single `repeat(auto-fit,minmax(min(240px,100%),1fr))` on an element that was
   ALSO the query container — three features stacked on one declaration, and if
   any of them mis-sizes, the fallback is a collapsed track, not a phone layout.
   That is exactly what shipped: on 2026-08-09 the cards rendered ~45px wide on
   a real handset with the text overflowing them, while every desktop engine and
   the live CSS itself (byte-identical to this file) laid them out correctly.

   So the failure mode is inverted. The base declaration is a plain single
   column that no engine can get wrong, the multi-column rule only ever ADDS
   tracks, and it lives behind a query — so if the query never matches, or the
   engine drops it, what remains is the correct mobile layout.

   No min() inside minmax() inside repeat() either. The 520px floor is why it is
   not needed: two 240px tracks plus the 16px gap is 496px, so the track minimum
   is guaranteed to fit before auto-fit is ever asked for. */
.tiers-c{container-type:inline-size}
.tiers{display:grid;grid-template-columns:minmax(0,1fr);gap:16px}
@container (min-width:520px){
  .tiers{grid-template-columns:repeat(auto-fit,minmax(240px,1fr))}
}
/* The empty badge slot exists to line headings up ACROSS tiers. Once the grid
   collapses to one column there is nothing to line up with, so it is just a
   hole above the title — measured off the container, not the viewport, so it
   tracks the actual column count wherever the component is used. */
@container (max-width:519px){.tier .t-tag:empty{display:none}}
/* Column so the CTA can bottom-align across tiers of different note lengths. */
.tier{border-radius:22px;padding:26px 26px 28px;display:flex;flex-direction:column;gap:13px;background:var(--surface);
  box-shadow:0 0 0 1px var(--line) inset;transition:transform var(--dur-3) var(--ease),box-shadow var(--dur-3) var(--ease);position:relative;
  /* Containment of last resort. Whatever a track does, words stay in the card:
     min-width:0 lets the item shrink instead of forcing the track wider, and
     `anywhere` (unlike break-word) also lowers the min-content contribution,
     so a squeezed card wraps its own text rather than spilling it sideways.
     break-word first for engines that never learned `anywhere`. */
  min-width:0;overflow-wrap:break-word;overflow-wrap:anywhere}
.tier:hover{transform:translateY(-4px);box-shadow:0 0 0 1px rgba(154,107,28,.3) inset,0 30px 60px -34px rgba(34,26,17,.4)}
.tier.pop{background:var(--espresso);color:var(--paper);box-shadow:0 26px 60px -28px rgba(74,42,21,.7)}
.tier.pop .t-note,.tier.pop .t-amt small{color:rgba(251,248,241,.72)}
.tier.pop .t-amt{color:var(--paper)}
/* The badge slot is always in the flow, empty or not, so the headings of a
   flagged and an unflagged tier sit on the same line. It used to be filled
   with a literal &nbsp;, which a screen reader read out as a blank line. */
/* line-height and min-height must match, or a tier with a badge sits a few
   pixels lower than one with an empty slot and the three headings go ragged. */
.tier .t-tag{font-size:11px;letter-spacing:.06em;font-weight:700;color:var(--brass-ink);line-height:1.2;min-height:1.2em}
.tier.pop .t-tag{color:var(--brass-bright)}
.tier h3{font-size:25px}
.tier .t-amt{font-family:var(--display);font-size:clamp(29px,3.2vw,38px);color:var(--espresso);line-height:1;letter-spacing:-.01em}
.tier .t-amt small{font-family:var(--body);font-size:12.5px;font-weight:500;color:var(--ink-soft);letter-spacing:0;display:block;margin-top:7px}
.tier .t-note{font-size:14px;color:var(--ink-soft);line-height:1.45}
/* auto pushes the CTA to the card floor so buttons line up across tiers; the
   flex gap guarantees clearance from the note above even in a short card. */
/* max-width:100% so the CTA can never be wider than the card it sits in — a
   column-flex item's cross size can shrink today, so this is not live, but it
   was one white-space:nowrap away from being the reported overflowing pill. */
.tier .btn{margin-top:auto;align-self:start;max-width:100%}
/* Below ~190px of layout viewport (a 320pt phone at Safari's 200% page zoom,
   which is a LAYOUT zoom) the pill's fixed 30px arrow circle plus 38px of
   padding stops fitting inside the card. Tighten the pill and let the label
   break rather than let the arrow escape the corner. */
@media (max-width:400px){
  .tier .btn{padding:11px 10px 11px 16px;gap:9px}
  .tier .btn .btn-t{min-width:0;overflow-wrap:anywhere}
  .tier .btn .ic-c{width:26px;height:26px}
}
.tier h3{margin-bottom:2px}

/* addons table */
.addons{display:grid;gap:2px}
.addon{display:flex;align-items:center;gap:16px;padding:16px 0;border-top:1px solid var(--line-2);flex-wrap:wrap}
.addon:first-child{border-top:none}
.addon .a-n{font-weight:600;font-size:15.5px;flex:none}
.addon .a-d{font-size:14px;color:var(--ink-soft);flex:1;min-width:180px;line-height:1.4}
.addon .a-p{font-family:var(--display);font-size:21px;color:var(--espresso);margin-left:auto;white-space:nowrap}
.addon .a-p .a-u{font-family:var(--body);font-size:12px;font-weight:500;color:var(--ink-soft)}

/* sticky buy rail */
.buybox{position:sticky;top:calc(var(--nav-h) + 14px);background:var(--surface);border-radius:24px;padding:28px;display:grid;gap:16px;
  box-shadow:0 0 0 1px var(--line) inset,0 30px 70px -44px rgba(34,26,17,.5)}
/* "From" is a caption bound to the number under it, not a sibling of it. */
.buybox .lab{margin-bottom:-10px}
/* The primary action gets a break from the copy above; the trust list is bound
   to the button it reassures. */
.buybox .btn{margin-top:6px}
.buybox .bb-note + .btn{margin-top:4px}
.buybox .bb-price{font-family:var(--display);font-size:40px;color:var(--espresso);line-height:1;letter-spacing:-.02em}
.buybox .bb-price small{font-family:var(--body);font-size:13px;color:var(--ink-soft);font-weight:500;letter-spacing:0;display:block;margin-top:8px}
/* Centred, not space-between: in a ~300px rail the arrow ended up marooned at
   the far edge with a hand-span of dead pill between it and the label. */
.buybox .btn{justify-self:stretch;justify-content:center;padding-left:20px}
.buybox .bb-note{font-size:13px;color:var(--ink-soft);line-height:1.5}
.buybox .bb-note b{color:var(--ink)}
.bb-trust{display:grid;gap:9px;padding-top:15px;border-top:1px solid var(--line-2)}
.bb-trust li{list-style:none;display:flex;gap:10px;align-items:flex-start;font-size:13.5px;color:var(--ink-soft);line-height:1.4}
.bb-trust svg{width:14px;height:14px;color:var(--brass);flex:none;margin-top:2px;stroke:currentColor;fill:none;stroke-width:2.4;stroke-linecap:round;stroke-linejoin:round}

/* region notice */
.region-note{display:flex;align-items:flex-start;gap:12px;padding:15px 18px;border-radius:16px;background:var(--sand);box-shadow:0 0 0 1px var(--line-2) inset;font-size:13.5px;color:var(--ink-soft);line-height:1.45}
.region-note svg{width:16px;height:16px;color:var(--brass);flex:none;margin-top:2px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.region-note b{color:var(--ink)}

/* ---------- process ---------- */
.steps{display:grid;grid-template-columns:repeat(4,1fr);gap:clamp(24px,2.8vw,34px)}
.step{padding-top:20px;border-top:1.5px solid var(--ink);display:grid;gap:12px;align-content:start}
/* The tracked-caps kicker above each step's heading is gone. On the process
   band it was literally ONE / TWO / THREE / FOUR — a numbered incrementer
   above four items already in order, the exact tell we don't ship. */
.step h3{font-size:22px}
.step p{font-size:14.8px;color:var(--ink-soft);line-height:1.5}
/* The icon variant. `.steps` already carries the grid, the rule and the type
   scale, so this adds a slot and nothing else — the alternative was a second
   four-across grid component that would drift from this one. */
.steps-ic .step{border-top:none;padding-top:0;gap:10px}
.step .s-ic{width:26px;height:26px;color:var(--brass);stroke:currentColor;fill:none;
  stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
.on-dark .step{border-top-color:rgba(205,160,85,.5)}
.on-dark .step p{color:rgba(251,248,241,.68)}
.on-dark .step .s-ic{color:var(--brass-bright)}

/* ---------- faq ---------- */
.faq{display:grid;gap:0;max-width:840px}
.faq details{border-top:1px solid var(--line);padding:0}
.faq details:last-child{border-bottom:1px solid var(--line)}
.faq summary{cursor:pointer;list-style:none;padding:22px 40px 22px 0;font-size:17.5px;font-weight:600;position:relative;transition:color var(--dur-2) var(--ease)}
.faq summary::-webkit-details-marker{display:none}
.faq summary::after{content:"";position:absolute;right:8px;top:50%;width:11px;height:11px;margin-top:-6px;border-right:2px solid var(--brass);border-bottom:2px solid var(--brass);transform:rotate(45deg);transition:transform var(--dur-3) var(--spring)}
.faq details[open] summary::after{transform:rotate(-135deg)}
.faq summary:hover{color:var(--brass-ink)}

/* The panel used to be a bare <details> child: the chevron rotated on --dur-3
   and the answer SNAPPED, which is the one place on the site with no motion at
   all. Three things were tried and measured in a browser before this shape:

   1. `grid-template-rows:0fr -> 1fr` as a plain CSS transition. It does not
      run. A closed <details> has its content skipped (content-visibility), so
      on open there is no previous computed value to interpolate FROM and the
      engine creates no CSSTransition at all — getAnimations() came back empty.
      Only the CLOSE produced a transition object, and that one is invisible
      because the content is hidden the instant `open` is removed.
   2. `::details-content` with block-size + allow-discrete. Also produced no
      transition object here, needs `interpolate-size:allow-keywords`, and is
      too new to be the only route. Overriding its content-visibility to make
      it animatable LEAKS — the answer renders while the panel is shut.
   3. What ships: faq.js drives the track with the Web Animations API, which
      interpolates fr units correctly (measured 0 -> 4.7 -> 18.8 -> 42.3 ->
      75.2px across 320ms) and, unlike a CSS transition, also runs on CLOSE
      because the script holds `open` until the animation finishes.

   So the DEFAULT below is the OPEN state, not the closed one. With JS off, or
   before faq.js arrives, <details> behaves exactly as it always did — the
   panel is correct and keyboard-operable, it simply does not animate. Nothing
   here gates the answer on motion. */
.faq .fa-w{display:grid;grid-template-rows:1fr}
.faq .fa{overflow:hidden;min-height:0}
.faq .fa-b{padding:0 0 24px;font-size:16px;color:var(--ink-soft);line-height:1.6;max-width:70ch}
/* The house entrance — fluid, blurry, motion-graphic — is applied to .fa-b by
   faq.js, not here, because it is a Web Animations keyframe rather than a CSS
   transition. It runs at the SURFACE duration (--dur-3), not the entrance one
   (--dur-4): a disclosure panel is a surface. faq.js reads --dur-3, --ease,
   --ease-out, --rv-y and --rv-blur off :root, so the tokens above stay the
   single definition and this block invents no duration of its own.
   prefers-reduced-motion is checked in the script — the global reduce block
   below only neutralises CSS transitions, and a WAAPI animation ignores it. */

/* ---------- guarantee ---------- */
/* Contrast is decided by the SECTION, not the body. The old rule keyed off a
   body.dark-ctx class that was never set anywhere, so the guarantee inside the
   onyx "Built, not pitched" band rendered warm brown on near-black (~1.3:1). */
.guarantee{display:flex;align-items:center;gap:24px;flex-wrap:wrap;padding:28px 32px;border-radius:22px;
  background:linear-gradient(180deg,rgba(154,107,28,.09),rgba(154,107,28,.03));box-shadow:0 0 0 1px rgba(154,107,28,.26) inset}
/* On the directory this box also carries a CTA; without this it got squeezed
   by the flexible paragraph instead of holding its own width. */
.guarantee .btn{flex:none}
.steps + .guarantee{margin-top:clamp(38px,4.6vw,56px)}
.guarantee .g-ic{width:46px;height:46px;border-radius:13px;flex:none;display:grid;place-items:center;background:rgba(154,107,28,.13);color:var(--brass)}
.guarantee .g-ic svg{width:24px;height:24px;stroke:currentColor;fill:none;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
.guarantee p{font-size:16px;color:var(--ink-soft);line-height:1.5;flex:1;min-width:240px}
.guarantee p b{color:var(--ink);font-weight:600}
.on-dark .guarantee{background:linear-gradient(180deg,rgba(205,160,85,.12),rgba(205,160,85,.035));box-shadow:0 0 0 1px rgba(205,160,85,.3) inset}
.on-dark .guarantee .g-ic{background:rgba(205,160,85,.16);color:var(--brass-bright)}
.on-dark .guarantee p{color:rgba(251,248,241,.84)}
.on-dark .guarantee p b{color:var(--paper)}

/* ---------- booking ---------- */
.bk{display:grid;grid-template-columns:1.25fr .9fr;gap:clamp(28px,3.4vw,44px);align-items:start}
.bk-card{background:var(--surface);border-radius:24px;padding:30px;box-shadow:0 0 0 1px var(--line) inset;display:grid;gap:22px}
/* A form is a list of fields, and an even gap between fields is CORRECT — a
   list is supposed to be even. What is not correct is the submit and its
   risk-reversal note taking that same gap: the contact form ran five fields,
   the button and the note all at 14px, so nothing in it grouped. The two rules
   under it are the break and the bind, expressed against the gap so they stay
   right if the rhythm is ever retuned. */
.bk-step{--stack:14px;display:grid;gap:var(--stack)}
/* the submit is a change of mode, not one more field */
.bk-step > * + .btn{margin-top:calc(var(--s-4) - var(--stack))}
/* ...and the note under it belongs to that button, not to whatever follows */
.bk-step > .btn + .cta-note{margin-top:calc(var(--s-2) - var(--stack))}
.bk-step-h{display:flex;align-items:center;gap:12px}
.bk-step-h .n{width:26px;height:26px;border-radius:999px;background:var(--ink);color:var(--paper);display:grid;place-items:center;font-size:12.5px;font-weight:700;flex:none}
.bk-step-h h3{font-size:21px}
.bk-step.done .bk-step-h .n{background:var(--ok)}
/* ---------- controls — the canonical block (docs/10-interface-standard.md §2)
   One geometry, public density. Copied, not re-derived: this block and the
   app-density one in the Console differ only by --ctl-h, --ctl-r and the two
   font sizes.

   Three defects lived here and all three are fixed below, so nobody
   reintroduces them:

     1. `appearance` was reset NOWHERE in this stylesheet, so every <select> —
        including the five-optgroup service picker on /contact/ — kept the UA's
        `menulist`, whose internal dropdown button carries its own vertical
        metrics OUTSIDE our padding, and rendered taller than the input beside
        it.
     2. `line-height` is inherited by a <select> but NOT by a text <input> —
        body's 1.5 gave the select a 24px inner line box while Chrome's UA sheet
        forced `normal` (~19px) on the input. Identical padding, ~5px taller
        select. Pinning 1.25 on both is the fix.
     3. Nothing held the two to the same box. A .field-row grid does not fix it:
        each control sits in its own wrapper, so align-items stretches the
        WRAPPERS, not the controls. Hence min-height on all three.
   ---------------------------------------------------------------------- */
.field{display:grid;gap:8px}
.field > label{font-size:var(--lbl-size);font-weight:600;color:var(--ink-soft);line-height:1.35;letter-spacing:.01em}
.field input,.field select,.field textarea{
  width:100%;
  min-height:var(--ctl-h);
  padding:var(--ctl-pad);
  font-size:var(--ctl-size);
  line-height:1.25;                     /* pinned — see note 2 above */
  border:none;border-radius:var(--ctl-r);
  background:var(--paper);
  box-shadow:0 0 0 1px var(--line) inset;
  color:var(--ink);
  outline:none;
  transition:box-shadow var(--dur-2) var(--ease)}
/* A select is a text field that happens to have a menu. Make it one. */
.field select{
  appearance:none;-webkit-appearance:none;
  padding-right:38px;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5 6 6.5 11 1.5' fill='none' stroke='%235C4F3D' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat:no-repeat;
  background-position:right 14px center;
  background-size:12px 8px}
.field textarea{min-height:calc(var(--ctl-h) * 2);line-height:1.55;resize:vertical;padding-top:12px}
/* ONE ring, never two. The old rule painted a brass INSET box-shadow on :focus
   and also inherited the global :focus-visible brass outline — a brass line
   inside the control and a brass ring outside it, which reads as a rendering
   fault. And it is `:focus`, not `:focus-visible`: a text field must show focus
   when CLICKED, not only when tabbed to, or a pointer user cannot see which
   field they are in. :focus-visible stays correct for buttons, links and rows. */
.field input:focus,.field select:focus,.field textarea:focus{
  outline:2px solid var(--focus);outline-offset:2px;
  box-shadow:0 0 0 1px var(--line) inset}
.field input::placeholder,.field textarea::placeholder{color:var(--ink-soft);opacity:.62}
.field input[disabled],.field select[disabled],.field textarea[disabled]{opacity:.5;cursor:not-allowed}
.field .hint{font-size:12.5px;color:var(--ink-soft);line-height:1.5}
/* A derived value SAYS it was derived, and the mark clears the moment the human
   edits the field (§3.3 — deriving is a favour, pretending the user typed it is
   a lie about where the data came from). Same treatment as the intake form's
   `.f .hint.derived`, so a filled-from-postcode value looks identical on both. */
.field .hint.derived{color:var(--espresso);font-weight:600}
/* Field rows — SUBGRID, so label sits level with label and control with
   control across the columns no matter which field is taller.

   This used to be `align-items:end`, and that is what the founder photographed
   on /book/: Email and Phone rest in line, then Phone fails validation, its
   `.err` un-hides, its box grows a third row — and `align-items:end` bottom-
   aligns the two BOXES, so Email is pushed DOWN to keep its base level with
   Phone's error text. Measured in the browser: Email's label top went 2677 →
   2711 while Phone's stayed at 2677. **33px of drift caused entirely by the
   neighbour.** The rule was written to line a hinted field up with a hintless
   one and it does that by moving the wrong thing — the row's job is to align
   the PARTS, not the boxes, and only subgrid can align parts.

   Same fix, same reasoning as `.fr`/`.fr3`/`.fr4`/`.two` in the console's
   app.css, which hit this from the other direction (Phone dropping below Email
   on the photographer drawer). Three rows per field: label / control / note.

   The row-gap below IS each field's internal label→control gap, because a
   subgrid takes its gaps from the parent — so it has to be the same 8px
   `.field` sets on its own or every paired field would space differently from
   every single one. Column gap stays 14px.

   NOTE the rows are NOT declared as `grid-template-rows:auto auto auto`. The
   children's `span 3` creates them on demand; declaring three explicit tracks
   would keep track 3 alive when no field in the row has anything to put in it,
   and an empty last track still pays its 8px row-gap — 8px of dead space under
   every resting field row, which is a new spacing defect traded for the old
   alignment one. */
.field-row{display:grid;grid-template-columns:1fr 1fr;column-gap:14px;row-gap:8px}
/* `span 3` is on every child, not just `.field`, so a hand-written cell can
   never take one row while the fields beside it take three. */
.field-row > *{min-width:0;max-width:100%;grid-row:span 3}
.field-row > .field{display:grid;grid-template-rows:subgrid}
/* A field with nothing in its note row must not pay for the row. `.err` is
   `display:none` until `.has-err`, so a resting field genuinely has two items
   and spanning three would leave 8px of dead gap under the control. `[hidden]`
   is excluded from the hint test because `#bTownHint` ships hidden and fills
   later — a hint that is not on screen is not a row. */
.field-row > .field:not(:has(.hint:not([hidden]))):not(.has-err){grid-row:span 2}
/* An error SUPERSEDES the hint rather than stacking under it — three rows are
   three rows, and a fourth visible child in a subgridded field has no track to
   land in, gets auto-placed into an implicit row on top of the control and
   swallows every click on it. That is ADR-065 exactly, and it made Name and
   Phone unfocusable on the console's photographer drawer. Nothing in a
   `.field-row` carries a hint today; this is here so that adding one cannot
   silently break the field it is added to. */
.field-row > .field.has-err > .hint{display:none}
/* Subgrid is the only thing here that a browser can lack (Safari <16, Chrome
   <117). `align-items:start` is the right degradation: labels line up, which is
   the defect being fixed. It gets a two-line label slightly wrong — the shorter
   neighbour's control sits a line high — and that is a far smaller error than
   dropping a whole field 33px because its neighbour showed a message. */
@supports not (grid-template-rows:subgrid){
  .field-row{align-items:start;row-gap:14px}
  .field-row > *,.field-row > .field:not(:has(.hint:not([hidden]))):not(.has-err){grid-row:auto}
  .field-row > .field{grid-template-rows:none;gap:8px}
}
.opt-grid{display:grid;gap:10px}
/* Whole row is the hit target and it now wraps a real checkbox, so it works
   with the keyboard and reports its state to assistive tech. */
.opt{position:relative;display:flex;align-items:center;gap:14px;padding:16px 18px;border-radius:16px;background:var(--paper);cursor:pointer;
  box-shadow:0 0 0 1px var(--line) inset;transition:box-shadow var(--dur-2) var(--ease),background var(--dur-2) var(--ease)}
.opt:hover{box-shadow:0 0 0 1px rgba(154,107,28,.35) inset}
.opt.on{box-shadow:0 0 0 1.8px var(--brass) inset;background:rgba(154,107,28,.05)}
.opt .o-in{position:absolute;opacity:0;width:1px;height:1px;margin:0}
.opt:has(.o-in:focus-visible){outline:2px solid var(--focus);outline-offset:3px}
.opt .o-txt{display:grid;gap:3px;min-width:0}
.opt .o-n{font-weight:600;font-size:15.5px}
.opt .o-d{font-size:13.2px;color:var(--ink-soft);line-height:1.35}
.opt .o-p{margin-left:auto;font-family:var(--display);font-size:20px;color:var(--espresso);white-space:nowrap;font-variant-numeric:tabular-nums}
.opt .o-box{width:20px;height:20px;border-radius:6px;flex:none;box-shadow:0 0 0 1.5px var(--line) inset;display:grid;place-items:center;
  transition:background var(--dur-2) var(--ease),box-shadow var(--dur-2) var(--ease)}
.opt.on .o-box{background:var(--brass);box-shadow:none}
.opt .o-box svg{width:12px;height:12px;stroke:#fff;fill:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;opacity:0;transition:opacity var(--dur-2) var(--ease)}
.opt.on .o-box svg{opacity:1}
.slots{display:grid;grid-template-columns:minmax(0,1fr);gap:8px}
@media (min-width:360px){.slots{grid-template-columns:repeat(auto-fill,minmax(104px,1fr))}}
.slot{padding:12px 8px;border-radius:12px;background:var(--paper);box-shadow:0 0 0 1px var(--line) inset;text-align:center;cursor:pointer;border:none;
  min-height:44px;
  font-size:14px;font-weight:600;
  transition:background var(--dur-2) var(--ease),color var(--dur-2) var(--ease),box-shadow var(--dur-2) var(--ease)}
.slot:hover{box-shadow:0 0 0 1px var(--brass) inset;color:var(--brass-ink)}
.slot.on{background:var(--ink);color:var(--paper);box-shadow:none}
.slot small{display:block;font-weight:500;font-size:11.5px;opacity:.7;margin-top:2px}
.daygroup{margin-bottom:18px}
.daygroup h4{font-size:14px;font-family:var(--body);font-weight:700;letter-spacing:.01em;margin-bottom:9px;color:var(--ink)}
.summary{position:sticky;top:calc(var(--nav-h) + 14px);background:var(--onyx);color:var(--paper);border-radius:24px;padding:28px;display:grid;gap:16px}
.summary h3{color:var(--paper);font-size:23px}
.sum-rows{display:grid;gap:10px;padding:16px 0;border-top:1px solid var(--line-dk);border-bottom:1px solid var(--line-dk)}
.sum-row{display:flex;justify-content:space-between;gap:16px;font-size:14.5px;color:rgba(251,248,241,.76)}
.sum-row b{color:var(--paper);font-weight:600;white-space:nowrap}
.sum-total{display:flex;justify-content:space-between;align-items:flex-end;gap:16px}
.sum-total span{font-size:13px;color:rgba(251,248,241,.7)}
.sum-total b{font-family:var(--display);font-size:38px;color:var(--paper);font-weight:400;line-height:1}
.summary .btn{justify-content:center;padding-left:20px}
.summary .fineprint{font-size:12.5px;color:rgba(251,248,241,.6);line-height:1.5}
.bk-alert{padding:16px 18px;border-radius:16px;background:rgba(154,107,28,.09);box-shadow:0 0 0 1px rgba(154,107,28,.28) inset;font-size:14px;color:var(--ink-soft);line-height:1.5}
.bk-alert b{color:var(--ink)}
.spin{width:16px;height:16px;border-radius:999px;border:2px solid rgba(34,26,17,.18);border-top-color:var(--brass);animation:spin .7s linear infinite;display:inline-block}
@keyframes spin{to{transform:rotate(360deg)}}

/* ---------- sticky mobile CTA ---------- */
.mobile-cta{position:fixed;left:0;right:0;bottom:0;z-index:35;display:none;padding:12px 16px calc(12px + env(safe-area-inset-bottom));
  background:rgba(251,248,241,.86);backdrop-filter:blur(18px);-webkit-backdrop-filter:blur(18px);box-shadow:0 -1px 0 var(--line)}
.mobile-cta .btn{width:100%;justify-content:space-between}
/* JS arms it, then slides it in once the in-page CTA scrolls away. Without JS
   it simply stays visible — the no-script path keeps the CTA reachable. */
.js .mobile-cta.is-armed{transform:translateY(112%);transition:transform var(--dur-3) var(--ease)}
.js .mobile-cta.is-armed.is-up{transform:none}

/* ---------- final ---------- */
.final{text-align:center;display:grid;gap:var(--s-4);justify-items:center;padding:clamp(84px,11vw,150px) 0}
.final .jingle{width:38px;height:58px;color:var(--brass);opacity:.9}
.final h2{font-size:clamp(50px,9vw,124px)}
.final p{font-size:18px;color:var(--ink-soft);max-width:44ch}

/* ---------- footer ---------- */
footer{border-top:1px solid var(--line);padding:var(--s-7) 0 var(--s-7)}
.foot-grid{display:grid;grid-template-columns:1.3fr repeat(3,1fr);gap:var(--s-5);margin-bottom:var(--s-6)}
/* Was: font-family:var(--body);font-size:12.5px;letter-spacing:.06em;font-weight:700.
   Two things were wrong here and only one of them was visible.

   The visible one is the one Ayan named: .06em on 12.5px bold body type is the
   tracked-label look. Footer column headings are a carve-out from the eyebrow
   ban as a STRUCTURE — a footer needs its columns labelled — but the carve-out
   was never a licence for the eyebrow's STYLING, and that is what had survived
   down here after 23 of them were stripped off the rest of the site.

   The invisible one is that this rule only half-overrode h1..h4. It took the
   family, weight and tracking from here and then kept line-height:.96 from the
   display rule it was overriding — a 12px line box wrapped around 12.5px body
   type. Measured at 1280: the ink overflowed the box by 1.5px at the top and
   1.0px at the bottom, and "Design & Build" put 2.44px of 'g' descender into a
   box with 2.98px of room under the baseline. A heading that half-inherits
   from the rule it is overriding will always drift like this.

   So the override goes rather than getting patched. Family, weight and tracking
   now come from h1..h4 like every other heading on the site, and what stays is
   only what a footer heading legitimately owns: its size, a line-height with
   room for a descender, and space measured in em off its own size (the house
   rule — one fixed px cannot serve a 96px display line and a 16px sub-head).

   .5em, not the .96em the old 12px/12.5px worked out to. Measured ink-to-ink in
   the Services column, the old value put 29.5px between the heading baseline
   and the first link's cap — the SAME gap as the break before "All 27
   services", against a 23px rhythm between the links themselves. A heading
   separated from its own list by exactly as much as the list is separated from
   the thing after it is the flat-run failure: three different relationships
   spelled with two numbers. .5em lands it at 25.5 — bound to the list it
   labels, just above the item rhythm — and the break below is widened to 35.5
   so the three tiers actually differ. */
.foot-col h4{font-size:16px;line-height:1.25;color:var(--ink);margin-bottom:.5em}
/* The heading's gap has to be re-set at mobile because the LINKS' gap changes
   there and this one does not. Below 680px `.foot-col a` takes min-height:44px
   for the tap-target floor, which flex-centres 14px text in a 44px box and
   opens the link-to-link rhythm from 23px to 34px. The heading kept its 8px
   margin, so measured at 375 the column read 30.1 (heading to list) against 34
   (link to link): the heading sat TIGHTER to its list than the list sat to
   itself, so the first link looked annexed to the heading and the rest looked
   loose. 1em restores the order — 38.1 · 34 · 47.4. It lives here next to the
   rule it corrects rather than in the shared responsive block at the foot of
   the file, so the two halves of one decision are read together. */
@media (max-width:680px){ .foot-col h4{margin-bottom:1em} }
/* Scoped to links only. This used to match any <span> in a footer column,
   which caught the brand LOCKUP itself — a <span> — and shrank the logo from
   26px to 14px, recoloured it and gave it stray padding, on every page. */
.foot-col a{display:block;font-size:14px;color:var(--ink-soft);padding:6px 0;transition:color var(--dur-2) var(--ease)}
.foot-col a:hover{color:var(--brass-ink)}
/* The lane count — what makes the shortened footer honest instead of silently
   short. The column used to list 18 of 27 services (slice(0,3) and slice(0,6))
   with nothing to say so; now it lists all five lanes and states how deep each
   one is, so the footer tells you the catalogue is bigger than the footer.
   Scoped to `.foot-col a .foot-n` deliberately: a bare `.foot-col span` rule
   once matched the brand LOCKUP — itself a <span> — and shrank the logo from
   26px to 14px on every page (see the note on .foot-col a above). It is the
   same mistake waiting in the same place, so this selector names the class and
   sits inside the anchor. */
.foot-col a .foot-n{margin-left:8px;font-size:12.5px;color:var(--ink-soft);opacity:.5;font-variant-numeric:tabular-nums}
.foot-col a:hover .foot-n{opacity:.75}
/* "All 27 services" closes the lane list — a change of mode, not the sixth
   lane — so it takes space above it rather than sitting in the list rhythm,
   and it is the one brass thing in the column so the eye knows where
   "everything" is without reading. --s-2 rather than --s-1: measured, --s-1
   put this break at 29.5px ink-to-ink, identical to the heading's gap above
   the list, so the column had one number doing two jobs. At --s-2 the column
   reads 25.5 (heading to list) · 23 (link to link) · 35.5 (list to All). */
.foot-col a.foot-all{margin-top:var(--s-2);color:var(--brass-ink);font-weight:600}
/* Tiers, not one number. The lockup and the line that explains it are one
   object (bound); the Black link is a change of mode and takes the break tier,
   supplied by its own margin-top so the two gaps in this column differ. */
.foot-brand{display:grid;justify-items:start;gap:var(--s-2)}
.foot-brand .lockup{font-size:26px;color:var(--espresso)}
.foot-brand p{font-size:14px;color:var(--ink-soft);line-height:1.55;max-width:34ch}
.foot-base{display:flex;justify-content:space-between;align-items:center;gap:20px;flex-wrap:wrap;padding-top:var(--s-4);border-top:1px solid var(--line-2);font-size:13px;color:var(--ink-soft)}

/* ---------- reveal ----------
   The house entrance: fluid, blurry, motion-graphic, on everything including
   grids. Unchanged in character. What changed is the weight — it was
   blur(8px)/38px at .9s with the easing HARDCODED three times, so the site's
   own reveal did not use the site's own --ease. Everything is a token now, and
   the longest chain settles inside 860ms instead of 1580ms. */
.js .reveal{opacity:0;transform:translateY(var(--rv-y)) scale(var(--rv-scale));filter:blur(var(--rv-blur));
  transition:opacity var(--dur-4) var(--ease-out),transform var(--dur-4) var(--ease-out),filter var(--dur-4) var(--ease-out);
  transition-delay:var(--rd,0ms);will-change:transform,filter,opacity}
.reveal.in{opacity:1;transform:none;filter:blur(0)}
/* The escape hatch for the rule above. Set by the inline head script if site.js
   has not booted within 1.5s, and by that script tag's own onerror. Without it a
   failed script load leaves every element at opacity:0 and the page is blank. */
.js.reveal-off .reveal{opacity:1;transform:none;filter:none;transition:none}
/* Hand-placed positions in the chain. One step is --rv-stagger, capped at 6. */
.reveal[data-d="1"]{--rd:50ms}.reveal[data-d="2"]{--rd:100ms}
.reveal[data-d="3"]{--rd:150ms}.reveal[data-d="4"]{--rd:200ms}

/* ---------- responsive ---------- */
@media (max-width:1080px){
  .rail{grid-template-columns:repeat(3,1fr)}
  /* wrapping to 3 columns leaves row 2 with no top rule — give the items one */
  .rail-item:nth-child(n+4){border-top:1px solid var(--line)}
  .rail-item:nth-child(4){border-left:none}
  .two-col{grid-template-columns:1fr;gap:38px}
  .buybox,.summary{position:static}
  .bk{grid-template-columns:1fr;gap:28px}
  .steps{grid-template-columns:repeat(2,1fr)}
}
@media (max-width:820px){
  .foot-grid{grid-template-columns:1fr 1fr;gap:var(--s-5)}
  .foot-brand{grid-column:1 / -1}
}
@media (max-width:680px){
  :root{--gutter:20px;--stack:18px}
  .nav-links,.nav .btn{display:none}
  .nav-inner{padding:8px 8px 8px 18px}
  .burger{display:grid;place-items:center;width:44px;height:44px;border-radius:999px;background:rgba(74,42,21,.07);cursor:pointer;position:relative;margin-left:auto}
  .burger span{position:absolute;width:17px;height:1.6px;background:var(--espresso);transition:transform var(--dur-3) var(--ease)}
  .burger span:nth-child(1){transform:translateY(-4px)}.burger span:nth-child(2){transform:translateY(4px)}
  .burger.x span:nth-child(1){transform:rotate(45deg)}.burger.x span:nth-child(2){transform:rotate(-45deg)}
  .rail{grid-template-columns:1fr;border-top:1px solid var(--line)}
  /* Stacked, the five lanes ran together as one block — a rule per row so each
     reads as its own tappable option. */
  .rail-item{border-left:2px solid var(--line);border-top:1px solid var(--line-2);padding:16px 18px 18px}
  .rail-item:first-child{border-top:none}
  .rail-item.is-active{border-left-color:var(--brass)}
  .rail-item::before{display:none}
  .float-mark{display:none}
  .steps{grid-template-columns:1fr}
  /* .svc-grid is not listed here any more — one column is now its BASE rule
     and the extra tracks are added at 681px, so stating the collapse twice
     would be two breakpoints to keep in sync for one behaviour. */
  /* One column means there is nothing left to align ACROSS, and the subgrid
     would then hold stacked fields to the 8px internal gap — one field's error
     text 8px off the next field's label, which reads as one field. Hand the
     rows back to normal flow so field→field is 14px again, as it was. */
  .field-row{grid-template-columns:1fr;row-gap:14px}
  .field-row > *,.field-row > .field:not(:has(.hint:not([hidden]))):not(.has-err){grid-row:auto}
  .field-row > .field{grid-template-rows:none;gap:8px}
  .foot-grid{grid-template-columns:1fr;gap:var(--s-4)}
  /* 14px type at 5px padding was a 31px row — under the 44px touch minimum.
     Padding alone only got it to 39px, so state the floor and centre the
     label in it rather than leaving the extra height below the text. */
  .foot-col a{display:flex;align-items:center;min-height:44px;padding:6px 0}
  .mobile-cta{display:block}
  body.has-mcta{padding-bottom:calc(74px + env(safe-area-inset-bottom))}
  .finder{border-radius:22px}
  .priceline .big{font-size:clamp(38px,11vw,54px)}
  /* Centring a ragged wrap left one lonely pill under a row of five. */
  .credits,.strip-label,.credits-note{text-align:left;justify-content:flex-start}
  .credits-note{margin-left:0}
  .guarantee,.blk-house{padding:24px}
  .buybox,.summary{padding:24px}
  .tier,.svc,.earn{padding:22px}
  /* Narrow, the three-column row wrapped the price onto its own line hard right,
     leaving the amount orphaned from the thing it prices. Name and price pair
     up on the first line; the description takes the second. */
  .addon{align-items:baseline;column-gap:12px;row-gap:3px}
  .addon .a-n{order:1}
  .addon .a-p{order:2;margin-left:auto}
  .addon .a-d{order:3;flex:1 0 100%;min-width:0}
}
@media (prefers-reduced-motion:reduce){
  .js .reveal{transition:none;opacity:1;transform:none;filter:none}
  .float-mark,.hero-glow,.card-sheen,.blk-aura,.blk-rays,.spin{animation:none}
  /* The sheen now sweeps on background-position, so stopping the animation
     needs a resting position or the highlight parks off the card entirely. */
  .card-sheen{background-position:50% 0}
  .svc:hover,.tier:hover,.credits li:hover,.perk:hover{transform:none}
  .card3d,.card{transform:none!important;transition:none}
  html{scroll-behavior:auto}
  *,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important}
}

/* ==========================================================================
   v3.1 polish layer — components added in the 2026-07-29 pass
   ========================================================================== */

/* ---------- shared utilities that replaced inline styles ---------- */
/* Inside a .rgroup the stack owns the gap; the margin is only for the handful
   of places a note follows a button outside one. */
.cta-note{font-size:13.5px;line-height:1.55;color:var(--ink-soft);max-width:56ch;text-wrap:pretty}
.cta-note.on-black{color:rgba(251,248,241,.66)}
/* The closing CTA is an `rgroup` like every other stack on the site, so it gets
   the relationship tiers instead of one flat number. It used to declare its own
   `display:grid;gap:var(--s-4)`, which put the mark, the heading, the paragraph,
   the button pair and the risk-reversal note ALL 26px apart — five different
   relationships flattened to one value, on the last thing a visitor reads
   before deciding. Now `--stack` sets only the DEFAULT and the rgroup rules do
   the rest: a heading spaces by its own size, the CTA cluster takes a real
   break, and the note binds up to the button it belongs to. */
.final-wrap{--stack:var(--s-4);justify-items:center}
.final-cta{justify-content:center}
.final-svc{font-size:clamp(40px,7vw,86px)}
.lane-panel{outline:none}
.lane-blurb{margin-bottom:var(--s-4);max-width:60ch}
.band-actions{margin-top:var(--s-6);display:flex;gap:14px;flex-wrap:wrap;align-items:center}
.band-actions-note{font-size:14px;color:var(--ink-soft)}
.tier-min{margin-top:var(--s-3);font-size:13.5px;color:var(--ink-soft)}

/* ---------- referral worth table (/partners/) ----------
   Its own component rather than a reuse of .sum-row: that one is styled for
   the dark booking summary (paper text at 76%) and would have shipped light
   grey on sand. Spacing is by relationship — 6px binds the sub-line to its
   heading, 18px of padding either side of the rule separates one case from
   the next. The figure keeps the display face and tabular numerals so the
   column of amounts lines up on the decimal.

   2026-08-12 — the block is now a panel and every row carries an icon. The
   1.5px ink rule that used to sit on top of the list is gone, deliberately: it
   was there to give a bare list an edge, and a panel has its own. Nothing else
   in this band aligned to it (the band is band-head → .worth → .fineprint), so
   removing it costs no alignment — unlike the .step rules, which do align to
   each other and are untouched. On sand, a --surface panel is what separates
   "a table someone typed" from "a figure someone stands behind". */
.worth{display:grid;background:var(--surface);border-radius:22px;padding:var(--s-1) var(--s-4);
  box-shadow:0 0 0 1px var(--line-2) inset,0 26px 60px -42px rgba(34,26,17,.55)}
/* align-items:center, not baseline — baseline was right when the row was two
   text spans; with a 44px chip in the row it would hang the chip off the cap
   height of the title and leave the sub-line dangling below it. */
.worth-row{display:flex;align-items:center;gap:var(--s-4);
  padding:var(--s-3) 0;border-bottom:1px solid var(--line-2)}
.worth-row:last-child{border-bottom:none}
/* The chip is the same object as .guarantee's .g-ic — same brass wash, same
   radius-to-size ratio, one step down in size because it repeats five times.
   Deliberately NOT a new icon treatment. */
.worth-ic{width:44px;height:44px;border-radius:13px;flex:none;display:grid;place-items:center;
  background:rgba(154,107,28,.11);color:var(--brass);
  transition:background var(--dur-2) var(--ease),color var(--dur-2) var(--ease),
    transform var(--dur-3) var(--spring)}
.worth-ic svg{width:22px;height:22px;stroke:currentColor;fill:none;stroke-width:1.7;
  stroke-linecap:round;stroke-linejoin:round}
.worth-t{flex:1;min-width:0;font-size:16.5px;font-weight:600;color:var(--ink);line-height:1.35}
.worth-s{display:block;margin-top:var(--s-1);font-size:13.5px;font-weight:400;
  color:var(--ink-soft);line-height:1.45}
.worth-v{flex:none;font-family:var(--display);font-size:clamp(23px,5.4vw,29px);font-weight:400;
  line-height:1;letter-spacing:-.02em;color:var(--espresso);white-space:nowrap;
  font-variant-numeric:tabular-nums;transition:color var(--dur-2) var(--ease)}
.worth-row:hover .worth-ic{background:rgba(154,107,28,.2);transform:translateY(-2px)}
.worth-row:hover .worth-v{color:var(--brass-ink)}
@media(max-width:520px){
  /* Stacked, the chip has to stay level with the title it labels rather than
     riding above a two-line block, so the row becomes a grid and the figure
     drops into the TEXT column — flush with the title, not under the chip. */
  .worth-row{display:grid;grid-template-columns:auto minmax(0,1fr);
    column-gap:var(--s-3);row-gap:var(--s-2);align-items:start}
  .worth-ic{grid-row:1;width:40px;height:40px;border-radius:12px}
  .worth-ic svg{width:20px;height:20px}
  .worth-t,.worth-v{grid-column:2}
}

/* ---- .crd — the published-rate panel (/partners/) ------------------------
   Fills what was an empty left column under "Credit, not cash. On purpose."

   It shipped for a few hours as a fact PAIR (docs/10-interface-standard.md §7a)
   — our 20% against the 10–12% a cash scheme stretches to, with bars at the
   real 12/20 ratio. The comparison row was cut on the founder's call: that
   range has no source in docs/, and charting an unsourced competitor number
   against our own turns a remark into evidence. See lumoria-ship §1.

   What is left is ONE figure, so it is no longer a fact pair and must not be
   drawn like one. `.is-solo` removes the bar track: a comparison bar with
   nothing to compare against is decoration pretending to be data. The rules
   below are kept rather than deleted because the pair is the right pattern the
   day a sourced second number exists. */
.crd{display:grid;gap:var(--s-4);max-width:430px;background:var(--surface);
  border-radius:22px;padding:var(--s-4);
  box-shadow:0 0 0 1px var(--line-2) inset,0 26px 60px -42px rgba(34,26,17,.55)}
.crd-row{display:grid;grid-template-columns:auto minmax(0,1fr) auto;
  column-gap:var(--s-3);row-gap:var(--s-2);align-items:center}
.crd-ic{width:40px;height:40px;border-radius:12px;display:grid;place-items:center;
  background:rgba(154,107,28,.11);color:var(--brass)}
.crd-ic svg{width:20px;height:20px;stroke:currentColor;fill:none;stroke-width:1.7;
  stroke-linecap:round;stroke-linejoin:round}
.crd-lab{font-size:15px;font-weight:600;color:var(--ink);line-height:1.3}
.crd-sub{display:block;margin-top:var(--s-1);font-size:13px;font-weight:400;
  color:var(--ink-soft);line-height:1.4}
.crd-v{font-family:var(--display);font-size:27px;line-height:1;letter-spacing:-.02em;
  color:var(--espresso);white-space:nowrap;font-variant-numeric:tabular-nums}
.crd-bar{grid-column:2 / -1;height:5px;border-radius:999px;background:rgba(34,26,17,.07)}
/* The comparison bar is a LOW-contrast neutral, not --ink-soft. At --ink-soft
   it rendered darker than the brass bar above it, so the row we are arguing
   against was the most salient thing in the panel — the emphasis inverted. */
.crd-bar i{display:block;height:100%;border-radius:inherit;transform-origin:left center;
  transform:scaleX(var(--v,1));background:rgba(34,26,17,.26)}
/* The second row is the comparison, not the offer: muted figure, muted bar,
   .6 of the length because 12 is .6 of 20. Brass stays on the row we mean. */
.crd-row:not(.is-lead){--v:.6}
.crd-row:not(.is-lead) .crd-ic{background:rgba(34,26,17,.055);color:var(--ink-soft)}
.crd-row:not(.is-lead) .crd-v{color:var(--ink-soft)}
.crd-row.is-lead .crd-bar i{background:linear-gradient(90deg,var(--brass),var(--brass-bright))}
/* One figure, no comparison: drop the bar's row entirely rather than leave an
   empty 5px track paying the row-gap above it. */
.crd-row.is-solo{row-gap:0}
/* The three frictions credit removes. Break tier off the pair above it, because
   it is a change of mode — comparison into a list. */
.crd-list{display:grid;gap:var(--s-2);padding-top:var(--s-4);border-top:1px solid var(--line-2)}
.crd-list li{display:grid;grid-template-columns:auto minmax(0,1fr);column-gap:var(--s-2);
  align-items:center;font-size:14.5px;color:var(--ink-soft);line-height:1.45}
.crd-list li svg{width:17px;height:17px;color:var(--brass);stroke:currentColor;fill:none;
  stroke-width:1.9;stroke-linecap:round;stroke-linejoin:round}
/* The bars draw themselves in behind the panel's own entrance. Gated on .in,
   which means gated on JS — so the resting state is FULL and only .js zeroes
   it, and .reveal-off (the 1.5s failsafe) puts it back. A bar that needs an
   observer to have fired before it has a length is content gated on motion. */
.js .crd-bar i{transform:scaleX(0);transition:transform var(--dur-4) var(--ease-out);
  transition-delay:calc(var(--rd,0ms) + 140ms)}
.js .crd.in .crd-bar i{transform:scaleX(var(--v,1))}
.js.reveal-off .crd-bar i{transform:scaleX(var(--v,1));transition:none}
@media (prefers-reduced-motion:reduce){
  /* The global reduced-motion block un-gates .reveal without waiting for .in;
     these two ride on .in and hover, so they need saying here or the bars sit
     at zero and the chip still springs. */
  .js .crd-bar i{transform:scaleX(var(--v,1));transition:none}
  .worth-row:hover .worth-ic{transform:none}
}
.band-book{padding-top:var(--s-4)}
/* The hero opens on ~126–168px of air above the headline and used to close on
   26px, which with the band's own 26px left the lead sitting 52px off the top
   of the form card — the whole composition top-heavy, and the lead reading as
   a label on the card below rather than as part of the hero it belongs to.
   Bottom air roughly half the top air is the balance point: enough to close
   the header, not so much that the form drops below the fold. */
.book-hero{padding-bottom:clamp(44px,4.4vw,58px)}
.region-inline{font-size:13.5px;color:var(--ink-soft)}
/* replaces style="margin-top:…" in the generator */
.who-buys{font-size:16px;color:var(--ink-soft);line-height:1.55;max-width:56ch}
.who-buys b{color:var(--ink);font-weight:600}
.checklist-loose{gap:22px}

/* ---- .deflist — a tick, a term, and the description of that term ----------
   `.checklist` is a list of ticked SENTENCES. This is a different thing: each
   row is a term and its explanation, and the two are not interchangeable.

   Run inline (`<b>Term.</b> description…`) every row's description starts at a
   different x, because it starts wherever that row's term happened to end. Set
   five of them under each other and the left edge of the prose is a staircase;
   a long term ("Don't send it as us.") wraps onto a second line and shunts its
   own description into the middle of the row. That is the bug on /partners/.

   The fix is one shared grid, not a grid per row. The UL owns the columns and
   every LI is `display:contents`, so all five rows resolve against the SAME
   track sizes and the description column starts at one x for the whole list —
   `max-content` on the term track means that x is set by the longest term and
   nothing is truncated to get there. A per-row grid cannot do this: each row
   would size its own tracks and the staircase would come straight back.

   Below 900px `max-content` would leave the description a sliver, so the row
   stacks instead: the term on its own line, the description under it, both in
   the column beside the tick. The tick stays on row 1 so it reads as a marker
   for the pair rather than floating against the description. */
.deflist{display:grid;grid-template-columns:auto max-content minmax(0,1fr);
  column-gap:var(--s-3);row-gap:var(--s-4);align-items:start}
.deflist li{display:contents}
.deflist li svg{width:17px;height:17px;color:var(--brass);stroke:currentColor;fill:none;
  stroke-width:2.3;stroke-linecap:round;stroke-linejoin:round;margin-top:4px}
.deflist li b{color:var(--ink);font-weight:600;font-size:16px;line-height:1.5}
.deflist li span{color:var(--ink-soft);font-size:16px;line-height:1.5;text-wrap:pretty}
.on-dark .deflist li b{color:var(--paper)}
.on-dark .deflist li span{color:rgba(251,248,241,.82)}
.on-dark .deflist li svg{color:var(--brass-bright)}
@media (max-width:900px){
  .deflist{grid-template-columns:minmax(0,1fr);row-gap:var(--s-4)}
  .deflist li{display:grid;grid-template-columns:auto minmax(0,1fr);
    column-gap:13px;row-gap:var(--s-1)}
  .deflist li svg{grid-column:1;grid-row:1}
  .deflist li b,.deflist li span{grid-column:2}
}
.opt-lbl{font-weight:400;opacity:.7}
.sum-tax{font-size:11.5px;opacity:.75}
.foot-legal{display:flex;gap:20px;flex-wrap:wrap}
.bk-find{justify-self:start}
.nf{padding-top:clamp(150px,20vw,220px)}
/* Prices are compared down a column — line the digits up. */
.svc-price b,.bb-price,.t-amt,.sum-total b,.a-p,.priceline .big{font-variant-numeric:tabular-nums}

/* ---------- booking: goal gradient ----------
   Opens at 25%, never 0. The closer people feel to done, the faster they move. */
.bk-prog{display:grid;gap:9px;padding-bottom:20px;border-bottom:1px solid var(--line-2)}
.bk-prog-bar{height:4px;border-radius:999px;background:var(--sand-deep);overflow:hidden}
/* transform, not width — `width` triggers layout on every frame of the fill.
   Drive it by setting --p (0–1) on the span. */
.bk-prog-bar span{display:block;height:100%;width:100%;border-radius:999px;background:var(--brass);
  transform:scaleX(var(--p,.25));transform-origin:left;
  transition:transform var(--dur-3) var(--ease)}
.bk-prog-t{font-size:12.5px;font-weight:600;letter-spacing:.02em;color:var(--ink-soft)}

/* ---------- forms: inline errors + alert states ----------
   An error sits UNDER ITS FIELD, always. Never toast-only — a toast names a
   problem the user then has to go hunting for. The span is baked into the
   markup rather than created on failure, so the message has a reserved place in
   the layout and announcing it cannot shift the fields under the user's thumb. */
.field .err{font-size:12.5px;font-weight:600;color:var(--err);line-height:1.45;display:none}
.field.has-err .err{display:block}
.faq .fa a{color:var(--brass-ink);font-weight:600}
.field.has-err input,.field.has-err textarea,.field.has-err select{box-shadow:0 0 0 1.6px var(--err) inset;background:var(--err-bg)}
.field.has-err input:focus,.field.has-err textarea:focus,.field.has-err select:focus{
  outline-color:var(--err);box-shadow:0 0 0 1.6px var(--err) inset}
.bk-alert.is-ok{background:var(--ok-bg);box-shadow:0 0 0 1px rgba(47,107,68,.28) inset}
.bk-alert.is-bad{background:var(--err-bg);box-shadow:0 0 0 1px rgba(140,47,30,.26) inset}
.bk-alert a{color:var(--brass-ink);font-weight:600;text-decoration:underline;text-underline-offset:2px}
.summary .fineprint a{color:var(--brass-bright);font-weight:600}
/* `.btn` is already a flex row with `align-items:center`, so the spinner was
   being nudged 2px off the centre the flexbox had already given it. */
.btn .spin{margin-left:8px;border-color:rgba(255,255,255,.25);border-top-color:currentColor}

/* ---------- member block on the buy box ----------
   Ships in the quiet state: the line about members paying less, and the link
   that says how a seat is granted. `.bb-mem-price` is display:none until
   site.js adds `.on`, which it only does when the Console publishes a rate
   (ADR-038) — so the price row cannot flash an em dash on the way in, and a
   visitor with JS off never sees an empty "Member price" label. */
.bb-member{padding-top:14px;border-top:1px solid var(--line-2);font-size:12.5px;color:var(--ink-soft);line-height:1.45}
.bb-mem-h{display:flex;align-items:flex-start;gap:9px}
.bb-member .jingle{width:11px;height:17px;color:var(--brass);flex:none;margin-top:2px}
.bb-member a{color:var(--brass-ink);font-weight:600}
.bb-member a:hover{text-decoration:underline;text-underline-offset:2px}

.bb-mem-price{display:none}
.bb-member.on .bb-mem-price{
  display:flex;align-items:baseline;justify-content:space-between;gap:12px;
  margin-top:10px;padding:9px 12px;border-radius:10px;background:var(--onyx);color:var(--paper)}
.bb-mem-lab{font-size:11px;letter-spacing:.09em;text-transform:uppercase;color:var(--brass-bright)}
.bb-member.on .bb-mem-price b{font-size:19px;font-weight:700;letter-spacing:-.02em;color:var(--paper);font-variant-numeric:tabular-nums}

.bb-mem-cta{margin-top:8px;font-size:12px;color:var(--ink-soft)}

/* ---------- footer black link ----------
   A quiet rule, not a pill. This used to be a filled onyx 999px capsule — the
   only filled element in a footer of plain text links, which borrowed the
   site's primary-CTA vocabulary for what is a nav link. The action treatment
   is reserved for primary/buy actions. It also contradicted the product: a
   Black seat is granted, never sold, so nothing about it should look pressable
   for purchase. Same construction as .bb-member above — hairline, brass ink,
   the Jingle as the only accent. 44px tall, and it takes the break tier off
   the paragraph so the change of mode is read as a change of mode. */
.foot-col a.foot-black{display:flex;align-items:center;gap:9px;
  justify-self:stretch;max-width:34ch;
  margin-top:calc(var(--s-5) - var(--s-2));padding:11px 0;min-height:44px;
  border-top:1px solid var(--line);border-radius:0;background:none;
  color:var(--brass-ink);font-size:14px;font-weight:600;letter-spacing:0;
  transition:color var(--dur-2) var(--ease)}
.foot-col a.foot-black:hover{color:var(--brass-bright);background:none;transform:none}
.foot-black .jingle{width:11px;height:17px;flex:none;
  transition:transform var(--dur-3) var(--spring)}
.foot-col a.foot-black:hover .jingle{transform:translateY(-2px)}

/* ---------- legal pages ---------- */
.legal-head{padding:clamp(124px,15vw,160px) 0 6px}
.legal-head + .band-tight{padding-top:clamp(30px,3.4vw,46px)}
.legal-updated{font-size:14px;color:var(--ink-soft);max-width:62ch;line-height:1.55}
.legal{max-width:74ch}
/* A clause label belongs to the clause below it, so the space above is much
   bigger than the space below — that asymmetry is what makes a long legal page
   scannable. It was 34/10, which read as evenly spaced. */
.legal h2{font-family:var(--body);font-size:15px;font-weight:700;letter-spacing:.01em;line-height:1.4;color:var(--ink);margin:40px 0 8px}
.legal h2:first-child{margin-top:0}
.legal p{font-size:16px;color:var(--ink-soft);line-height:1.68;text-wrap:pretty}
.legal p b{color:var(--ink);font-weight:600}
.legal a{color:var(--brass-ink);font-weight:600;text-decoration:underline;text-underline-offset:2px}
.legal-foot{margin-top:44px;padding-top:24px;border-top:1px solid var(--line)}

/* ==========================================================================
   LUMORIA BLACK
   ========================================================================== */

/* --- the card. Rendered, not shipped as an image, so it stays sharp. --- */
.card3d{perspective:1400px;width:min(420px,100%);margin-inline:auto}
/* isolation:isolate contains .card-grain's overlay blend to the card. .card
   already makes a stacking context via transform, so this is insurance, not
   the corner fix — that lives on the two child layers below. */
.card{position:relative;aspect-ratio:1.586;border-radius:20px;overflow:hidden;isolation:isolate;padding:clamp(20px,4.6cqw,28px);
  display:grid;grid-template-rows:auto 1fr auto;container-type:inline-size;
  background:
    linear-gradient(147deg,#22201C 0%,#0B0A08 42%,#141210 72%,#050504 100%);
  box-shadow:
    0 0 0 1px rgba(205,160,85,.34) inset,
    0 1px 0 rgba(255,255,255,.09) inset,
    0 44px 80px -40px rgba(0,0,0,.9),
    0 12px 28px -18px rgba(0,0,0,.8);
  transform:rotateX(var(--rx,7deg)) rotateY(var(--ry,-13deg));
  transition:transform 900ms var(--ease-out)}
/* The Black card keeps a longer entrance and settle — it is the one documented
   exception to the 860ms ceiling, because the card IS the moment (ADR-048).
   will-change is scoped to the pointer interaction rather than left standing. */
.card3d:hover .card{transition:transform var(--dur-2) var(--ease);will-change:transform}
/* The corner fix. .card is a rounded overflow:hidden box carrying a real 3D
   transform, and WebKit downgrades that overflow clip to a RECTANGLE for any
   composited descendant of a 3D ancestor — so a radius-0 child paints square
   corners straight over the card's brass inset ring, which reads as a doubled
   or notched corner on iPhone. clip-path on .card would fix it in one line but
   would also clip the card's own outer drop shadows, so instead neither child
   is allowed to be rectangular or to overspill: both sit at inset:0 and carry
   the parent radius themselves. The sheen therefore sweeps on
   background-position rather than transform (a transformed layer would have to
   overspill to cross the card), and drops will-change, which was what forced
   promotion in the first place. */
.card-sheen{position:absolute;inset:0;border-radius:inherit;pointer-events:none;
  background:linear-gradient(102deg,transparent 32%,rgba(255,246,225,.16) 46%,rgba(205,160,85,.26) 50%,rgba(255,246,225,.12) 54%,transparent 68%);
  background-size:200% 100%;background-repeat:no-repeat;
  animation:sheen 9s var(--ease) infinite}
@keyframes sheen{0%,68%{background-position:150% 0}100%{background-position:-50% 0}}
.card-grain{position:absolute;inset:0;border-radius:inherit;pointer-events:none;opacity:.4;mix-blend-mode:overlay;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='c'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23c)'/%3E%3C/svg%3E")}
.card-top,.card-base{position:relative;z-index:2;display:flex;align-items:center;justify-content:space-between;gap:12px}
.card-mid{position:relative;z-index:2;display:grid;align-content:center;gap:5cqw;justify-items:start}
.card-mark{width:5.5cqw;height:8.7cqw;color:var(--brass-bright)}
.card-since,.card-holder,.card-no{font-size:2.5cqw;letter-spacing:.26em;text-transform:uppercase;color:rgba(205,160,85,.72);font-weight:600;white-space:nowrap}
.card-holder{color:rgba(251,248,241,.44)}
.card-no{font-variant-numeric:tabular-nums}
/* Width only — the viewBox on each svg supplies the height. See .blk-wm. */
.card-wm{width:44cqw;height:auto;color:#F6EFE1}
.card-blk{width:26cqw;height:auto;color:var(--brass-bright)}

/* --- homepage tease band --- */
.black-tease{position:relative;overflow:hidden;background:#080706;color:#F6EFE1;padding:var(--band-y) 0}
.black-veil{position:absolute;inset:0;pointer-events:none;
  background:radial-gradient(760px 460px at 78% 10%,rgba(205,160,85,.16),transparent 62%),
             radial-gradient(600px 420px at 6% 96%,rgba(205,160,85,.07),transparent 62%)}
.black-tease .lead{color:rgba(246,239,225,.72)}
.bt-inner{display:grid;grid-template-columns:1.1fr .9fr;gap:clamp(36px,5vw,72px);align-items:center}
.bt-inner h2{display:flex;align-items:baseline;gap:.22em;flex-wrap:wrap;color:#F6EFE1}
.bt-inner .lockup{color:#F6EFE1}
.bt-black{color:var(--brass-bright)}
.bt-card{perspective:1400px}

/* --- the page --- */
body.theme-black{
  --paper:#080706; --surface:#100E0C; --sand:#141210; --sand-deep:#1C1916;
  --ink:#F6EFE1; --ink-soft:#A79C8B; --espresso:#F6EFE1; --onyx:#050404;
  --line:rgba(205,160,85,.18); --line-2:rgba(246,239,225,.08);
  --nav-bg:rgba(12,10,9,.72); --nav-line:rgba(205,160,85,.2);
  --menu-bg:rgba(8,7,6,.96); --brand-ink:#F6EFE1; --focus:var(--brass-bright);
  color-scheme:dark;
}
body.theme-black::before{opacity:.05;mix-blend-mode:screen}
body.theme-black .btn-ghost{color:var(--ink);box-shadow:0 0 0 1px rgba(205,160,85,.34) inset}
body.theme-black .btn-ghost .ic-c{background:rgba(205,160,85,.12)}
body.theme-black .btn-ghost:hover{box-shadow:0 0 0 1px var(--brass-bright) inset}
body.theme-black footer{border-top-color:var(--line)}
/* No .foot-black override any more — it is a hairline text link, and on the
   black theme --brass-ink already flips to --brass-bright (see below). */
body.theme-black .nav-links a::after{background:var(--brass-bright)}
body.theme-black{--brass-ink:var(--brass-bright)}
.gold{color:var(--brass-bright)}

.btn-gold{background:linear-gradient(180deg,#E4BE7A,#B98B36);color:#120E07;
  box-shadow:0 1px 0 rgba(255,255,255,.35) inset,0 18px 40px -18px rgba(205,160,85,.6)}
.btn-gold .ic-c{background:rgba(18,14,7,.16)}
.btn-gold:hover{box-shadow:0 1px 0 rgba(255,255,255,.35) inset,0 24px 50px -18px rgba(205,160,85,.75)}
.btn-gold:hover .ic-c{transform:translate(3px,-2px);background:#120E07}
.btn-gold:hover .ic-c svg{stroke:#E4BE7A}
.btn-black{background:#F6EFE1;color:#0A0806;box-shadow:0 16px 36px -18px rgba(0,0,0,.9)}
.btn-black .ic-c{background:rgba(10,8,6,.1)}
.btn-black:hover .ic-c{transform:translate(3px,-2px);background:var(--brass-bright)}

.blk-hero{position:relative;overflow:hidden;padding:calc(var(--head-top) + 14px) 0 clamp(56px,7vw,90px)}
/* Longer, softer falloffs than the first version (.2 over 60% became .15 over
   78%). A radial gradient that reaches its end colour quickly quantises into
   visible rings on a near black ground; stretching the ramp puts more screen
   between each 1/255 step than the eye can resolve. */
.blk-aura{position:absolute;inset:0;pointer-events:none;
  background:radial-gradient(980px 620px at 76% 0%,rgba(205,160,85,.15),rgba(205,160,85,.045) 46%,transparent 78%),
             radial-gradient(820px 580px at 2% 96%,rgba(205,160,85,.07),rgba(205,160,85,.02) 48%,transparent 80%);
  animation:glowdrift 22s ease-in-out infinite}
/* The rays stay — they are the reason the Black hero feels like a room with a
   light in it. What goes is the cost. This was 150vw × 150vh, so on a laptop it
   was a ~2.9-megapixel layer rotating forever, and being a RECTANGLE its painted
   bounding box grew and shrank through every degree of the rotation. A SQUARE
   rotating about its own centre keeps a constant bounding box, so the compositor
   holds one texture and just spins it — and capping the side halves the pixels
   again. Same light, roughly a third of the work.

   THEN IT LOOKED LIKE A BUG, AND IT WAS TWO. Founder: "background gradient anim
   is buggy in visual on black page."
     1. A CONIC GRADIENT'S STOPS ARE HARD EDGES. `transparent 0deg` straight to
        `rgba(...,.09) 14deg` draws a visible straight line, and rotating it
        swept that line across the hero once a minute — a wedge with a crisp
        border, which is exactly what a rendering fault looks like. Each ray now
        fades in and out through intermediate stops, so it has no edge to see.
     2. THE LAYER'S OWN SQUARE EDGE WAS VISIBLE. The gradient ran to the element
        bounds and stopped, so the rotating box cut a second straight line
        across the section. A radial mask fades the whole layer to nothing well
        inside its own bounds, which means there is no boundary to catch.
   Lower opacity as well: light this soft cannot band, and banding on a near
   black ground is the other thing that reads as a fault on an 8-bit display. */
.blk-rays{position:absolute;top:34%;left:50%;width:min(900px,150vw);height:min(900px,150vw);
  transform:translate(-50%,-50%);pointer-events:none;opacity:.42;
  background:conic-gradient(from 200deg at 62% 18%,
    transparent 0deg,
    rgba(205,160,85,.012) 5deg, rgba(205,160,85,.05) 13deg, rgba(205,160,85,.075) 18deg,
    rgba(205,160,85,.04) 25deg, rgba(205,160,85,.01) 33deg, transparent 42deg,
    transparent 172deg,
    rgba(205,160,85,.01) 184deg, rgba(205,160,85,.035) 196deg, rgba(205,160,85,.05) 203deg,
    rgba(205,160,85,.03) 212deg, rgba(205,160,85,.008) 222deg, transparent 232deg,
    transparent 360deg);
  -webkit-mask-image:radial-gradient(closest-side,#000 22%,rgba(0,0,0,.55) 52%,transparent 82%);
  mask-image:radial-gradient(closest-side,#000 22%,rgba(0,0,0,.55) 52%,transparent 82%);
  animation:rays 44s linear infinite}
@keyframes rays{to{transform:translate(-50%,-50%) rotate(360deg)}}
.blk-hero-grid{display:grid;grid-template-columns:1.05fr .95fr;gap:clamp(40px,6vw,88px);align-items:center}
/* This h1 is two vector wordmarks, not text, so the em-proportional heading
   space can't scale off a font-size — it needs the air stated outright. */
.blk-title{display:grid;gap:clamp(10px,1.4vw,18px);justify-items:start;margin-bottom:clamp(6px,1.3vw,16px)}
/* No aspect-ratio here any more. It was stated twice — once in this file as a
   pair of magic numbers and once in the font outline the mark is generated
   from — and the day "BLACK" became "Black" the two disagreed and the mark
   stretched. Each svg now carries the geometry's own viewBox, which gives it
   an intrinsic ratio, so height:auto is correct by construction. */
.blk-wm{width:min(430px,72vw);height:auto;color:#F6EFE1}
.blk-wm-2{width:min(232px,40vw);color:var(--brass-bright)}
.blk-rule{font-size:14.5px;letter-spacing:.02em;color:var(--brass-bright);font-weight:600;text-wrap:pretty}
.blk-card-wrap{perspective:1500px}

.blk-band{padding:var(--band-y) 0;border-top:1px solid var(--line-2)}
.blk-note{margin-top:var(--s-4);font-size:14px;color:var(--brass-bright);font-weight:600}

.perks{list-style:none;display:grid;grid-template-columns:minmax(0,1fr);gap:2px 44px;counter-reset:p}
@media (min-width:780px){.perks{grid-template-columns:repeat(auto-fit,minmax(320px,1fr))}}
/* SEVEN ITEMS DO NOT DIVIDE BY TWO OR THREE, so the last row was always short.
   Spanning the final item across whatever tracks exist (1 / -1 resolves against
   the explicit tracks auto-fit creates) squares the block off at every
   breakpoint without inventing an eighth benefit. Its text keeps a normal
   measure rather than stretching to the full width — a full-bleed line of 15px
   prose is unreadable and would look like a mistake of a different kind. */
@media (min-width:780px){
  .perk:last-child{grid-column:1 / -1}
  .perk:last-child p{max-width:64ch}
}
.perk{display:flex;gap:18px;align-items:flex-start;padding:26px 0;border-top:1px solid var(--line-2);
  transition:transform var(--dur-3) var(--ease)}
.perk:hover{transform:translateX(4px)}
/* A tile, not a bare glyph. Seven identical sparkles read as decoration; seven
   different marks in seven identical frames read as a legend, and the frame is
   what keeps a tag, a phone and a door optically the same size. */
.perk-i{width:38px;height:38px;flex:none;margin-top:3px;border-radius:11px;display:grid;place-items:center;
  background:rgba(205,160,85,.12);box-shadow:0 0 0 1px rgba(205,160,85,.2) inset;
  transition:background var(--dur-3) var(--ease)}
.perk-i svg{width:19px;height:19px;color:var(--brass-bright);stroke:currentColor;fill:none;
  stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}
.perk:hover .perk-i{background:rgba(205,160,85,.2)}
.perk h3{font-size:23px;margin-bottom:8px;color:var(--ink)}
.perk p{font-size:15px;color:var(--ink-soft);line-height:1.6;max-width:46ch;text-wrap:pretty}

.earn-grid{display:grid;grid-template-columns:minmax(0,1fr);gap:16px}
@media (min-width:620px){
  .earn-grid{grid-template-columns:repeat(auto-fit,minmax(250px,1fr))}
  /* Five cards, same problem, same answer — the fifth spans the row it lands
     on rather than sitting alone with a gap beside it. */
  .earn:last-child{grid-column:1 / -1}
}
.earn{padding:26px 26px 28px;border-radius:20px;background:var(--surface);box-shadow:0 0 0 1px var(--line-2) inset;
  display:grid;gap:12px;align-content:start;
  transition:box-shadow var(--dur-3) var(--ease),transform var(--dur-3) var(--ease)}
.earn:hover{box-shadow:0 0 0 1px rgba(205,160,85,.36) inset;transform:translateY(-4px)}
.earn-i{width:40px;height:40px;border-radius:12px;display:grid;place-items:center;
  background:rgba(205,160,85,.12);box-shadow:0 0 0 1px rgba(205,160,85,.2) inset}
.earn-i svg{width:20px;height:20px;color:var(--brass-bright);stroke:currentColor;fill:none;
  stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}
.earn h3{font-size:24px;margin:0}
.earn p{font-size:14.6px;color:var(--ink-soft);line-height:1.58;text-wrap:pretty;margin:0;max-width:52ch}

/* ---------- what is planned, which is a list and used to be a paragraph ---- */
.laters{list-style:none;display:grid;grid-template-columns:minmax(0,1fr);gap:2px 44px;margin:0;padding:0}
@media (min-width:720px){.laters{grid-template-columns:repeat(2,minmax(0,1fr))}}
.later{display:flex;gap:18px;align-items:flex-start;padding:24px 0;border-top:1px solid var(--line-2)}
.later-i{width:38px;height:38px;flex:none;margin-top:2px;border-radius:11px;display:grid;place-items:center;
  background:rgba(205,160,85,.09);box-shadow:0 0 0 1px rgba(205,160,85,.16) inset}
.later-i svg{width:19px;height:19px;color:var(--brass-bright);opacity:.85;stroke:currentColor;fill:none;
  stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}
.later h3{font-size:20px;margin-bottom:6px;color:var(--ink)}
.later p{font-size:14.6px;color:var(--ink-soft);line-height:1.58;max-width:42ch;text-wrap:pretty}

.blk-house{display:flex;align-items:flex-start;gap:22px;flex-wrap:wrap;margin-top:var(--s-6);padding:28px 32px;border-radius:22px;
  background:linear-gradient(180deg,rgba(205,160,85,.11),rgba(205,160,85,.03));box-shadow:0 0 0 1px rgba(205,160,85,.28) inset}
.blk-house .g-ic{width:46px;height:46px;border-radius:13px;flex:none;display:grid;place-items:center;background:rgba(205,160,85,.16);color:var(--brass-bright)}
.blk-house .g-ic svg{width:24px;height:24px;stroke:currentColor;fill:none;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
.blk-house p{font-size:15.5px;color:var(--ink-soft);line-height:1.6;flex:1;min-width:260px;text-wrap:pretty}
.blk-house p b{color:var(--ink);font-weight:600}
.blk-honest{font-size:13.5px;line-height:1.6;color:var(--ink-soft);
  padding-left:16px;border-left:2px solid rgba(205,160,85,.4);text-wrap:pretty;max-width:62ch}
/* It closes the band now rather than hanging off a column, so it needs the
   break tier above it — the caveat is a change of mode from the list it
   qualifies, not the next item in it. */
.blk-later .blk-honest{margin-top:var(--s-6)}

.blk-final{text-align:center;display:grid;gap:var(--s-4);justify-items:center;padding:clamp(80px,11vw,150px) 0;
  border-top:1px solid var(--line-2)}
/* An `rgroup`, for the same reason `.final-wrap` is one: the mark, the heading,
   the paragraph, the button pair and the "already a member" line were all 26px
   apart on the closing CTA of the membership page. */
.blk-final .wrap{--stack:var(--s-4);justify-items:center}
.blk-mark{width:34px;height:54px;color:var(--brass-bright);opacity:.9}
.blk-final h2{font-size:clamp(40px,6.4vw,84px)}
.blk-final p{font-size:17.5px;color:var(--ink-soft);max-width:52ch;line-height:1.6;text-wrap:pretty}
.blk-cta{justify-content:center}
.blk-already{font-size:13.5px!important;color:var(--ink-soft)!important;max-width:46ch!important}

@media (max-width:900px){
  .bt-inner,.blk-hero-grid{grid-template-columns:1fr}
  .bt-card{order:-1;max-width:360px}
  .blk-card-wrap{max-width:380px}
  .perks{gap:0}
}
@media (max-width:680px){
  .brand{font-size:17px}
  .credits li{padding:8px 14px}
  .credits b{font-size:13.5px}
  .card3d{width:min(340px,100%)}
  .blk-house{padding:24px}
}

/* The card tilt is a pointer affordance; on touch it just sits still. */
@media (hover:none){ .card{transform:rotateX(4deg) rotateY(-8deg)} }

/* nav lift on scroll — a class swap, not a per-scroll-event inline style write */
.nav-inner.is-lifted{box-shadow:0 0 0 1px rgba(154,107,28,.18) inset,0 18px 50px -24px rgba(34,26,17,.5)}
body.theme-black .nav-inner.is-lifted{box-shadow:0 0 0 1px rgba(205,160,85,.28) inset,0 18px 50px -24px rgba(0,0,0,.9)}
.fres[aria-selected="true"]{background:var(--sand)}
.fres-empty a{color:var(--brass-ink);font-weight:600}

/* ==========================================================================
   THE QUOTE CONFIGURATOR  (/book/)
   Appended layer — nothing above this line is modified.

   Spacing here is by RELATIONSHIP, not by one number: 12px binds a thing to
   the thing it belongs to (a note under its control), 22px is the default
   flow, 30–36px marks a change of mode (one step into the next). Space after
   a heading is em on the heading, so it scales with the type instead of
   fighting it.
   ========================================================================== */

.qc{display:grid;grid-template-columns:1.3fr .85fr;gap:clamp(28px,3.6vw,48px);align-items:start}
.qc-main{display:grid;gap:var(--s-8);min-width:0}
.qc-step{display:grid;gap:var(--s-4);min-width:0}
.qc-h{font-size:clamp(25px,3vw,34px);margin-bottom:.34em}
/* The heading owns its own space; the grid gap must not add a second one. */
.qc-step > .qc-h{margin-bottom:calc(.34em - var(--s-4))}
.qc-sub{font-size:15px;color:var(--ink-soft);line-height:1.5;text-wrap:pretty;margin-top:calc(-1 * var(--s-4) + var(--s-2))}
.qnote{font-size:13px;color:var(--ink-soft);line-height:1.5;text-wrap:pretty}
.qrecheck{padding:14px 16px;border-radius:14px;background:var(--sand);color:var(--ink)}
/* A stack of fields inside a step. THREE TIERS, not one number
   (lumoria-build "spacing expresses relationship"; a flat column of identical
   gaps is the defect even when nothing collides):

     bound  8px  — label → control → hint → err, inside one .field
     flow   22px — field → field, here, because they answer one question
     break  72px — step → step, the .qc-main gap: a change of mode

   Before this the step itself was the only container, so every stacked pair —
   label group to label group, and the last field to the next step's heading —
   sat at the same 26px, and the contact step read as one undifferentiated
   column of six boxes. */
.qfields{display:grid;gap:var(--stack);min-width:0}
.qfields > *{min-width:0;max-width:100%}

/* ---------- the property card ---------- */
.qprops{display:grid;gap:var(--s-4)}
.qprop{background:var(--surface);border-radius:24px;padding:clamp(20px,3vw,28px);
  box-shadow:0 0 0 1px var(--line) inset;display:grid;gap:var(--s-4);min-width:0}
.qprop-top,.qprop-lab{display:none}
.qprops.is-multi .qprop-top{display:flex;align-items:center;justify-content:space-between;gap:12px}
.qprops.is-multi .qprop-lab{display:grid}
.qprop-n{font-size:12.5px;font-weight:700;color:var(--ink-soft)}
.qprop-x{border:none;background:none;padding:6px 2px;min-height:44px;font:inherit;font-size:13px;font-weight:600;
  color:var(--ink-soft);cursor:pointer;text-decoration:underline;text-underline-offset:3px}
.qprop-x:hover{color:var(--err)}
/* Fires when a property card is ADDED. It used to run at .9s with a 14px blur
   on every quote recalculation, so every tier, speed and option click re-blurred
   the card — a price that re-blurs each time you poke it is unreadable. It is on
   the entrance tokens now, and book.js only applies it to a genuinely new card. */
@keyframes qin{from{opacity:0;transform:translateY(var(--rv-y)) scale(var(--rv-scale));filter:blur(var(--rv-blur))}
  to{opacity:1;transform:none;filter:blur(0)}}
.qprop-new{animation:qin var(--dur-4) var(--ease-out) both}

.qtypes{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}
.qtype{display:grid;gap:3px;text-align:left;padding:14px 15px;border:none;border-radius:16px;background:var(--paper);
  box-shadow:0 0 0 1px var(--line-2) inset;cursor:pointer;font:inherit;color:var(--ink);min-width:0;
  transition:box-shadow var(--dur-3) var(--ease),background var(--dur-3) var(--ease),transform var(--dur-3) var(--spring)}
.qtype b{font-size:15.5px;font-weight:600;letter-spacing:-.01em}
.qtype small{font-size:11.8px;color:var(--ink-soft);line-height:1.3}
.qtype:hover{box-shadow:0 0 0 1px rgba(154,107,28,.36) inset}
.qtype.is-on{background:rgba(154,107,28,.06);box-shadow:0 0 0 1.8px var(--brass) inset}

.qsteps{display:grid;gap:var(--s-2)}
.qstep{display:flex;align-items:center;gap:14px;padding:12px 8px 12px 4px;border-bottom:1px solid var(--line-2)}
.qstep:last-child{border-bottom:none}
.qstep-l{flex:1;min-width:0;font-size:15.5px;font-weight:600;letter-spacing:-.01em;display:grid;gap:3px}
.qstep-l small{font-size:12.4px;font-weight:400;color:var(--ink-soft);line-height:1.35}
.qstep-c{display:flex;align-items:center;gap:4px;flex:none}
.qstep-b{width:44px;height:44px;border-radius:999px;border:none;background:var(--paper);cursor:pointer;
  font:inherit;font-size:19px;line-height:1;color:var(--ink);box-shadow:0 0 0 1px var(--line) inset;
  display:grid;place-items:center;transition:box-shadow var(--dur-2) var(--ease),transform var(--dur-2) var(--spring),opacity var(--dur-2)}
.qstep-b:hover{box-shadow:0 0 0 1.4px var(--brass) inset;color:var(--brass-ink)}
.qstep-b:active{transform:scale(.92)}
.qstep-b[disabled]{opacity:.3;cursor:default;box-shadow:0 0 0 1px var(--line-2) inset}
.qstep-v{min-width:34px;text-align:center;font-family:var(--display);font-size:22px;line-height:1;
  font-variant-numeric:tabular-nums;color:var(--espresso)}
.qstep.is-set .qstep-v{color:var(--brass-ink)}

.qassume{font-size:14.6px;line-height:1.7;color:var(--ink-soft);text-wrap:pretty;
  padding:14px 16px;border-radius:16px;background:var(--sand)}
/* The spaces they set themselves carry the weight — those are the levers. The
   inferred ones sit quiet, which is exactly what they are: our assumption,
   stated so it can be corrected. */
.qchip{white-space:nowrap}
.qchip.is-set{color:var(--ink);font-weight:600}
/* Bound to the line it qualifies, not floating in the default flow. */
.qassume + .qnote{margin-top:calc(-1 * var(--s-4) + var(--s-2))}

.qmore{border-top:1px solid var(--line-2);padding-top:var(--s-3)}
.qmore > summary{list-style:none;cursor:pointer;font-size:14.5px;font-weight:600;color:var(--brass-ink);
  display:flex;align-items:center;gap:9px;padding:4px 0;min-height:44px}
.qmore > summary::-webkit-details-marker{display:none}
.qmore > summary::before{content:"";width:8px;height:8px;border-right:1.8px solid currentColor;border-bottom:1.8px solid currentColor;
  transform:rotate(45deg) translate(-2px,-2px);transition:transform var(--dur-2) var(--ease);flex:none}
.qmore[open] > summary::before{transform:rotate(-135deg) translate(-3px,-3px)}
.qmore-in{padding-top:var(--s-2)}

.qextras{display:grid;gap:var(--s-2);border-top:1px solid var(--line-2);padding-top:var(--s-4)}
.qextras-h{font-family:var(--body);font-size:13px;font-weight:700;letter-spacing:.01em;color:var(--ink);margin-bottom:.2em}
.qextras .opt-grid{gap:8px}
.qextras .qnote{margin-top:var(--s-1)}

.qadd{display:flex;align-items:center;gap:14px;width:100%;text-align:left;padding:18px 20px;border:none;cursor:pointer;
  border-radius:20px;background:transparent;box-shadow:0 0 0 1.5px var(--line) inset;font:inherit;color:var(--ink);
  transition:box-shadow var(--dur-3) var(--ease),background var(--dur-3) var(--ease)}
.qadd:hover{box-shadow:0 0 0 1.5px var(--brass) inset;background:rgba(154,107,28,.04)}
.qadd-p{width:32px;height:32px;border-radius:999px;background:var(--ink);color:var(--paper);flex:none;
  display:grid;place-items:center;font-size:19px;line-height:1}
.qadd-t{display:grid;gap:3px;font-size:15.5px;font-weight:600;letter-spacing:-.01em;min-width:0}
.qadd-t small{font-size:12.4px;font-weight:400;color:var(--ink-soft);line-height:1.35}

/* ---------- finish + speed cards ---------- */
.qtiers{display:grid;grid-template-columns:repeat(3,1fr);gap:12px}
.qtier{position:relative;display:grid;gap:8px;align-content:start;text-align:left;padding:22px 20px 24px;border:none;cursor:pointer;
  border-radius:20px;background:var(--surface);box-shadow:0 0 0 1px var(--line) inset;font:inherit;color:var(--ink);min-width:0;
  transition:box-shadow var(--dur-3) var(--ease),transform var(--dur-3) var(--spring),background var(--dur-3) var(--ease)}
.qtier:hover{box-shadow:0 0 0 1px var(--brass) inset;transform:translateY(-3px)}
.qtier.is-on{box-shadow:0 0 0 2px var(--brass) inset;background:rgba(154,107,28,.05)}
.qtier-pick{position:absolute;top:-9px;left:20px;padding:4px 11px;border-radius:999px;background:var(--brass);color:#fff;
  font-size:11.5px;font-weight:700;letter-spacing:.01em}
.qtier-n{font-family:var(--display);font-size:27px;line-height:1;letter-spacing:-.02em}
.qtier-amt{font-family:var(--display);font-size:34px;line-height:1;letter-spacing:-.02em;color:var(--espresso);
  font-variant-numeric:tabular-nums;margin-bottom:.1em}
.qtier.is-on .qtier-amt{color:var(--brass-ink)}
.qtier-line{font-size:14px;color:var(--ink);line-height:1.45;text-wrap:pretty}
.qtier-note{font-size:12.6px;color:var(--ink-soft);line-height:1.45;text-wrap:pretty}

.qspeeds{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}
.qspeed{display:grid;gap:5px;text-align:left;padding:16px 17px 18px;border:none;cursor:pointer;border-radius:18px;
  background:var(--surface);box-shadow:0 0 0 1px var(--line) inset;font:inherit;color:var(--ink);min-width:0;
  transition:box-shadow var(--dur-3) var(--ease),background var(--dur-3) var(--ease)}
.qspeed:hover{box-shadow:0 0 0 1px var(--brass) inset}
.qspeed.is-on{box-shadow:0 0 0 2px var(--brass) inset;background:rgba(154,107,28,.05)}
.qspeed-n{font-size:16px;font-weight:600;letter-spacing:-.01em}
.qspeed-d{font-size:12.5px;color:var(--ink-soft);line-height:1.35}
.qspeed-amt{margin-top:4px;font-family:var(--display);font-size:21px;line-height:1;color:var(--espresso);font-variant-numeric:tabular-nums}

.qwhere{display:grid;gap:var(--s-3);margin-top:var(--s-3);padding-top:var(--s-5);border-top:1px solid var(--line-2)}
.qwhere .btn{justify-self:start}
.qslots-h{font-family:var(--body);font-size:13px;font-weight:700;letter-spacing:.01em;margin-bottom:.9em}

/* ---------- the receipt ---------- */
.qc-side{position:sticky;top:calc(var(--nav-h) + 14px)}
/* The recap panel is the last thing read before paying, and it ran the line
   items, the total, the volume note, the CTA and the fineprint ALL 18px apart.
   The total is the conclusion of the rows, the note explains the total, the
   button is a change of mode and the fineprint is bound to the button — four
   different relationships that were all spelled the same. */
.qrec{--stack:var(--s-3);background:var(--onyx);color:var(--paper);border-radius:24px;padding:26px;display:grid;gap:var(--stack)}
.qrec > .qrec-total + .qvol{margin-top:calc(var(--s-2) - var(--stack))}
.qrec > * + .qcta{margin-top:calc(var(--s-5) - var(--stack))}
.qrec > .qcta + .fineprint{margin-top:calc(var(--s-2) - var(--stack))}
.qrec-h{color:var(--paper);font-size:22px;margin-bottom:.15em}
.qrec-rows{display:grid;gap:11px;padding:16px 0;border-top:1px solid var(--line-dk);border-bottom:1px solid var(--line-dk)}
.qrec-row{display:flex;justify-content:space-between;align-items:baseline;gap:16px;font-size:14px}
.qrec-l{color:rgba(251,248,241,.8);display:grid;gap:2px;min-width:0}
.qrec-l small{font-size:11.8px;color:rgba(251,248,241,.5);line-height:1.35}
.qrec-row b{color:var(--paper);font-weight:600;white-space:nowrap;font-variant-numeric:tabular-nums}
.qrec-row.is-off b{color:var(--brass-bright)}
.qrec-row.is-black .qrec-l{color:var(--brass-bright)}
.qrec-total{display:flex;justify-content:space-between;align-items:flex-end;gap:16px}
.qrec-total > span{font-size:13px;color:rgba(251,248,241,.7)}
.qrec-total b{font-family:var(--display);font-size:40px;line-height:1;font-weight:400;color:var(--paper);font-variant-numeric:tabular-nums}
.qvol{font-size:12.8px;line-height:1.5;color:rgba(251,248,241,.62);text-wrap:pretty}
.qvol b{color:var(--brass-bright);font-weight:600}
.qblack{font-size:12.8px;line-height:1.5;color:var(--brass-bright);font-weight:600}
.qcta{justify-content:space-between;width:100%}
/* The statutory cancellation consent. Deliberately quiet rather than a warning
   box — it is a normal part of booking, and dressing it as a hazard would cost
   conversions for a sentence that protects both sides. 44px minimum touch
   target on the control per docs/10-interface-standard.md. */
.qconsent{display:flex;gap:11px;align-items:flex-start;margin:0 0 14px;cursor:pointer;
  font-size:13px;line-height:1.5;color:var(--soft,#5C4F3D)}
.qconsent input{flex:0 0 auto;width:20px;height:20px;margin:1px 0 0;accent-color:var(--brass,#9A6B1C);
  cursor:pointer}
.qconsent span{min-width:0}
.qconsent.miss{color:var(--bad,#8C2F2F)}
.qconsent.miss input{outline:2px solid var(--bad,#8C2F2F);outline-offset:2px}
.qconsent-err{margin:-8px 0 12px;font-size:12.5px;color:var(--bad,#8C2F2F)}
.qrec .fineprint{font-size:12.3px;color:rgba(251,248,241,.6);line-height:1.55;text-wrap:pretty}
.qrec .fineprint b{color:var(--paper)}
/* The number moved because you moved it. */
@keyframes qtick{0%{transform:scale(1)}38%{transform:scale(1.055)}100%{transform:scale(1)}}
.is-tick{animation:qtick .42s var(--spring);transform-origin:right center;display:inline-block}
.qtier-amt.is-tick{transform-origin:left center}

.qdone{grid-column:1/-1;text-align:center;display:grid;justify-items:center;gap:var(--s-3);padding:clamp(46px,7vw,72px) 24px}
.qdone .lead{margin:0 auto}
.qdone-note{font-size:14px;color:var(--ink-soft);max-width:46ch}
.qdone .btn{margin-top:var(--s-3)}

/* ---------- the sticky price bar ----------
   configure → price → pay is the money path, so the number and the action are
   never more than a thumb away from each other. */
.qbar{position:fixed;left:0;right:0;bottom:0;z-index:36;display:none;
  padding:11px 16px calc(11px + env(safe-area-inset-bottom));
  background:rgba(251,248,241,.9);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);
  box-shadow:0 -1px 0 var(--line),0 -18px 40px -30px rgba(34,26,17,.6)}
.js .qbar{transition:transform var(--dur-3) var(--ease)}
/* Down, not gone — the receipt's own button is on screen and owns the action. */
.js .qbar.is-down{transform:translateY(118%);pointer-events:none}
.qbar-in{display:flex;align-items:center;gap:14px;max-width:var(--maxw);margin:0 auto}
.qbar-p{display:grid;gap:1px;flex:none}
.qbar-p small{font-size:11px;color:var(--ink-soft);letter-spacing:.01em}
.qbar-p b{font-family:var(--display);font-size:27px;line-height:1;color:var(--espresso);font-variant-numeric:tabular-nums}
.qbar .btn{flex:1;justify-content:space-between;min-width:0}
.qbar .btn .btn-t{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}

/* ---------- motion: house rule, on the shared tokens ----------
   This was the heaviest entrance in the estate — blur(17px), 50px of travel and
   1.1s, on the one page where the visitor is trying to read a price. It now
   uses the same tokens as every other surface: the entrance still blurs in and
   still cascades, it just stops taking over a second to become legible.

   The per-child delay is written inline by the generator so it cannot depend on
   a script having run first. */
.js .qc .reveal,.js .book-hero .reveal{
  opacity:0;transform:translateY(var(--rv-y)) scale(var(--rv-scale));filter:blur(var(--rv-blur));
  transition:opacity var(--dur-4) var(--ease-out),transform var(--dur-4) var(--ease-out),filter var(--dur-4) var(--ease-out);
  transition-delay:var(--rd,0ms);will-change:transform,filter,opacity}
.js .qc .reveal.in,.js .book-hero .reveal.in{opacity:1;transform:none;filter:blur(0)}
.js.reveal-off .qc .reveal,.js.reveal-off .book-hero .reveal{opacity:1;transform:none;filter:none;transition:none}
/* The tier and speed cards used to carry `opacity 1.1s, filter 1.1s` here so
   they could participate in the entrance — which left a permanent 1.1s
   transition on two properties of an element you HOVER, for the whole session.
   They reveal on the tokens with everything else instead. */
.js .qtier,.js .qspeed{transition:box-shadow var(--dur-2) var(--ease),transform var(--dur-2) var(--spring),
  background var(--dur-2) var(--ease),opacity var(--dur-4) var(--ease-out),filter var(--dur-4) var(--ease-out)}

/* ---------- responsive ---------- */
@media (max-width:1000px){
  .qc{grid-template-columns:1fr;gap:var(--s-6)}
  .qc-side{position:static}
  .qbar{display:block}
  body.has-qbar{padding-bottom:calc(78px + env(safe-area-inset-bottom))}
  .qtiers{grid-template-columns:1fr}
  .qtier{display:grid;grid-template-columns:1fr auto;align-items:baseline;column-gap:16px}
  .qtier-n{grid-column:1}
  .qtier-amt{grid-column:2;grid-row:1;text-align:right;margin-bottom:0}
  .qtier-line,.qtier-note{grid-column:1/-1}
  .qtier-pick{left:auto;right:20px}
}
@media (max-width:680px){
  .qc-main{gap:var(--s-7)}
  .qspeeds{grid-template-columns:1fr}
  .qspeed{display:grid;grid-template-columns:1fr auto;align-items:baseline;column-gap:14px}
  .qspeed-n{grid-column:1}
  .qspeed-amt{grid-column:2;grid-row:1;margin-top:0;text-align:right}
  .qspeed-d{grid-column:1/-1}
  .qtypes{grid-template-columns:1fr;gap:8px}
  .qtype{display:grid;grid-template-columns:auto 1fr;align-items:baseline;column-gap:10px}
  .qtype small{text-align:right}
  .qstep-l{font-size:15px}
  .qbar-p b{font-size:24px}
  .qbar .btn{font-size:14px;padding-left:16px}
  .qrec{padding:22px}
  .qrec-total b{font-size:34px}
}
@media (prefers-reduced-motion:reduce){
  .js .qc .reveal,.js .book-hero .reveal{transition:none;opacity:1;transform:none;filter:none}
  .qprop-new{animation:none}
  .is-tick{animation:none}
  .qtier:hover{transform:none}
}

/* ==========================================================================
   MEDIA LAYER — plates, galleries, before/after, video, card thumbnails
   --------------------------------------------------------------------------
   Every block here is emitted by src/media.mjs only when there are real files
   on disk for it. There are deliberately NO empty states in this section: a
   service with no photograph shows no frame, no skeleton, no "coming soon"
   tile. Most of the 27 services will be in that state for a long time and it
   has to be indistinguishable from a page that was never meant to have one.

   Three rules run through all of it:
     · The FIGURE holds the caption; a child .m-frame holds the photo. The
       ratio lock and overflow:hidden live on the frame, because putting them
       on the figure clips the caption straight out of the page.
     · Every frame states an aspect-ratio and every <img> ships width/height,
       so a photo landing late cannot move what is under the reader's thumb.
     · Same geometry as the rest of the site — 24px radii, the card's inset
       ring, the card's shadow. A photo block is another surface in the system,
       not an embed dropped into it.
   ========================================================================== */

.m-band{padding:0 0 var(--band-y-tight)}
/* The plate takes the hero's own bottom space instead of opening a second one
   underneath it — otherwise the title and its picture read as two sections. */
.m-band-plate{padding-top:0;margin-top:calc(-1 * var(--s-4))}

/* --- the photo surface --- */
.m-frame{position:relative;overflow:hidden;border-radius:24px;background:var(--sand);
  box-shadow:0 0 0 1px var(--line) inset,0 26px 60px -40px rgba(34,26,17,.5)}
.m-frame > img,.m-frame video{display:block;width:100%;height:100%;object-fit:cover}
.m-plate,.m-lead,.m-shot,.m-cmp,.m-vid{margin:0;display:block}

/* Phones are portrait, and a 16:9 plate on one is a letterbox strip. The crop
   opens up as the viewport narrows rather than staying "cinematic". */
.m-frame-plate{aspect-ratio:3/2}
.m-frame-plate.m-plate-tall{aspect-ratio:4/5}
.m-frame-lead{aspect-ratio:3/2}
.m-frame-cell{aspect-ratio:4/3}
.m-frame-vid{aspect-ratio:var(--m-ar,16/9);background:var(--onyx)}
@media (min-width:681px){
  .m-frame-plate{aspect-ratio:16/9}
  .m-frame-plate.m-plate-tall{aspect-ratio:4/3}
  .m-frame-lead{aspect-ratio:16/9}
}

/* Caption is BOUND to its image — the 12px tier, not the 22px flow gap, or it
   floats between the photo above and whatever is under it. */
.m-cap{margin-top:12px;font-size:13.5px;color:var(--ink-soft);line-height:1.45;text-wrap:pretty}
.m-credit{margin-top:var(--s-3);font-size:13px;color:var(--ink-soft)}

/* --- gallery --- */
/* Uniform cells on purpose. Five photographs at their own ratios read as a
   layout fault; five identical crops read as a contact sheet, which is what a
   studio's work band is. Single column is the BASE and the extra tracks are
   only ever added, so a query that never matches leaves the phone layout
   standing rather than collapsing it. */
.m-lead{margin-bottom:16px}
.m-gal{display:grid;grid-template-columns:minmax(0,1fr);gap:16px}
@media (min-width:681px){.m-gal{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media (min-width:1000px){.m-gal{grid-template-columns:repeat(3,minmax(0,1fr))}}
/* Two photographs in a three-track row leave a hole where the third would be,
   and a hole reads as a missing image rather than a deliberate pair. Reachable
   whenever a folder declares its own hero and keeps two for the grid, so the
   track count follows the item count once the items can't fill the row. */
@media (min-width:1000px){.m-gal:has(> :last-child:nth-child(2)){grid-template-columns:repeat(2,minmax(0,1fr))}}
/* Four in three tracks is 3 + 1, and that trailing single cell reads as a photo
   that failed to load rather than the fourth photo. Two-by-two is the only
   arrangement of four that has no hole in it. Four is also the likeliest count
   anyone drops into a folder, so this is the common case, not the edge. */
@media (min-width:1000px){.m-gal:has(> :last-child:nth-child(4)){grid-template-columns:repeat(2,minmax(0,1fr))}}
/* Any other count that strands ONE cell on the last row: let it take the row.
   A full-width closing frame is a shape a person would choose; a lone cell
   floating left with two empty tracks beside it is not. Chained :nth-child are
   ANDed, so `:nth-child(3n+1):nth-child(n+7)` reads "the 7th, 10th, 13th…" —
   the n+7 guard keeps this off the counts the track-drop already fixed.
   A spanning cell is twice as wide, so its crop widens with it; a 4:3 held at
   double width would be a half-page-tall block. */
@media (min-width:1000px){
  .m-gal > :last-child:nth-child(3n+1):nth-child(n+7){grid-column:1/-1}
  .m-gal > :last-child:nth-child(3n+1):nth-child(n+7) .m-frame-cell{aspect-ratio:16/9}
}
/* Two tracks strand a single cell on every odd count — including three, which
   is exactly what the home page ships today. Same answer, one track earlier. */
@media (min-width:681px) and (max-width:999px){
  .m-gal > :last-child:nth-child(odd):nth-child(n+3){grid-column:1/-1}
  .m-gal > :last-child:nth-child(odd):nth-child(n+3) .m-frame-cell{aspect-ratio:16/9}
}
.m-shot .m-frame{transition:transform var(--dur-3) var(--ease),box-shadow var(--dur-3) var(--ease)}
.m-shot:hover .m-frame{transform:translateY(-4px);box-shadow:0 0 0 1px rgba(154,107,28,.3) inset,0 30px 60px -34px rgba(34,26,17,.45)}

/* --- before / after --- */
.m-cmps{display:grid;grid-template-columns:minmax(0,1fr);gap:20px}
@media (min-width:900px){.m-cmps{grid-template-columns:repeat(2,minmax(0,1fr))}}
/* --m-ar is the real pixel ratio of the AFTER frame, so both images sit in
   exactly the same box. Without that the divider compares two different crops
   and the whole device lies. */
.m-cmp-stage{aspect-ratio:var(--m-ar,1.5);touch-action:pan-y;cursor:ew-resize}
.m-cmp-stage img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
.m-cmp-clip{position:absolute;inset:0;clip-path:inset(0 calc(100% - var(--m-pos,50%)) 0 0)}
.m-cmp-line{position:absolute;top:0;bottom:0;left:var(--m-pos,50%);width:2px;margin-left:-1px;
  background:var(--paper);box-shadow:0 0 0 1px rgba(21,18,13,.28);pointer-events:none}
.m-cmp-line::after{content:"";position:absolute;top:50%;left:50%;width:38px;height:38px;transform:translate(-50%,-50%);
  border-radius:999px;background:var(--paper);box-shadow:0 4px 14px -4px rgba(21,18,13,.5)}
.m-cmp-tag{position:absolute;top:12px;padding:5px 11px;border-radius:999px;font-size:11.5px;font-weight:700;
  letter-spacing:.02em;background:rgba(21,18,13,.62);color:var(--paper);pointer-events:none}
.m-cmp-tag-b{left:12px}
.m-cmp-tag-a{right:12px}

/* The control is a real <input type=range>. That is what makes the slider work
   from a keyboard, work with a screen reader, and still move with JavaScript
   switched off — a JS drag handler would have none of the three. It is
   invisible and pinned over the stage, so the white divider IS the handle. */
.m-cmp-range{position:absolute;inset:0;width:100%;height:100%;margin:0;
  appearance:none;-webkit-appearance:none;background:none;cursor:ew-resize}
.m-cmp-range::-webkit-slider-thumb{-webkit-appearance:none;width:44px;height:44px;border-radius:999px;background:transparent;cursor:ew-resize}
.m-cmp-range::-moz-range-thumb{width:44px;height:44px;border:none;border-radius:999px;background:transparent;cursor:ew-resize}
.m-cmp-range:focus{outline:none}
.m-cmp-stage:focus-within{outline:2px solid var(--focus);outline-offset:3px}

/* Without clip-path there is no divider, so the pair becomes what it already
   was underneath: two labelled frames, side by side. */
@supports not (clip-path:inset(0 0 0 0)){
  .m-cmp-range,.m-cmp-line{display:none}
  .m-cmp-stage{aspect-ratio:auto;display:grid;grid-template-columns:1fr 1fr;gap:2px;background:none;box-shadow:none;cursor:auto}
  .m-cmp-stage img{position:relative;inset:auto;aspect-ratio:var(--m-ar,1.5)}
  .m-cmp-clip{position:relative;inset:auto;clip-path:none;order:-1}
}

/* --- video --- */
.m-vid{margin-top:16px}

/* --- service card thumbnail --- */
/* Bleeds to the card's own edges and inherits its top corners, so the card
   reads as one object rather than a photo dropped inside a box. .svc already
   clips, so the radius only has to match. */
.svc-thumb{margin:-26px -26px 14px;aspect-ratio:16/9;overflow:hidden;background:var(--sand);
  border-bottom:1px solid var(--line-2)}
.svc-thumb img{display:block;width:100%;height:100%;object-fit:cover;
  transition:transform var(--dur-3) var(--ease)}
.svc.has-thumb:hover .svc-thumb img{transform:scale(1.03)}
/* A flag pill sitting directly under a photograph needs its own air; with no
   photo it is the first thing in the card and the card padding already gives
   it that. */
.svc.has-thumb .svc-top{margin-top:2px}

@media (max-width:680px){
  .svc-thumb{margin:-22px -22px 12px}
  .m-frame{border-radius:20px}
}

@media (prefers-reduced-motion:reduce){
  .m-shot:hover .m-frame{transform:none}
  .svc.has-thumb:hover .svc-thumb img{transform:none}
}

/* --- a photo band on the onyx ground --- */
/* The frame's ring and its placeholder fill are both tuned for paper: a light
   inset line and a sand backdrop, which on onyx becomes a bright rectangle
   flashing before the photograph lands. Both flip; the shadow goes because
   there is nothing for it to fall on. Captions carry the same contrast the rest
   of .on-dark uses rather than a second set of greys. */
.on-dark .m-frame{background:#221D16;box-shadow:0 0 0 1px rgba(205,160,85,.22) inset}
.on-dark .m-shot:hover .m-frame{box-shadow:0 0 0 1px rgba(205,160,85,.45) inset,0 30px 60px -34px rgba(0,0,0,.75)}
.on-dark .m-cap{color:rgba(251,248,241,.72)}
.on-dark .m-credit{color:rgba(251,248,241,.6)}

/* ==========================================================================
   THE SLOT PREVIEW — `node src/build.mjs --slots`, and never a deploy.

   Everything below is unreachable on the live site: no page emits .m-slot
   unless the build was run with the flag. It stays in the sheet rather than in
   a separate one because a second stylesheet is a second thing to fingerprint,
   cache-bust and forget, and this is ~20 lines of rules that cost nothing to
   ship unmatched.
   ========================================================================== */
.m-slot{background:repeating-linear-gradient(135deg,transparent,transparent 9px,rgba(154,107,28,.05) 9px,rgba(154,107,28,.05) 18px),var(--sand);
  box-shadow:0 0 0 2px var(--brass) inset;display:grid;place-items:center;text-align:center}
.m-slot-in{display:grid;gap:6px;padding:18px;max-width:44ch}
.m-slot-file{font-family:var(--display);font-size:clamp(18px,2.4vw,26px);color:var(--espresso);line-height:1.05;word-break:break-word}
.m-slot-path{font-size:12.5px;color:var(--brass);word-break:break-all}
.m-slot-shape{font-size:12.5px;color:var(--ink-soft)}
.m-slot-note{margin-top:6px;font-size:13px;color:var(--ink-soft);line-height:1.45;text-wrap:pretty}
/* The compare slot has no real photograph to take a ratio from, and an unset
   ratio collapses the frame to the height of its own label. */
.m-cmps .m-slot{aspect-ratio:3/2}
/* The card thumbnail is a 16:9 sliver, so it only has room for the filename. */
.m-slot-thumb .m-slot-in{gap:2px;padding:10px}
.m-slot-thumb .m-slot-file{font-size:15px}
.m-slot-thumb .m-slot-note{display:none}
@media (max-width:680px){.m-slot-note{display:none}}

/* ==========================================================================
   /listing-score/ — the free scorecard

   Built on the components that already exist. `.opt`, `.field`, `.bk-prog`,
   `.summary` and `.bk-alert` are reused verbatim rather than re-derived, per
   the rule the crew portal established: a component that exists on another
   LUMORIA surface is never drawn twice.

   Every grid below states its single-column base and only ever ADDS a track
   behind a query (ADR-057). The failure mode of an unstated `auto` track is
   the phone collapse that took a day to find.
   ========================================================================== */

/* The honesty line. It sits beside the claim, never in a footnote — a caveat
   that has to be hunted for is a caveat that was hidden. */
.sc-honest{font-size:13.5px;color:var(--ink-soft);line-height:1.55;max-width:56ch}
.sc-honest a{color:var(--brass-ink);font-weight:600}

.sc-card{gap:var(--s-5)}
.sc-prog{position:sticky;top:calc(var(--nav-h) + 6px);z-index:3;
  background:var(--surface);margin:-8px -8px 0;padding:12px 8px 18px;border-radius:12px}

.sc-chunk{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--s-4)}
/* Sections are separated by a BREAK; the questions inside one flow. One
   uniform gap would make the five chunks read as ten unrelated questions. */
.sc-chunk + .sc-chunk{margin-top:var(--s-6);padding-top:var(--s-5);border-top:1px solid var(--line-2)}
/* Sentence case, no tracking, no caps. A tracked-caps kicker above a heading is
   banned estate-wide — it is the single most reliable tell of a generated page.
   The section still names itself; it does it in a voice instead of a costume. */
.sc-ch{font-family:var(--body);font-size:14px;font-weight:700;letter-spacing:-.01em;
  color:var(--brass-ink);display:flex;align-items:center;gap:12px}
.sc-ch::after{content:"";flex:1;height:1px;background:var(--line-2)}
.sc-q{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--s-2)}
.sc-qt{font-family:var(--display);font-weight:400;font-size:clamp(20px,2.2vw,25px);letter-spacing:-.01em;line-height:1.2}
/* Bound to its question, not floating between two of them. */
.sc-qh{font-size:14px;color:var(--ink-soft);margin-top:calc(var(--s-2) * -1 + 4px);line-height:1.45}
.sc-opts{margin-top:var(--s-1)}
.sc-pc{max-width:320px}

.sc-go{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--s-2);justify-items:start;
  margin-top:var(--s-5);padding-top:var(--s-5);border-top:1px solid var(--line-2)}
.sc-submit{justify-self:start}

/* Once the result exists the card is still reachable — it is not hidden, it is
   demoted. Hiding it would strand anyone who wants to change one answer. */
.sc-card[data-done]{opacity:.62;transition:opacity var(--dur-3) var(--ease)}
.sc-card[data-done]:hover,.sc-card[data-done]:focus-within{opacity:1}

/* ---------- the result ---------- */
.sc-result{margin-top:var(--s-8);display:grid;grid-template-columns:minmax(0,1fr);gap:var(--s-6);outline:none}
.sc-head{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--s-5);align-items:center;justify-items:center;text-align:center}
.sc-headtext{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--s-3);justify-items:center}
.sc-headtext .lead{max-width:44ch}
@media (min-width:820px){
  .sc-head{grid-template-columns:auto minmax(0,1fr);justify-items:start;text-align:left;gap:var(--s-7)}
  .sc-headtext{justify-items:start}
}

.sc-dial{position:relative;width:min(232px,62vw);aspect-ratio:1;flex:none}
.sc-dial svg{width:100%;height:100%;transform:rotate(-90deg)}
.sc-dial circle{fill:none;stroke-width:13;stroke-linecap:round}
.sc-track{stroke:var(--sand-deep)}
.sc-fill{stroke:var(--brass);transition:stroke-dashoffset 1.1s var(--ease-out)}
.sc-num{position:absolute;inset:0;display:grid;place-content:center;justify-items:center;gap:2px}
.sc-num b{font-family:var(--display);font-weight:400;font-size:clamp(46px,11vw,66px);line-height:1;
  color:var(--espresso);font-variant-numeric:tabular-nums}
.sc-num span{font-size:12.5px;color:var(--ink-soft);letter-spacing:.02em}

/* ---------- the four pillars ---------- */
.sc-pillars{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--s-3)}
@media (min-width:620px){.sc-pillars{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media (min-width:1000px){.sc-pillars{grid-template-columns:repeat(4,minmax(0,1fr))}}
.sc-pill{background:var(--surface);border-radius:18px;padding:20px;box-shadow:0 0 0 1px var(--line) inset;
  display:grid;grid-template-columns:minmax(0,1fr);gap:10px;align-content:start}
.sc-pill-h{display:flex;justify-content:space-between;align-items:baseline;gap:12px}
.sc-pill-h b{font-size:15.5px}
.sc-pill-h span{font-family:var(--display);font-size:19px;color:var(--espresso);font-variant-numeric:tabular-nums}
.sc-pill-bar{height:5px;border-radius:999px;background:var(--sand-deep);overflow:hidden}
.sc-pill-bar span{display:block;height:100%;border-radius:999px;background:var(--brass);
  transform:scaleX(var(--p,0));transform-origin:left;transition:transform var(--dur-4) var(--ease-out)}
.sc-pill p{font-size:13px;color:var(--ink-soft);line-height:1.45}

/* ---------- leaks beside the ask ---------- */
.sc-cols{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--s-6);align-items:start}
@media (min-width:900px){.sc-cols{grid-template-columns:1.15fr .85fr;gap:clamp(28px,3.4vw,44px)}}
.sc-h3{font-family:var(--display);font-weight:400;font-size:clamp(23px,2.6vw,30px);letter-spacing:-.01em;margin-bottom:var(--s-3)}

.sc-leak{background:var(--surface);border-radius:18px;padding:22px;box-shadow:0 0 0 1px var(--line) inset;
  display:grid;grid-template-columns:minmax(0,1fr);gap:9px}
.sc-leak + .sc-leak{margin-top:var(--s-3)}
.sc-leak-h{display:flex;justify-content:space-between;align-items:baseline;gap:14px}
.sc-leak-h b{font-size:16.5px;line-height:1.3}
.sc-cost{font-size:13px;font-weight:700;color:var(--err);white-space:nowrap;font-variant-numeric:tabular-nums}
.sc-leak-bad{font-size:14.5px;color:var(--ink-soft);line-height:1.5}
.sc-leak-fix{font-size:14.5px;line-height:1.55}
.sc-leak-fix b{font-weight:700}
.sc-leak-ev{font-size:12.5px;color:var(--ink-soft);line-height:1.5;padding-top:9px;border-top:1px solid var(--line-2)}
.sc-leak-free{font-size:13px;font-weight:600;color:var(--brass-ink)}
/* A locked row states plainly that something is behind the ask. It is never
   blurred lorem or a fake preview — a fake preview is a small lie and this
   page's whole argument is that we do not tell them. */
.sc-leak.is-locked{background:transparent;box-shadow:0 0 0 1px var(--line-2) inset}
.sc-leak-lock{font-size:13.5px;color:var(--ink-soft);font-style:italic}
.sc-leak-none b{font-size:17px}
.sc-leak-none p{font-size:14.5px;color:var(--ink-soft);line-height:1.5;margin-top:8px}

.sc-ask{background:var(--onyx);color:var(--paper);border-radius:24px;padding:28px;
  display:grid;grid-template-columns:minmax(0,1fr);gap:var(--s-3);align-content:start}
.sc-ask .sc-h3{color:var(--paper);margin-bottom:0}
.sc-askp{font-size:14.5px;color:rgba(251,248,241,.78);line-height:1.55}
.sc-ask form{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--s-3)}
.sc-ask label{color:rgba(251,248,241,.72)}
.sc-ask input{background:rgba(251,248,241,.06);color:var(--paper);box-shadow:0 0 0 1px var(--line-dk) inset}
.sc-ask input::placeholder{color:rgba(251,248,241,.4)}
.sc-ask input:focus{background:rgba(251,248,241,.1)}
.sc-ask .btn{justify-content:center;padding-left:20px}
.sc-ask .cta-note{color:rgba(251,248,241,.55);font-size:12.5px}
.sc-ask .opt{background:rgba(251,248,241,.05);box-shadow:0 0 0 1px var(--line-dk) inset}
.sc-ask .opt .o-n{color:var(--paper)}
.sc-ask .opt .o-d{color:rgba(251,248,241,.62)}
.sc-ask .opt .o-box{box-shadow:0 0 0 1.5px rgba(251,248,241,.3) inset}
.sc-ask .bk-alert{background:rgba(251,248,241,.07);box-shadow:0 0 0 1px var(--line-dk) inset;color:rgba(251,248,241,.8)}
.sc-ask .bk-alert b,.sc-ask .bk-alert a{color:var(--brass-bright)}

/* ---------- the last step ---------- */
.sc-next{background:var(--sand);border-radius:24px;padding:28px;
  display:grid;grid-template-columns:minmax(0,1fr);gap:var(--s-3);justify-items:start}
.sc-next .sc-h3{margin-bottom:0}
.sc-next .sc-askp{color:var(--ink-soft)}
.sc-next form{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--s-3);width:100%;max-width:380px}
.sc-next .opt{background:var(--paper)}

.sc-wins{background:var(--surface);border-radius:24px;padding:28px;box-shadow:0 0 0 1px var(--line) inset}
.sc-winlist{display:grid;grid-template-columns:minmax(0,1fr);gap:10px;list-style:none}
@media (min-width:760px){.sc-winlist{grid-template-columns:repeat(2,minmax(0,1fr))}}
.sc-winlist li{position:relative;padding-left:26px;font-size:14.5px;color:var(--ink-soft);line-height:1.5}
.sc-winlist li::before{content:"";position:absolute;left:0;top:.5em;width:12px;height:2px;background:var(--ok);border-radius:2px}
.sc-again{justify-self:start}

/* ---------- the published rubric ---------- */
.sc-rubric{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--s-3)}
@media (min-width:620px){.sc-rubric{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media (min-width:1000px){.sc-rubric{grid-template-columns:repeat(4,minmax(0,1fr))}}
.sc-rub{background:var(--surface);border-radius:18px;padding:20px;box-shadow:0 0 0 1px var(--line) inset;
  display:grid;grid-template-columns:minmax(0,1fr);gap:8px;align-content:start}
.sc-rub-h{display:flex;justify-content:space-between;align-items:baseline;gap:12px}
.sc-rub-h b{font-size:15.5px}
.sc-rub-h span{font-size:12.5px;color:var(--brass-ink);font-weight:700;white-space:nowrap}
.sc-rub p{font-size:13.5px;color:var(--ink-soft);line-height:1.45}

@media (max-width:680px){
  .sc-prog{top:calc(var(--nav-h) - 24px)}
  .sc-ask,.sc-next,.sc-wins{padding:22px}
  .sc-leak{padding:18px}
}
@media (prefers-reduced-motion:reduce){
  .sc-fill,.sc-pill-bar span{transition:none}
}

/* The scorecard cross-link on the homepage's property band. Deliberately NOT a
   button: one primary action per screen, and the brass is reserved for the one
   that takes money. A second brass button here would make neither read as the
   button (the dilution failure the CTA doctrine names). */
.sc-cross{font-size:14.5px;color:var(--ink-soft);line-height:1.55;max-width:52ch}
.sc-cross a{color:var(--brass-ink);font-weight:600;text-decoration:underline;text-underline-offset:3px;
  text-decoration-thickness:1px;text-decoration-color:rgba(154,107,28,.4)}
.sc-cross a:hover{text-decoration-color:var(--brass)}
