/* Dermazur « Ivoire » v2 — built to DESIGN.md. One system, held everywhere. */

/* ---------- tokens ---------- */
/* DEFAULT = « Dermazur », the LIVE SITE'S OWN PALETTE (owner 2026-08-23: "use the current
   colors of the current live website, and make the buttons similar to it too").
   Every value below was read off a real render of dermazur.ca with getComputedStyle — not
   copied out of its stylesheet, which carries several overridden variants.
   Two deliberate departures from live, both for contrast (memory: never copy a reference's
   accessibility bug):
     · --muted is #5A6080, not live's #8A90B0 — the live value is ~3.4:1 on this ground and
       is used here for 13px captions, which must clear 4.5:1.
     · --brass-text is #8A6A12, not live's --gold-2 #B8952A (~3.3:1) — small gold text only.
       The gold METAL itself is live's #D4AF37 exactly; only gold WORDS are darkened. */
:root{
  --paper:#FFFDF7; --paper-2:#F7F2E6; --paper-3:#F6F0E3; --surface:#FFFFFF; --night:#141B3D;
  --ink:#141B3D; --ink-2:#2E3860; --muted:#5A6080;
  --line:#E7DECA; --line-strong:#DACFB4;
  --royal:#1A23BA; --royal-2:#2A38D6; --brass:#D4AF37; --brass-text:#8A6A12;
  /* ---- --gold-opt: the gold that survives the contrast gate as WORDS (2026-09-01) ----
     The three colour rows at the end of this file offer « Or » on the arrow links, the section
     titles and the entry gate. Neither standing gold token can carry it. Measured with a WCAG
     script against --paper, --paper-2, --paper-3 and --surface in versions L, A and B — twelve
     grounds — plus the gate's real ground at 375px:
       --brass-text  worst 3.39:1  (#8A6A12 on version A's --paper-3 #DFD2BA)  → under 4.5
       --gold-dk     worst 2.31:1  (#A8861F on the same ground)                → under even 3.0
     Picking the other token rescues nothing: --gold-dk is the LIGHTER of the two. The value had
     to go darker.
     ⚠ AND THE FLAT GROUNDS ARE NOT THE HARD CASE — THE GATE IS. Under 900px the gate photo
     becomes the ground (.sp-photo, absolute inset:0, --paper-2 with the image at opacity .16 and
     grayscale(.2) over it). Reading the real composited pixels under the gate's own text boxes at
     375px, the darkest ground is #CFCBC1 / #C5BBAC / #BFC6CA in versions L / A / B. #6E550E — the
     obvious 80 %-of---brass-text pick, and 4.74:1 on the worst flat ground — measures only
     3.73:1 there. #5A450B measures 4.84:1 on that same darkest pixel and 6.14:1 on the worst flat
     ground, so it is the value that ships.
     ⚠ ONE gold for the whole row, not a second lighter one for the big headings. .title and
     .sp-title only need 3.0:1 and could take a lighter gold, but a section title and its own
     sub-title in two visibly different golds reads as a fault, and this value clears the stricter
     4.5 floor everywhere anyway.
     ⚠ Declared HERE, in :root, and deliberately NOT re-declared inside html[data-version="A"|"B"]:
     inheriting version A's own darker --brass-text is exactly what produced the 3.39 above.
     ⛔ Pre-existing and NOT fixed here, but found by the same measurement and worth someone's
     time: .sp-eyebrow and .sp-ed already paint var(--brass-text) on that gate ground, and they
     measure 3.05–3.12:1 at 375px today, before any row is touched. */
  --gold-opt:#5A450B;
  --gold-dk:#A8861F; --metal-hi:rgba(255,255,255,.45); --metal-lo:rgba(70,52,6,.42); --sheen:rgba(255,255,255,.55);
  --cta-bg:#D4AF37; --cta-ink:#2C2408; --cta-bg-hover:#C9A42E;
  /* squared, like every other shape on this site (owner 2026-08-23: "the buttons are all
     rounded, it makes the website look bad, make the shapes back to how it was"). His
     complaint was the SHAPE only — the gold body, the bevel, the sheen sweep and the
     hover lift below are the live site's and stay. Versions A and B already said 2px,
     so all three agree now; the token stays to document that the radius is one decision. */
  --btn-radius:2px;
  /* Azur's reply bubble = --royal 7% over --surface, WORKED OUT ONCE AND WRITTEN DOWN.
     ⚠ Do not put color-mix() back here. Chrome computes color-mix(in srgb, …) to the string
     `color(srgb 0.937137 0.939608 0.981059)`, and every contrast checker — craft-audit included —
     reads backgroundColor by pulling the numbers out of an rgb() string. It scored that as a
     1.25:1 failure on a bubble that actually measures ~15:1. A false failure is worse than no
     check: it teaches you to wave through the gate that exists to catch the real one.
     color-mix stays fine for shadows and veils, which no checker walks. A painted TEXT
     background has to resolve to rgb(). */
  --azur-bubble:#EFF0FA;
  --nav-link:#1A23BA; --nav-link-hover:#8A6A12;
  --nav-line:linear-gradient(100deg,#D8CDB4 0%,#FFFDF4 50%,#D8CDB4 100%);
  --serif:'Cormorant Garamond',Georgia,serif; --sans:'Work Sans',"Helvetica Neue",Arial,system-ui,sans-serif;
  --gap:24px; --wrap:1280px; --pad:clamp(20px,5vw,64px); --sec:clamp(72px,9vw,136px);
  /* ONE place the header's height lives. The sticky Femmes/Hommes switch parks under it
     (.seg-wrap top), so a hard-coded 64px in two files is how that switch ends up half
     behind the header the next time the header changes. Owner 2026-08-23: bigger header,
     bigger words in it. */
  --nav-h:84px;
  --ease-rise:cubic-bezier(.25,1,.5,1); --ease-cine:cubic-bezier(.83,0,.17,1);
  color-scheme:only light;
}
/* ---------- headline type, switchable (owner 2026-08-24) ----------
   Badia finds the Bodoni headers hard to read — the hairline strokes vanish at a glance and
   the swash italic reads as handwriting. Elegant-but-sturdier faces to compare on the real
   pages, same sizes, same italic device. All keep a true italic (the ONE contrast device,
   DESIGN.md §2). Every display rule already reads var(--serif), so one token swaps the whole
   site. The faces are self-hosted and load only when picked.
   ⭐ CORMORANT IS THE DEFAULT SINCE 2026-08-30 (owner set the demo's defaults to his own picks,
   screenshot in hand, so Badia's first open shows the version he chose). The default is always
   the attribute-less state, so cormorant has NO html[data-font=…] rule — the base tokens above
   and the base weight rules below ARE cormorant. Bodoni became the option:
     · bodoni    — Bodoni Moda: the old default; still PINNED on the home page (see below).
     · cormorant — Cormorant Garamond: garalde, calligraphic but open; runs light, so the
       display weight sits at 600 in the base rules (400 is wire-thin at 85px). Bodoni,
       Fraunces (500) and DM Serif (400) each carry their own weight override.
     · fraunces  — Fraunces: soft old-style, the warmest and most readable; has a real opsz
       axis, so the existing "opsz" variation settings keep working. */
/* ---- the BODY typeface is settled: Work Sans, and nothing else ----
   There used to be a ?corps= palette row here that swapped `--sans` between Work Sans, Karla,
   Lora and Source Serif 4. The owner picked Work Sans on 2026-09-01 and asked for the other
   three to go, so the row, its boot segment, its labels, its query param and the three
   `html[data-corps=…]` rules were all deleted, and Karla/Lora/Source Serif 4 were dropped from
   build/fonts.mjs (their .woff2 files removed). `--sans` in :root above is now the whole story.
   ⚠ Work Sans never had a rule here and must not get one: it was the default, and the default is
   always the attribute-less state. */
/* Playfair out, DM Serif Display in (owner 2026-08-26). Chosen because it has a REAL ITALIC:
   these headlines set their second half in <em>, and craft-audit's C2 fails a hero with no
   contrast device — which rules out every italic-less display serif (Marcellus, Prata, Italiana,
   Gilda Display) however well it would otherwise suit a med spa. It ships at weight 400 only, so
   it must NOT be given the 500/600 the variable faces use, or the browser fakes the bold. */
/* Crimson Pro is the one alternate left (owner 2026-09-05: "leave the current option for
   cormorant as default, take out the other options and add Crimson Pro"). Bodoni, DM Serif and
   Fraunces are gone. The face is already self-hosted for the ?accueil= row below. */
html[data-font="crimsonpro"]{--serif:'Crimson Pro',Georgia,serif}
html[data-font="crimsonpro"] .display,html[data-font="crimsonpro"] .title,html[data-font="crimsonpro"] h3,html[data-font="crimsonpro"] .tlist-name{font-weight:600}
/* ⭐ THE HOME PAGE IS ALWAYS BODONI. Owner 2026-08-26: *"For the home headline, I want it to be
   Bodoni, the current one… as for the other pages I want the headline to be Fraunces or
   Cormorant."* So ?font= now chooses the face for every page EXCEPT the home page.
   It re-declares the TOKEN, not the property, so every serif element on the home page moves
   together and the page can never end up half-Bodoni.
   ⚠ It must sit AFTER the html[data-font=…] rules: `body[...]` and `html[...]` are the same
   specificity for a custom property on different elements — but --serif set on <html> is
   INHERITED by <body>, and a value declared on <body> itself wins over an inherited one, so this
   works regardless of order. It is placed here anyway so a reader sees the exception beside the
   rule it excepts. The weight overrides above are element-scoped and would still apply on home,
   so they are neutralised too. */
/* ⭐ THE HOME PAGE'S PIN — EB Garamond since 2026-09-04 (owner: *"make it EB garamond"*).
   It was Bodoni Moda. Being the PIN is what makes it the attribute-less ?accueil= default, so
   there is deliberately no html[data-accueil="ebgaramond"] rule at the end of this file. */
body[data-page="home"]{--serif:'EB Garamond',Georgia,serif}
body[data-page="home"] .display,body[data-page="home"] .title,body[data-page="home"] h3,body[data-page="home"] .tlist-name{font-weight:500}

/* ---------- body ink, switchable (owner 2026-08-24: running text in dark blue) ----------
   Swaps the SECONDARY ink only — ledes, paragraphs, asides, list copy, prices — the tier that
   carries the running text. Headings stay --ink so the hierarchy still steps. #232F80 is a
   true navy (bluer than --night, calmer than --royal) and clears AA on every ground it sits
   on (10.4:1 on --paper-2, the worst case). Night-ground text is set in explicit light
   colours and is untouched. */

/* 🗑 Version A « Ivoire » was DELETED on 2026-09-04 — *"remove Ivory as a palette option."*
   Two palettes remain: the attribute-less default (Dermazur, the live site) and B « Azur ».
   ⚠ THE `:not()` CHAINS BELOW LOST A LINK WITH IT. `html:not([data-version="A"]):not(…"B")` was
   how the default palette's own button treatment was scoped; with A gone the A clause would have
   matched everything forever — harmless-looking, and exactly the kind of leftover that makes the
   next reader think A still exists. They now read `html:not([data-version="B"])`. */
/* Version B « Azur » — sampled from the beach photo (sky #ABCBDD/#D5E0E7, sea #6B9ECC/#6493AA, sand) */
html[data-version="B"]{
  --btn-radius:2px; --azur-bubble:#EBF1F5; --nav-link:#3E5068; --nav-link-hover:#10233D; --nav-line:#7FA6BF;
  --paper:#F3F6F8; --paper-2:#E3ECF1; --paper-3:#D3E0E8; --surface:#FBFCFD; --night:#0F2D4A;
  --ink:#10233D; --ink-2:#3E5068; --muted:#56687C;
  --line:rgba(16,35,61,.16); --line-strong:rgba(16,35,61,.38);
  --royal:#175A85; --royal-2:#1B6795; --brass:#7FA6BF; --brass-text:#2B6384;
  /* ⚠ --royal-2 IS REDEFINED HERE ON PURPOSE. :root's #2A38D6 is a lighter BLUE royal; inherited
     into this teal palette it made the Azur launcher jump hue on hover — a blue flash on a
     sea-green page. #1B6795 is B's own royal lifted the same way :root lifts its own.
     White on it measures 6.14:1, the mint presence dot 4.01:1 (script, not eyeball).
     Version A needs no line: its --royal IS :root's, so :root's --royal-2 is already correct. */
  /* gold, like the default palette — the owner ruled buttons gold on 2026-09-04 and the ruling
     is not per-palette. #2C2408 on #D4AF37 measures 9.1:1. */
  --cta-bg:#D4AF37; --cta-ink:#2C2408; --cta-bg-hover:#C9A42E;
}
html[data-version="B"] .ground-night .eyebrow,html[data-version="B"] .ground-night .b-n,html[data-version="B"] .ground-night .step-n,html[data-version="B"] .ground-night .tlist-n{color:#9CCBE8}
html[data-version="B"] .ground-night .eyebrow::before{background:#9CCBE8}
html[data-version="B"] ::selection{background:#C9DEEA}
html[data-version="B"] .hero-media::after{background:linear-gradient(180deg,color-mix(in srgb,var(--paper) 12%,transparent) 58%,color-mix(in srgb,var(--paper) 30%,transparent) 76%,color-mix(in srgb,var(--paper) 78%,transparent) 90%,var(--paper) 99%)}
/* ⭐ GOLD, EVERYWHERE, NO LONGER A CHOICE (owner 2026-09-04: *"Remove the buttons option in
   palette i decided it to be gold."*)
   :root's --cta-* tokens ARE the gold, so the default palette needed nothing. What DID need
   changing is version B: it declared its own dark-blue --cta-bg a few lines above and won on the
   element, which is the only reason `html[data-cta="or"]` ever existed. B's declaration is now
   the same gold, so one ruling holds on both palettes instead of one palette quietly disagreeing.
   🗑 Both `html[data-cta=…]` blocks are gone, and so is every trace of « Encre ». */

/* ---------- base ---------- */
*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
html:focus-within{scroll-behavior:smooth}
body{margin:0;background:var(--paper);color:var(--ink);font:400 17px/1.6 var(--sans);-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;font-feature-settings:"kern";overflow-x:hidden}
img{max-width:100%;height:auto;display:block}
a{color:inherit;text-decoration:none}
ul,ol,dl,dd,h1,h2,h3,h4,p,figure{margin:0;padding:0}
ul,ol{list-style:none}
button{font:inherit;color:inherit;background:none;border:0;padding:0;cursor:pointer}
.vh{position:absolute!important;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
.skip{position:absolute;left:12px;top:-80px;background:var(--ink);color:var(--paper);padding:14px 16px;min-height:48px;border-radius:2px;z-index:100}
.skip:focus{top:12px}
:focus-visible{outline:2px solid var(--royal);outline-offset:3px}
::selection{background:#DCCDA7;color:var(--ink)}
.wrap{max-width:var(--wrap);margin:0 auto;padding:0 var(--pad)}

/* ---------- type ----------
   READABILITY FIX, owner 2026-08-23 ("it looks nice, but it's just hard to read").
   Bodoni Moda is a Didone with a real OPTICAL SIZE axis (6-96). The site was pinning
   `opsz 96` - the DISPLAY cut - at every size, including 20px headings. That is the
   textbook Didone mistake: at small sizes the heavy verticals dominate while the hairlines
   vanish, which typographers call "dazzle". Every serif rule below now names an opsz close
   to its own rendered pixel size, and the serif runs at weight 500 instead of 400.
   Same typeface, same character, sturdier strokes.
   And DESIGN.md's own floor is enforced at last: NOTHING is set in Bodoni under 22px.
   Several rules were breaking it at 1.25rem/1.35rem (20-21.6px) - the exact sizes where a
   Didone falls apart. Do not shrink a serif rule back under 1.45rem. */
/* weight 600 IS Cormorant's weight (the default face since 2026-08-30 — it runs light, 400 is
   wire-thin at 85px). Each other face carries its own override up beside its --serif rule. */
.display{font-family:var(--serif);font-weight:600;font-size:clamp(3.25rem,7.6vw,7.25rem);line-height:.96;letter-spacing:-.015em;text-wrap:balance;font-variation-settings:"opsz" 72}
.title{font-family:var(--serif);font-weight:600;font-size:clamp(2.2rem,4.4vw,4.1rem);line-height:1.04;letter-spacing:-.01em;text-wrap:balance;font-variation-settings:"opsz" 48}
h3{font-family:var(--serif);font-weight:600;font-size:1.6rem;line-height:1.18;letter-spacing:-.005em;font-variation-settings:"opsz" 20}
.display em,.title em{font-style:italic;font-weight:500}
/* Owner 2026-08-25: *"the paragraph text on the bottom of the treatment page titles is hard to
   read. Maybe make the text smaller."* Two things were working against it and only one was size:
   it set 19.44px at 1440, and `max-width:62ch` rendered **799px** — the `ch` unit is the width of
   a ZERO, which is much narrower than an average letter, so 62ch buys roughly 88 characters a
   line. The comfortable range is 55–65. Smaller type AND a real 55-ish measure: 17.28px over
   ~600px. Line-height goes up, not down — a shorter line with more air is the readable trade. */
/* Owner 2026-08-26: *"make the body text have longer space for the sentences to appear longer in
   width… not the header of the pages, more so just the paragraph."* Yesterday's 46ch measured 527px
   in a 738px column — textbook-correct at 62 characters, but visibly cramped next to a headline
   twice its width, which is what he is reacting to. 56ch lands ~640px / ~70 characters: still
   inside the readable band (55–75), and it no longer reads as a narrow ribbon.
   ⚠ The size is NOT touched — he asked for width only. `.story-p` (62ch ≈ 630px) and `.faq dd`
   (60ch ≈ 617px) were already measured in range and are deliberately left alone. */
.lede{font-size:clamp(1rem,1.2vw,1.08rem);line-height:1.62;color:var(--ink-2);max-width:56ch;margin-top:24px}
.eyebrow{font-family:var(--sans);font-weight:500;font-size:11.5px;letter-spacing:.16em;text-transform:uppercase;color:var(--royal);display:flex;align-items:center;gap:12px;margin-bottom:14px}
/* 🗑 THE GOLD HAIRLINE BEFORE EVERY EYEBROW IS GONE (owner 2026-09-04: *"remove the EM dash. It
   feels AI-made."*)
   ⚠ REMOVED SITE-WIDE, NOT JUST IN THE HERO HE WAS LOOKING AT, and that is a deliberate reading of
   the note. The objection is to the DEVICE — a decorative rule before a small-caps label is one of
   the commonest tells of a generated page — and the device sits on 13 eyebrows per page across all
   50. Taking it off one and leaving twelve would not read as a choice, it would read as a bug.
   🗑 The entry gate's own copy (.sp-eyebrow::before, far below) went with it for the same reason.
   ↩ ONE LINE TO RESTORE, in each place:  content:"";width:24px;height:1px;background:var(--brass)
   ⚠ The two colour overrides for the night bands and version B now have nothing to paint. They are
   left in place on purpose — they are two harmless declarations, and deleting them would mean
   re-deriving both colours if the rule ever comes back. */
.sec-head .title,.thero .display{margin-top:0}
.sec-head{max-width:760px;margin-bottom:clamp(36px,4vw,56px)}
.sec-head-wide{max-width:900px}
.aside{font-size:15.5px;line-height:1.6;color:var(--ink-2);max-width:62ch;margin-top:32px;padding-top:18px;border-top:1px solid var(--line)}
small{font-size:.82em}

/* buttons — one language: squared letterspaced caps, 52px */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:10px;min-height:52px;padding:0 28px;background:var(--cta-bg);color:var(--cta-ink);border-radius:var(--btn-radius);font-weight:500;font-size:12.5px;letter-spacing:.14em;text-transform:uppercase;position:relative;overflow:hidden;isolation:isolate;transition:background .25s var(--ease-rise),transform .15s var(--ease-rise)}
.btn::after{content:"";position:absolute;left:0;right:100%;bottom:0;height:1px;background:var(--brass);transition:right .45s var(--ease-rise)}
.btn:hover{background:var(--cta-bg-hover)}
.btn:hover::after{right:0}
.btn:active{transform:scale(.985)}
.btn-sm{min-height:50px;padding:0 22px;font-size:12.5px}
/* the header book button holds an invisible copy of the OTHER language's label in the same grid
   cell, so FR and EN render at identical width — see navBookBtn() in build/build.mjs for why the
   width is derived from a word and never typed as a pixel value */
/* the hidden-sizer trick is shared with the hero BOOK (see btnBook in build.mjs), so both
   buttons are the same box in both languages without either one naming a pixel width. */
.nav-book,.btn-book{display:grid;place-items:center}
.nav-book>*,.btn-book>*{grid-area:1/1}
.nav-book-sizer{visibility:hidden;pointer-events:none;user-select:none}

/* THE LIVE SITE'S BUTTON (default version only). Flat gold body — Badia rejected a gradient
   body, it read striped — with the two cues real metal actually has: bevelled edges (bright
   inner top, dark inner bottom) and a reflection that SWEEPS across on hover. Ported from
   dermazur.ca so Badia meets the button she already knows.
   ⚠️ The hover re-declares `background` in full. The live file carries a scar here: a hover
   that only moved background-position lost to a `background:` shorthand elsewhere and made
   the gold button flash blue. Do not "simplify" these back to single-property hovers. */
html:not([data-version="B"]) .btn{
  border:1px solid var(--gold-dk);
  box-shadow:inset 0 1px 0 var(--metal-hi), inset 0 -2px 0 var(--metal-lo),
             inset 1px 0 0 rgba(255,255,255,.14), 0 6px 16px -10px rgba(90,68,10,.55);
  transition:box-shadow .2s ease, transform .18s ease, filter .2s ease}
html:not([data-version="B"]) .btn::after{
  top:-4px;bottom:auto;left:-70%;right:auto;width:46%;height:auto;
  background:linear-gradient(105deg,transparent 0%,var(--sheen) 50%,transparent 100%);
  transform:skewX(-18deg);pointer-events:none;transition:left .55s ease;z-index:1}
html:not([data-version="B"]) .btn:hover{
  background:var(--cta-bg);border-color:var(--gold-dk);color:var(--cta-ink);
  filter:brightness(1.05);transform:translateY(-1px);
  box-shadow:inset 0 1px 0 var(--metal-hi), inset 0 -2px 0 var(--metal-lo),
             inset 1px 0 0 rgba(255,255,255,.14), 0 12px 26px -12px rgba(124,96,20,.8)}
html:not([data-version="B"]) .btn:hover::after{left:125%;right:auto}
html:not([data-version="B"]) .btn:active{
  transform:translateY(0);filter:brightness(.97);box-shadow:inset 0 2px 6px rgba(90,68,10,.4)}
.link{display:inline-flex;align-items:center;gap:8px;font-weight:500;font-size:13.5px;letter-spacing:.02em;text-decoration:underline;text-underline-offset:5px;text-decoration-thickness:1px;text-decoration-color:var(--line-strong)}
/* ---- the 24px tap floor for standalone CTAs — M6, 2026-08-30, and it is v2.15's OWN ruling
   finally applied at every width ----
   v2.15 measured « Voir les tarifs → » at 22px and decided to raise it: *"Standalone CTAs
   measured 22px tall — 2px under the 24px floor in WCAG 2.2 AA (2.5.8 Target Size Minimum).
   Only STANDALONE controls are raised."* ⛔ **But the rule was written INSIDE the
   `@media (max-width:600px)` block**, so for five weeks it only ever applied to phones and
   `.link` still measured **21.6px at 1440**. Nobody had probed a desktop viewport — `tap.mjs`
   hard-coded 375. The owner re-ruled it on 2026-08-30 as the same decision, correctly scoped.
   ⚠ NO `display` CHANGE IS NEEDED AND NONE IS WANTED: `.link` is already `inline-flex` above,
   so `min-height` applies as it stands. (The M5 lesson: read the computed display first — an
   element that is already inline-flex or a flex ITEM honours min-height without help, and
   adding `align-items:center` to "tidy" it shifts the text off its line-mates' baseline.)
   ⚠ EXACTLY 24, NEVER 25. craft-audit's C7 enrols any a/button/[class*=btn] wider than 60px and
   **taller than 24px** that is filled, bordered or named — and then demands 48. At 24 these stay
   outside it; at 25 every one of them would fail all 50 pages.
   Cost, measured at 1440 on /tarifs and /epilation-laser: page height Δ0, nav row Δ0,
   horizontal overflow 0. It buys 2.4px of target and moves nothing. */
.link,.brand{min-height:24px}
.link span{transition:transform .3s var(--ease-rise)}
.link:hover{text-decoration-color:var(--brass-text)}
.link:hover span{transform:translateX(4px)}

/* ---------- layout / grounds ---------- */
.sec{padding:var(--sec) 0;position:relative}
.ground-1{background:var(--paper)}
.ground-2{background:var(--paper-2)}
.ground-3{background:var(--paper-3)}
.ground-night{background:var(--night);color:var(--paper)}
.ground-night .lede,.ground-night .aside,.ground-night p{color:#D7D9E6}
.ground-night .eyebrow{color:#CDB577}
.ground-night .eyebrow::before{background:#CDB577}
.ground-night .link{text-decoration-color:rgba(244,238,227,.4)}
.ground-night .link:hover{text-decoration-color:#CDB577}
.ground-night .display,.ground-night .title,.ground-night h3,.ground-night dt{color:var(--paper)}
.ground-night .btn{--cta-bg-hover:#E2BE49}
/* ⚠ NO PER-VERSION NIGHT RULE ANY MORE. B used to swap the night button to ivory because its own
   --cta-bg was dark blue and vanished on a dark band; now that B's button IS the gold, the base
   `.ground-night .btn` hover above is correct for both palettes and a version rule would only
   re-introduce the disagreement the gold ruling just removed. */

/* ---------- nav ---------- */
/* the header runs 8px wider than the page column on each side (56px pad vs 64px). That is the
   last 16px the row needed; 8px of off-grid on a sticky bar reads as nothing, a wrapped nav
   reads as broken. */
/* The header spans the FULL viewport since 2026-08-24 (owner: "utilize the entire header
   space — put Dermazur more on the left, away from the section names, but balance the section
   names"). The wordmark sits at the page's true left edge, the links are centred in the
   viewport, phone + Réserver + langue sit at the true right — a 1fr | auto | 1fr grid, so the
   two sides always mirror. Page CONTENT stays in the 1152px column; only the header escapes it. */
/* ⭐ ASYMMETRIC PADDING (owner 2026-09-03): the LEFT keeps the wordmark where he put it in
   August ("put Dermazur more on the left"); the RIGHT gives the cluster ~19px back.
   ⛔ AND FIRST, THE PREMISE THAT HAD TO BE CORRECTED. DESIGN.md §2 still says the row is full
   at 1280 with 21px spare, and the brief for this round repeated it. It is not true any more:
   `@media (max-width:1365px)` below turns the links into a burger, so AT 1280 AND 1320 THERE
   IS NO DESKTOP ROW AT ALL, and the nav ships EIGHT links, not seven (« Accueil » was added
   2026-08-25). The only widths where this row exists are 1366 and up. DESIGN.md line 385 is
   the one entry that already said 1365; the other three are stale.
   ⚠ AND AT 1366 THE ROW WAS ALREADY OVER CAPACITY — measured, not felt: `.nav-links`'s
   auto-margin computed to 0px in both languages and the cluster stood 18.2px (FR) / 32.5px
   (EN) PAST the wrap's own right padding, leaving EN 8.47px from the window edge. It never
   scrolled because it was eating padding, not viewport — a silent failure exactly as §2
   warns. So this change is not only "push it right": widening the box AND shrinking the
   cluster (see `.nav-right` below) pulls its right edge back off the edge at 1366 while
   moving it outward at 1440, where there were 43.2px going spare. */
.nav .wrap{max-width:none;padding:0 clamp(20px,3vw,44px)}
.nav{position:sticky;top:0;z-index:50;background:color-mix(in srgb,var(--paper) 90%,transparent);backdrop-filter:saturate(1.2) blur(10px);-webkit-backdrop-filter:saturate(1.2) blur(10px);border-bottom:1px solid var(--line)}
/* ⚠ THE HEADER IS FULL. Measured at 1280px (the narrowest width the full row serves, and the
   content column is 1152px there and at every width above it): brand + 7 links + phone +
   Réserver + the language link need 1131px of the 1152, leaving 21px. That is the whole
   budget. The 15px link size, these gaps, the 56px nav padding and the SHORT « EN »/« FR »
   toggle were all chosen together to buy it — at 13.5px and the old loose gaps the row was
   already at 100.0%, so the bigger type the owner asked for on 2026-08-23 had to be paid for.
   Change ANY of them, or add a link, and re-measure at 1440/1366/1320 before shipping. */
/* Owner 2026-08-25: "keep the Dermazur logo where it is… make the other headers nicer in their
   placement." MEASURED before changing anything — the grid was `1fr auto 1fr`, which only centres
   the middle track when the two side tracks are equal. They were not: the actions block (phone +
   RÉSERVER + EN) is 277px and the wordmark is 123px, so the wider side forced its track open and
   dragged everything right. The result at 1440 was 118px of air after the logo and only **24px**
   before the phone number — a lonely wordmark and a crush on the right, which is exactly what the
   owner was looking at.

   Flex with `margin-inline:auto` on the links splits the LEFTOVER space equally instead, so the two
   gaps are always the same number whatever the two side blocks weigh: 75/75 at 1440, 146/146 at
   1600, 41/41 at 1366. It also cannot overflow, because auto margins only ever distribute space
   that is already free — a true centre would need 802px of links centred inside 884px of room at
   1366 and would collide with the actions.
   ⚠ `.nav-right` must NOT also carry `margin-left:auto`. Auto margins share free space between ALL
   of them, so a third one would hand the right gap two thirds and re-break the balance the other
   way. The stripped ads-page nav (`.nav-bare`, no links at all) re-adds it on purpose below. */
.nav-in{height:var(--nav-h);display:flex;align-items:center;column-gap:24px}
.nav-in .nav-links{margin-inline:auto}
/* the bare (ads) header has no centre column — brand left, actions right, same edges */
.nav-bare .nav-in .nav-right{margin-left:auto}
/* BLUE, because the logo is blue (owner 2026-08-24: black "is out of place, and the writing
   for the Dermazur logo is blue to begin with"). dermazur.ca's own wordmark computes to
   rgb(26,35,186) — read off a real render, and the same value this system already calls
   --royal, which is also the nav links and the hero's blue option. One blue, used three
   times, not three blues. --brand-light below keeps the footer wordmark ivory on navy. */
.brand{font-family:var(--sans);font-weight:500;font-size:16px;letter-spacing:.24em;text-transform:uppercase;color:var(--royal);padding-left:.24em}
.brand-light{color:var(--paper)}
.nav-links ul{display:flex;gap:clamp(16px,2vw,30px)}
/* the hairline under a nav word is DRAWN from the left on hover, not faded in — growth
   reads as intent, a fade reads as a glitch (the live site's rule, ported) */
/* six links since « Soins » absorbed the laser entry (2026-08-24) — they must never wrap mid-word */
.nav-links a{position:relative;white-space:nowrap;font-size:15px;font-weight:500;letter-spacing:.02em;color:var(--nav-link);padding:6px 0;transition:color .2s}
.nav-links a::after{content:"";position:absolute;left:0;right:100%;bottom:0;height:2px;border-radius:2px;background:var(--nav-line);transition:right .28s ease}
.nav-links a:hover::after,.nav-links a[aria-current="page"]::after{right:0}
.nav-links a:hover,.nav-links a[aria-current="page"]{color:var(--nav-link-hover)}
/* a hairline between "where can I go" and "what can I do" — the phone number, the booking
   button and the language switch are actions, not more navigation, and reading as one undivided
   run of nine items is half of why the right side felt crowded */
/* 16px → 12px between phone · RÉSERVER · FR/EN, and the divider's 4px right margin goes:
   12px reclaimed for the links at every width, and 12px of headroom handed back at 1366,
   where the cluster is overflow-driven and its right edge therefore moves LEFT as it
   narrows. The three items keep a 12px gap, which is the same step `.nav-in` uses on a
   phone — not a new number. */
.nav-right{display:flex;align-items:center;gap:12px}
.nav-right::before{content:"";flex:0 0 1px;width:1px;height:20px;background:var(--line);margin-right:0}
.nav-bare .nav-right::before{display:none}
.nav-tel{white-space:nowrap;font-size:15px;font-weight:500;font-variant-numeric:tabular-nums;color:var(--ink-2)}
/* ---- the FR/EN toggle's tap floor — M6, 2026-08-30 ----
   It measured **26 × 19.9px**: tall enough, and 4.1px too NARROW. ⚠ 2.5.8 wants a 24×24 AREA,
   not a 24px height — the same axis that hid `.bk-x` at 34 × 23.7px in M5. Check both.
   ⚠ min-WIDTH ALONE, NO `display` CHANGE, and that is measured rather than assumed: this `<a>`
   is a flex ITEM of `.nav-right` (which is `display:flex`), so it computes to `display:block`
   and honours `min-width` as it stands. Reaching for `inline-flex` here would be the mistake
   M5 recorded — read the computed display before changing it.
   `text-align:center` keeps « EN » centred in the 24px box instead of sitting left of it, which
   matters because the box is now wider than its own text.
   Cost, measured at 1440: nav row Δ0, page height Δ0, overflow 0. The nav has 118px (FR) /
   141px (EN) of slack at 1440 and is not near its 1365px burger breakpoint. */
.nav-lang{font-size:13px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-2);padding:6px 0;line-height:1;border-bottom:1px solid transparent;min-width:24px;text-align:center}
.nav-lang:hover{border-bottom-color:var(--brass)}
.nav-lang:hover,.nav-tel:hover{color:var(--nav-link-hover)}
.nav-burger{display:none;width:48px;height:48px;border-radius:2px;flex-direction:column;align-items:center;justify-content:center;gap:6px;margin-right:-10px}
.nav-burger span:not(.vh){display:block;width:22px;height:1.5px;background:var(--ink)}

/* sheets (mobile nav, booking note, chat note) */
.sheet{position:fixed;inset:0;z-index:90;background:var(--paper);color:var(--ink);padding:24px var(--pad) 40px;overflow:auto}
.sheet[hidden]{display:none}
.sheet-head{display:flex;align-items:center;justify-content:space-between;min-height:40px;margin-bottom:24px}
.sheet-x{width:48px;height:48px;border-radius:2px;font-size:30px;line-height:1;margin-right:-12px;color:var(--ink-2)}
.sheet-links li{border-top:1px solid var(--line)}
.sheet-links li:last-child{border-bottom:1px solid var(--line)}
.sheet-links a{display:block;padding:18px 0;font-family:var(--serif);font-weight:500;font-size:2.4rem;line-height:1.05;letter-spacing:-.01em;font-variation-settings:"opsz" 32}
.sheet-foot{display:flex;gap:28px;margin-top:32px;font-size:15px;font-weight:500}
.sheet-book,.sheet-chat,.sheet-cart{inset:auto 16px 16px auto;width:min(440px,calc(100vw - 32px));border:1px solid var(--line-strong);background:var(--surface);box-shadow:0 24px 60px rgba(20,27,61,.18);padding:22px 24px 26px;border-radius:2px}
/* the cart panel rises from the pill, not from the corner — same object, same place */
.sheet-cart{inset:auto auto 78px 50%;transform:translateX(-50%);width:min(460px,calc(100vw - 32px));border-top:2px solid var(--brass)}
.sheet-cart .cart-total b{color:var(--brass-text)}
.sheet-cart .sheet-head{margin-bottom:4px}
/* the treatment the visitor actually pressed, named back to them before the demo notice */
.book-pick{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;margin:0 0 14px;padding:12px 14px;background:var(--paper);border-left:2px solid var(--brass)}
.book-pick[hidden]{display:none}
.book-pick-l{font-size:11.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);font-weight:500}
.book-pick-v{font-size:15.5px;color:var(--ink);font-weight:500;font-variant-numeric:tabular-nums}
.sheet-alt{margin-top:12px;font-size:14px}
.sheet-book h3{margin-bottom:10px}
.sheet-book p,.sheet-chat p{color:var(--ink-2);font-size:15.5px;line-height:1.55}
.sheet-actions{display:flex;flex-wrap:wrap;gap:18px 24px;align-items:center;margin-top:18px}
.sheet-actions .btn{min-height:48px;padding:0 20px}
/* ---- the Azur launcher reads as a CHAT (owner 2026-08-26) ----
   A speech bubble with three dots, then the name. The bubble carries the presence dot in its
   top-right corner — the placement every messaging app uses — instead of a loose dot in front of
   a word. Sized so the whole control stays 8px clear of the cart pill's row at 375px; the pill
   rides at bottom:66px on a phone and this sits at bottom:8px, so they never share a line. */
/* ---- FILLED, NOT OUTLINED (owner 2026-09-01: "make Azur more obvious") ----
   It was an ivory chip with an ivory-chip neighbour on either side — three quiet outlines in a
   row, and the one that opens the assistant looked like the one that changes the palette. It is
   now the only SOLID control on the page, so it reads first without shouting: one field, royal,
   white type, and the same 2px corner as everything else.
   ⚠ --azur-fab-bg IS THE POINT OF THE CUSTOM PROPERTY, not decoration. The field, the border and
   the presence dot's ring must all be the SAME colour; hover changes one declaration and the
   three follow. Setting them separately is how a hover state drifts by one token and nobody
   notices until a screenshot. */
.chat-fab{--azur-fab-bg:var(--royal);position:fixed;right:16px;bottom:16px;z-index:80;display:inline-flex;align-items:center;gap:9px;min-height:56px;padding:0 18px 0 14px;border-radius:2px;background:var(--azur-fab-bg);border:1px solid var(--azur-fab-bg);font-size:12.5px;font-weight:500;letter-spacing:.12em;text-transform:uppercase;color:#fff;box-shadow:0 12px 34px rgba(20,27,61,.20)}
.chat-fab-ic{position:relative;display:inline-flex;color:#fff;line-height:0}
.chat-fab-ic svg{display:block}
/* the presence dot rides the bubble's corner; the RING is what keeps it legible against the
   stroke it overlaps, so do not remove the box-shadow thinking it is decoration.
   ⚠ TWO VALUES CHANGED HERE AND BOTH WERE MEASURED. (1) The ring is the pill's OWN ground, not
   ivory: an ivory ring on a royal field reads as a white dot with a green speck in it. (2) The
   green went #2F8F5B → #6EE7AE, because #2F8F5B on royal measures 2.61:1 — a dot nobody can see
   is not a presence indicator. #6EE7AE measures 6.88 / 5.20 / 4.83 / 4.01:1 on the four royal
   fields this pill can wear (default, its hover, version B, B's hover); the floor for a
   non-text graphic is 3:1. */
.chat-fab .dot{position:absolute;top:-1px;right:-1px;width:7px;height:7px;border-radius:50%;background:#6EE7AE;box-shadow:0 0 0 2px var(--azur-fab-bg)}
.chat-fab-l{line-height:1}
.chat-fab:hover{--azur-fab-bg:var(--royal-2)}
/* ---- ONE ring, ONCE per session ----
   A launcher that pulses forever is an ad. This fires a single 1.9 s expansion on the first page
   of a session and is then never added again (site.js, sessionStorage 'dz2-azur-ring'), so the
   attention is spent once.
   ⚠ The reduced-motion rule REMOVES it from paint (`display:none`), it does not slow it down: a
   ring that still expands, only politely, is the same thing the setting asked us not to do. */
.chat-fab.azur-ring::after{content:'';position:absolute;inset:-1px;border-radius:2px;border:2px solid var(--royal);opacity:0;pointer-events:none;animation:azur-ring 1.9s ease-out 1}
@keyframes azur-ring{0%{opacity:.5;transform:scale(1)}70%{opacity:.12}100%{opacity:0;transform:scale(1.13)}}
@media (prefers-reduced-motion:reduce){.chat-fab.azur-ring::after{display:none;animation:none}}
body.sheet-open .chat-fab{display:none}

/* ---------- home hero (full-bleed media) ---------- */
.hero{min-height:clamp(640px,100svh,960px);display:flex;align-items:flex-start;padding:0 0 clamp(92px,9vw,124px);overflow:hidden;background:var(--paper-2)}
.hero-media{position:absolute;inset:0;z-index:0}
.hero-media img{width:100%;height:100%;object-fit:cover;object-position:50% 22%;transform-origin:50% 50%}
/* a haze from the top-left so the headline sits on calm sky whatever the crop; the wave stays clear on the right */
/* TWO gradients, and they do different jobs.
   1. A haze from the top-left, only as wide as the words need. The hero carries far less
      text since Badia asked for it (headline, one line, two buttons), so this is lighter
      than it was and lets more of the photo through.
   2. The BOTTOM FADE, ported from the live site (`.hero-sea::after` on dermazur.ca, owner
      2026-08-23: "the gradient following the beach at the bottom of the picture"). It has
      to reach the ground colour FULLY - live goes to `var(--bg)` at 94%. Ours stopped at
      72% opacity, which left a visible seam where the photo ended instead of the sand
      melting into the page. Do not lower that last stop below 100%; the seam comes back. */
/* ⚠ THE WASH OVER THE PHOTO IS GONE (owner 2026-08-23: "take out the gradient in the main
   picture"). What was here was TWO stacked gradients: a 102deg haze from the top-left that
   veiled the picture so the headline could sit on it, and a 180deg fade so the photo meets
   the page cleanly at the bottom. Only the HAZE was removed — the bottom fade stays, or the
   photo ends on a hard horizontal edge against the ivory ground.
   Legibility now comes ENTIRELY from the per-glyph halo on .hero-in below, which is what
   DESIGN.md §3 asked for in the first place ("legibility comes from a per-glyph halo, never
   more veil"). ⚠ If the hero photo is ever swapped for a busier or darker one, re-check the
   headline BY EYE and measure contrast — there is no veil left to hide behind. */
/* ⭐ THE TOP OF THIS GRADIENT IS 12 % PAPER, NOT 0 %, AND IT IS THE PRICE OF DELETING THE HALOS.
   Removing the per-letter glow (owner 2026-09-04) was measured on composited screenshot pixels
   straight afterwards, and it broke the contrast the glow had been buying:
     royal #1A23BA headline, worst pixel inside its own box ... 2.95:1 at 1280 · 3.03:1 at 1440
   The headline is 76-85px, so its bar is WCAG AA large text = 3:1 - and 2.95 is a FAIL, by 0.05,
   on the commonest laptop width. A 12 % wash of paper over the whole photograph lifts that worst
   pixel from rgb(88,139,194) toward paper and clears the bar with room.
   THIS IS NOT THE THING HE ASKED ME TO REMOVE, and the difference is the point: a text-shadow is
   a white glow that hugs each letter and is visible AS a glow - that is what "some white
   surrounding the words" named. This is a uniform tint on the picture, with no edge and no
   relationship to the type; it reads as the light in the photograph, not as a highlight.
   12 % IS THE MINIMUM THAT CLEARS THE BAR, solved for rather than dialled in by eye. Do not lower
   it without re-running the contrast probe, and re-run it too if the hero photograph is ever
   swapped for a darker or busier one. Applied to all THREE copies of this gradient (here, version
   B's, and the <=900px one) - a veil on two of three is a page that fails at one screen size. */
.hero-media::after{content:"";position:absolute;inset:0;background:linear-gradient(180deg,color-mix(in srgb,var(--paper) 12%,transparent) 58%,color-mix(in srgb,var(--paper) 30%,transparent) 76%,color-mix(in srgb,var(--paper) 78%,transparent) 90%,var(--paper) 99%)}
/* Raised (2026-08-23): with the wash removed, the actions row sat ON the dark sea and the
   « Voir les tarifs » link nearly vanished. The block moves up and the photo's horizon moves
   down (object-position below) so the ask lands on sky. ⚠ Re-check this pair together if
   either the hero height or the headline size changes. */
.hero-in{position:relative;z-index:1;width:100%;padding-top:clamp(72px,8.5vh,104px)}
/* Legibility on the photo comes from a PER-GLYPH HALO, never from more veil over the
   picture - the live site's rule, and the reason Badia can keep her beach visible. The halo
   is the paper colour, so it reads as light haze around the letters rather than a box. */
/* ⭐ EVERY HALO IS OFF (owner 2026-09-04: *"remove the highlight behind the blue words. there's
   like some white surrounding the words."*)
   Four rules used to paint a white glow behind the hero text so it lifted off the photograph —
   the headline, the eyebrow, the lede and the action link. All four are gone. The gradient on
   `.hero-media::after` is UNTOUCHED and is now the only thing separating text from picture; it is
   what the copy actually sits on by the time it is read, and it is why the words do not need the
   glow. ⚠ Contrast was re-measured on composited screenshot pixels after this, not assumed. */
.hero-in .eyebrow{margin-bottom:0}
/* Smaller (owner 2026-08-23). Was clamp(3.25rem,7.6vw,7.25rem) = 109px at 1440; now 85px —
   below the live site's own 92.5px, and still clear of craft-audit C2's 72px hero floor.
   The halo is stacked tight-to-wide instead of one soft blur: with the veil gone it has to
   carry the whole job, and a tight ring reads as crisp type while one big blur reads as glow. */
.hero .display{font-size:clamp(2.9rem,5.9vw,5.6rem)}
/* the hero ink defaults to BLEU (#1A23BA via --royal) since 2026-08-30 — the owner's pick, and
   the live site's own hero blue. The halo is the thicker one a lighter ink needs (see the
   ?h= block near the end of this file); ?h=encre carries the old navy + thinner halo. */
/* ⭐ TWO LINES, MAX — AND THE CAP IS PER LANGUAGE (owner 2026-09-04: *"make it longer. I want
   them on 2 lines MAX. Laser hair removal first, and then virtually painless after."*)
   It was 14ch, which broke the English into THREE ("Laser hair / removal, / virtually painless.").
   The cap has to sit ABOVE the longest single line and BELOW the whole string, and those two
   numbers are different in the two languages, so one value cannot serve both:
     EN  "Laser hair removal," 19 · "virtually painless." 19 · whole 39   → cap between 20 and 38
     FR  "L'épilation au laser," 21 · "pratiquement sans douleur." 26 · whole 47 → between 27 and 46
   ⚠ ch IS NOT A CHARACTER. It is the advance width of "0", and EB Garamond's zero is narrower than
   its average letter — so these were set by MEASURING the rendered line count at 375 / 768 / 1280 /
   1440 / 1920 in both languages, not by counting characters. Re-measure if the face changes.
   ⚠ NO <br>. A hard break is right at one width and wrong at every other; below ~600px the phone
   genuinely needs three or four lines and the cap lets it have them. */
.hero-in .display{max-width:26ch;color:var(--royal)}
html[lang="fr"] .hero-in .display{max-width:33ch}
/* ⛔ THE HOME HEADLINE NEVER HYPHENATES, AND THIS BUG WAS INVISIBLE TO THE LINE COUNTER.
   The ≤600px block far above sets `hyphens:auto` on every `.display` — written for
   « Thermocoagulation », a one-word headline with nowhere to wrap — and its comment claims it
   "does not touch a headline that already has somewhere to wrap". Once this headline was widened
   to two lines that claim became FALSE: at 320–480px the browser preferred to fill line 1 and
   split the word, rendering « L'épilation au laser, prat / iquement sans douleur. »
   ⚠ A LINE COUNT SAID 2 AND WAS RIGHT. It was 2 lines. It was the wrong 2 lines, and only a
   screenshot showed it — the probe now reads the GLYPHS ON EACH LINE, not just how many.
   `.hero-in .display` is (0,2,0) against that rule's (0,1,0), so it wins wherever it is written.
   It is scoped to the home hero: /thermocoagulation still needs and still gets `hyphens:auto`. */
.hero-in .display{hyphens:manual;-webkit-hyphens:manual}

/* ⭐ AND WITH HYPHENATION OFF, THE PHONE SIZE HAS TO EARN THE SECOND LINE — measured, per language.
   The break has to land at the comma, so line 2 must hold the whole second half; the size where
   that becomes true was found by shrinking in 0.5px steps until the rendered glyphs came back as
   exactly two lines, at five real phone widths:
     FR « pratiquement sans douleur. »  320→27px · 360→30.5px · 375→32px · 414→36px · 430→37px
                                        = 8.44 / 8.47 / 8.53 / 8.70 / 8.60 vw   → 8.4vw is under all
   ⭐ WHY `min()` AND NOT A MEDIA QUERY. 8.4vw crosses the clamp's own 46.4px floor at 552px, so
   min() hands over exactly there and the two curves meet at the same number — no step, no
   breakpoint to maintain, and nothing to re-derive if the clamp changes. A @media (max-width:479px)
   would have dropped 6px in one pixel of width.
   ⚠ THE COST, STATED PLAINLY: a French phone headline is ~30 % smaller than the English one on the
   same screen. French is simply longer here — 21 + 26 characters against 19 + 19 — and "2 lines
   MAX" on the longer language has to be paid for somewhere. The alternatives were three clean
   lines, or the hyphen-split above; the owner ruled two lines. */
.hero-in .display{font-size:min(9.2vw, clamp(2.9rem,5.9vw,5.6rem))}
html[lang="fr"] .hero-in .display{font-size:min(8.4vw, clamp(2.9rem,5.9vw,5.6rem))}
/* ⭐ THE BOTTOM LINE — the lede and the place, side by side (owner 2026-09-04).
   The eyebrow used to sit ABOVE the headline; it now closes the hero beside the lede. `.eyebrow`
   is itself `display:flex` (for its own gold rule ::before), so it stays a flex ITEM here and its
   rule and letterspacing are untouched — nothing about the eyebrow component changed, only where
   it is placed. It wraps to its own line below ~520px rather than squeezing. */
/* ⚠ THE COLUMN GAP GREW WHEN THE DASH LEFT, AND THAT IS ARITHMETIC, NOT TASTE. The eyebrow's gold
   hairline was 24px of rule plus its own 12px gap — 36px of separation between the two halves of
   this line. Removing it (owner 2026-09-04) left only this 22px gap, so the sentence and the place
   read closer together than before rather than the same. 34px puts the rhythm back without the
   device. The two halves are still told apart by their own type: sentence case against letterspaced
   small caps. */
.hero-sub{display:flex;flex-wrap:wrap;align-items:baseline;gap:6px 34px;margin-top:26px}
.hero-sub .lede{margin-top:0;max-width:none}
.hero-sub .eyebrow{align-self:center}
.hero-actions{display:flex;flex-wrap:wrap;align-items:center;gap:20px 32px;margin-top:36px}
/* ⭐ The treatment hero's three links sit in a 2×2 beside « Réserver » (owner 2026-09-03, corrected
   the same day). His words: *"I want 'understand the treatment' to the right of 'See the prices'
   and 'Ask Azur for a price' under 'See the prices'."* So:
       [ RÉSERVER ]   See the prices  →     Understand the treatment  →
                      Ask Azur for a price →
   A first pass stacked all three in ONE column, which is not what he asked for.
   ⚠ THE ORDER FALLS OUT OF THE DOM, NOT OUT OF PLACEMENT RULES. `grid-auto-flow` is row by
   default, so the three links land r1c1, r1c2, r2c1 in the order build.mjs prints them — prices,
   understand, Azur. Nothing is hand-placed, so adding or removing a link cannot leave a hole.
   ⚠ `justify-items:start`, or a 13.5px link would stretch to its whole grid track and its underline
   would run the full column width instead of the width of its own words.
   `.hero-actions` is already `align-items:center`, so the block centres itself on the button. */
.hero-links{display:grid;grid-template-columns:auto auto;gap:12px clamp(24px,2.6vw,40px);justify-items:start;align-items:center}
.hero-meta{margin-top:28px;font-size:13.5px;color:var(--ink-2);display:flex;flex-wrap:wrap;gap:10px 14px;align-items:center}
.hero-meta .sep{color:var(--muted)}
.hero-meta a{text-decoration:underline;text-underline-offset:4px;text-decoration-color:var(--line-strong)}
.hero-meta a:hover{text-decoration-color:var(--royal)}

/* ---------- sub-page hero ---------- */
/* ⚠ SUB-PAGE HEROES CAME DOWN ~12% on 2026-08-25. Owner: *"the header texts or images are maybe
   too big on other pages… I think it's the pictures that are too big and it forces you to make
   the text bigger."* That diagnosis is the fix: the PHOTO column was driving the headline, so the
   photo was narrowed first (5fr → 4.5fr of a 12fr row: 450 → 402px wide, and the 4:5 ratio then
   takes the height down with it, 563 → 502px) and the display cut followed it (6.3vw → 5.5vw:
   91 → 79px at 1440).
   ⚠ FLOOR: craft-audit C2 wants a real hero headline at 1440 — 5.5vw leaves 79px, and going much
   below this starts failing the gate rather than looking quieter. Re-run the audit after any
   further trim.
   ⚠ The HOME hero is a different component (.hero) and is untouched, and the ADS landing page
   overrides both rules below (.ads-hero .thero-grid is one column, .ads-fig img sets its own
   16:9) — its full-width faded photo was a deliberate ask and must not follow this change. */
.thero{padding-top:clamp(48px,6vw,88px)}
/* Owner 2026-08-25: *"'Laser hair removal, virtually painless.' — there's too much space on the
   right side of it… It's like an amateur did it."* He was right, and the cause is mechanical.

   These headlines are written `Text before, *italic word* rest.` and the italic is forced onto a
   NEW LINE (`em::before{content:"\A"}`, see the Dead ends note — `display:block` strands the
   closing period). So the half BEFORE the italic must fit on one line, or it wraps and leaves a
   stub. It did not fit: at 1440 « Laser hair removal, » measures 665px against a 649px cap
   (`max-width:13ch`), missing by 16px, so it broke into 356 + 309 and left the long italic half
   at 616 underneath — two stubs and a ragged hole beside the photo. Worse on French:
   « La microdermabrasion, » stranded **« La » alone on an 80px line.**

   Measured, not guessed: 12 treatment pages × 5 widths (1280→1920) were rendered against 15
   combinations of type-cap, vw-step and column split. The old values scored **17/60 clean, 12
   orphaned lines and 8 overflow/overlap cases**. These score **43/60 clean, 0 orphans, 0
   overflow**. "Clean" = both halves each on exactly one line.
   The column also grew 7.5fr → 8.2fr, which is what directly answers "too much space on the
   right" — the copy gets the room and the gap to the photo closes.
   ⚠ FLOOR: craft-audit fails a photo-led hero under 56px (72px without a photo). The narrowest
   this renders is **63px at 1280**. Do not lower the vw step without re-running that gate. */
/* ⭐ `start`, NOT `end` — THE STANDING RULE (owner 2026-09-03). `align-items:end` bottom-aligned
   the two tracks: measured across all 68 two-column hero rows, copy.bottom === fig.bottom to
   within 0.1px, so whenever the photo was the taller column the whole copy block was pushed
   down by the difference. On /en/chemical-peel-montreal that was 145.0px at 1440 and 160.2px
   at 1280 — the eyebrow began a screen-and-a-half of white below the photo's top edge and the
   block read as floating. 67 of 72 measured rows failed a 24px top-gap bar; `start` makes the
   gap 0 by construction on every one of them.
   ⚠ It also fixes the mirror case nobody had named: on /epilation-laser the COPY is taller, so
   `end` was pushing the PHOTO down 40px instead. Same defect, other column.
   ⛔ `.ads-hero .thero-grid` (further down) already says `start` and is a one-column stack; it
   is unaffected and deliberately excluded from the 24px bar.
   The rule generalises and is written down as a standing rule for every site build, Part 1 §4 of
   ~/.claude/skills/repond-allo-client-sites/references/design-rulebook.md — ⚠ that file is OUTSIDE
   this worktree, so a grep scoped to the repo will not find it (one already didn't). In short: a
   text block aligns to the TOP of the thing beside it — never centred or bottomed against a taller
   neighbour, never left holding a blank column under a short heading — and the top gap gets
   measured before the layout is called done. */
.thero-grid{display:grid;grid-template-columns:minmax(0,8.2fr) minmax(0,3.8fr);gap:clamp(32px,5vw,80px);align-items:start}
.thero-copy .display{max-width:none;font-size:clamp(2.5rem,4.9vw,4.3rem)}
/* ⚠ THE COLUMN GAP IS 12px, NOT 14px, AND THE 2px IS MEASURED (2026-09-03). This row is the
   hero's « Durée · X · à partir de N $ » on treatments and « Aller à : a · b · c » on concerns.
   It wraps by design and has never broken — but /en/mens-facial-montreal carries EIGHT items
   and used 736.55px of a 738.00px column at 1440: 1.45px of headroom, i.e. one glyph of font
   drift from dropping a treatment name onto a second row. Seven gaps × 2px returns 14px and
   takes that page to ~15px. Every other .thero-meta on the site had ≥312px spare, so nothing
   else is affected and 2px is invisible at 13.5px type. */
.thero-meta{margin-top:22px;font-size:13.5px;color:var(--ink-2);display:flex;flex-wrap:wrap;gap:8px 12px;font-variant-numeric:tabular-nums}
.thero-meta .sep{color:var(--muted)}
.thero-fig img{width:100%;aspect-ratio:4/5;object-fit:cover;filter:saturate(.88) sepia(.06)}
/* The technology hero is a PRODUCT shot, not a photograph of a person, and the two cannot share
   one rule. `object-fit:cover` on a 4/5 box crops a 520×720 render — it would slice the top of the
   machine off — and on a phone the shared rule squeezes it to 16/10, which would cut a standing
   1.5m device down to a letterbox. So: `contain`, its own taller box, no crop at any width.
   It also sits on a plinth. The render's own background is pure white, and dropped straight onto
   this site's cream paper a white rectangle reads as a pasted cutout — one of the "looks
   AI-generated" tells DESIGN.md exists to avoid. A soft surface panel with a hairline makes the
   same white read as deliberate product photography. The warm `sepia(.06)` is dropped here too:
   it is there to marry skin tones to the paper, and on brushed metal it just looks dirty. */
.thero-fig-device img{aspect-ratio:5/6;object-fit:contain;filter:none;background:var(--surface);border:1px solid var(--line);padding:clamp(18px,2.4vw,34px)}
@media (max-width:900px){.thero-fig-device img{aspect-ratio:5/6}}
.thero-fig figcaption,figure figcaption{font-size:14.5px;color:var(--muted);margin-top:12px;line-height:1.5;max-width:48ch}
.thero-plain .sec-head{margin-bottom:clamp(40px,5vw,64px)}
.thero-plain{padding-bottom:clamp(40px,5vw,64px)}
/* ---- the contact page's head, smaller (owner 2026-08-26) ----
   Measured before: craft-audit read this h1 at 109.4px on a page whose job is to hand over a
   phone number. .display-sm caps it at 64px, which is still comfortably above craft-audit's C2
   floor (56px for a photo-led hero; this one has no photo at all). The head block also gives back
   ~24px of its own margin, so a LONGER sentence and a LONGER paragraph still occupy less height
   than the short ones did. */
.display-sm{font-size:clamp(2.1rem,4.2vw,4rem)}
/* ---- WIDER HEAD, ONE-LINE HEADLINE (owner 2026-08-27) ----
   *"make the header and the first paragraph longer in width. Especially the header, I think the
   header being 1 line is better."*
   ⚠ THE ONE LINE IS BOUGHT WITH TYPE SIZE, and there is no other currency. The head can only be
   as wide as .wrap allows — 1152px at 1440, and STILL 1152 at 1920, because --wrap caps at 1280
   and the pads at 64. The FRENCH headline is the long one at 63 characters. Measured: it draws
   25.8px of width per point of font size, so 1152px of room buys about 44px and this caps at
   40px, which lands the line at 1033px with 119px of slack at every width from 1280 up.
   That is down from the 64px .display-sm gave it: **this page's headline is deliberately smaller
   than every other page's, and the one line is what was bought with it.** If you re-word the
   headline, re-measure — a longer sentence spends the slack.
   ⚠ NO white-space:nowrap. A headline that cannot wrap turns a narrow window into sideways
   scroll. This one is SIZED to fit, and below 900px it goes back to the ordinary .display-sm
   scale and wraps like every other headline — 63 French characters on one 375px line would be
   5px type, so the phone was never going to hold it and should not pay for the attempt.
   ⚠ craft-audit's C2 wants a bigger hero headline and will flag this page. That was already true
   at 64px and is recorded in content.mjs: this page has no hero photo and its job is to hand
   over a phone number, so the home-page rule does not apply to it. */
.sec-head-full{max-width:none}
/* ⚠ THE CAP IS THE MEASURED ONE-LINE MAXIMUM, NOT A TASTE CALL (owner 2026-08-27: "as big as
   possible to keep it on one line"). The FRENCH headline draws 25.8px of line width per point
   of font size and the head is capped at 1152px by --wrap, so the ceiling is 1152 / 25.8 =
   44.6px; 44 leaves 17px of slack. The slope keeps the same guarantee below 1280 (avail ≈ 90vw,
   so ≤ 3.42vw always fits). English is shorter and could carry ~56px, but the two languages
   share one scale everywhere else on this site and French is the one that governs.
   Re-word the headline one word longer and the cap must be re-measured. */
.display-line{font-size:clamp(1.8rem,3.42vw,2.75rem);line-height:1.14}
/* ⚠ 940px IS MEASURED, NOT ROUND. The opening sentence draws 820px (FR) / 813px (EN) at 17.28px,
   so the measure has to clear ~820 for line one to hold it; and the REST is 207 French characters
   ≈ 1786px, which needs ≥ ~900px a line to land in two rather than spilling three words onto a
   fourth. 940 satisfies both with slack. Re-measure if the copy changes.
   ⚠ THIS IS ~95 CHARACTERS, WIDER THAN THE 55–75 THE REST OF THE SITE HOLDS TO. The owner asked
   for this exact break on this page. Do not copy the width to another one. */
.lede-wide{max-width:940px}
/* ---- the /tarifs headline: ONE line at the craft floor (owner 2026-08-27) ----
   It rendered 109.4px over three lines in half a grid; the owner called it too huge. 72px is
   not a taste number: craft-audit's C2 floor for a typographic hero is 72px, and its C4 accepts
   this photo-less hero only as "type-as-image", which needs ≥64px. This is the smallest header
   the gates permit — and at full width it is one line, so the block loses ~75 % of its height.
   The slope (5.6vw) keeps the ENGLISH line (the long one, ~15.5px of width per point of font)
   inside the head down to ~1024; below that it wraps like any headline. */
.display-one{font-size:clamp(2.2rem,5.6vw,4.5rem)}
/* ⚠ display:block IS THE LINE BREAK. See the note in content.mjs: it holds at every width,
   where a width-tuned break moves the first time a word changes. */
.lede-a{display:block}
.thero-tight{padding-top:clamp(36px,4.4vw,64px)}
.thero-tight .sec-head{margin-bottom:clamp(28px,3.2vw,40px)}
.thero-tight .lede{max-width:64ch}
.thero-tight .lede-wide{max-width:940px}
/* below 900 the headline cannot be one line at a readable size, so it stops trying and takes the
   page's ordinary scale back */
@media (max-width:899px){.display-line{font-size:clamp(2.1rem,4.2vw,4rem)}}

/* ---------- treatments list (home + soins + "other treatments") ---------- */
.tlist-grid{display:grid;grid-template-columns:minmax(0,7fr) minmax(0,5fr);gap:clamp(32px,5vw,80px);align-items:start}
.tlist{border-top:1px solid var(--line)}
.tlist-row{border-bottom:1px solid var(--line)}
.tlist-row a{display:grid;grid-template-columns:44px 1fr auto 28px;gap:20px;align-items:baseline;padding:22px 0;transition:padding .3s var(--ease-rise)}
.tlist-row a:hover{padding-left:8px}
.tlist-n{font-size:12px;font-weight:500;letter-spacing:.12em;color:var(--muted);font-variant-numeric:tabular-nums}
.tlist-main{display:flex;flex-direction:column;gap:4px}
.tlist-name{font-family:var(--serif);font-weight:600;font-size:clamp(1.5rem,2.3vw,2.1rem);line-height:1.14;letter-spacing:-.005em;font-variation-settings:"opsz" 26}
.tlist-hook{font-size:16px;color:var(--ink-2)}
.tlist-meta{display:flex;flex-direction:column;align-items:flex-end;gap:4px;font-size:13px;color:var(--ink-2);font-variant-numeric:tabular-nums;white-space:nowrap}
.tlist-arrow{color:var(--muted);transition:transform .3s var(--ease-rise),color .2s}
.tlist-row a:hover .tlist-arrow{transform:translateX(4px);color:var(--ink)}
.tlist-fig{position:sticky;top:96px;aspect-ratio:4/5;overflow:hidden;background:var(--paper-2)}
.tlist-fig img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;filter:saturate(.88) sepia(.06);display:none}
.tlist-fig img.is-on{display:block}
.sec-others .tlist-grid{grid-template-columns:1fr}
.sec-others .eyebrow{margin-bottom:24px}

/* ---------- steps ---------- */
.steps{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:clamp(28px,4vw,56px);border-top:1px solid var(--line);padding-top:32px}
.steps li{display:flex;flex-direction:column;gap:12px}
.step-n{font-size:12px;font-weight:500;letter-spacing:.12em;color:var(--muted);font-variant-numeric:tabular-nums}
.steps h3{font-size:1.75rem;font-variation-settings:"opsz" 24}
.steps p{color:var(--ink-2);font-size:16px;line-height:1.6;max-width:38ch}
.ground-night .steps{border-top-color:rgba(244,238,227,.2)}
.ground-night .steps p,.ground-night .steps h3{color:#D7D9E6}
.ground-night .steps h3{color:var(--paper)}
.ground-night .step-n,.ground-night .tlist-n{color:#CDB577}

/* ---------- tech / benefits (night band) ---------- */
.tech-grid{display:grid;grid-template-columns:minmax(0,5fr) minmax(0,7fr);gap:clamp(32px,6vw,96px);align-items:start}
/* ---- the six Soprano benefits (owner 2026-08-26) ----
   ⚠ WHAT THIS REPLACED, so nobody rebuilds it: a 2-column grid whose rows were `36px 1fr` — a
   number, then the text. Two of the six titles wrap to a second line, which pushed their row's
   bottom hairline out of line with its neighbour's, and the section read as broken rather than as
   dense. The numbers are gone (nothing was counting benefits) and the mark is now the icon the
   extractor already reads off the live site.
   The component is shared: three columns on the home technology section, two and smaller
   (.benefits-sm) inside the laser page's hero. `data-cols` carries the count so the grid and the
   ring animation agree about how many items are on a row. */
.benefits{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:clamp(22px,2.6vw,36px)}
.benefits[data-cols="2"]{grid-template-columns:repeat(2,minmax(0,1fr))}
/* ---- column flow: fill the FIRST column top to bottom, then the second (2026-08-27) ----
   Only the laser hero asks for it, so that its six benefits read down the left column and then
   down the right — and so a phone, which flattens the grid to one column, reads them in that
   same order instead of the interleaved one row-flow produces.
   ⚠ --b-rows IS SET BY build.mjs FROM THE ITEM COUNT. A hard-coded 3 here is right until the
   extraction returns five benefits or seven.
   ⚠ IT MUST BE UNDONE WHERE THE GRID COLLAPSES TO ONE COLUMN — see the phone block. With one
   explicit column and auto-flow:column, the browser invents an implicit second column and the
   list goes back to two-across, which is the exact bug the phone rule already exists to stop. */
.benefits[data-flow="col"]{grid-auto-flow:column;grid-template-rows:repeat(var(--b-rows,3),auto)}
.benefits li{display:grid;grid-template-columns:auto minmax(0,1fr);column-gap:14px;row-gap:5px;align-content:start}
/* the icon square: a tinted chip, not a filled button — DESIGN.md §Shape, radius 2px, no shadow */
.b-ic{grid-row:1/3;width:34px;height:34px;display:grid;place-items:center;border-radius:2px;font-size:15px;line-height:1;
  color:var(--brass-text);background:color-mix(in srgb,var(--brass) 12%,transparent);border:1px solid color-mix(in srgb,var(--brass) 42%,transparent)}
.benefits .b-t{font-family:var(--serif);font-weight:500;font-size:1.28rem;line-height:1.2;letter-spacing:-.004em;margin:0;color:var(--ink);font-variation-settings:"opsz" 18;align-self:center}
.benefits p{font-size:15px;line-height:1.55;color:var(--ink-2);margin:0}
/* on the inverted ground the chip has to carry its own contrast — the tint is invisible on navy */
.ground-night .b-ic{color:#E8CE7A;background:rgba(232,206,122,.13);border-color:rgba(232,206,122,.42)}
.ground-night .benefits .b-t{color:var(--paper)}
.ground-night .benefits p{color:#C3C7DA}
/* ---- the technology section head: the display headline gets the WIDE side ---- */
.tech-head{display:grid;grid-template-columns:minmax(0,8fr) minmax(0,4fr);gap:clamp(32px,4vw,56px);align-items:end;margin-bottom:clamp(34px,4vw,52px)}
.tech-head .sec-head{margin-bottom:0;max-width:none}
.tech-side{display:flex;flex-direction:column;gap:18px;align-items:flex-start}
/* compact: the laser hero, where these sit under the h1 in a ~600px column */
.benefits-sm{gap:16px 26px}
.benefits-sm li{column-gap:11px;row-gap:3px}
.benefits-sm .b-ic{width:28px;height:28px;font-size:13px}
.benefits-sm .b-t{font-family:var(--sans);font-weight:500;font-size:14.5px;line-height:1.25;letter-spacing:.005em}
.benefits-sm p{font-size:13.5px;line-height:1.45;color:var(--muted)}
/* the strip inside the laser hero: it takes the vertical space the lede used to, no more. The
   ring animation still drives it — .thero-benefits carries the wrapper ring, the items carry
   their own, so the six arrive as two rows rather than six separate beats. */
.thero-benefits{margin:clamp(20px,2.2vw,30px) 0 clamp(22px,2.4vw,30px)}
.thero-benefits .benefits{gap:15px 24px}

/* ---------- price tables (the printed card) ---------- */
.ptable{background:var(--surface);border:1px solid var(--line);border-radius:2px;padding:6px 22px 8px;margin-top:22px}
.ptable:first-child{margin-top:0}
.pt-head{display:flex;justify-content:space-between;align-items:baseline;gap:16px;padding:16px 0 12px;border-bottom:1px solid var(--line-strong)}
.pt-head>span:first-child{font-family:var(--serif);font-weight:500;font-size:1.45rem;line-height:1.15;font-variation-settings:"opsz" 20}
.pt-meta{font-size:11.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);font-weight:500}
/* ⚠ `minmax(0,1fr)`, never a bare `1fr`, on the NAME column. A `1fr` track carries an automatic
   minimum of its content, so a long treatment name cannot shrink and the whole grid silently
   overflows its container instead — which is exactly what happened when the action column grew
   from 44px to 152px to fit « Réserver » beside the +: the table pushed ~66px past the 1152px
   text column. It went unseen at 1440 (the overspill still landed inside the viewport) and only
   surfaced as a horizontal scrollbar at 1280. minmax(0,…) lets the name wrap instead. */
.pt-cols,.pt-row{display:grid;grid-template-columns:minmax(0,1fr) 100px 136px 136px 152px;gap:12px 18px;align-items:baseline}
/* Laser has no Cure Essentielle, so its tables run one column shorter (see cureTable). The two
   price columns are given ROOM rather than left hugging the name — a table that simply lost a
   column reads as broken; one that re-spaces reads as designed. */
.pt-cols-3,.pt-row-3{grid-template-columns:minmax(0,1fr) 116px 168px 152px}
.pt-cols{padding:12px 0 8px;font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);font-weight:500;border-bottom:1px solid var(--line);text-align:right}
.pt-cols small{display:block;font-size:10px;letter-spacing:.06em;color:var(--brass-text);text-transform:none;margin-top:2px;white-space:nowrap}
.pt-row{padding:11px 0;border-bottom:1px solid var(--line);font-variant-numeric:tabular-nums}
.pt-row:last-child{border-bottom:0}
.pt-row:hover{background:linear-gradient(90deg,transparent,rgba(20,27,61,.025) 20%,rgba(20,27,61,.025) 80%,transparent)}
.pt-name{font-size:15.5px;color:var(--ink);line-height:1.35}
.pt-sub{display:block;font-size:13px;color:var(--muted);line-height:1.45;margin-top:3px;max-width:52ch}
.pt-p{text-align:right;font-size:15.5px;color:var(--ink-2);white-space:nowrap}
.pt-p b{font-weight:500;color:var(--ink)}
/* Each price's own column name (M1, 2026-08-27). ABOVE 600px it is VISUALLY hidden but still
   in the accessibility tree — the .pt-cols header strip is aria-hidden="true", so until now a
   screen reader heard « Front 75 $ 360 $ » with no idea which figure was which, at every width.
   ⚠ Do NOT "tidy" this to display:none — that removes it from the a11y tree and puts the defect
   back on desktop. The phone rule below turns it visible; that is the only difference. */
.pt-lab{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
.pt-row-flat{grid-template-columns:minmax(0,1fr) auto 152px}
.pt-row-req,.pt-row-norm{grid-template-columns:minmax(0,1fr) auto}
.pt-row-flat .pt-p{color:var(--ink);font-weight:500}
/* ---- the add-to-cart button on every priced row (owner 2026-08-24, like the live site) ----
   48px tall for the C7 tap floor; the negative margins hand the extra height back to the row,
   so the tables gain a little air instead of doubling in length. Radius 2px = the one button
   radius this site allows. On the night ground the tables sit on their own --surface card,
   so one colour set works everywhere. */
/* Réserver leads, + follows — the order the owner asked for: booking one treatment must not
   require assembling a cart first. Both are 48px for the C7 tap floor and hand the extra height
   back to the row with negative margins, so the tables gain air instead of doubling in length. */
.pt-acts{grid-column:-2/-1;justify-self:end;align-self:center;display:inline-flex;gap:6px;margin:-9px 0}
.pt-book{min-height:48px;padding:0 14px;background:var(--surface);border:1px solid var(--gold-dk);border-radius:2px;color:var(--brass-text);font:500 12.5px/1 var(--sans);letter-spacing:.06em;text-transform:uppercase;white-space:nowrap;cursor:pointer;transition:background .18s,color .18s}
.pt-book:hover{background:var(--cta-bg);color:var(--cta-ink)}
.pt-book:focus-visible{outline:2px solid var(--royal);outline-offset:2px}
.pt-add{width:44px;height:48px;display:grid;place-items:center;background:var(--surface);border:1px solid var(--line-strong);border-radius:2px;color:var(--royal);font:500 20px/1 var(--sans);cursor:pointer;transition:border-color .18s,color .18s,background .18s}
.pt-add:hover{border-color:var(--gold-dk);color:var(--brass-text)}
.pt-add:focus-visible{outline:2px solid var(--royal);outline-offset:2px}
.pt-add.added{border-color:var(--gold-dk);color:var(--brass-text);background:#FBF6E7}
/* A required add-on carries a word where the others carry a button — there is nothing to press,
   because site.js adds and removes it with its treatment. The line under the name says so. */
.pt-req-tag{grid-column:-2/-1;justify-self:end;align-self:center;font:500 11px/1 var(--sans);letter-spacing:.12em;text-transform:uppercase;color:var(--brass-text);border:1px solid var(--line-strong);border-radius:2px;padding:7px 10px;white-space:nowrap}
.pt-row-req-addon .pt-name{color:var(--ink)}
.pt-row-req-addon .pt-sub{display:block;margin-top:3px;font-size:12.5px;color:var(--muted);letter-spacing:0;text-transform:none}
.cart-li-req{font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--brass-text)}
.pt-req{font-style:italic;color:var(--muted);font-size:14.5px}
.pt-h3{margin:40px 0 14px;font-size:1.3rem;color:var(--ink-2)}
.sec-tprices .title{margin-bottom:36px}
.sec-tprices .lede{margin-top:-12px;margin-bottom:32px}
.pt-note{font-size:15px;color:var(--ink-2);max-width:62ch;margin-top:18px;line-height:1.6}
/* ⚠ 14.5px IS A FLOOR, NOT A PREFERENCE — do not shrink these three rules back.
   design-rulebook.md §1: small type (11–14px) is for LABELS only; a running SENTENCE never
   goes under 14.5px. These three carried real sentences at 12.5–13px (the footer's 243-char
   price/GST note, the same note above the price tables, and the hero figcaption that defends
   the « pratiquement » claim), in BOTH languages. Measured 2026-08-31 on /epilation-laser.
   ⚠ AND `ch` IS NOT CHARACTERS: `ch` is the width of "0", which is wider than the average
   letter in Work Sans, so the old 80ch/86ch rendered as 104 and 111 REAL characters — well
   past the 88 the rulebook bans. Measured ratio here is ~1.30 real chars per ch, so 54ch
   lands at ~70, inside the 65–75 target. Re-measure that ratio before changing the face. */
.pt-legal{font-size:14.5px;color:var(--muted);max-width:54ch;margin-top:36px;line-height:1.6}
/* ---- GAP 4 — « Certaines conditions s'appliquent », three forms, ONE hex ----
   #7A5E16 is the live site's own conditions gold, copied on purpose: live's rule is "one hex for
   one meaning across the site", and this note means the same thing on both. Measured on this
   site's four light grounds — --paper 6.00:1 · --paper-2 5.46:1 · --paper-3 5.38:1 · --surface
   6.10:1. All AA with room.
   ⛔ DO NOT "tidy" this to var(--brass-text) (#8A6A12): it measures 4.45:1 on --paper-3, which
   the tarifs and treatment price sections actually use — i.e. it FAILS AA on a ground this note
   is printed on. The rulebook's own line: contrast is measured per SURFACE, not per token.
   ⚠ 14.5px, NOT live's 12.4/11px. Same reason .pt-legal is 14.5 six lines up: these are legal
   sentences about money, and 11–14px is for labels. The words are live's; the size is this
   site's floor. If it reads heavy on a package card that is a taste call and cheap to reverse —
   shipping legal fine print under the floor is the thing the floor exists to stop. */
.cond-note{font-size:14.5px;color:#7A5E16;max-width:54ch;margin-top:26px;line-height:1.6}
/* the same sentence, under a package LIST (owner 2026-09-01). Its own class so assertConditions()
   can count it against .pkg-set; same hex and same 14.5px floor as .cond-note, because it is
   literally the same words and one meaning gets one look.
   ⚠ ITALIC IS THE ONLY DIFFERENCE, and it is what the owner asked for. It sets the note apart
   from the card text above it without reaching for a second colour or a smaller size — the two
   moves the rulebook bans on a running legal sentence.
   ⚠ text-align:left is explicit, not inherited: the home page's set sits inside .sec-prices,
   whose head block is centred at some widths, and a centred legal line reads as a caption. */
.pkg-legal{font-size:14.5px;font-style:italic;color:#7A5E16;max-width:54ch;margin:26px 0 0;line-height:1.6;text-align:left}
.ground-night .cond-note,.ground-night .pkg-legal,.ground-night .cond-inline{color:#CDB577}
.ptable-extras .pt-head>span:first-child{font-size:1.45rem}
.seg-wrap{position:sticky;top:var(--nav-h);z-index:6;padding:12px 0 14px;margin-bottom:14px;background:var(--paper)}
.ground-2 .seg-wrap{background:var(--paper-2)}
.seg-wrap::after{content:"";position:absolute;left:0;right:0;bottom:0;height:1px;background:var(--line)}
.seg{display:inline-flex;border:1px solid var(--line-strong);border-radius:2px;background:var(--surface)}
.seg-btn{min-height:48px;border-radius:2px;padding:0 22px;font-size:12.5px;letter-spacing:.12em;text-transform:uppercase;font-weight:500;color:var(--ink-2)}
.seg-btn+.seg-btn{border-left:1px solid var(--line-strong)}
.seg-btn.is-on{background:var(--ink);color:var(--paper)}
.seg-pane[hidden]{display:none}
/* ---- the pane's own <h2> (task 9a, owner 2026-09-01) ----
   « Épilation laser pour femmes / pour hommes » is a REAL heading, so it is a `.title` and
   keeps `.title`'s family, weight, colour and reveal. Only the SIZE is narrowed: the section
   heading directly above it runs up to 4.1rem and two headlines that size in a row read as a
   duplicated title rather than as a section and its part.
   ⚠ PLACED AFTER `.sec-tprices .title{margin-bottom:36px}` ON PURPOSE. That rule is (0,2,0)
   and so is this one — a tie, which the LATER rule wins. Move this block above line 700 and
   the 36px bottom margin comes back and the heading floats away from its own table. */
.seg-pane>.seg-h2{font-size:clamp(1.5rem,2.4vw,2rem);margin:0 0 22px}
/* Femmes and Hommes stopped being side-by-side COLUMNS when the packages became cards
   (2026-08-25): a card is ~330px wide, so two columns of them left one card per row and a page
   that scrolled for ever. Each segment now takes the full text column and its cards run three
   across, the way dermazur.ca lays them out. */
/* .pkg-cols was deleted 2026-08-27 with the combined packages section — each gender's cards
   now sit in their own full-width section on /tarifs (see tarifsPage). */
/* ---- the /tarifs jump bar (M2, 2026-08-27) ----
   Measured before: every one of the 10 links rendered 21.6px tall — 2.4px UNDER the 24px floor
   of WCAG 2.2 AA 2.5.8 (Target Size Minimum), at EVERY width, not only on a phone. They are not
   inline-in-a-sentence links, so the 2.5.8 inline exemption v2.15 leaned on does not reach them:
   each one is a standalone navigation control, and on a phone it is the ONLY in-page navigation
   the reader gets (.rail is desktop-only, see the 1279px rule below).
   ⚠ HOW IT SURVIVED EVERY SWEEP: sweep.mjs reports these as `TOUCH<40px`, and the documented
   `grep -v "TOUCH<40px"` noise filter swallows the sub-24 rows together with the benign 24–39px
   ones. Verify this bar with a DIRECT getBoundingClientRect probe, never through the filtered
   sweep.
   ⚠ THE MIN-HEIGHT ONLY WORKS BECAUSE THE LINKS ARE FLEX ITEMS of .jump and are re-declared
   inline-flex here — a plain inline <a> ignores min-height entirely, which is how 21.6px (a bare
   line-height) is what was rendering. */
.jump{display:flex;flex-wrap:wrap;gap:10px 22px;align-items:center;font-size:13.5px;font-weight:500;border-top:1px solid var(--line);border-bottom:1px solid var(--line);padding:16px 0}
.jump span{color:var(--muted);letter-spacing:.1em;text-transform:uppercase;font-size:11.5px}
.jump a{display:inline-flex;align-items:center;min-height:24px;text-decoration:underline;text-underline-offset:5px;text-decoration-color:var(--line-strong)}
.jump a:hover{text-decoration-color:var(--royal)}
/* On a phone they become real chips — the same affordance .bk-day/.bk-time already use in the
   booking modal, so this is the site's existing "pick one of these" object, not a new one.
   The underline goes: a bordered chip already reads as a target, and 10 underlined full-width
   rows read as a wall of links.
   ⚠ 48px, NOT the 34px of the booking drawer's chips, AND THE REASON IS MEASURED: the moment
   these gained a fill they became BUTTONS to craft-audit's C7 (a/button wider than 60px, taller
   than 24px, filled or bordered or named), which fails a page whose shortest button is under 48.
   At 34px it reported `✗ C7 a button is 34px (<48)` on a page that was CRAFT FLOOR CLEAN at
   HEAD. The drawer's 34px chips escape C7 only because the drawer is `hidden` when closed and
   the gate cannot see it (v2.20) — that is an accident of visibility, not a lower standard.
   ⚠ THIS OVERRIDE LIVES BESIDE THE BASE RULE, not in the shared responsive block — that block is
   declared EARLIER in this file (line ~1281) and would win here by source order today, but the
   .cart-pill lesson is that the moment a component moves, a distant override silently dies. */
@media (max-width:600px){
  .jump{gap:8px;padding:14px 0}
  .jump span{width:100%;margin-bottom:2px}
  .jump a{min-height:48px;padding:0 14px;background:var(--paper);border:1px solid var(--line);border-radius:2px;color:var(--ink-2);text-decoration:none;font-size:13px}
  .jump a:active{background:var(--night);border-color:var(--night);color:var(--paper)}
}
.prices-grid{display:grid;grid-template-columns:minmax(0,5fr) minmax(0,7fr);gap:clamp(32px,6vw,96px);align-items:start}

/* ---- the pricing key: a flat 4-column legend strip (owner 2026-08-27) ----
   It replaced .legend-grid + .legend — a bordered box floating right of the headline, sharing
   its grid but not its baseline, which the owner called "ugly and out of place". Flat on the
   ground, each term over its meaning, a hairline above each column: it reads as a legend FOR
   the tables below instead of competing with the headline as a second card.
   ⚠ THE MEDIA RULES LIVE HERE, BESIDE THE BASE RULE — the shared responsive block is declared
   EARLIER in this file and loses on source order (the .cart-pill lesson, already recorded). */
.pkey{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:clamp(18px,2.4vw,36px);margin:clamp(30px,3.4vw,44px) 0 0}
.pkey div{border-top:1px solid var(--line-strong);padding-top:14px}
.pkey dt{font:500 11.5px/1.3 var(--sans);letter-spacing:.13em;text-transform:uppercase;color:var(--royal)}
.pkey dd{margin:7px 0 0;font-size:14px;line-height:1.5;color:var(--ink-2)}
@media (max-width:900px){.pkey{grid-template-columns:1fr 1fr}}
@media (max-width:600px){.pkey{grid-template-columns:1fr;gap:14px}.pkey div{padding-top:12px}}
/* ---------- who / proof ---------- */
.who-grid{display:grid;grid-template-columns:minmax(0,5fr) minmax(0,7fr);gap:clamp(32px,6vw,96px);align-items:start}
.who-grid-rev{grid-template-columns:minmax(0,7fr) minmax(0,5fr)}
.who-fig img{width:100%;aspect-ratio:4/5;object-fit:cover;filter:saturate(.9)}
.who-role{font-size:14px;letter-spacing:.02em;color:var(--ink-2);margin:12px 0 26px}
.who-copy>p.r{color:var(--ink-2);max-width:60ch;margin-bottom:16px;font-size:16.5px}
/* the one lead paragraph the home page keeps: a size up from body, so it reads as the statement
   it is rather than as the first of two identical grey blocks */
.who-lead{font-size:17.5px!important;line-height:1.6;color:var(--ink-2)}
.who-more{display:flex;flex-wrap:wrap;gap:12px 24px;align-items:center}
.who-facts{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:20px 26px;border-top:1px solid var(--line);padding-top:22px;margin:28px 0 24px}
/* ⚠ 1.15rem, DOWN FROM 1.45. At 1.45 every one of the three titles wrapped to two lines inside a
   ~200px column, so the row read as three little headings arguing with the h2 above them. These
   are labels on a definition list, not headings. */
.who-facts dt{font-family:var(--serif);font-weight:500;font-size:1.15rem;line-height:1.25;margin-bottom:5px;font-variation-settings:"opsz" 16}
.who-facts dd{font-size:15px;color:var(--ink-2);line-height:1.55}

/* ---------- faq (open, two columns) ---------- */
.faq-grid{display:grid;grid-template-columns:minmax(0,5fr) minmax(0,7fr);gap:clamp(32px,6vw,96px);align-items:start}
.faq-grid .sec-head{margin-bottom:0;position:sticky;top:96px}
.faq{border-top:1px solid var(--line)}
.faq-item{padding:24px 0;border-bottom:1px solid var(--line)}
.faq dt{font-family:var(--serif);font-weight:500;font-size:1.5rem;line-height:1.2;letter-spacing:-.005em;margin-bottom:10px;text-wrap:balance;font-variation-settings:"opsz" 20}
.faq dd{color:var(--ink-2);font-size:15.5px;max-width:60ch}
.ground-night .faq,.ground-night .faq-item{border-color:rgba(244,238,227,.2)}
.ground-night .faq dd{color:#C9CCDD}

/* ⭐ THE AZUR CONTROL NEEDS AIR ABOVE IT (2026-09-03) — caught by LOOKING, not by measuring.
   Its <p> inherits margin-top:0, so on the six treatment pages the link sat 2.8px under the price
   card while the conditions note below it sat 26.4px away. Every number about it was correct and
   it still read as the last row OF the card rather than as its own control. 26px matches the gap
   underneath, so the link sits in its own band with equal air on both sides.
   ⚠ Scoped to `.pt-azur`, which build.mjs stamps ONLY on the treatment pages. The two Tarifs
   nudges sit directly under an <h2> that already carries its own margin; giving them this rule
   would double the space there. */
/* two links side by side on the laser page (Ask Azur · Laser configurator), one everywhere else —
   flex with the same column gap the hero's link grid uses, so the pair reads like that pair. */
.pt-azur{margin-top:26px;display:flex;flex-wrap:wrap;gap:12px clamp(24px,2.6vw,40px);align-items:center}

/* ---------- brief / concerns / room / contact ---------- */
/* Owner 2026-08-25: *"the in-brief section in every page should maybe go at the end, or you can
   make that section much smaller and just take up less space so that people can get to the point
   more. Don't make the website like a labyrinth."*

   Kept where it is and shrunk, rather than moved to the end — it is a SUMMARY, and a summary that
   arrives after the thing it summarises is furniture. What made it a wall was not the words: on
   /electrolyse it is two short lines, but it wore a full `--sec` band (129.6px of padding top AND
   bottom at 1440, ~260px of empty ground) and set them in 1.45rem serif across a 3-column grid.
   It is now one hairline-separated strip that sits under the hero and is read in a glance.
   The section is ~85 % shorter and no fact was dropped. */
.sec-brief{padding:clamp(18px,2vw,26px) 0}
.brief-wrap{display:flex;align-items:baseline;gap:clamp(18px,2.6vw,40px)}
.brief-wrap .eyebrow{margin:0;flex:0 0 auto}
.brief{display:flex;flex-wrap:wrap;gap:6px 26px;border-top:none;padding-top:0}
.brief li{font-family:var(--sans);font-weight:400;font-size:15px;line-height:1.5;color:var(--ink-2);max-width:56ch;padding-left:26px;border-left:1px solid var(--line)}
.brief li:first-child{padding-left:0;border-left:none}
/* ---- ⭐ THE STRIP NEVER BREAKS MID-LIST (owner 2026-09-03) ----
   Two different defects lived in the four lines above, and only one of them was visible.
   (1) `.brief li{max-width:56ch}` = 522.5px. On FR /electrolyse item 2 is 547px of natural text,
       so it was CLIPPED to the cap and wrapped to a second text line — inside a row with 141.6px
       of unused width beside it. Because `.brief` is a flex row with the default
       `align-items:stretch`, its neighbour stretched to the same 45px and the whole strip stood
       two lines tall for no reason. No checker saw it: the items were still on ONE row, so a
       row-break test reads clean. `max-width:none` + `white-space:nowrap` ends that class of
       failure everywhere — an item is now atomic and either sits on the row or moves to the next.
   (2) The ROW break the owner named, on /microneedling (3 items; every other page has 2).
       `.brief-wrap-stack` (build.mjs stamps it at >=3 points) puts the label on its own line and
       hands the list the full 1152px content box instead of 1020.6px. That is +131.4px and it is
       what puts /en/microneedling on one row (needs 1085.1px).
   ⚠ FR /microneedling needs 1198.3px and therefore still wraps — by 46.3px, at EVERY width,
     because the column is 1152px from 1280 up. It now wraps by whole item with a real row-gap.
   ⛔ SCOPED TO >=701px. Below that the block above turns the strip into a stacked list and
     `white-space:nowrap` would push a 547px sentence off the side of a 375px phone.
   ⛔ AFTER the rules it overrides, not before — `flex` and `max-width` are re-declared here and a
     later declaration wins on a tie, whatever the selector. (The `gap:` / `row-gap:` lesson this
     file records three times over.) */
@media (min-width:701px){
  .brief{flex:1 1 auto;min-width:0;row-gap:12px}
  .brief li{max-width:none;flex:0 1 auto;min-width:0}
  .brief-wrap-stack{display:block}
  .brief-wrap-stack .eyebrow{margin-bottom:10px}
}
/* ⛔ `white-space:nowrap` STARTS AT 900, AND THE 769 IT SAID FIRST WAS WRONG BY 85× ----
   Written at 701 with `flex:0 0 auto`, an item could neither wrap nor shrink, so on FR
   /electrolyse-montreal — whose second fact is 547px of unbreakable text, « Forfaits Fidélité :
   5 h −10 % · 10 h −15 % *certaines conditions s'appliquent » — a 573.6px box escaped a 514.6px
   list and `body{overflow-x:hidden}` CUT IT OFF. The reader saw « …certaines conditions
   s'appliqu ». A condition notice truncated mid-word is worse than any layout defect this round
   set out to fix.
   ⚠⚠ THEN 769 WAS PICKED FROM THE WRONG MEASUREMENT, AND AN ADVERSARIAL PASS CAUGHT IT.
   727 was the width at which the DOCUMENT stopped overflowing; the real question is whether the
   widest item fits its own LIST, and at 769 that headroom is 0.47px on /electrolyse and
   −71.9px on the worst page in the set. The comment claimed "~40px". It was off by 85×, and one
   character added upstream would have re-opened the same clip.
   ⭐ THE RULE THAT COMES OUT OF IT: measure the constraint you are actually asserting, on the
   worst member of the set, not the one page that prompted the bug.
   Re-measured across all 10 strips × 9 widths, min widest-item headroom:
       769 → −71.9   800 → −44.9   900 → +42.5   960 → +95   1024 → +150.9   1280 → +374.7
   900 is the first width where EVERY item fits its list, and it is a step this file already uses.
   Below 900 items keep `max-width:none` and simply wrap inside their own full width — a two-line
   item, never a cut one. */
@media (min-width:900px){ .brief li{white-space:nowrap;flex:0 0 auto} }

/* ⭐ NO HAIRLINE ON THIS STRIP AT ALL — IT SEPARATES BY SPACE (2026-09-03) ----
   `.brief li{border-left}` + `:first-child{border-left:none}` strips the divider from the first
   item of the LIST, not from the first item of each ROW. A wrapped row therefore opens with an
   orphan 1px rule and a 26px indent, which reads as a rendering fault. There is no CSS selector
   for "first in row".
   ⚠⚠ AND THE FIRST FIX SCOPED THIS TO THE THREE-FACT STRIP ONLY, on the written claim that "the
   five two-fact strips provably never wrap". THAT CLAIM WAS FALSE and an adversarial pass proved
   it: the claim was measured at ≥1280, where the column is at its widest. Measured across every
   width, the number of strips wrapping to two rows is
       769 → 10/10   900 → 8/10   960 → 7/10   1024 → 4/10   1152 → 2/10   ≥1280 → 1/10.
   Nine of the twelve pages showed the orphan rule somewhere between 700 and 1280. A rule that is
   only true at the width you happened to measure is not a rule.
   So every `.brief` separates by a 36px gap, at every width. One treatment, no orphan rule
   possible, nothing depending on which page or which window is in front of you. */
.brief li{padding-left:0;border-left:none}
@media (min-width:701px){ .brief{column-gap:36px} }

@media (max-width:700px){
  .brief-wrap{display:block}
  .brief-wrap .eyebrow{margin-bottom:10px}
  .brief{display:block}
  .brief li{padding-left:0;border-left:none;max-width:none}
  .brief li+li{margin-top:6px}
}
.concerns{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:0 clamp(24px,3vw,48px);border-top:1px solid var(--line)}
/* one sentence per card since 2026-08-24 (see firstSentence() in build.mjs) — the padding comes
   down with the text, or nine short cards just sit in nine tall boxes and read as empty */
.concerns li{padding:20px 0;border-bottom:1px solid var(--line)}
.concerns h3{font-size:1.35rem;margin-bottom:7px;font-variation-settings:"opsz" 18}
.concerns p{font-size:15.5px;line-height:1.55;color:var(--ink-2);max-width:40ch}
.concern-go{margin-top:12px;font-size:13px;font-weight:500}
.concern-go a{text-decoration:underline;text-underline-offset:4px;text-decoration-color:var(--line-strong)}
/* ═══ THE CONCERNS SECTION CLOSES ON A 28px LINK, SO IT DOES NOT NEED A BLOCK'S PADDING ═══
   Only /soins-de-la-peau and /skin-treatments carry .sec-concerns, so this is scoped to them.

   THE MEASUREMENT (1440, before): the grid's last row is three short cards, and grid rows stretch
   to the tallest, so columns 2 and 3 already run empty for 77-101px up to the closing rule. Under
   that rule sits « Every concern, in detail » — a 28px utility line, in column 1 only. Then the
   section's own --sec bottom (129.6px) and the CTA's --sec top (129.6px) add 259px more. Columns
   2 and 3 therefore held ~400px of unbroken paper between their last word and the BOOK band, and
   that is the hole the owner pointed at on 2026-09-05.

   --sec is sized for a section that ends in a content block. This one does not, so BOTH sides of
   that one boundary come in: the grid's bottom, and the CTA's top on these two pages only. Cutting
   the concerns side alone was measured first and left 178px — better, still a band, because half
   the gap was never the concerns section's to give. 1440: 129.6 + 129.6 = 259px → 49 + 95 = 143px.
   ⛔ ABOVE 600px ONLY, AND THAT BOUND IS THE WHOLE REASON THE RULE IS CORRECT. The hole is made
   by a PARTLY FILLED last grid row: 9 cards over 3 columns (>900px) leave a row of three short
   ones, and over 2 columns (601-900px) leave a single card beside an empty column. At <=600px the
   grid is ONE column (see the max-width:600px block below), every row is full, the link follows
   the last card directly and there is no hole — so the site's normal --sec rhythm is right there
   and this must not touch it. Measured before adding the bound: 375px was being pulled from the
   standard 144px to 96px, tighter than every other section break on the phone, fixing nothing.
   ⛔ NOT a --sec override like .sec-story's: that pulls the TOP in too, and the space ABOVE this
   grid is doing its job — it separates the grid from the section head.
   ⛔ The CTA rule is scoped by the adjacent sibling, so the gold band keeps its full air on the
   other 50 pages. Only .sec-concerns can trigger it, and only 2 pages carry that. */
@media (min-width:601px){
  .sec-concerns{padding-bottom:clamp(40px,3.4vw,52px)}
  .sec-concerns + .sec-cta{padding-top:clamp(56px,6.6vw,96px)}
}
.concern-go a:hover{text-decoration-color:var(--royal)}
.room-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--gap)}
.room-grid img{width:100%;aspect-ratio:4/3;object-fit:cover;filter:saturate(.92)}
.room-grid figure:first-child{grid-column:span 2}
.room-grid figure:first-child img{aspect-ratio:2/1}
.contact-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--gap)}
.contact-card{background:var(--surface);border:1px solid var(--line);padding:26px 28px 30px;border-radius:2px}
.contact-card .eyebrow{margin-bottom:12px}
.contact-card .contact-big{font-family:var(--serif);font-weight:500;font-size:clamp(1.5rem,2.4vw,2.1rem);color:var(--ink);line-height:1.2;margin-bottom:10px;font-variant-numeric:tabular-nums;font-variation-settings:"opsz" 26}
.contact-card p{color:var(--ink-2);font-size:15px}
.contact-card .cta-actions{margin-top:18px}

/* ---------- story (the education, after the prices) ---------- */
.story-anchor{position:relative;top:-72px;height:0}
.sec-story .title{margin-bottom:28px;max-width:22ch}
.story-grid{display:grid;grid-template-columns:minmax(0,7fr) minmax(0,5fr);gap:clamp(32px,5vw,80px);align-items:start}
.story-grid-1{grid-template-columns:minmax(0,760px)}
.story-p{color:var(--ink-2);font-size:16.5px;line-height:1.65;max-width:62ch;margin-bottom:16px}
.story-p strong{color:var(--ink);font-weight:500}
.ground-night .story-p{color:#D7D9E6}
.story-fig img{width:100%;aspect-ratio:4/5;object-fit:cover;filter:saturate(.88) sepia(.06)}
.story-items{border-top:1px solid var(--line);margin-top:8px}
.story-items li{padding:12px 0;border-bottom:1px solid var(--line);font-size:15.5px;color:var(--ink-2);line-height:1.5}
.story-items strong{color:var(--ink);font-weight:500}
.story-items-2{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:0 clamp(24px,3vw,48px)}
.punch{font-family:var(--serif);font-style:italic;font-weight:500;font-size:clamp(1.5rem,2vw,1.8rem);line-height:1.34;color:var(--ink);max-width:30ch;margin:8px 0 8px;padding-left:20px;border-left:1px solid var(--brass);font-variation-settings:"opsz" 24}
.cards{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:0 clamp(24px,3vw,48px);border-top:1px solid var(--line);margin-top:8px}
.cards-3{grid-template-columns:repeat(3,minmax(0,1fr))}
.cards-4{grid-template-columns:repeat(4,minmax(0,1fr))}
.cards li{padding:20px 0 22px;border-bottom:1px solid var(--line)}
.cards h3{font-size:1.45rem;margin-bottom:8px;font-variation-settings:"opsz" 18}
.cards p{font-size:15.5px;color:var(--ink-2);line-height:1.55;margin:0}
.timeline{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:clamp(20px,3vw,40px);border-top:1px solid var(--line);padding-top:22px;margin-top:12px}
.timeline li{display:flex;flex-direction:column;gap:8px}
.tl-tag{font-size:11.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--royal);font-weight:500}
.timeline h3{font-size:1.45rem;font-variation-settings:"opsz" 18}
.timeline p{font-size:15.5px;color:var(--ink-2);line-height:1.55}
.steps-n{margin-top:8px}
.story-photos{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--gap);margin-top:40px}
.story-photos img{width:100%;aspect-ratio:3/2;object-fit:cover;filter:saturate(.9)}
.tags{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}
.tags li{border:1px solid var(--line-strong);border-radius:2px;padding:7px 12px;font-size:12.5px;letter-spacing:.06em;color:var(--ink-2)}
.sec-story-quiet .story-p{font-size:15.5px}
/* concerns: hub list + per-concern answers */
.clist{border-top:1px solid var(--line);max-width:900px}
.tlist-grid .clist{max-width:none}
.clist li{border-bottom:1px solid var(--line)}
.clist a{display:grid;grid-template-columns:44px 1fr 28px;gap:20px;align-items:start;padding:24px 0;transition:padding .3s var(--ease-rise)}
.clist a:hover{padding-left:8px}
.clist-main{display:flex;flex-direction:column;gap:6px}
.clist-name{font-family:var(--serif);font-weight:500;font-size:clamp(1.5rem,2.3vw,2.1rem);line-height:1.14;letter-spacing:-.005em;font-variation-settings:"opsz" 26}
.clist-say{font-size:15px;color:var(--ink-2);line-height:1.55;max-width:62ch;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
.clist-go{font-size:12.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);font-weight:500;margin-top:4px}
.clist-tight a{padding:16px 0}
.clist-tight .clist-name{font-size:1.45rem;font-variation-settings:"opsz" 18}
.clist .tlist-n{padding-top:6px}
.clist .tlist-arrow{padding-top:6px}
.clist a:hover .tlist-arrow{transform:translateX(4px);color:var(--ink)}
.answers{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:clamp(24px,3vw,48px);align-items:start}
/* ⭐ ROOM TO BREATHE (owner 2026-09-05: "the headlines, titles, body txt all feel a bit too
   condensed. Put a bit of space in-between them"). Four gaps opened, each one a different
   relationship: rule → title 20→30, title → the price meta 6→10, meta → body 14→20, and the
   body → « Learn more » 0→22. The h2 above the grid gets its own space from .sec-head. */
.answer{border-top:1px solid var(--line);padding-top:30px}
.answer h3{font-size:1.6rem;margin-bottom:10px;font-variation-settings:"opsz" 20}
.answer .story-p{margin-top:0;line-height:1.66}
.answer .story-p + p{margin-top:22px}
.answer h3 a{text-decoration:underline;text-underline-offset:5px;text-decoration-color:var(--line-strong)}
.answer h3 a:hover{text-decoration-color:var(--royal)}
.answer-meta{font-size:12.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);font-weight:500;margin-bottom:20px;font-variant-numeric:tabular-nums}
.thero-meta a{text-decoration:underline;text-underline-offset:4px;text-decoration-color:var(--line-strong)}
.thero-meta a:hover{text-decoration-color:var(--royal)}
/* ---- M5, 2026-08-28: the concern pages' « Le soin : » strip, 21.6px × 30 links ----
   The third and last family the corrected tap probe found. 18 concern pages (9 FR + 9 EN), one
   to four links each, every one under the 24px floor of WCAG 2.2 AA 2.5.8.
   ⚠ NO `display` CHANGE IS NEEDED HERE, AND THAT DIFFERENCE IS THE WHOLE POINT OF THE FAMILY.
   `.thero-meta` is `display:flex` (line ~438), so its links are already flex ITEMS and therefore
   blockified — computed display:block, each with a box of its own. min-height applies straight
   away. It also means 2.5.8's inline exception CANNOT reach them: a flex item's height is not
   constrained by the line-height of surrounding text, because there is no surrounding text on
   its line — the « · » beside it is an aria-hidden separator between two targets, not prose.
   ⚠ min-height ALONE — NO `display` AND NO `align-items`, AND BOTH OMISSIONS ARE DELIBERATE.
   A first version added `display:flex;align-items:center`; an adversarial review measured what
   that actually did, and it was strictly negative:
     · `display:flex` buys nothing. The anchor is already a flex ITEM of `.thero-meta`, so it is
       blockified to display:block and honours min-height without help.
     · `align-items:center` centres the 21.6px line inside its 24px box, which pushes the link's
       baseline **1.2px below** « Le soin : » and the « · » sitting on the same line — Range-
       measured on /acne-cicatrices-montreal: SPAN 536.39 · A 537.59 · SPAN.sep 536.39 · A 537.59
       with it, and 536.39 across the board without it. The strip is 24px tall either way.
   The comment here previously claimed centring was needed "or the underline drops away from the
   separator". **That was backwards** — the top position is the one that shares the separator's
   baseline. Measured, not reasoned.
   Cost, measured on /acne-cicatrices-montreal: strip 51.2 → 56px and page +5px at 375;
   strip 21.6 → 24px and page +0 at 1440. Zero overflow at both. */
.thero-meta a{min-height:24px}
.sec-qa .sec-head .lede{font-size:1rem;max-width:44ch}
/* concerns (legacy hub blocks) */
.concern-grid{display:grid;grid-template-columns:minmax(0,7fr) minmax(0,5fr);gap:clamp(32px,5vw,80px);align-items:start;margin-top:clamp(24px,3vw,40px)}
.concern-h{font-size:1.45rem;margin:22px 0 8px;font-variation-settings:"opsz" 18}
.concern-h a{text-decoration:underline;text-underline-offset:4px;text-decoration-color:var(--line-strong)}
.concern-h a:hover{text-decoration-color:var(--royal)}
.limits{border-left:1px solid var(--brass);padding-left:20px;margin:26px 0 10px}
.limits .concern-h{margin-top:0}
.limits p{font-size:15px;color:var(--ink-2);line-height:1.6;max-width:58ch}
.faq-tight{margin-top:16px}
.faq-tight .faq-item{padding:16px 0}
.faq-tight dt{font-size:1.45rem;font-variation-settings:"opsz" 18}
.faq-tight dd{font-size:15px}
.concern-go{margin-top:20px;font-size:13.5px;display:flex;flex-wrap:wrap;gap:10px 18px;align-items:center}
.concern-fig img{width:100%;aspect-ratio:4/5;object-fit:cover;filter:saturate(.88) sepia(.06)}
.concern-fig{position:sticky;top:96px}
.palette-panel{width:260px}

/* ---------- the pricing rail (tarifs only): quiet ticks, labels on hover, active follows scroll ---------- */
/* no transform and no opacity here — the design gate treats those as stuck animations (memory rule:
   deliberate hiding is display, deliberate fades are colour) */
.rail{position:fixed;left:14px;top:0;bottom:0;z-index:40;display:none;pointer-events:none}
/* Owner 2026-08-25 asked for this on every page that has sections worth jumping to, so the
   `body[data-page="tarifs"]` gate is gone. `railFor()` in build.mjs decides WHICH pages get one
   (3 sections minimum); this rule only decides when it is big enough to show. */
.rail{display:flex;align-items:center}
@media (max-width:1279px),(max-height:640px){.rail{display:none}}
.rail ul{display:flex;flex-direction:column;gap:2px;pointer-events:auto}
.rail a{display:flex;align-items:center;gap:10px;padding:6px 4px;color:var(--ink-2)}
/* ⚠ The tick was a 1px `--line-strong` hairline — #DACFB4, pale sand. That was fine while the rail
   only ever appeared on /tarifs, which is cream paper. Now it appears on the home page too, where
   it lands on a bright sky photo, and pale-on-pale is invisible: zoomed in at 3× the eight ticks
   read as scratches on the image rather than as a control. `--muted` (#5A6080) with a soft paper
   glow holds on a light photo, on cream, and on the night grounds, and is still quiet enough to
   stay the discreet device it was designed to be. 2px rather than 1 so the active tick has
   somewhere to go. */
.rail i{width:16px;height:2px;border-radius:2px;background:var(--muted);box-shadow:0 0 4px rgba(255,253,247,.9);transition:width .25s var(--ease-rise),background .25s}
/* The open labels FLOAT OVER the page, and they have to look like they mean to. Measured with the
   labels forced open: the widest chip reaches 148px on a treatment page and 216px on /tarifs, while
   the text column starts at 64px (1280), 107px (1366) and 144px (1440) — so on every common laptop
   the chips land on top of the copy. That is inherent to a fixed hover-menu at the screen edge, it
   pre-dates this change (/tarifs overlapped by 152px at 1280 before the rail went anywhere else),
   and the owner likes the interaction as it is, so the interaction is not what changes here.
   What changes is that an opaque chip with a hairline and no depth reads as a COLLISION, while the
   same chip with a shadow reads as a menu open above the page. Shadow + a slightly stronger border,
   and the paper-coloured ring keeps it legible where it lands on a photo. */
.rail span{display:none;font-size:12px;font-weight:500;letter-spacing:.03em;line-height:1;background:var(--surface);border:1px solid var(--line-strong);border-radius:2px;padding:6px 9px;white-space:nowrap;box-shadow:0 0 0 3px var(--paper),0 10px 26px -10px rgba(20,27,61,.45)}
/* ⚠ ONE label at a time — the tick the pointer is actually on. This rule used to be
   `.rail:hover span`, which opened ALL of them together: eight chips, ~30px apart, forming a
   150px-wide wall down the left edge. Measured, that wall lands ON the copy at every common
   laptop width (text starts at 64px @1280, 107 @1366, 144 @1440; the widest chip reaches 148 on a
   treatment page and 216 on /tarifs) and a screenshot at the worst case shows it covering the H1's
   second line and three lines of the lede. A shadow was tried first and was not enough — it made
   the wall read as a floating wall.
   A single transient chip is the standard flyout behaviour and is the one the reader is looking
   at anyway, so the overlap costs one line instead of a column. Every label is still reachable,
   and the ticks themselves stay clickable with no hover at all.
   ⚠ Do NOT add `.rail a.is-active span` here: the active tick is always somewhere on screen, so
   that would put a permanent chip on top of the copy — the same bug, minus the hover. */
.rail a:hover span,.rail a:focus-visible span{display:inline-block}
.rail a:hover i{width:24px;background:var(--ink)}
.rail a.is-active i{width:24px;background:var(--royal)}
.rail a.is-active{color:var(--ink)}
.rail a.is-active span{border-color:var(--line-strong)}
/* The « Aperçu » tick means "back to the top of the page", so it must land at 0, not at the hero
   section's own offset. The hero starts 85px down (the sticky header occupies that space in flow),
   so clicking it stopped at scrollY 85 — visually identical under an 84px sticky nav, but not
   actually the top. An over-large scroll-margin makes the browser clamp the target to 0.
   ⚠ scroll-margin does not move the element, so the rail's scroll-spy is unaffected. */
#apercu{scroll-margin-top:200px}
@media (prefers-reduced-motion:reduce){.rail i,.rail span{transition:none}}

/* ---------- the phone jump bar (M3, 2026-08-27) — the rail's stand-in below 1280px ----------
   The rail is `display:none` under 1280px wide, so on a phone /tarifs (32.4 screens after M1)
   and /epilation-laser (30.7) had nothing to navigate with once the hero scrolled away. This is
   a 44px sticky bar under the header: the section you are in, and a tap to go somewhere else.
   ⭐ IT HAS NO LIST OF ITS OWN — build.mjs hands it the rail's rows, and site.js marks it active
   from the SAME `data-rail` scroll-spy the rail uses. One list, one spy, two presentations.
   ⚠ HIDDEN ABOVE 1024px, WHERE THE RAIL IS STILL HIDDEN TOO. The rail's own cut-off is 1279px,
   so 1024–1279 is a real gap; the bar covers it rather than leaving tablet users with nothing.
   Do not "align" this to the rail's 1279 without checking that gap again.
   ⚠ --pjump-h IS LOAD-BEARING: `.seg-wrap` on /epilation-laser is ALSO sticky at var(--nav-h),
   so without the offset below the two would stack on the same line and the segment control would
   be swallowed. The offset is written against the variable, not a copied 44. */
/* 48px button + its 1px rule. ⚠ THE 48 IS craft-audit's C7 FLOOR, not taste: C7 counts any
   a/button/[class*=btn] wider than 60px, taller than 24px, that is filled, bordered or NAMED a
   button — `.pjump-btn` and the chips are all three — and fails the page if the shortest is
   under 48. Measured at 34/44px first: `✗ C7 a button is 34px (<48)` on all four combos, on a
   page that was CRAFT FLOOR CLEAN at HEAD. */
:root{--pjump-h:49px}
.pjump{display:none}
@media (max-width:1024px){
  /* ⚠ A SOLID BACKGROUND, NOT color-mix(), AND THAT IS A RULE THIS REPO ALREADY LEARNED ONCE:
     "a painted TEXT background must resolve to rgb()". Chrome computes
     `color-mix(in srgb, var(--paper) 94%, transparent)` to `color(srgb 1 0.992 0.969 / 0.94)`,
     and every contrast checker (craft-audit's bgOf included) pulls numbers out of an `rgb()`
     string — it read that as near-BLACK and reported 1.25:1 on the section label and 3.4:1 on
     « Aller à », both of which actually measure 6.07:1 and better on paper.
     The second reason is not the gate at all: this bar scrolls OVER the navy package cards and
     the navy CTA bands, and a 94 % bar would take their colour up into the one label whose job
     is to stay readable. Solid is correct here even without a checker. */
  .pjump{display:block;position:sticky;top:var(--nav-h);z-index:20;background:var(--paper);border-bottom:1px solid var(--line)}
  /* stood down while the page's own « Aller à » strip is on screen — see site.js.
     ⚠ `display:none`, not opacity/visibility: this codebase's rule is that deliberate hiding is
     `display` and deliberate fades are colour, and design-capture's checker flags anything under
     0.99 opacity as a possible frozen reveal (recorded in Dead ends). */
  .pjump.is-idle{display:none}
  /* ⚠ minus the 1px rule, so --pjump-h is the WHOLE bar's height and every consumer of it
     (.seg-wrap's sticky top, :target's scroll-margin) offsets by a number that is actually
     true. Measured before this line existed: bar bottom y117 against a seg-wrap pinned at 116. */
  .pjump-btn{display:flex;align-items:center;gap:10px;width:100%;min-height:calc(var(--pjump-h) - 1px);padding:0 clamp(16px,4vw,32px);background:none;border:0;text-align:left;cursor:pointer}
  .pjump-lab{flex:0 0 auto;font:500 10.5px/1 var(--sans);letter-spacing:.14em;text-transform:uppercase;color:var(--muted)}
  /* the section you are in — it truncates rather than wraps, because the bar's height is the
     thing keeping it out of the reader's way */
  .pjump-now{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font:500 13px/1.2 var(--sans);color:var(--ink)}
  .pjump-caret{flex:0 0 auto;width:9px;height:9px;border-right:1.5px solid var(--ink-2);border-bottom:1.5px solid var(--ink-2);transform:rotate(45deg) translate(-2px,-2px);transition:transform .18s}
  .pjump-btn[aria-expanded="true"] .pjump-caret{transform:rotate(225deg) translate(-2px,-2px)}
  /* the open list: the same chip the /tarifs « Aller à » strip uses, so it is one object the
     reader has already met at the top of the page rather than a second kind of control */
  .pjump-list{display:flex;flex-wrap:wrap;gap:8px;margin:0;padding:4px clamp(16px,4vw,32px) 14px;list-style:none;max-height:min(52vh,340px);overflow-y:auto;border-top:1px solid var(--line);background:var(--paper)}
  .pjump-list[hidden]{display:none}
  .pjump-list a{display:inline-flex;align-items:center;min-height:48px;padding:0 14px;background:var(--surface);border:1px solid var(--line);border-radius:2px;color:var(--ink-2);font:500 13px/1 var(--sans)}
  .pjump-list a.is-active{background:var(--night);border-color:var(--night);color:var(--paper)}
  /* ⚠ stack the segment control BELOW the bar — both are sticky and both would otherwise pin to
     var(--nav-h). Scoped to the laser page because that is the only page carrying both. */
  body[data-page="laser"] .seg-wrap{top:calc(var(--nav-h) + var(--pjump-h))}
  /* every jump target has to clear the header AND the bar, or the section title lands underneath
     them. ⚠ #apercu keeps its own 200px (it means "the very top", see above) — an ID selector
     outranks :target, so that rule wins without needing to be repeated here. */
  :target{scroll-margin-top:calc(var(--nav-h) + var(--pjump-h) + 12px)}
}
@media (prefers-reduced-motion:reduce){.pjump-caret{transition:none}}

/* ---------- the combination ladder (laser + tarifs) ---------- */
/* a band that carries only two short sentences must not keep a full-height section's padding */
.sec-tight{padding:clamp(48px,6vw,84px) 0}
.sec-tight .story-p:last-child{margin-bottom:0}
.sec-ladder .steps{margin-bottom:clamp(36px,4vw,56px)}
/* ⭐ THE MULTI-ZONE HEADING GETS THE ROW, SO IT HOLDS ONE LINE (owner 2026-09-04: "make it
   'Several areas, same day, lower price' in ONE line… it saves space and makes it look elegant").
   Shortening the words was not enough on its own: `.sec-head` caps at 760px and the line needs
   more than that at 63.36px, so it still broke after two commas — the worst place to break, since
   the three beats are the whole point of the rewrite.
   ⚠ ONLY THE HEADING WIDENS. `.sec-head` also holds the lede, and a paragraph set to 1152px would
   run past 90 characters a line — so the lede keeps the old 760px measure and the h2 alone escapes.
   Widening the shared container would have fixed the heading by breaking the paragraph under it. */
.sec-ladder .sec-head{max-width:none}
.sec-ladder .sec-head .lede{max-width:760px}
.ladder-grid{display:grid;grid-template-columns:minmax(0,5fr) minmax(0,7fr);gap:clamp(28px,5vw,72px);align-items:start}
.ladder-notes .pt-note{margin-top:16px}
.ladder-notes .pt-note b{font-weight:500;color:var(--ink)}
.stack-note{font-size:16.5px;line-height:1.6;color:var(--ink);max-width:60ch;padding-left:20px;border-left:1px solid var(--brass)}
/* the form that RIDES WITH THE DISCOUNT, inside the ladder card itself (owner 2026-08-28: "all
   'Conditions apply' next to the multizone discount text. In gold too like the other places").
   A block, not an inline span, because it follows .pt-row rows — its hairline separates it from
   the last rung the way the rungs are separated from each other. Colour + size: see .cond-note. */
.cond-inline{display:block;font-size:14.5px;color:#7A5E16;line-height:1.45;padding:12px 0 6px}
/* ⚠ NO border-top HERE, and the reason is one line further up: `.pt-row:last-child{border-bottom:0}`
   stopped firing the moment this note became the table's last child, so the final rung kept its
   hairline. A border of its own drew a SECOND one 1px below — visible as a double rule under
   « 4+ grandes zones ». The row's own border is the separator; this note just sits under it. */

/* the zones whose price is set in person: named compactly, not 26 rows of "Sur demande" */
.req-block{margin-top:44px;border-top:1px solid var(--line-strong);padding-top:26px}
.req-block .pt-h3{margin-top:0}
.req-block .pt-note{margin-bottom:26px}
.req-block .eyebrow{margin:22px 0 12px}
.req-block .tags li{background:var(--surface)}

/* ---------- reviews, quoted with the name ---------- */
/* ---- reviews, reformatted (owner 2026-08-26: "make it easier to read, better formatted") ----
   Cards, not three paragraphs sharing two hairlines — see the note above reviewsSection() for
   what each change is answering. No shadow: DESIGN.md §Shape, "depth is colour and hairlines". */
.rev-score{display:flex;align-items:center;gap:12px;margin:0 0 14px;font-variant-numeric:tabular-nums}
.rev-score b{font-weight:500;font-size:15px;color:var(--ink)}
.rev-stars{color:var(--brass-text);letter-spacing:.14em;font-size:15px;line-height:1}
.quotes{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:clamp(16px,2vw,24px)}
.quotes li{display:flex;flex-direction:column;gap:14px;background:var(--surface);border:1px solid var(--line);border-radius:2px;padding:22px 24px 18px}
.q-stars{color:var(--brass-text);letter-spacing:.16em;font-size:14px;line-height:1;margin:0}
/* a blockquote, because these are somebody's words; the browser's default margin is cleared here
   rather than in a reset so the element keeps its meaning */
.q-t{font-size:16.5px;line-height:1.62;color:var(--ink-2);margin:0;flex:1}
.q-f{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-top:auto;padding-top:14px;border-top:1px solid var(--line)}
.q-src{font-size:12px;letter-spacing:.1em;text-transform:uppercase;color:var(--muted)}
.ground-night .quotes li{background:rgba(244,238,227,.05);border-color:rgba(244,238,227,.2)}
.ground-night .q-t{color:#D7D9E6}
.ground-night .q-f{border-top-color:rgba(244,238,227,.2)}
.q-by{font-family:var(--serif);font-weight:500;font-size:1.05rem;line-height:1.2;letter-spacing:0;text-transform:none;color:var(--ink);font-variation-settings:"opsz" 16}
.ground-night .q-by{color:var(--paper)}
.sec-proof .wrap>p.r{margin-top:26px}

/* ---------- Azur (home) ---------- */
/* ---- Azur: what she can do, and the chat that shows it (rebuilt 2026-08-23) ----
   The explaining paragraph is gone. Left = the ranked capability list, right = the conversation.
   Equal columns: neither half is the illustration of the other, they are the two halves of the
   same claim. */
.azur-grid{display:grid;grid-template-columns:minmax(0,6fr) minmax(0,6fr);gap:clamp(32px,5vw,72px);align-items:start}
.azur-can{border-top:1px solid var(--line);margin:28px 0 0}
.azur-can li{padding:15px 0;border-bottom:1px solid var(--line);font-family:var(--serif);font-weight:500;font-size:1.45rem;line-height:1.3;font-variation-settings:"opsz" 18;color:var(--ink)}
.azur-grid .cta-actions{margin-top:32px}

/* the chat, dressed as a chat — a card, a live dot, one exchange, a message box */
.azur-chat{background:var(--surface);border:1px solid var(--line);border-radius:2px;overflow:hidden}
.azur-chat-head{display:flex;align-items:center;gap:12px;padding:16px 18px;border-bottom:1px solid var(--line)}
.azur-av{width:40px;height:40px;flex:none;display:grid;place-items:center;background:var(--royal);color:#fff;font-weight:500;font-size:16px;letter-spacing:.02em;border-radius:2px}
.azur-who{display:flex;flex-direction:column;gap:3px;min-width:0}
.azur-who b{font-size:15px;font-weight:500;color:var(--ink)}
.azur-who i{font-style:normal;font-size:12.5px;color:var(--muted);display:flex;align-items:center;gap:7px}
.azur-dot{width:7px;height:7px;border-radius:50%;background:#1F9D55;flex:none}
.azur-chat-body{padding:18px}
.azur-msg{margin:0;font-size:15.5px;line-height:1.55;padding:13px 15px;border-radius:2px;max-width:92%}
.azur-msg-you{background:var(--paper-2);color:var(--ink-2);margin-left:auto}
.azur-msg-az{background:var(--azur-bubble);border-left:2px solid var(--royal);color:var(--ink);margin-top:12px}
.azur-msg-az+.azur-msg-you{margin-top:16px}
/* the conversation is shorter than the list beside it; centring splits the leftover height
   above and below instead of hanging it all under the card */
.azur-col{align-self:center}
/* looks like a message box, IS a button — it opens the honest "this is a demo" notice */
.azur-chat-foot{display:flex;align-items:center;justify-content:space-between;gap:12px;width:100%;border-radius:2px;padding:15px 18px;border-top:1px solid var(--line);background:var(--paper);color:var(--muted);font:400 15px/1 var(--sans);text-align:left;cursor:pointer;transition:background .2s,color .2s}
.azur-chat-foot:hover{background:var(--paper-2);color:var(--ink-2)}
.azur-send{display:grid;place-items:center;width:34px;height:34px;flex:none;background:var(--royal);color:#fff;border-radius:2px;font-size:15px}

/* ---------- legal page (the live Law 25 text, re-set in this system) ---------- */
/* the legal title is the live page's own sentence, so it is long — smaller than a marketing
   hero but still above the design floor (≥72px at 1440), and it keeps the italic device */
.display-legal{font-size:clamp(2.8rem,5.4vw,5rem);font-variation-settings:"opsz" 48}
.legal-body{max-width:74ch}
.legal-body h2{font-family:var(--serif);font-weight:500;font-size:1.7rem;line-height:1.2;color:var(--ink);margin:44px 0 12px;padding-top:22px;border-top:1px solid var(--line);font-variation-settings:"opsz" 22}
.legal-body h2:first-child{margin-top:0;padding-top:0;border-top:0}
.legal-body p,.legal-body li{font-size:16px;line-height:1.7;color:var(--ink-2)}
.legal-body p{margin-bottom:14px}
.legal-body ul{list-style:none;padding:0;margin:8px 0 18px}
.legal-body li{padding:10px 0 10px 22px;border-bottom:1px solid var(--line);position:relative}
.legal-body li::before{content:"";position:absolute;left:0;top:1.05em;width:10px;height:1px;background:var(--brass)}
.legal-body b{font-weight:500;color:var(--ink)}
.legal-body a{text-decoration:underline;text-underline-offset:4px;text-decoration-color:var(--line-strong)}
.legal-body a:hover{text-decoration-color:var(--royal)}
.legal-body .note{background:var(--surface);border:1px solid var(--line);border-radius:2px;padding:16px 18px;margin:8px 0 18px;font-size:15px}
.sec-ask .cta-actions{margin-top:8px}

/* ---------- technology page ---------- */
.claims{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:0 clamp(24px,4vw,64px);border-top:1px solid var(--line)}
.claims li{padding:20px 0;border-bottom:1px solid var(--line);font-family:var(--serif);font-weight:500;font-size:1.45rem;line-height:1.3;font-variation-settings:"opsz" 18;max-width:34ch}
.numbers{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:0 clamp(20px,3vw,48px);border-top:1px solid var(--line-strong)}
.numbers li{padding:26px 0 28px;border-bottom:1px solid var(--line);display:flex;flex-direction:column;gap:10px}
.numbers b{font-family:var(--serif);font-weight:500;font-size:clamp(2.6rem,4.6vw,4rem);line-height:1;letter-spacing:-.01em;font-variation-settings:"opsz" 44;font-variant-numeric:tabular-nums}
.numbers span{font-size:15px;color:var(--ink-2);line-height:1.5;max-width:24ch}
/* the Fitzpatrick strip — six real tones, the one component on this site that is a picture
   made of data rather than a photo; ported from the live page, which already carries it */
.fitz{background:var(--surface);border:1px solid var(--line);border-radius:2px;padding:26px 28px 24px;max-width:820px}
.fitz-lab{font-size:11.5px;letter-spacing:.16em;text-transform:uppercase;color:var(--royal);font-weight:500;margin-bottom:18px}
.fitz-scale{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:10px}
.fitz-scale li{display:flex;flex-direction:column;gap:8px;align-items:flex-start}
.fitz-scale i{display:block;width:100%;height:56px;border-radius:2px;border:1px solid var(--line)}
.fitz-scale span{font-size:12.5px;font-weight:500;letter-spacing:.1em;color:var(--ink-2)}
.fitz-note{font-size:15px;color:var(--ink-2);line-height:1.6;max-width:62ch;margin-top:20px;padding-top:18px;border-top:1px solid var(--line)}
/* ⭐ 7/5 + a 4/3 photo (owner 2026-09-04, the empty-space sweep). The « Viora Pristine » row ran
     a 735px photo beside a 359px list — a 371px void, 51 % of the row, and the second-worst on the
     site. Measured after: 34px at 1440, 36.5 at 1280 (−91 %).
     ⭐ AND 4/3 IS THE FILE'S OWN SHAPE. clinic-machines.jpg is natively 1600×1200 = 4:3, so the old
     4/5 was cropping 40 % of the picture away to make a hole underneath it. This is the rare fix
     that costs nothing: the image gets bigger AND the void goes. */
.dev-grid{display:grid;grid-template-columns:minmax(0,7fr) minmax(0,5fr);gap:clamp(28px,5vw,72px);align-items:start}
.devices{border-top:1px solid var(--line)}
.devices li{padding:22px 0;border-bottom:1px solid var(--line)}
.devices h3{font-size:1.5rem;margin-bottom:6px;font-variation-settings:"opsz" 20}
.devices p{font-size:15.5px;color:var(--ink-2);line-height:1.55;max-width:44ch}
.dev-fig img{width:100%;aspect-ratio:4/3;object-fit:cover;filter:saturate(.92)}
.sec-trio .tech-copy .tags li,.sec-tech .tech-copy .tags li{border-color:rgba(244,238,227,.34);color:#D7D9E6}
.sec-tech .tech-side .tags{margin:0}
.sec-tech .tech-copy p.r{display:flex;flex-wrap:wrap;gap:14px 26px;align-items:center}
.sec-trio .lede{max-width:52ch}

/* ---------- cta band ---------- */
.cta-grid{display:grid;grid-template-columns:minmax(0,7fr) minmax(0,5fr);gap:clamp(32px,6vw,96px);align-items:end}
.cta-actions{display:flex;flex-wrap:wrap;align-items:center;gap:20px 32px;margin-top:32px}
.cta-meta{display:grid;gap:18px;border-top:1px solid var(--line-strong);padding-top:22px}
.cta-meta dt{font-size:11.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--muted);font-weight:500;margin-bottom:4px}
.cta-meta dd{font-size:15px;color:var(--ink);line-height:1.5}
.cta-meta a{text-decoration:underline;text-underline-offset:4px;text-decoration-color:var(--line-strong)}
/* ---- M5, 2026-08-28: the CTA band's Google-reviews link, 18px, on 48 of the 50 pages ----
   Same defect and same link TEXT as the footer one raised below — and v2.15's ruling named the
   `<dd>` alongside the footer, so this reverses that half of it too.
   ⚠ IT LOOKS EXEMPT AND IS NOT. The markup is
   `<dd>20 min, par téléphone<br><a>5,0 · 200+ avis Google</a></dd>` — the `<br>` puts the link
   alone on its own line, so neither half of 2.5.8's inline exception applies: it is not in a
   sentence, and no non-target text shares its line to constrain it.
   ⭐ THIS IS THE ONE THE PROBE ITSELF WAS HIDING. `tap.mjs` v1 tested "does the parent hold ≥12
   more characters than the link", which the `<br>`-separated sibling satisfied, so it reported
   this as *exempt* on every page. The heuristic now walks siblings and stops at a <br>. **A
   checker that over-exempts is the same failure as the sweep filter this probe exists to answer**
   — fix the checker first, then look at what it finds.
   Cost, measured: the <dl> grows 1.5px, the page 1px at 375 and 0 at 1440. */
.cta-meta dd a{display:inline-flex;align-items:center;min-height:24px}

/* ---------- footer ---------- */
.foot{background:var(--night);color:#C9CCDD;padding:clamp(56px,7vw,88px) 0 28px;font-size:14.5px}
.foot-grid{display:grid;grid-template-columns:2fr 1fr 1fr 1fr;gap:clamp(24px,4vw,56px);padding-bottom:40px;border-bottom:1px solid rgba(244,238,227,.16)}
.foot h4{font-size:11.5px;letter-spacing:.14em;text-transform:uppercase;color:#8E94B4;font-weight:500;margin:0 0 10px}
.foot h4+p+h4{margin-top:22px}
.foot p{margin:0 0 10px;line-height:1.55;max-width:34ch}
.foot a:hover{color:#fff}
.foot li{margin-bottom:6px}
/* ---- M5, 2026-08-28: the footer links meet the 24px tap floor ----
   ⭐ THIS REVERSES v2.15's RULING, ON THE OWNER'S WORD (2026-08-28), AND THE REASON IS THAT THE
   OLD RULING RESTED ON TWO WRONG FACTS. v2.15 left these at their natural height because
   "2.5.8 exempts inline links" and "padding them out would wreck that text's rhythm".
     · The exemption does NOT reach them. WCAG 2.2 AA 2.5.8 exempts a target IN A SENTENCE, or
       one whose size is constrained by surrounding non-target text. An <li> whose entire content
       is one link is not a sentence, and neither is an e-mail address alone on its line. These
       were real AA failures — same class as the /tarifs jump links v2.27 fixed.
     · The rhythm cost is half a percent. MEASURED before changing anything, by injecting this
       exact rule into the live page: the footer goes 818.8 → 822.8px at 375 (+4.0) and
       457.2 → 462.1 at 1440 (+4.9). Each link box goes 17 → 24px, but `.foot li{margin-bottom:6px}`
       already reserved most of that space, so the footer absorbs the rest. Before/after
       screenshots at 375 and 1440 are indistinguishable.
   ⚠ THE `display` IS THE WHOLE FIX; min-height ALONE DOES NOTHING. A plain inline <a> has no
   height of its own — 17px was a bare line-height, exactly as the /tarifs jump links' 21.6px was.
   ⚠ IT CLEARS TEN LINKS, NOT THE SEVEN THE HANDOFF NAMED. The seven were what the tap probe
   reported as failures; it had exempted three more (the phone number, « Confidentialité », the
   address) as arguably-inline. This rule raises all of them, which is the right side to err on.
   ⚠ Three already-tall links grow ~5px too (the 3-line address, « Clientèle trans & non-binaire »,
   and « Peelings chimiques + LED » when it wraps at 375) — inline-flex changes their line-box
   maths. Verified they still wrap identically; do not add exceptions to chase those 5px. */
.foot li a,.foot-rev a,.foot p a{display:inline-flex;align-items:center;min-height:24px}
.foot-rev a{text-decoration:underline;text-underline-offset:4px;text-decoration-color:rgba(244,238,227,.3)}
.foot-legal{font-size:14.5px;color:#8E94B4;margin-top:22px;line-height:1.6}
/* ⚠ `.foot .foot-legal`, not `.foot-legal`. The `max-width:86ch` here used to sit on the
   bare class and was silently beaten by `.foot p{max-width:34ch}` two rules up — same
   weight class, but the `p` type selector wins — so DESIGN.md §2's "one-line legal strip"
   rendered as a 282px, six-line column on ALL 50 pages. Both gates pass it either way:
   the text is visible and AA-legible, just the wrong shape. Found 2026-08-23 by looking
   at the footer while building the ads page. */
.foot .foot-legal{max-width:54ch}
.foot .foot-demo{max-width:86ch}
.foot-demo{font-size:12px;color:#8E94B4;margin-top:12px}
/* ---- M4, 2026-08-27: the last footer lines were under the three floating widgets ----
   Measured at 375 with 2 items in the selection, scrolled to the very bottom of /tarifs:
     .cart-pill   y 698–746  covered .foot-legal  (the price/GST paragraph, y 648–745)
     .palette     y 756–804  covered .foot-demo   (the "this is a demo" note, y 755–774)
     .chat-fab    y 756–804  covered .foot-demo   as well
   Nothing overflowed and nothing was hidden, so no gate in this repo could see it — the pill
   is `position:fixed` and simply paints on top. Found by screenshot, confirmed by intersecting
   the widget boxes with the text boxes.
   ⚠ THE NUMBER IS THE TOP OF THE HIGHEST WIDGET, NOT A ROUND FIGURE. The pill's top sits 114px
   off the bottom of an 812px viewport, and the last line used to end 38px off it, so the
   footer owes the widgets 114 − 38 = 76px more than its 28px — 112px, with the remainder as
   slack for a longer pill label. The pill is the deciding one because it is the highest of the
   three; solve for palette/chat-fab alone and an occupied cart puts the legal text back under it.
   ⚠ THIS OVERRIDE LIVES BESIDE THE FOOTER'S OWN RULE, not in the shared responsive block — the
   .cart-pill lesson, recorded twice in the handoff: a distant override dies silently the moment
   a component moves. */
@media (max-width:600px){.foot{padding-bottom:112px}}

/* ---------- palette panel (QA gate 30) ---------- */
.palette{position:fixed;left:16px;bottom:16px;z-index:80;font-size:12.5px}
.palette-btn{min-height:48px;padding:0 14px;border:1px solid var(--line-strong);border-radius:2px;background:var(--surface);font-weight:500;letter-spacing:.12em;text-transform:uppercase;font-size:11px;color:var(--ink-2)}
/* ⚠ THE PANEL IS TALLER THAN THE SCREEN AND MUST SCROLL. Owner 2026-08-26: *"I'm not sure if I can
   see everything right now."* He could not: measured with all rows open, the panel stands **965px**
   tall and its top sat at **−129px on a 900px screen, −217px at 812, −329px at 700** — the first
   rows (the version and CTA choices) were simply above the window with no way to reach them,
   because the panel is anchored to its button at the BOTTOM and grows upward off-screen.
   Capped to the viewport minus the button and a margin, and scrolled internally. `overscroll-behavior`
   stops a flick inside the panel from scrolling the page behind it once it hits the end. */
.palette-panel{position:absolute;left:0;bottom:48px;width:240px;background:var(--surface);border:1px solid var(--line-strong);border-radius:2px;padding:16px 16px 14px;box-shadow:0 18px 50px rgba(20,27,61,.16);max-height:calc(100dvh - 84px);overflow-y:auto;overscroll-behavior:contain}
.palette-panel[hidden]{display:none}
.palette-lbl{font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);margin:10px 0 6px;font-weight:500}
.palette-row{display:flex;flex-wrap:wrap;gap:6px}
.palette-row button{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--line);border-radius:2px;padding:5px 8px;font-size:12px}
.palette-row button i{width:12px;height:12px;border-radius:50%;border:1px solid var(--line-strong);display:inline-block}
.palette-row button.is-on{border-color:var(--ink)}
.palette-reset{margin-top:12px;font-size:11.5px;color:var(--muted);text-decoration:underline;text-underline-offset:3px}
/* ================================================================== BOOKING ====
   The funnel's own surface. Every token here is the site's — ivory surface, hairlines, 2px
   radius, Bodoni for the screen headings, brass for anything about money. DESIGN.md §Shape:
   "no shadows; depth is colour and hairlines" — the ONE shadow below is the modal's lift off the
   dimmed page, which is the only place this design admits depth, and it matches .sheet's.
   ⚠ It is a real modal: it dims the page, traps focus and locks the body scroll. The corner
   .sheet pattern could not hold this — the laser service list alone is 33 rows. */
.bk-ov{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;padding:24px}
.bk-ov[hidden]{display:none}
.bk-back{position:absolute;inset:0;background:rgba(20,27,61,.42);backdrop-filter:saturate(1.1) blur(2px)}
.bk-modal{position:relative;display:flex;flex-direction:column;width:min(680px,100%);max-height:calc(100dvh - 48px);
  background:var(--surface);border:1px solid var(--line-strong);border-top:2px solid var(--brass);border-radius:2px;
  box-shadow:0 30px 80px rgba(20,27,61,.28)}
body.bk-lock{overflow:hidden}
.bk-bar{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:14px 18px 12px;border-bottom:1px solid var(--line);flex:0 0 auto}
.bk-crumb{font:500 11.5px/1 var(--sans);letter-spacing:.14em;text-transform:uppercase;color:var(--muted)}
/* ⚠ min-WIDTH as well as min-height (2026-08-28). 2.5.8 wants a 24×24 AREA, and this one had
   `min-height:34px` but only `padding:0 4px` around a 26px « × » — it measured **34 × 23.7px**,
   0.3px short on the axis nobody had checked. On the button that CLOSES the modal.
   ⛔ IT IS INSIDE THE `hidden` MODAL, WHICH IS WHY IT SURVIVED EVERYTHING: craft-audit skips a
   hidden drawer (v2.20), the 50-page sweep never opens the modal, and `build/tap.mjs` measures
   only what is painted and says so. It was found by opening the modal and measuring on purpose.
   34 matches every other control in that modal. */
.bk-x{background:none;border:0;font-size:26px;line-height:1;color:var(--ink-2);cursor:pointer;padding:0 4px;min-height:34px;min-width:34px}
.bk-x:hover{color:var(--ink)}
.bk-body{padding:22px 24px 26px;overflow-y:auto;flex:1 1 auto;min-height:0}
.bk-loading{color:var(--muted);padding:40px 0;text-align:center}
.bk-h{font-family:var(--serif);font-weight:500;font-size:clamp(1.5rem,2.6vw,2rem);line-height:1.16;color:var(--ink);margin:0 0 8px;font-variation-settings:"opsz" 28}
.bk-sub{color:var(--ink-2);font-size:15px;line-height:1.55;margin:0 0 18px;max-width:60ch}
.bk-list{display:flex;flex-direction:column;gap:8px;margin-bottom:18px}
/* the family label above a run of options — a label, not a heading, so it stays quiet */
.bk-fam{font:500 11px/1 var(--sans);letter-spacing:.16em;text-transform:uppercase;color:var(--muted);margin:14px 0 2px}
.bk-fam:first-child{margin-top:0}
.bk-opt{display:flex;align-items:center;gap:14px;width:100%;text-align:left;background:var(--paper);border:1px solid var(--line);border-radius:2px;
  padding:13px 14px;cursor:pointer;transition:border-color .16s,background .16s}
.bk-opt:hover{border-color:var(--line-strong);background:var(--surface)}
.bk-opt:focus-visible{outline:2px solid var(--royal);outline-offset:2px}
/* the family stripe: colour is the only thing separating hair removal from skin from other,
   exactly as on the live booking modal, and it is a 3px edge rather than a tint so the rows stay
   ivory. ⚠ THE CLASS SAYS hair, NOT laser (2026-08-27): the family now holds électrolyse, which
   is a filament, beside the laser menus and the packages. */
.bk-fam-hair{border-left:3px solid var(--royal)}
.bk-fam-skin{border-left:3px solid #2F8F8B}
.bk-fam-other{border-left:3px solid var(--line-strong)}
.bk-consult{border-left:3px solid var(--brass);background:color-mix(in srgb,var(--brass) 7%,var(--paper))}
.bk-opt-t{display:flex;flex-direction:column;gap:3px;flex:1;min-width:0}
.bk-opt-t b{font-weight:500;font-size:15px;color:var(--ink);line-height:1.3}
.bk-opt-t small{font-size:13px;line-height:1.45;color:var(--ink-2)}
.bk-svc-p{font-size:13px;color:var(--ink-2);margin-top:3px;font-variant-numeric:tabular-nums}
.bk-svc-p b{font-weight:500;color:var(--ink)}
.bk-svc-p s{color:var(--muted)}
/* the <em> is the package's « Économisez » figure. ⚠ .bk-svc-cure was DELETED here on
   2026-08-27 with the « 6 séances … » upsell line it painted — see the note in bkSvc(). */
.bk-svc-p em{font-style:normal;color:var(--brass-text)}
.bk-dur{font-size:12.5px;color:var(--muted);white-space:nowrap}
.bk-chev{color:var(--muted);font-size:19px;line-height:1}
/* ---- the way back, bottom left, on every step after the first (owner 2026-08-27) ----
   ⚠ IT IS ALWAYS THE LAST THING IN .bk-body AND ALWAYS LEFT. That is the whole point: the same
   word, in the same corner, on all five screens, so it is found by position and not re-read. The
   paragraph is what guarantees the "last thing" part — before this the link was a bare <button>
   appended after a list on three screens and missing entirely on the fourth. */
.bk-foot{margin:8px 0 0}
.bk-backlink{background:none;border:0;padding:8px 0;min-height:34px;color:var(--muted);font:500 13px/1.3 var(--sans);cursor:pointer;text-align:left}
.bk-backlink:hover{color:var(--ink)}
/* ---- the form ---- */
.bk-field{margin-bottom:16px}
.bk-field>label{display:block;font:500 11.5px/1.3 var(--sans);letter-spacing:.13em;text-transform:uppercase;color:var(--royal);margin-bottom:7px}
.bk-hint{font-size:13px;line-height:1.5;color:var(--muted);margin:0 0 9px}
.bk-hint-demo{color:var(--brass-text)}
.bk-explain{font-size:13.5px;line-height:1.55;color:var(--ink-2);margin:0;padding:12px 14px;background:var(--paper);border-left:2px solid var(--brass)}
.bk-sel{display:flex;align-items:flex-start;gap:12px;justify-content:space-between;background:var(--paper);border:1px solid var(--line);border-radius:2px;padding:11px 13px}
.bk-sel-t{font-size:14.5px;line-height:1.45;color:var(--ink);min-width:0}
.bk-modif{background:none;border:0;padding:2px 0;min-height:26px;color:var(--muted);font:500 12.5px/1 var(--sans);text-decoration:underline;text-underline-offset:3px;cursor:pointer;white-space:nowrap}
.bk-modif:hover{color:var(--ink)}
.bk-seg{display:inline-flex;border:1px solid var(--line-strong);border-radius:2px;overflow:hidden;background:var(--surface);flex-wrap:wrap}
.bk-seg button{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;font:500 12px/1.15 var(--sans);min-height:40px;padding:6px 13px;background:none;border:0;border-left:1px solid var(--line);color:var(--ink-2);cursor:pointer;white-space:nowrap}
.bk-seg button small{font-size:10.5px;font-weight:400;opacity:.72;font-variant-numeric:tabular-nums}
.bk-seg button:first-child{border-left:0}
.bk-seg button[aria-pressed="true"]{background:var(--night);color:var(--paper);box-shadow:inset 0 2px 0 var(--brass)}
/* ---- ONE line under the session picker, holding the whole cure (owner 2026-08-27) ----
   It replaced a per-session figure printed inside every button. The brass edge marks it as the
   ANSWER to the choice just made rather than another thing to choose; it is a quiet 13px because
   the numbers in it are already the most specific thing on the screen. */
.bk-cure-line{margin:9px 0 0;padding-left:10px;border-left:2px solid var(--brass);
  font:500 13px/1.45 var(--sans);color:var(--ink-2);font-variant-numeric:tabular-nums}
.bk-days,.bk-times,.bk-wins{display:flex;flex-wrap:wrap;gap:7px}
/* ---- two months of dates (owner 2026-08-27) ----
   ⚠ THIS OVERRIDES THE FLEX DECLARED ONE LINE ABOVE, and it must stay below it: same
   specificity, so source order is the only thing deciding which wins.
   The strip used to be ten chips on one wrapping line. It is now ~50 open days over two months,
   and fifty chips wrapping have no rhythm to scan — a grid gives every date the same column, so
   the eye runs down a week rather than hunting along a paragraph of dates.
   ⚠ padding-right IS NOT DECORATION. macOS overlay scrollbars reserve no gutter and
   `scrollbar-gutter:stable` does nothing about it, so without real padding the bar sits on the
   last column's chips. Same lesson as .cart-lines. */
/* ⚠ 222px IS A HALF-ROW, ON PURPOSE. A chip row is 41px (34 + the 7px gap), so 198px lands on
   an exact 5-row boundary and the grid then looks FINISHED — measured at 375, where September's
   26 days need 9 rows and the cut fell perfectly between two of them with nothing to say so.
   222 = five rows plus half of the sixth, which is the only cue a phone gets that there is more:
   macOS overlay scrollbars are invisible until something scrolls. Change the chip height or the
   gap and this number has to move with them. */
.bk-days{display:grid;grid-template-columns:repeat(auto-fill,minmax(88px,1fr));gap:7px;
  max-height:222px;overflow-y:auto;padding-right:8px;align-content:start}
.bk-months{margin-bottom:10px;max-width:100%}
/* which day is being shown below — without it, switching months redraws the grid around a
   selection that is no longer on screen and the times underneath look unexplained */
.bk-chosen{margin:10px 0 0;font:500 12.5px/1.3 var(--sans);color:var(--royal);font-variant-numeric:tabular-nums}
.bk-times{margin-top:10px}
.bk-day,.bk-time,.bk-win{font:500 12.5px/1 var(--sans);min-height:34px;padding:0 11px;background:var(--paper);border:1px solid var(--line);border-radius:2px;color:var(--ink-2);cursor:pointer;font-variant-numeric:tabular-nums}
.bk-day:hover,.bk-time:hover,.bk-win:hover{border-color:var(--line-strong);color:var(--ink)}
.bk-day[aria-pressed="true"],.bk-time[aria-pressed="true"],.bk-win[aria-pressed="true"]{background:var(--night);border-color:var(--night);color:var(--paper)}
/* six windows a day, not three — the day label holds its own column and the windows wrap inside
   theirs, so a long label like « 10 h - 12 h » cannot push the row out of the modal */
/* ---- THE CALL PICKER, AS TWO STEPS (owner 2026-08-31) ----
   `.bk-wday` / `.bk-wrow` / `.bk-wd` / `.bk-windows` are the ONE-SCREEN version this replaced:
   twelve days each carrying six windows, inside a 300px scroll box. They are deleted rather than
   left behind — a dead layout that still styles live class names is how the next person "fixes"
   the wrong one. The day grid now reuses `.bk-days`/`.bk-day` (the appointment picker's own
   classes, which is the surface the owner said he likes), so the two cannot drift apart. */
.bk-step{display:flex;align-items:center;gap:9px;margin:16px 0 9px;font-size:14.5px;color:var(--ink)}
.bk-step:first-of-type{margin-top:10px}
.bk-step-n{flex:0 0 auto;display:grid;place-items:center;width:22px;height:22px;border-radius:50%;
  background:var(--night);color:var(--paper);font-size:12px;font-weight:600;font-variant-numeric:tabular-nums}
.bk-wins{display:flex;flex-wrap:wrap;gap:7px}
.bk-wins-none{margin:0}
/* a day that already carries picks says so on its face, so step 1 doubles as the overview */
.bk-day.has-pick{border-color:var(--brass);box-shadow:inset 0 0 0 1px var(--brass)}
.bk-day-n{display:inline-grid;place-items:center;min-width:17px;height:17px;margin-left:6px;padding:0 4px;
  border-radius:9px;background:var(--brass);color:#2C2408;font-size:11px;font-weight:600;font-variant-numeric:tabular-nums}
.bk-day[aria-pressed="true"] .bk-day-n{background:var(--brass);color:#2C2408}
/* the running answer: what is chosen on the days that are NOT open, without leaving this step */
.bk-picked{margin-top:16px;padding-top:14px;border-top:1px solid var(--line)}
.bk-picked-l{margin:0 0 8px;font-size:12.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);font-weight:500}
.bk-picked-d{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin-bottom:7px}
.bk-picked-day{flex:0 0 auto;min-width:96px;font-size:14px;color:var(--ink-2);font-variant-numeric:tabular-nums}
.bk-chip{display:inline-flex;align-items:center;gap:7px;min-height:34px;padding:0 10px;font:inherit;font-size:14px;
  color:var(--ink);background:var(--paper-2);border:1px solid var(--line-strong);border-radius:2px;cursor:pointer}
.bk-chip span{color:var(--muted);font-size:15px;line-height:1}
.bk-chip:hover{border-color:var(--night)}
.bk-chip:hover span{color:var(--ink)}
.bk-count{font-size:12.5px;color:var(--muted);margin:9px 0 0;font-variant-numeric:tabular-nums}
.bk-count.is-ok{color:#2F8F5B}
.bk-adds{list-style:none;margin:0 0 18px;padding:0}
.bk-add{display:flex;align-items:center;gap:12px;padding:11px 0;border-bottom:1px solid var(--line)}
.bk-add label{display:flex;align-items:center;gap:10px;flex:1;cursor:pointer;min-width:0}
.bk-add.is-req label{cursor:default}
.bk-add-n{font-size:14.5px;color:var(--ink)}
.bk-add-p{font-size:14.5px;color:var(--ink);font-variant-numeric:tabular-nums}
.bk-add-req{font:500 10.5px/1 var(--sans);letter-spacing:.12em;text-transform:uppercase;color:var(--brass-text);border:1px solid color-mix(in srgb,var(--brass) 45%,transparent);border-radius:2px;padding:5px 8px}
.bk-2col{display:grid;grid-template-columns:1fr 1fr;gap:0 16px}
.bk-field input,.bk-field select,.bk-field textarea{width:100%;font:400 15px/1.4 var(--sans);color:var(--ink);background:var(--paper);
  border:1px solid var(--line-strong);border-radius:2px;padding:10px 12px;min-height:44px}
.bk-field textarea{min-height:74px;resize:vertical}
.bk-field input:focus-visible,.bk-field select:focus-visible,.bk-field textarea:focus-visible{outline:2px solid var(--royal);outline-offset:-1px}
.bk-opt-l{font-weight:400;letter-spacing:0;text-transform:none;color:var(--muted)}
.bk-check{display:flex;align-items:flex-start;gap:10px;margin:0 0 12px;cursor:pointer;font-size:14px;line-height:1.45;color:var(--ink-2)}
.bk-check input{margin-top:2px;flex:0 0 auto;width:17px;height:17px}
.bk-check small{display:block;font-size:12.5px;color:var(--muted);margin-top:2px}
.bk-total{display:flex;align-items:baseline;gap:10px;justify-content:space-between;margin:16px 0 12px;padding-top:12px;border-top:1px solid var(--line-strong);font-size:15px;color:var(--ink)}
.bk-total b{font-weight:500;font-size:19px;color:var(--brass-text);font-variant-numeric:tabular-nums;margin-left:auto}
.bk-total small{font-size:12px;color:var(--muted)}
.bk-err{color:#A8341F;font-size:13.5px;line-height:1.45;margin:0 0 12px;padding:10px 12px;background:rgba(168,52,31,.07);border-left:2px solid #A8341F}
.bk-err[hidden]{display:none}
.bk-actions{display:flex;flex-wrap:wrap;gap:12px 18px;align-items:center;margin:4px 0 0}
.bk-actions .btn{min-height:46px;padding:0 22px;font-size:12px}
.bk-again{background:none;border:0;cursor:pointer;font-family:var(--sans);color:var(--muted)}
/* the ending. It is deliberately NOT dressed as a success: no tick, no green, no "confirmed". */
.bk-demo-note{font-size:14.5px;line-height:1.55;color:var(--ink);margin:0 0 12px;padding:12px 14px;background:color-mix(in srgb,var(--brass) 10%,var(--paper));border-left:2px solid var(--brass)}
.bk-recap-l{font:500 11.5px/1 var(--sans);letter-spacing:.14em;text-transform:uppercase;color:var(--muted);margin:20px 0 8px}
.bk-recap{margin:0 0 20px;border-top:1px solid var(--line)}
.bk-recap div{display:flex;gap:16px;justify-content:space-between;padding:9px 0;border-bottom:1px solid var(--line)}
.bk-recap dt{font-size:12.5px;color:var(--muted);flex:0 0 34%}
.bk-recap dd{font-size:14px;color:var(--ink);margin:0;text-align:right;min-width:0;word-break:break-word}
@media (max-width:600px){
  /* full screen on a phone: a 680px column inside a 375px viewport is a scroll bar with a dialog
     attached, and this funnel has a real form in it */
  .bk-ov{padding:0}
  .bk-modal{width:100%;max-height:100dvh;height:100dvh;border-left:0;border-right:0;border-bottom:0;border-radius:0}
  .bk-2col{grid-template-columns:1fr}
  .bk-recap div{flex-direction:column;gap:2px}
  .bk-recap dd{text-align:left}
}
@media print{.palette,.chat-fab,.sheet{display:none!important}}

/* ---------- motion: the load-in spec ---------- */
@keyframes rise{from{opacity:0;transform:translateY(24px)}to{opacity:1;transform:none}}
@keyframes cine{from{opacity:0;transform:scale(1.2)}to{opacity:1;transform:scale(1)}}
[data-ring="0"]{--ring:0}[data-ring="1"]{--ring:1}[data-ring="2"]{--ring:2}[data-ring="3"]{--ring:3}[data-ring="4"]{--ring:4}[data-ring="5"]{--ring:5}[data-ring="6"]{--ring:6}[data-ring="7"]{--ring:7}[data-ring="8"]{--ring:8}
/* hold everything until the single ready flag; nothing animates off raw mount */
.js .r{opacity:0}
.js .hero-media img{opacity:0;transform:scale(1.2)}
.js.is-ready .r{animation:rise 1000ms var(--ease-rise) both;animation-delay:calc(var(--ring,0) * 100ms)}
.js.is-ready .hero-media img{animation:cine 1800ms var(--ease-cine) both}
/* below-the-fold sections wait for their first intersection instead */
.js.is-ready [data-reveal] .r{animation:none;opacity:0}
.js.is-ready [data-reveal].is-in .r{animation:rise 1000ms var(--ease-rise) both;animation-delay:calc(var(--ring,0) * 100ms)}
/* once an element has played, it is settled for good (a resize or a display toggle must not replay it) */
.js .r.settled,.js .hero-media img.settled{animation:none!important;opacity:1!important;transform:none!important}
/* the nav plays once per session, then jumps to the settled state */
.js.nav-settled .nav .r{animation:none!important;opacity:1}
@media (prefers-reduced-motion:reduce){
  .js .r,.js.is-ready .r,.js [data-reveal] .r,.js .hero-media img,.js.is-ready .hero-media img{animation:none!important;opacity:1!important;transform:none!important}
  html:focus-within{scroll-behavior:auto}
  .link span,.tlist-arrow,.btn,.btn::after,.nav-links a::after,.tlist-row a{transition:none}
}
/* page-to-page: a quiet cross-document crossfade where supported */
@view-transition{navigation:auto}
::view-transition-old(root),::view-transition-new(root){animation-duration:320ms;animation-timing-function:var(--ease-rise)}

/* ---------- responsive ---------- */
/* ⚠ 1365px since 2026-08-25 — the eighth link (« Accueil ») was paid for here.
   RE-MEASURED in both languages, not extrapolated from the seven-link number:
     1440   FR 118px / EN 141px of left slack   ← comfortable
     1366   FR  58px / EN  82px                 ← the last width with real air
     1320   FR  24px / EN  45px                 ← FR already jammed on its 24px gap floor
     1280   FR  24px / EN  24px                 ← both at the floor, still no overflow
     1240   scrollWidth 1269 (FR) / 1245 (EN)   ← actually breaks
   So the row SURVIVES to 1280, and the breakpoint is deliberately set 85px above that. Sitting
   at the gap floor is not a failure the gates catch, but a wordmark pressed against the first
   link is not what this site is for. 1365 also keeps the very common 1366-wide laptop on the
   full nav — one step lower and that machine would get a burger it does not need.
   ⚠ The dropdown PANELS are not the constraint; the header row is. If a link is added or the
   type grows, RE-MEASURE at 1440/1366/1320/1280 in FR AND EN before shipping. */
/* ⭐ THE NARROWED RIGHT PADDING LIVES HERE, NOT ON THE BASE RULE (owner 2026-09-03) ----
   It was written on the base `.nav .wrap` first and that was WRONG in two places at once, both
   caught by measuring rather than by reading:
     · THE PHONE. `clamp(14px,1.7vw,24px)` floors at 14px, BELOW the base clamp's 20px floor, so at
       every width ≤600 the nav's right padding fell 20px → 14px and the burger's ink moved from
       23px off the window edge to 17px. The brief said the phone header is untouched; it was not.
     · THE BURGER WIDTHS (601–1365). The row this change exists to help does not exist there — the
       links are display:none below 1366 — so all the narrower padding did was drag the burger
       17px toward the edge (its box gap at 1320 went 29.59 → 12.44) against a wordmark still
       sitting 39.6px off the left. Asymmetry bought for nothing.
   Scoped to ≥1366 it touches only the widths that have the problem, and the 14px floor can never
   bind there (1.7vw is already 23.22px at 1366), so the clamp's real range is 23.22–24px. */
@media (min-width:1366px){ .nav .wrap{padding-right:clamp(14px,1.7vw,24px)} }

@media (max-width:1365px){
  .nav-links{display:none}
  /* ⚠ Below the breakpoint the links are display:none, so the `margin-inline:auto` that balances
     the header has no element to sit on and the actions collapse back against the wordmark —
     measured at 1024 the block sat at x=412 in a 1024px header instead of at the right edge.
     The burger layout is brand-left / actions-right and needs its own auto margin. */
  .nav-right{margin-left:auto}
  /* the hairline divides NAVIGATION from ACTIONS — with the links in the burger there is no
     navigation on this row, so it would just be a stray rule between the wordmark and the button */
  .nav-right::before{display:none}
  .nav-tel{display:none}
  .nav-burger{display:flex}
  .foot-grid{grid-template-columns:1fr 1fr}
  /* The five-column price row (name · 1 · 3 · 6 · actions) is measured to fit only at 1320 and
     up. Below that the two buttons take their own right-aligned row instead of squeezing the
     name — the same treatment the phone gets, starting one step earlier. Without this the grid
     pushes past the text column: at 1024 the actions landed at x=1090 in a 972px-wide page. */
  .pt-cols,.pt-row{grid-template-columns:minmax(0,1fr) 100px 136px 136px}
  .pt-cols-3,.pt-row-3{grid-template-columns:minmax(0,1fr) 116px 168px}
  .pt-cols>span:last-child{display:none}
  .pt-row-flat{grid-template-columns:minmax(0,1fr) auto}
  .pt-acts{grid-column:1/-1;justify-self:end;margin:6px 0 0}
}
@media (max-width:900px){
  /* ⛔ `minmax(0,1fr)`, NOT `1fr` — AND THIS WAS A REAL 9px OVERFLOW, NOT TIDYING (found 2026-09-04).
     `1fr` is shorthand for `minmax(auto,1fr)`, and an `auto` MINIMUM means the track may not shrink
     below its content's min-content width — which for a headline is its longest unbreakable word.
     At 320px, /en/microdermabrasion-montreal's « Microdermabrasion, » sets 309px at the 40px display
     size inside a 305px column, and /en/thermocoagulation-montreal's « Thermocoagulation, » sets
     308px: the whole content column was pushed 9px and 8px past the viewport and the page scrolled
     sideways on a small phone. The French twins are shorter words and never showed it.
     `minmax(0,1fr)` lets the track shrink to the column, and the ≤600px `hyphens:auto` rule then
     breaks the long word the way it was always meant to.
     ⚠ PRE-EXISTING — reproduced identically on b6875c2, before any of this round's work. It was
     missed until now because the URL list feeding ovf.mjs had been word-split by the shell in a way
     that dropped pages, and because ovf prints only a URL's LAST path segment, so
     `/en/microdermabrasion-montreal` and `/microdermabrasion-montreal` read identically in its
     output — the failure looked like the French page, which is clean. */
  .thero-grid,.tlist-grid,.tech-grid,.tech-head,.prices-grid,.who-grid,.who-grid-rev,.faq-grid,.cta-grid,.azur-grid,.ladder-grid{grid-template-columns:minmax(0,1fr)}
  .benefits,.benefits[data-cols]{grid-template-columns:repeat(2,minmax(0,1fr))}
  .quotes{grid-template-columns:1fr 1fr}
  .ladder-notes{margin-top:8px}
  .thero-fig{order:-1}
  .thero-fig img{aspect-ratio:16/10}
  .tlist-fig{display:none}
  .faq-grid .sec-head{position:static;margin-bottom:8px}
  .steps,.brief,.concerns,.who-facts,.cards-3,.cards-4,.timeline{grid-template-columns:1fr 1fr}
  .dev-grid{grid-template-columns:1fr}
  .dev-fig{max-width:520px}
  .numbers{grid-template-columns:1fr 1fr}
  .answers{grid-template-columns:1fr}
  .story-grid,.concern-grid{grid-template-columns:1fr}
  .story-fig,.concern-fig{max-width:520px;position:static}
  .story-grid .story-fig{order:-1}
  .room-grid{grid-template-columns:1fr 1fr}
  .pkg-grid{grid-template-columns:1fr}
  .who-fig{max-width:520px}
  .who-grid-rev .who-fig{order:-1}
}
@media (max-width:600px){
  /* ⚠ A ONE-WORD HEADLINE CAN BE WIDER THAN THE PHONE. « Thermocoagulation » sets 394px at the
     48px display size — it has no space to wrap at, so it pushed /thermocoagulation to a
     scrollWidth of 438 on a 375px screen. (Pre-existing, found by sweeping every page at every
     width rather than the two that were being worked on.) `hyphens:auto` lets the browser break
     it properly, WITH a hyphen, using the page's own lang; `break-word` is the fallback where
     no hyphenation dictionary exists. Neither touches a headline that already has somewhere to
     wrap, and both are scoped to phones only. */
  .display,.title,.tlist-name,.clist-name{hyphens:auto;-webkit-hyphens:auto;overflow-wrap:break-word}
  :root{--nav-h:72px}
  .nav-in{gap:12px}
  .nav-right{gap:10px}
  .nav-lang{display:none}
  .brand{font-size:13px;letter-spacing:.2em}
  .hero{padding-bottom:76px}
  .hero-media img{object-position:50% 70%}
  .hero-media::after{background:linear-gradient(180deg,color-mix(in srgb,var(--paper) 12%,transparent) 58%,color-mix(in srgb,var(--paper) 40%,transparent) 80%,var(--paper) 99%)}
  .hero-in{padding-top:72px}
  .hero-in .display{max-width:none}
  .steps,.brief,.concerns,.who-facts,.room-grid,.contact-grid,.benefits,.benefits[data-cols],.cards,.cards-3,.cards-4,.timeline,.story-items-2,.story-photos,.quotes,.claims,.numbers{grid-template-columns:1fr}
  /* ⚠ ONE COLUMN MEANS ROW FLOW AGAIN. grid-template-columns:1fr alone does not stop
     grid-auto-flow:column — the browser just adds an implicit second column and the six
     benefits stay two-across, which is the phone bug this whole block exists to prevent. */
  .benefits[data-flow="col"]{grid-auto-flow:row;grid-template-rows:auto}
  .fitz{padding:20px 18px 18px}
  .fitz-scale{gap:6px}
  .fitz-scale i{height:40px}
  .azur-can li{font-size:1.45rem}
  .azur-chat-body{padding:14px}
  .azur-msg{max-width:100%}
  .seg-wrap{top:var(--nav-h)}
  .room-grid figure:first-child{grid-column:auto}
  .room-grid figure:first-child img{aspect-ratio:4/3}
  .tlist-row a,.clist a{grid-template-columns:32px 1fr 20px;gap:12px}
  .tlist-meta{grid-column:2;flex-direction:row;gap:12px;align-items:baseline;justify-content:flex-start;white-space:normal}
  .tlist-arrow{grid-column:3;grid-row:1}
  .ptable{padding:4px 14px 6px}
  /* 375px cannot hold three prices AND two buttons on one line. The actions drop to their own
     right-aligned row instead of being shrunk into an unreachable column — the tap targets are
     the last thing that should pay for a narrow screen. */
  /* ---- M1, 2026-08-27: the phone price row, re-laid-out and re-labelled ----
     BEFORE (measured on /epilation-laser at 375): three stacked bands — name 22.9px, then
     « 75 $  360 $ » right-aligned in two 147.5px columns, then the two buttons on a band of
     their own at 48px. 135.5px a row, mostly air, ×33 laser zones = ~4 470px of scrolling.
     Two numbers with no names on them, because the header strip is 30 rows above.
     AFTER: name across the top, each price on its own line WITH its column name in front of it
     (`.pt-lab`, emitted by cureTable from the same `colName()` the header uses), and the
     actions moved OUT of their own band into the right-hand column, spanning the price lines.
     One band fewer, every number named.
     ⚠ THE PLACEMENT IS BY :nth-child AND THE ORDER IS FIXED IN cureTable(): name, price…,
     .pt-acts. Laser (`.pt-row-3`) emits 2 prices, the skin tables (`.pt-row-4`) emit 3. Add or
     reorder a cell there and these rules must move with it — there is no other coupling.
     ⚠ `.pt-cols` KEEPS ITS OWN COLUMN LAYOUT and is deliberately NOT restructured with the
     rows: it is where « Cure Prestige −20 % » prints, which v2.15 fought to keep on a phone.
     It is now a heading over labelled rows rather than the only key to them. */
  .pt-cols{grid-template-columns:repeat(3,minmax(0,1fr));gap:8px 10px}
  .pt-cols-3{grid-template-columns:repeat(2,minmax(0,1fr))}
  .pt-cols>span:first-child,.pt-cols>span:last-child{display:none}
  .pt-row{grid-template-columns:minmax(0,1fr) auto;gap:8px 10px;row-gap:3px;align-items:center}
  .pt-row .pt-name{grid-column:1/-1;padding-bottom:2px;align-self:baseline}
  .pt-row-3>:nth-child(2){grid-area:2/1/3/2}
  .pt-row-3>:nth-child(3){grid-area:3/1/4/2}
  .pt-row-3>.pt-acts{grid-area:2/2/4/3}
  .pt-row-4>:nth-child(2){grid-area:2/1/3/2}
  .pt-row-4>:nth-child(3){grid-area:3/1/4/2}
  .pt-row-4>:nth-child(4){grid-area:4/1/5/2}
  .pt-row-4>.pt-acts{grid-area:2/2/5/3}
  /* The label sits in front of its figure on one line — ABOVE it would give back the band the
     new layout just saved. Uppercase + muted is the .pt-cols header's own voice, so a labelled
     row reads as that column continued, not as a new kind of text. */
  .pt-lab{position:static;width:auto;height:auto;overflow:visible;clip:auto;font-size:10.5px;letter-spacing:.06em;text-transform:uppercase;color:var(--muted);font-weight:500}
  /* label LEFT, figure RIGHT, on one line — so the figures of a row line up on a common right
     edge the way a price column should, instead of floating at whatever x their label ends at.
     ⚠ white-space goes to the two CHILDREN, not the cell: the cell must be allowed to wrap
     (otherwise a long « 6 SÉANCES 1 096,50 $ » clips at 375 and everything clips at 320), while
     each of label and figure must not break inside itself. */
  .pt-row .pt-p{display:flex;flex-wrap:wrap;align-items:baseline;justify-content:space-between;gap:1px 8px;white-space:normal;text-align:left}
  .pt-lab,.pt-fig{white-space:nowrap}
  .pt-row-flat,.pt-row-req,.pt-row-norm{grid-template-columns:minmax(0,1fr) auto}
  .pt-row-flat .pt-name,.pt-row-req .pt-name,.pt-row-norm .pt-name{grid-column:auto}
  .pt-row-flat .pt-p,.pt-row-req .pt-p,.pt-row-norm .pt-p{text-align:right}
  .pt-acts{grid-column:1/-1;justify-self:end;align-self:center;margin:0}
  .pt-p,.pt-cols{font-size:13.5px}
  /* The « Cure Prestige −20 % » label stays on a phone. It was display:none here, which
     left a phone reader seeing « 6 séances · 360 $ » with nothing saying that is a 20 %
     saving — the single most persuasive number on the table, hidden on the device most
     customers use. Measured before re-enabling: the label renders inside its column with
     ZERO document overflow at 375, 390 and 320px (probe-cure.mjs). It wraps rather than
     nowraps here, because a 2-line label is cheaper than a clipped one. */
  .pt-cols small{display:block;white-space:normal;font-size:10.5px;line-height:1.3}
  .sheet-book,.sheet-chat{inset:auto 8px 8px 8px;width:auto}
  .palette{left:8px;bottom:8px}
  .chat-fab{right:8px;bottom:8px}
  .cta-actions,.hero-actions{gap:16px 24px}
  /* ⚠ ONE COLUMN ON A PHONE, MEASURED. At 375 the English pair « See the prices » +
     « Understand the treatment » needs 347px of an available 335, so the second label wrapped
     to two lines INSIDE itself — 3 rows where the design says 2, and a broken-looking label.
     FR fits at 375 and EN fits from ~430 up, but a 2×2 that survives only in one language is not
     a layout. The site's own phone breakpoint is 600, and reusing it beats inventing a number. */
  .hero-links{grid-template-columns:1fr;gap:10px}
  .btn{padding:0 22px}
  /* ⛔ THE TOUCH-TARGET RULE THAT USED TO LIVE HERE MOVED OUT ON 2026-08-30 (M6).
     It was `.link,.brand{min-height:24px}`, and being inside this phone-only block is the
     reason `.link` still measured 21.6px at 1440 for five weeks after v2.15 decided to raise
     it. It is now beside `.link`'s own rule (~line 230), unconditional. Nothing replaces it
     here — a touch-target floor that only applies to one viewport was never the intent.
     ⚠ The long M5 note about the footer that also sat here has moved with it. */
}

/* ---------- the nav dropdowns (owner 2026-08-24: "same drop-downs as the live website") ----
   Three menus — Soins, Préoccupations, Tarifs — opened by hover AND by :focus-within, which is
   what makes them keyboard-reachable without turning the nav row into a set of buttons. Esc
   closes (site.js blurs). `visibility` + `opacity` rather than `display`, so the panel can
   transition and so the checker never sees a display-toggled ghost.

   ⚠ THE HOVER BRIDGE. The panel is offset below the link, and a mouse travelling into it crosses
   a gap where it is over neither — the menu would close mid-reach, the classic dropdown bug.
   `.has-menu::after` lays an invisible strip across that gap so the pointer never leaves the
   hover area. The live site carries the same fix with the same warning next to it. Do not
   "clean up" this empty pseudo-element. */
/* The row is stretched to the FULL header height on purpose: `top:100%` on the panel then means
   "the header's bottom edge" and stays true if --nav-h ever changes again, instead of a magic
   offset measured against a 28px-tall link that would leave the menu floating inside the bar. */
.nav-links{align-self:stretch}
.nav-links>ul{height:100%;align-items:stretch}
/* every row centres itself in the stretched header, menu or not — without this the four plain
   links sit at the top of the bar while the three menu links sit centred */
.nav-links>ul>li{display:flex;align-items:center}
.has-menu{position:relative}
.has-menu::after{content:"";position:absolute;left:0;right:0;top:100%;height:12px}
.has-menu>.caret{display:inline-block;width:0;height:0;margin-left:5px;vertical-align:middle;
  border-left:3.5px solid transparent;border-right:3.5px solid transparent;border-top:4px solid currentColor;
  color:var(--nav-link);opacity:.65;transition:transform .2s,opacity .2s}
.has-menu:hover>.caret,.has-menu:focus-within>.caret{opacity:1;transform:rotate(180deg)}
/* ⚠ A CLOSED MENU IS `display:none`, NOT `opacity:0` — this codebase's standing rule
   ("deliberate hiding is display, deliberate fades are colour"), and it is enforced by a gate.
   design-capture.mjs walks `body *` and reports anything with opacity < 0.99 or a transform over
   4px, WITHOUT looking at visibility — it cannot tell a deliberately-closed menu from a reveal
   animation frozen half-way, which is the real bug it exists to catch. Left on opacity, three
   closed menus reported as suspects on every page, and a gate that cries wolf is a gate people
   start waving through.
   The fade survives anyway: `transition-behavior: allow-discrete` plus @starting-style animates
   from display:none in current Chrome, and degrades to an instant menu elsewhere — which is what
   a hover menu did for twenty years. */
.drop{position:absolute;top:calc(100% + 8px);left:50%;transform:translateX(-50%) translateY(-4px);
  z-index:60;min-width:250px;padding:10px 0;background:var(--surface);border:1px solid var(--line);
  box-shadow:0 26px 60px -28px rgba(20,27,61,.5);border-radius:2px;
  display:none;opacity:0;
  transition:opacity .18s ease,transform .18s ease,display .18s allow-discrete}
.has-menu:hover>.drop,.has-menu:focus-within>.drop{display:block;opacity:1;transform:translateX(-50%) translateY(0)}
@starting-style{
  .has-menu:hover>.drop,.has-menu:focus-within>.drop{opacity:0;transform:translateX(-50%) translateY(-4px)}
}
/* ⚠ `.nav-links ul{display:flex}` up the file is (0,1,1) and outranks a bare `.drop-cols`
   (0,1,0) — it laid the eight concern names out as ONE 8-wide row spilling past the panel.
   Scoping these two under `.nav-links` makes them (0,2,1)/(0,2,0), which wins. Any future rule
   for a list inside this panel needs the same scope or the nav's flex will quietly take it. */
.nav-links .drop ul{display:block;list-style:none;margin:0;padding:0}
/* Owner 2026-08-25: the rows "look like a bunch of words stuck together". They were 11px apart
   in a flat stack with a full-width hover wash — a list of strings, not a menu. What changed:
   air (11 → 13px rows, 28px side margin), a hairline between rows so each name owns a line, the
   weight dropped 500 → 450 so seven names do not shout in unison, and the hover moved from a
   background wash to the site's own gold: a rule that draws in from the left, the same gesture
   the nav links already use. */
.drop{padding:8px 0}
.drop a{position:relative;display:block;margin:0 28px;padding:13px 0;font-size:15px;font-weight:450;
  line-height:1.35;letter-spacing:.005em;color:var(--nav-link);white-space:nowrap;
  border-bottom:1px solid var(--line);transition:color .16s}
.drop li:last-child a{border-bottom:0}
.drop a::after{content:"";position:absolute;left:0;right:100%;bottom:-1px;height:1px;background:var(--gold-dk);transition:right .26s ease}
.drop a:hover::after,.drop a:focus-visible::after{right:0}
.drop a:hover,.drop a:focus-visible{color:var(--nav-link-hover)}
/* Préoccupations is live's "mega" — a lede, then the eight names in two columns */
.drop-mega{min-width:min(600px,92vw);padding:22px 30px 20px}
.drop-lede{font-family:var(--serif);font-size:1.05rem;font-style:italic;color:var(--ink-2);margin:0 0 14px;padding-bottom:14px;border-bottom:1px solid var(--line);letter-spacing:.005em}
.nav-links .drop .drop-cols{display:grid;grid-template-columns:1fr 1fr;gap:0 34px}
.drop-mega a{margin:0;padding:11px 0;white-space:normal;line-height:1.4}
/* the two columns each end on their own last row, so :last-child is not enough */
.drop-cols li:nth-last-child(-n+2) a{border-bottom:0}
/* the phone has no hover: the same rows become an indented list inside the burger sheet */
.sheet-sub ul{list-style:none;margin:2px 0 6px;padding:0 0 0 18px;border-left:1px solid var(--line)}
/* ⚠ font-family is set explicitly, NOT inherited. `.sheet-links a` is Bodoni at 2.4rem, and a
   nested row inherits that — which would put the serif at 15.5px, under the 1.45rem floor this
   site holds for it (the owner's own readability rule; Bodoni's hairlines disappear below it).
   Sub-rows are a list to scan, not a headline: sans, quieter ink, one step down. */
.sheet-sub a{display:block;padding:11px 0;font-family:var(--sans);font-size:16px;font-weight:400;letter-spacing:0;color:var(--ink-2)}
.sheet-sub a:hover{color:var(--ink)}

/* ---------- package cards (owner 2026-08-25) ----------
   dermazur.ca's forfait cards, rebuilt in this site's language. Live's card is a royal-blue
   header with a white pill badge, an Inter price at 37.5px and a mint-green savings chip — four
   accent colours on one card. Kept: the STRUCTURE and every number (name → badge → price with its
   struck compare-at → the cure block → the included zones → the two actions), because that
   sequence is what makes the offer legible in one look. Changed: the palette, down to the two
   this site owns. The header band is --night with the name in Bodoni; the badge is a hairline on
   the band, not a filled pill; the savings figure is gold, which is this site's "money" colour
   everywhere else; the mint green and the royal-blue field are gone. Same argument, quieter voice.

   ⚠ The struck prices are `text-decoration:line-through` on --muted, never a lighter grey: a
   crossed-out number still has to clear 4.5:1, and it is the one number a reader checks twice. */
.pkg-group + .pkg-group{margin-top:clamp(32px,4vw,52px)}
.pkg-gt{font-family:var(--sans);font-size:11.5px;font-weight:500;letter-spacing:.14em;text-transform:uppercase;color:var(--muted);margin:0 0 16px}
.pkg-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:clamp(16px,2vw,24px)}
/* the home page shows exactly three, so they share the row evenly instead of auto-filling a
   fourth slot's worth of empty space at 1152px.
   ⚠ ITS RESPONSIVE RULES LIVE HERE, NOT IN THE SHARED RESPONSIVE BLOCK ABOVE — measured: written
   up there they lost on SOURCE ORDER (this base rule is declared ~150 lines later) and the grid
   stayed three columns at every width, giving 101px cards on a 375px phone. Exactly the trap the
   handoff already records for .cart-pill. Same rule, second component. */
.sec-prices .pkg-grid{grid-template-columns:repeat(3,minmax(0,1fr))}
@media (max-width:1024px){ .sec-prices .pkg-grid{grid-template-columns:repeat(2,minmax(0,1fr))} }
@media (max-width:680px){ .sec-prices .pkg-grid{grid-template-columns:minmax(0,1fr)} }
.pkg-head .sec-head{margin-bottom:clamp(28px,3vw,40px)}
.pkg-all{display:flex;flex-wrap:wrap;gap:14px 26px;align-items:center;margin-top:clamp(22px,2.4vw,32px)}
.pkg-card{display:flex;flex-direction:column;background:var(--surface);border:1px solid var(--line);border-radius:2px;overflow:hidden;transition:border-color .18s,box-shadow .18s,transform .18s}
.pkg-card:hover{border-color:var(--line-strong);box-shadow:0 22px 46px -30px rgba(20,27,61,.5);transform:translateY(-2px)}
.pk-head{background:var(--night);padding:18px 20px 16px;display:flex;flex-direction:column;gap:11px;align-items:flex-start}
.pk-n{font-family:var(--serif);font-weight:500;font-size:1.5rem;line-height:1.14;letter-spacing:-.005em;color:var(--paper);margin:0;font-variation-settings:"opsz" 24}
.pk-badge{font:500 11.5px/1 var(--sans);letter-spacing:.12em;text-transform:uppercase;color:#E8CE7A;border:1px solid rgba(232,206,122,.5);border-radius:2px;padding:6px 11px;white-space:nowrap}
.pk-body{padding:18px 20px 20px;display:flex;flex-direction:column;gap:14px;flex:1}
.pk-price{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;margin:0}
.pk-price b{font-family:var(--sans);font-size:2.1rem;font-weight:500;line-height:1;color:var(--ink);font-variant-numeric:tabular-nums;letter-spacing:-.01em}
.pk-was{font-size:14px;color:var(--muted);text-decoration:line-through;font-variant-numeric:tabular-nums}
.pk-per{font-size:14px;color:var(--ink-2)}
.pk-cure{display:flex;align-items:baseline;gap:9px;flex-wrap:wrap;margin:0;padding:11px 13px;background:var(--paper);border-left:2px solid var(--brass);font-size:14.5px;color:var(--ink-2)}
.pk-cure b{color:var(--ink);font-weight:500;font-variant-numeric:tabular-nums}
.pk-cure-save{margin-left:auto;color:var(--brass-text);font-weight:500;white-space:nowrap}
.pk-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:8px}
.pk-list li{position:relative;padding-left:22px;font-size:15.5px;line-height:1.4;color:var(--ink-2)}
/* a hairline tick, drawn — not a ✓ glyph, which lands as an emoji on some Androids */
.pk-list li::before{content:"";position:absolute;left:2px;top:.45em;width:9px;height:5px;border-left:1.5px solid var(--gold-dk);border-bottom:1.5px solid var(--gold-dk);transform:rotate(-45deg)}
.pk-cta{display:flex;gap:8px;margin:auto 0 0;padding-top:4px}
.pk-cta .pt-book{flex:1;min-height:48px}
.pk-cta .pt-add{flex:0 0 48px}
/* ⏸ THE IN-CARD `.pk-cond` RULE WAS DELETED 2026-09-01 (owner). The short note used to sit here,
   after .pk-cta, once per card — 15 copies on the laser page. The full sentence now prints ONCE
   under each package list instead (.pkg-legal, ~1100 lines up, beside .cond-note).
   ⛔ Do not restore this rule from an old card: assertConditions() fails the build on any
   `.pk-cond` at all, so the markup would go red before the CSS ever mattered. */
.ground-2 .pkg-card,.ground-2 .pk-cure{background:var(--surface)}
.ground-2 .pk-cure{background:var(--paper-2)}
@media (max-width:600px){
  .pkg-grid{grid-template-columns:1fr}
  .pk-price b{font-size:1.9rem}
}

/* ---------- the cart (owner 2026-08-24: every price adds to a cart, like dermazur.ca) ----
   It exists only while the cart holds something — the live site's rule: it appears as a reward,
   never as an empty fixture. The drawer reuses the .sheet pattern. */
/* ⚠ The cart used to be a full-width bar under the header with its button at the top right,
   while the panel opened bottom-right — two halves of one object at opposite corners of the
   screen. The owner's word for it was "all over the place", and he was right. Now the pill IS
   the button, it sits bottom-centre, and the panel rises from directly above it: one object,
   one place, one gesture. The header is left alone, which also gave the sticky Femmes/Hommes
   switch its plain `top: var(--nav-h)` back — the `html.cart-on` offset that used to push it
   down was deleted with the bar, not kept "just in case". */
/* ⚠ IVORY, NOT NAVY (owner 2026-08-25: "the current colour is just too ugly and stands out too
   much"). A navy block bottom-centre competed with the page for attention on every scroll. The
   pill now speaks the same language as every card on this site — warm surface, hairline, a gold
   rule along the top and gold on the total — so it reads as part of the page and is still the
   only thing down there. Gold is the accent, never the field: a gold slab would be the same
   mistake in a warmer colour. */
.cart-pill{position:fixed;left:50%;transform:translateX(-50%);bottom:18px;z-index:70;
  display:inline-flex;align-items:baseline;gap:10px;min-height:48px;padding:0 24px;
  background:var(--surface);color:var(--ink);border:1px solid var(--line-strong);
  border-top:2px solid var(--brass);border-radius:2px;cursor:pointer;
  font:500 14.5px/1 var(--sans);letter-spacing:.02em;font-variant-numeric:tabular-nums;white-space:nowrap;
  box-shadow:0 20px 44px -22px rgba(20,27,61,.45);transition:border-color .18s,box-shadow .18s}
/* ⚠ REQUIRED, not tidying. `display:inline-flex` above is an author rule and outranks the UA's
   `[hidden]{display:none}`, so without this the empty cart sits on every page announcing
   "PANIER · 0 soin · 0 $" — the opposite of the rule that it appears only as a reward. The
   .sheet block carries the identical guard for the identical reason. */
.cart-pill[hidden]{display:none}
.cart-pill:hover{border-color:var(--gold-dk);box-shadow:0 24px 50px -22px rgba(20,27,61,.55)}
.cart-pill:focus-visible{outline:2px solid var(--royal);outline-offset:3px}
.cart-pill .cart-pill-l{letter-spacing:.14em;text-transform:uppercase;font-size:11.5px;color:var(--muted)}
.cart-pill .sep{color:var(--line-strong)}
.cart-pill b{font-weight:500;color:var(--ink)}
.cart-pill [data-cart-t]{color:var(--brass-text);font-weight:500}
.cart-pill-was{color:var(--muted);font-weight:400;margin-right:7px;text-decoration-thickness:1px}
/* ⚠ display:contents, so BELOW 960 THIS WRAPPER DOES NOT EXIST as far as layout is concerned
   and the pill lays out its four spans exactly as it did before the wrapper was added. It turns
   into a real flex row only in the >=960 block further down. */
.cart-pill-b{display:contents}
/* ⚠ THE >=960 TAB'S COUNT BADGE DOES NOT EXIST BELOW 960. The phone pill already prints
   « 3 soins · 480 $ » in full; a second copy of the number beside it would be the same fact twice
   on the smallest screen. It becomes a real box only inside the min-width:960 block below. */
.cart-tab-n{display:none}
@keyframes cart-pop{0%{transform:translateX(-50%) scale(1)}45%{transform:translateX(-50%) scale(1.06)}100%{transform:translateX(-50%) scale(1)}}
.cart-pill.flash{animation:cart-pop .5s ease-out 1}
.sheet-cart .cart-lines{list-style:none;margin:14px 0 0;padding:0 10px 0 0;max-height:min(46vh,430px);overflow:auto}
/* Owner 2026-08-26: *"the way that each treatment takes up that amount of space, make it take up
   less space… Things are taking up too much height. It's making the design look clunky."*
   Measured: every row was **121px** and only **3 of 6** picked treatments fit in the list.
   The row was two stacked bands — (name + price) then (session control + Retirer) — because at
   620px there was no room to put them side by side. At 870px there is. One line now:
   name over its context, the control, then the price and Retirer. ~121px → ~64px.
   ⚠ Three columns cannot fit a 375px phone, so the phone keeps the stacked layout — see the
   max-width:600px block, which also protects the 44px touch target on the session control. */
.sheet-cart .cart-lines li{display:grid;grid-template-columns:minmax(0,1fr) auto auto;align-items:center;gap:2px 16px;padding:8px 0;border-bottom:1px solid var(--line)}
.sheet-cart .cart-lines li .cart-li-n{grid-column:1;grid-row:1}
.sheet-cart .cart-lines li .cart-li-foot{grid-column:2;grid-row:1;margin-top:0;justify-content:flex-start;gap:14px}
.sheet-cart .cart-lines li .cart-li-p{grid-column:3;grid-row:1}
.cart-li-n{font-size:15.5px;color:var(--ink);line-height:1.35}
.cart-li-c{font-size:12.5px;color:var(--muted)}
/* GAP 3 — the combination discount, as ONE named line pinned to the total (live's own .zb-mz
   shape). Gold, because on this site gold is what a saving is: the .pk-cure strip, the
   « Économisez » chip and the conditions note all use the same family. The figure is negative, so
   it reads as taken off rather than as another thing being bought.
   ⚠ It sits OUTSIDE .cart-lines on purpose — that list is `overflow:auto` with a 46vh cap, and at
   375px three laser rows already fill it, so a line rendered as its last <li> was invisible on a
   phone while its discount stayed in the total. */
.cart-combo{display:flex;align-items:baseline;justify-content:space-between;gap:16px;
  margin:10px 0 0;padding-top:10px;border-top:1px solid var(--line);color:#7A5E16;font-size:15px}
.cart-combo-k{color:#7A5E16;font-weight:500}
.cart-combo-p{color:#7A5E16;font-weight:600;font-variant-numeric:tabular-nums;white-space:nowrap}
.cart-li-p{grid-column:2;grid-row:1;text-align:right;font-size:15.5px;color:var(--ink);font-variant-numeric:tabular-nums;white-space:nowrap}
.cart-li-foot{grid-column:1/-1;display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:6px}
/* Owner 2026-08-25: *"Make the selection longer in length. Right now it's not long enough in
   length. But just make this on computer browser, not mobile. It would look better on computer
   that way."* Longer in BOTH senses, because on a desktop both were short and both were free:

   TALLER — measured at 1440×900 with six treatments picked: the list showed 414px of a 725px
   list, so **311px and roughly half the selection were below the fold** behind a scrollbar, in a
   viewport with 104px of unused room above the panel. The cap was `min(46vh,430px)`, a fixed
   ceiling that ignored how much screen there actually was. The panel is now a flex column that
   grows to the viewport and hands every spare pixel to the list.
   WIDER — 460px → 620px, so a long row ("Cou complet (avant + arrière)") stops crowding its price.

   ⚠ This block sits HERE, immediately after the cart's own rules, and not up in the shared
   responsive section — the cart is declared after it, so an override placed there loses on source
   order and silently does nothing. That exact mistake already cost this file one debugging round
   (see the phone cart-pill note in the handoff). Phones keep the old behaviour on purpose: there
   is no spare height or width to hand out on a 375px screen. */
/* The flex column and the viewport cap apply at EVERY width, because they are also the fix for a
   defect that predates this change: measured on the untouched CSS at 375×812 with six treatments
   picked, the panel stood 698px tall with its top at **−10px** — the « Sélection » heading and the
   close button were cut off above the screen, which is the worst possible thing to clip on a panel
   whose only exit is that button. Capping to the viewport costs a phone ~70px of list and gives it
   back a header it can reach. Only the WIDTH is desktop-only, which is what the owner asked to keep
   off mobile: a 620px panel cannot exist on a 375px screen. */
/* The cap is derived, not picked: the panel stands off the bottom by 78px, and it must stop
   BELOW the sticky header rather than floating across the nav links — a panel with no dimmed
   backdrop that covers « Soins · Préoccupations · Technologie » reads as a rendering fault, not
   as a modal. 78 (bottom offset) + var(--nav-h) + 14 of breathing room. Because it is written
   against the token, the 72px phone/short-viewport header re-derives it for free. */
.sheet-cart{display:flex;flex-direction:column;max-height:calc(100dvh - 92px - var(--nav-h))}
.sheet-cart .cart-lines{max-height:none;flex:1 1 auto;min-height:0}
@media (min-width:601px){
  /* +40 % (620 → 870px), owner 2026-08-26. The extra width is what pays for the one-line row
     below: name, session control and price stop stacking and sit beside each other. */
  .sheet-cart{width:min(870px,calc(100vw - 48px))}
}
/* ---- the session selector ----
   Owner 2026-08-25: *"make the selector look sleeker and nicer too."* It was three outlined
   boxes — every option drawn with the same weight as the one actually chosen, so the control
   shouted three times and answered nothing. It is now ONE segmented field: a single hairline
   frame holding the options, hairline dividers between them, and only the CHOSEN one filled —
   ivory on a gold-edged ground, the same "this is the live value" gesture the price rail and the
   Femmes/Hommes switch already use. Unchosen options carry no border of their own.
   ⚠ It has to look deliberate at TWO widths, not just three: laser has no Cure Essentielle, so
   its rows show two options. `flex:1 1 0` makes the field share itself evenly either way, so the
   two-option state reads as a designed pair rather than a three-option control with a gap in it.
   ⚠ 44px, not 34px — the old height failed a touch target on a phone by 14px. */
/* ⚠ 34px, DOWN FROM 44 (owner 2026-08-26: "make the button smaller… it all looks too spacey.
   Especially on mobile it'll look bad"). 44 was this file's own comfort target, not a rule: the
   accessibility floor that actually applies is WCAG 2.2 AA 2.5.8 (Target Size Minimum) = 24px,
   and 34 clears it by 10. The 44px figure belongs to 2.5.5 Target Size Enhanced, which is AAA —
   nothing else on this site is held to AAA. Six rows × 10px is 60px of drawer height returned,
   which on a 375px phone is a whole extra treatment above the fold. Do not go below 24. */
.cart-ses{display:inline-flex;border:1px solid var(--line-strong);border-radius:2px;overflow:hidden;background:var(--surface)}
.cart-ses button{flex:1 1 0;font:500 11.5px/1 var(--sans);letter-spacing:.02em;min-height:34px;padding:0 10px;white-space:nowrap;
  background:none;border:0;border-left:1px solid var(--line);color:var(--ink-2);cursor:pointer;transition:color .16s,background .16s}
.cart-ses button:first-child{border-left:0}
.cart-ses button:hover{background:var(--paper);color:var(--ink)}
.cart-ses button[aria-pressed="true"]{background:var(--night);color:var(--paper);box-shadow:inset 0 2px 0 var(--brass)}
.cart-ses button:focus-visible{outline:2px solid var(--royal);outline-offset:-2px}
.cart-rm{background:none;border:0;padding:4px 0;min-height:34px;color:var(--muted);font:500 11.5px/1 var(--sans);text-decoration:underline;text-underline-offset:3px;cursor:pointer}
.cart-rm:hover{color:var(--ink)}
.cart-empty{padding:14px 0;color:var(--ink-2);font-size:15px;line-height:1.55;border-bottom:0!important;display:block!important}
/* ---- the price detail on every selection row (owner 2026-08-26) ----
   Five numbers per line — per-session cost, the single-session price it replaces, the discount and
   the money saved — set small and quiet so the row still reads name-first. The saved amount is the
   only thing bolded, because that is the number he says his customers are actually shopping on.
   ⚠ It lives INSIDE .cart-li-n (the name cell), not in a fourth column: the name cell was already
   two lines tall, so a third short line grows the row by ~17px instead of forcing a new column and
   another width negotiation. */
/* INLINE, running on from « Laser · Femmes », not on its own line. As a block it pushed every row
   from 67px to 84px and put the sixth treatment back below the fold — undoing the height fix the
   owner had just asked for. Inline it costs nothing on a wide panel and wraps by itself when the
   panel is narrow, which is the same two-line row it would have been anyway. */
.cart-li-d{font-size:12.5px;line-height:1.45;color:var(--muted);font-variant-numeric:tabular-nums}
.cart-li-d::before{content:" · ";color:var(--line-strong)}
.cart-li-d b{font-weight:500;color:var(--brass-text)}
/* ---- the footer, cut down (owner 2026-08-26: "the bottom part… is taking up too much space") ----
   « Total avant taxes » is just « Total » now, the three-line note is one line, and the gaps are
   tighter. The tax caveat did not disappear with the label — it moved into that one line. */
.cart-total{display:flex;justify-content:space-between;gap:16px;margin:10px 0 0;padding-top:12px;border-top:1px solid var(--line-strong);font-size:16px;color:var(--ink)}
.cart-total b{font-weight:500;font-variant-numeric:tabular-nums}
/* what the same selection costs one session at a time — struck, quiet, and to the LEFT of the
   price it is being discounted from, the way the live bar prints it. No `display` rule: it is
   toggled with the `hidden` attribute and an author display would outrank the UA's [hidden]. */
.cart-was{color:var(--muted);font-weight:400;margin-right:8px;text-decoration-thickness:1px}
/* ---- the WHOLE-SELECTION session switch (owner 2026-08-26) ----
   Live's `.zb-sess`, in this site's language: the segmented field on the left, then the money
   saved and the price of a single visit as quiet chips. It replaced the standalone
   « Vous économisez » line rather than joining it — same number, one row fewer, which is the
   direction the owner has asked this drawer to move twice now. */
.cart-all{display:flex;flex-wrap:wrap;align-items:center;gap:8px 14px;margin-top:10px}
.cart-all[hidden]{display:none}
.cart-all-seg{display:inline-flex;border:1px solid var(--line-strong);border-radius:2px;overflow:hidden;background:var(--surface)}
.cart-all-seg button{font:500 11.5px/1 var(--sans);letter-spacing:.02em;min-height:34px;padding:0 12px;white-space:nowrap;
  background:none;border:0;border-left:1px solid var(--line);color:var(--ink-2);cursor:pointer;transition:color .16s,background .16s}
.cart-all-seg button:first-child{border-left:0}
.cart-all-seg button:hover{background:var(--paper);color:var(--ink)}
.cart-all-seg button[aria-pressed="true"]{background:var(--night);color:var(--paper);box-shadow:inset 0 2px 0 var(--brass)}
.cart-all-seg button:focus-visible{outline:2px solid var(--royal);outline-offset:-2px}
.cart-all-chip{font-size:12.5px;color:var(--brass-text);font-variant-numeric:tabular-nums}
.cart-all-chip b{font-weight:500}
.cart-all-per{color:var(--ink-2)}
.cart-note{font-size:12.5px;color:var(--muted);line-height:1.5;margin-top:8px;max-width:none}
/* The drawer's own button is smaller than a page CTA on purpose: by the time it is on screen the
   visitor has already chosen, so it confirms rather than persuades. 42px still clears the 24px
   AA target floor comfortably. */
.sheet-cart .sheet-actions{margin-top:10px;gap:12px 18px}
.sheet-cart .sheet-actions .btn{min-height:42px;padding:0 18px;font-size:11.5px;letter-spacing:.11em}
/* ⚠ THIS MEDIA QUERY LIVES HERE, not up in the responsive section, and that is deliberate: the
   cart's own rules are declared further down the file than that section, so an override written
   up there loses on source order and silently does nothing. (It did: the pill kept its 18px
   bottom and the overlap test still failed.)
   On a 375px screen the centred pill measures 247px and the Azur and palette buttons start 8px
   from each edge — a measured overlap with BOTH. Desktop fits all three on one line; a phone does
   not, so the pill rides one row above them and the drawer follows it up. */
@media (max-width:600px){
  .cart-pill{bottom:66px}
  /* ⚠ The « was » figure comes OFF the pill on a phone. With it the pill measured 366px of a
     375px screen — edge to edge, which stops reading as a pill and starts reading as a bar the
     layout could not fit. On a phone this control is a trigger, not a summary: the same struck
     figure is one tap away on the drawer's own Total line. */
  .cart-pill-was{display:none}
  /* The one-line cart row is a DESKTOP layout — three columns will not fit 375px. The phone keeps
     the original two-band row, which is also what keeps the 44px session control reachable. */
  .sheet-cart .cart-lines li{grid-template-columns:1fr auto;align-items:start;gap:2px 14px;padding:9px 0}
  .sheet-cart .cart-lines li .cart-li-foot{grid-column:1/-1;grid-row:auto;margin-top:6px;justify-content:space-between}
  .sheet-cart .cart-lines li .cart-li-p{grid-column:2;grid-row:1}
  /* ⚠ The cap lives HERE, not with the panel's other sizing rules, because the number is derived
     from THIS rule: the phone panel stands off the bottom by 124px (clearing the lifted pill), not
     the 78px it uses on a desktop, so a shared `100dvh − 118px` overshoots by 46px and pushes the
     header off the top — measured at 375×812, top went to −28px. 124 + 16 of breathing room = 140. */
  .sheet-cart{inset:auto auto 124px 50%;width:calc(100vw - 16px);max-width:none;max-height:calc(100dvh - 140px)}
}

/* ---- THE TABLET BAND, 601–959px: the wider Azur launcher pushed into the pill ----
   Measured after the launcher gained its verb and its 56px height (2026-09-01), with two
   treatments picked, before this rule existed:
     601×900   pill x 136.5–464.5   .chat-fab x 379.2–585   →  85.3px overlap
     700×900   pill x 186.0–514.0   .chat-fab x 478.2–684   →  35.8px overlap
     768×900   pill x 220.0–548.0   .chat-fab x 546.2–752   →   1.8px overlap   (iPad portrait)
     900×900   clear                                        →   the band ends here
   « Azur » was 99.5px wide; « Demander à Azur » is 205.8px, and the 106px it gained came out of
   the room the bottom-centre pill had beside it. A desktop still fits all three controls on one
   line; this band no longer does, so the pill rides one row above them exactly as the phone does.
   ⚠ THE NUMBERS ARE DERIVED FROM THE LAUNCHER, NOT PICKED. Launcher: bottom 16 + 56 tall = 72
   off the floor; + 10 of air = 82 for the pill. Panel = 82 + 48 (pill) + 12 = 142. Its cap is
   142 + 14 of breathing room, the same +14 the base rule uses, so the panel still stops below
   the sticky header instead of floating across the nav links.
   ⚠ AND IT LIVES HERE, beside the cart's other overrides, for the reason written three times in
   this file already: the cart is declared further down than the shared responsive section, so an
   override written up there loses on source order and silently does nothing. */
@media (min-width:601px) and (max-width:959.98px){
  .cart-pill{bottom:82px}
  .sheet-cart{inset:auto auto 142px 50%;max-height:calc(100dvh - 156px - var(--nav-h))}
}

/* ================= THE SELECTION MOVES TO THE RIGHT EDGE — >=960px ONLY (owner 2026-09-01) ==
   ⛔ PHONE AND TABLET ARE UNTOUCHED BY THIS BLOCK. Everything below is inside one min-width:960
   query, and 375px was measured identical before and after (pill 269.7×48 at x 52.7, y 698–746).

   ⚠ IT IS THE LAST WORD ON .cart-pill AND .sheet-cart IN THIS FILE, and it has to be. Between
   them those two selectors are declared at NINE places above — the shared sheet box (~L387), the
   bottom-centre anchor (~L389), the base pill (~L1893), the flex-column cap (~L1969), the 601+
   870px widening (~L1974), the two phone blocks (~L2057/2072) and the 601–959 band just above.
   A @media query adds no specificity, so any of those would beat this one on source order if
   this block were written earlier. Nothing about .cart-pill or .sheet-cart goes below here.

   ⚠ .cart-pill-b's base rule is `display:contents` (declared with the pill, above). This block is
   where it becomes a real box — the pill stacks on two lines so it stays narrow against the right
   edge, and the label's separator goes with the line break that made it meaningless. */
@media (min-width:960px){
  /* ⭐ A THIN VERTICAL TAB, NOT A CARD (owner 2026-09-02).
     The collapsed state was a two-line horizontal card floating 16px off the right edge — a
     second object competing with the page. It is now a TAB: 44px wide, flush against the edge
     (right:0, no right border, radius on the left corners only), « Sélection » set vertically,
     and the count as a small badge. Ivory ground and a gold hairline, the same two materials
     every card on this site is made of. The shadow is thrown LEFT, so nothing crosses the edge.
     ⚠ THE BUTTON'S ACCESSIBLE NAME IS UNCHANGED. `.cart-pill-b` is VISUALLY hidden here, not
     display:none — the tab still announces « Sélection · 3 soins · 480 $ ». Hiding it properly
     would have traded a layout win for a screen-reader regression that no screenshot can show.
     ⚠ 44px is the WCAG 2.5.5 enhanced target size, and the tab is ~180px tall, so the hit area is
     comfortable in the axis that matters. */
  /* ⭐⭐ THE TAB TURNED THE RIGHT WAY UP (owner 2026-09-04: *"make the 'selection' cart on the right
     side to be from left to right. and put it as a rectangle sticking out more on the top right of
     the page but not too on the top"*).
     It was a 54px vertical strip, mid-height, with the word running down it. It is now a normal
     horizontal rectangle: the label reads left-to-right like every other control on the site, and
     it sits near the TOP right rather than centred — `top:22vh`, which is high without being under
     the header. Below the header on every screen: the header is ~72-88px and 22vh is 149px at the
     shortest laptop this is shown on (678px), so it never tucks under it.
     ⚠ `translateY(0)` REPLACES THE OLD `-50%` — the centring transform has to go, or the box hangs
     half a height above where top: puts it. The flash keyframe below writes its own transform and
     was forked for exactly this reason; it is left alone because it now agrees again.
     ⚠ `writing-mode` and `text-orientation` are RESET on the label further down, not merely
     overridden here — an upright vertical label inside a horizontal box lays one letter per line. */
  .cart-pill{left:auto;right:0;bottom:auto;top:22vh;transform:translateY(0);
    /* ⭐ SLIM (owner 2026-09-04: *"make the 'selection' cart button a slim rectangle so it's more
       elegant"*). 60px tall and 167 wide read as a slab; 40px tall reads as a tab. The height comes
       out of the PADDING (16 → 10px) and the type (12.5 → 11.5px, badge 26 → 20px), not out of a
       fixed height — so the box still grows if the label ever gets longer or the count reaches four
       figures, instead of clipping.
       ⚠ The gold edge goes 3 → 2px with it — at 40px tall a 3px rule is a stripe, not an edge.

       ⭐⭐ SLIMMER AGAIN, BY 30 % EXACTLY (owner 2026-09-04: *"make the selection cart slimmer by
       30%"*). MEASURED, not eyeballed: the tab stood 143.8×42 at 1440 (1px border + 10 pad + a
       20px badge + 10 pad + 1px border). It now stands 29px — 30.95 % off, the nearest whole
       pixel to 42 × 0.7 = 29.4.
       ⛔ THE HEIGHT IS THE ONLY AXIS THAT MOVES, and that is what « slimmer » means on a tab that
       is already 3.4× wider than it is tall: padding 10 → 5px and the badge 20 → 17px. THE LABEL
       KEEPS ITS 11.5px. Scaling the type down with the box would have bought ~7px of width the
       owner did not ask for and cost the one word on the control its legibility — the exact
       complaint (*"It's not readable right now"*) that put this label at 13px two rounds ago.
       ⚠ 29px STILL CLEARS WCAG 2.5.8's 24px, and the target is 135.8×29 — this is the only door
       back into the selection on desktop, so the floor was re-measured after the change, not
       assumed. It may not go lower without moving that door somewhere else.
       ⚠ THE HEIGHT IS STILL NOT DECLARED. `min-height:0` and intrinsic content, exactly as
       before: a four-figure count or a longer label grows the box instead of clipping inside a
       fixed 29px. */
    flex-direction:row;align-items:center;justify-content:flex-start;gap:9px;
    width:auto;min-width:0;padding:5px 14px;min-height:0;
    border:1px solid var(--line-strong);border-right:0;
    border-left:2px solid var(--brass);border-radius:2px 0 0 2px;
    box-shadow:-16px 0 36px -24px rgba(20,27,61,.42)}
  .cart-pill:hover{border-left-color:var(--gold-dk);box-shadow:-18px 0 44px -22px rgba(20,27,61,.55)}
  /* offset INWARD: an outset ring on an element flush with the viewport edge is clipped. */
  .cart-pill:focus-visible{outline:2px solid var(--royal);outline-offset:-4px}
  .cart-pill-sep1{display:none}
  /* ⛔ `display:block` IS THE LOAD-BEARING DECLARATION HERE, not the clip. The base rule is
     `.cart-pill-b{display:contents}` — an element with display:contents generates NO BOX, so
     position, width, height and clip are all ignored on it and its children keep laying out as
     direct children of the tab. Written without this line the rule looked correct, changed
     nothing, and printed « 3 soins · 225 $ 140 $ » spilling out of a 44px tab. Caught by looking
     at a screenshot, not by any checker. */
  .cart-pill-b{display:block;position:absolute!important;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
  /* ⭐ THE LETTERS STAND UP (owner 2026-09-04: *"Make the letters top to bottom positioned
     upright"* — and *"It's not readable right now"*, which is the same complaint).
     ⛔ `text-orientation:upright`, NOT `mixed`. That ONE keyword is the whole difference: in a
     vertical writing mode `mixed` lays Latin script on its side (the reader tilts their head) while
     `upright` keeps every glyph the right way up and stacks them down the column. It also makes the
     text intrinsically wider, which is part of why the tab went to 54px above.
     ⚠ letter-spacing becomes the VERTICAL gap between stacked glyphs in upright mode, so .18em is
     now leading, not tracking — loosened to .2em so the column does not read as one blur, and
     colour lifted from --ink-2 territory to full --ink for the readability he asked for.
     ⚠ font-size 11.5 → 13px: the label is the only word on the control and it was the smallest
     type on the page. */
  /* ⛔ BOTH PROPERTIES RESET, not just one. `writing-mode:horizontal-tb` alone leaves
     `text-orientation:upright` in place, which is harmless in a horizontal flow — but leaving a dead
     declaration behind is how the next person re-derives why the label once stood on its end.
     Letter-spacing drops .2em → .14em: in the vertical box that value was LEADING between stacked
     glyphs, and .2em of horizontal tracking on 13px small caps reads as a gap, not as spacing. */
  .cart-pill .cart-pill-l{writing-mode:horizontal-tb;text-orientation:mixed;
    letter-spacing:.15em;text-transform:uppercase;font-size:11.5px;font-weight:500;color:var(--ink);
    white-space:nowrap}
  /* ⚠ THE BADGE IS WHAT SETS THE TAB'S HEIGHT — it is the tallest thing in the row (17px against
     the label's 11.5px line box), so 20 → 17px here is 3 of the 13px the tab lost. The radius
     follows it (10 → 9, half of 17 rounded up) or the pill goes back to being a rectangle, and
     the type steps 11.5 → 11px so a two-digit count still sits inside the circle rather than
     touching it. Padding stays 5px: that is what widens it for « 12 » and « 120 ». */
  .cart-tab-n{display:grid;place-items:center;min-width:17px;height:17px;padding:0 5px;
    border-radius:9px;background:var(--night);color:var(--paper);
    font:500 11px/1 var(--sans);font-variant-numeric:tabular-nums}
  /* ⚠ THE TAB STANDS DOWN WHILE ITS OWN PANEL IS OPEN. The panel is 870px anchored at right:16px,
     so a 44px tab at right:0 would show a 16px sliver past the panel's edge and read as a
     rendering fault. « Closing returns to the tab » is the whole gesture: one control, two states.
     ⚠ THIS IS WHY site.js NOW RETURNS FOCUS to the opener on close — Escape would otherwise leave
     a keyboard visitor with no focused element and no visible control to tab back to. */
  body.sheet-open .cart-pill{display:none}
  /* ⛔ THE FLASH KEYFRAME HAD TO BE FORKED, AND THIS IS NOT COSMETIC. `cart-pop` writes
     `translateX(-50%)` into all three of its frames — the centring transform the bottom pill
     needs. Re-used on a right-edge pill it OVERWRITES translateY(-50%) for the half-second the
     animation runs, so every « add » would fling the control down and across the screen and
     snap it back. `cart-pop-r` carries translateY(-50%) instead. Same 500ms, same scale. */
  .cart-pill.flash{animation-name:cart-pop-r}
  /* the panel opens beside the pill, not over the page. */
  /* ⭐ 870px, AND THE BRIEF'S 560 WAS MEASURED OUT, NOT ARGUED OUT. The rule the panel has to
     keep is v2.18's: every cart row stands 58.8px on ONE line, because the two-band row it
     replaced was 121px and put the sixth pick below the fold. I swept the width from 560 to 870
     in 10px steps with EVERY addable treatment on five pages in the cart, and compared each
     row's height against the SAME row at the shipped 870px:
       560–620  the NAME wraps          « Bikini intégral (inclut le sillon interfessier) » → 100.7px
       640–730  the CONTEXT line wraps  « Cou complet (avant + arrière) », « Bras complets »  → 79.8px
       740      still 9–23 rows wrapped « Le Trio Majeur », « Jambes Divines », « Le Duo Star »
       760–840  still 8 rows wrapped on /tarifs and /tarifs-trans:
                « Éclat Signature + LED », « Velours Sublime + LED », « Régénération Sublime + LED »,
                « Signature Radiance (Microdermabrasion + LED) », « Protocole Clarifiant »,
                « Soin Hydra Glow » (and their two English twins)
       860      3 of those 8 still wrapped
       870      ZERO rows worse than the shipped panel, on all five pages, FR and EN
     So 870 is not "the old number kept out of caution" — it is the narrowest width at which no
     row in this repo's real price list gives back the 121→58.8px win. 42vw was dropped with the
     rest: at 1440 it computes to 604.8px, inside the wrapping band.
     ⚠ Three rows on /tarifs (eleven on /en/pricing) are ALREADY two lines at 870 in the shipped
     build. That is pre-existing, it is unchanged here, and it is why the test is "no row worse
     than 870" rather than "no row over one line".
     ⚠ The panel is anchored right and vertically centred, so with a max-height of
     100dvh − nav-h − 140 its top lands at (nav-h + 140)/2 = 112px — 27px BELOW the sticky
     header. A panel with no dimmed backdrop that crosses the nav links reads as a rendering
     fault, not a modal. */
  .sheet-cart{inset:auto 16px auto auto;top:50%;transform:translateY(-50%);
    width:min(870px,calc(100vw - 32px));max-height:calc(100dvh - var(--nav-h) - 140px)}
}
@keyframes cart-pop-r{0%{transform:translateY(-50%) scale(1)}45%{transform:translateY(-50%) scale(1.06)}100%{transform:translateY(-50%) scale(1)}}

/* ---------- hero text colour: DECIDED, no longer switchable (owner 2026-09-04) ----------
   *"it's decided that i want the color blue so remove the other options from the palette."*
   The blue was never a guess: dermazur.ca's own hero <h1> computes to rgb(26,35,186) = #1A23BA,
   read off a real render with getComputedStyle, and that is the exact value this site already
   carries as --royal. It is set in the base `.hero-in .display` rule, so it needs nothing here.
   🗑 DELETED WITH THE ROW: html[data-hero="encre"] (navy, the old default), html[data-hero="azur"]
   (#175A85, the sea in the photo) and html[data-hero="blanc"] (the box-decoration-break plate,
   far below) — plus every --plate-t / --plate-b / --plate-lh the plate needed.
   ⚠ Only the HERO is blue. Every headline below the fold stays --ink, which is also what live
   does — its h2s are #141B3D. A site where every heading is blue is a different site.

   ---------- and the ?photo= row went the same way ----------
   *"the home photo i decided will be cropped so remove the option to select that too."*
   « Cadrée » was the attribute-less default, so the teardown was purely deletion: the whole
   `html[data-photo="cote"]` block (copy left / whole 3:2 photo right, stacking below 900px) is
   gone. Its reasoning, and the older ?photo=entier it replaced, are in git and in the handoffs.
   ⚠ ONE THING THAT BLOCK DID IS NOW DONE UNCONDITIONALLY: it switched every hero halo OFF, and
   that is the default now — see `.hero-in .display` above, where the owner asked for the white
   glow to go on 2026-09-04. */

/* ---------- the ads landing page (/epilation-laser-soprano) ----------
   Nothing here is a new component. It re-uses .thero, .claims, .fitz, .numbers, .steps, the
   price table, the reviews and the closing band; these rules only re-fit them for a page whose
   hero carries the whole argument. Placed last on purpose: `.ads-hero .display` and
   `.thero-copy .display` have the same specificity, so this block has to win on order. */

/* The stripped header keeps the phone where the full nav drops it (≤1180px, where the burger
   normally takes over). On a page bought for one action, the number IS the action. Below 600px
   it steps aside — the hero, the closing band and the footer all still carry it. */
.nav-bare .nav-tel{display:block}

/* THE HERO PHOTO SITS ON TOP, LIKE THE HOME PAGE — owner, 2026-08-24: *"make it like the main
   site in terms of how the photo is placed and FADED at the sand, make the entire photo visible
   until the bottom."* Three separate defects were being named at once, and each needs its own
   rule below:
     1. PLACEMENT — the photo was a column beside the copy; the home page puts it across the top.
     2. CROPPED — a 1600x900 render forced into the site's 4:5 portrait frame. `object-fit:cover`
        threw away 55% of the picture: the vase was cut on the left, the plant on the right. The
        box is given the photograph's OWN 16:9 ratio, so cover and contain are the same thing and
        every pixel is on the page. (Same fix, same reason, as ?photo=entier on the home hero.)
     3. HARD EDGE — it ended on a cut. It now dissolves into the paper.
   ⚠ THE COST, so nobody "re-fixes" it by accident: the note further down this file records that
   the Réserver button was deliberately lifted ABOVE the Fitzpatrick strip because at ~965px it
   fell off a 1440x900 laptop, and on a page bought for one click the button cannot be below the
   fold. A photo across the top spends that budget again — which is why the height is capped hard
   below rather than left to fill the column. The header's own BOOK button stays fixed and visible
   the whole way down, so the ask is never actually off the screen. Flagged to the owner. */
.ads-hero .thero-grid{grid-template-columns:1fr;align-items:start;gap:clamp(28px,3vw,44px)}
.ads-hero .ads-fig{order:-1}
/* Flush under the header, exactly as ?photo=entier places the home plate — the section's usual
   100px of top air is what pushed the headline off a 1440x900 screen, and the photo reads as the
   top of the page rather than as an illustration inside it. */
.ads-hero{padding-top:0}
/* Height, not width, is the size dial — the same rule the home hero learned: full-bleed 16:9 is
   810px tall at 1440 and would push the headline off the screen entirely. Capping the HEIGHT and
   letting the width follow (height x 16/9) holds 16:9 exactly, so shrinking it crops NOTHING.
   The 300px reserve is measured, not guessed: on a 1440x900 laptop it is what the caption, the
   eyebrow and BOTH lines of the Bodoni headline need underneath the photo — the promise the ad
   was clicked on is readable without scrolling. It also lands the plate at 917px wide, within 7px
   of the home page's 924px plate, so the two pages read as one site rather than as near-misses. ⚠ Never add a max-height here — that widens the box
   past 16:9 and reopens the crop this rule exists to close.
   Sized against 100% (the .wrap column), never 100vw: 100vw includes the scrollbar and would push
   a 375px phone into sideways scroll. */
.ads-fig img{aspect-ratio:16/9;width:min(100%,calc((100svh - var(--nav-h) - 300px)*(16/9)));object-position:50% 50%;
  -webkit-mask-image:linear-gradient(180deg,#000 74%,rgba(0,0,0,.5) 89%,transparent 100%);
  mask-image:linear-gradient(180deg,#000 74%,rgba(0,0,0,.5) 89%,transparent 100%)}
/* One line each for « Soprano Titanium, » and the italic « Édition Spéciale ». The site display
   cut (6.3vw) wraps both halves and turns a two-part headline into four lines of Bodoni.
   5.2vw = 74.9px at 1440, so this still clears craft-audit C2's 72px hero floor. */
.ads-hero .display{font-size:clamp(2.9rem,5.2vw,5.1rem);max-width:20ch}
/* The italic half sits on its own line — the ONE contrast device (§2). Not a second colour: the
   live page's blue-on-navy two-tone headline is exactly the tell this rebuild exists to remove.
   ⚠ This is a forced line BREAK, not `display:block` on the <em>. Blockifying the italic splits
   the h1 into three blocks, and the headline's closing period — which sits AFTER the </em> —
   becomes a third block, i.e. a full stop floating alone on its own line. Both gates passed that
   page; it was found by opening the screenshot. Keep the em inline. */
.ads-hero .display em::before{content:"\A";white-space:pre}
/* proof rows: /technologie's hairline list, one column beside the plate. No icons, no checks. */
.ads-hero .claims{grid-template-columns:1fr;margin-top:34px;max-width:46ch}
.ads-hero .claims li{max-width:none}
/* the Fitzpatrick strip runs bare in the hero — no card, no border. On the ivory ground next to
   a dark plate a white panel reads as a widget; hairlines only, like everything else here. */
.ads-hero .fitz{background:none;border:0;padding:0;margin-top:34px;max-width:46ch}
.ads-hero .fitz-scale i{height:40px}
.ads-hero .hero-actions{margin-top:34px}
/* (The old `object-position:66% 50%` lived here. It nudged the machine into the middle of a 4:5
   crop; the frame is the photo's own 16:9 now, so there is no crop left to nudge and the rule was
   removed rather than left behind to look meaningful. History: handoff 2026-08-24.) */

.foot-bare-grid{display:grid;grid-template-columns:2fr 1fr 1fr;gap:clamp(24px,4vw,56px);padding-bottom:32px;border-bottom:1px solid rgba(244,238,227,.16)}
/* the site footer clamps every <p> to 34ch, which split « QC H4K / 1J9 » across two lines.
   The bare footer's columns are already narrow enough by the grid. */
.foot-bare-grid p{margin-bottom:0;max-width:none}

@media (max-width:900px){
  .ads-hero .display{font-size:clamp(2.9rem,7.4vw,4.4rem)}
  .foot-bare-grid{grid-template-columns:1fr 1fr}
}
@media (max-width:600px){
  .nav-bare .nav-tel{display:none}
  .ads-hero .display{max-width:none}
  .ads-hero .claims,.ads-hero .fitz{max-width:none}
  .foot-bare-grid{grid-template-columns:1fr;gap:18px}
}

/* ---- GAP 2 — Azur's real panel (only rendered on the app-served build) ----
   Bubbles borrow the site's own two grounds rather than inventing chat colours: the visitor's
   line uses --azur-bubble, the token worked out once at the top of this file (⛔ do NOT put color-mix()
   back there — Chrome resolves it to a color(srgb …) string that every contrast checker misreads
   as a 1.25:1 failure). The visitor sits on --paper-2 with the
   hairline the rest of the site uses. 15px, not the 13px a chat UI is tempted into: the rulebook's
   size floor applies to a bubble exactly as it applies to a paragraph. */
/* ⚠ `.sr-only` DID NOT EXIST IN THIS STYLESHEET until 2026-08-31, and the chat form shipped a
   <label class="sr-only"> that rendered as a third of the input row — caught by opening a 375px
   screenshot, not by any check. The near-miss to avoid is `.pt-lab`: it looks like the same
   thing and is not, because below 600px it deliberately BECOMES visible (it is the price table's
   phone column label). Borrowing it here would have hidden the label on a desktop and printed it
   on the phone, which is the one width where it was already wrong. */
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
  clip:rect(0 0 0 0);white-space:nowrap;border:0}
.sheet-chat{display:flex;flex-direction:column;max-height:min(70vh,560px)}
.chat-log{flex:1 1 auto;min-height:0;overflow:auto;margin:12px 0 0;padding-right:6px;display:flex;flex-direction:column;gap:8px}
.chat-b{max-width:88%;padding:9px 12px;border-radius:2px;font-size:15px;line-height:1.5;white-space:pre-wrap}
.chat-b-bot{align-self:flex-start;background:var(--azur-bubble);border:1px solid var(--line);color:var(--ink)}
.chat-b-me{align-self:flex-end;background:var(--paper-2);border:1px solid var(--line-strong);color:var(--ink)}
.chat-b-typing{color:var(--muted);font-style:italic}
.chat-note{font-size:13px;color:var(--muted);margin:0 0 4px;letter-spacing:.02em}
.chat-form{display:flex;gap:8px;margin-top:12px}
.chat-form input{flex:1 1 auto;min-width:0;padding:11px 12px;font:inherit;font-size:15px;color:var(--ink);
  background:var(--surface);border:1px solid var(--line-strong);border-radius:2px}
.chat-form button{flex:0 0 auto;min-height:44px;padding:0 16px;font:inherit;font-size:14.5px;font-weight:500;
  color:var(--cta-ink);background:var(--cta-bg);border:1px solid var(--cta-bg);border-radius:var(--btn-radius);cursor:pointer}
.chat-foot{margin:10px 0 0;font-size:14px}

/* the three wavelengths, as three plain figures — the machine's one specification a visitor can
   actually hold on to. Tabular numerals so 755 / 810 / 1064 line up rather than jitter. */
.waves{display:flex;flex-wrap:wrap;gap:10px;list-style:none;margin:0 0 26px;padding:0}
.waves li{padding:8px 16px;font-size:16.5px;color:var(--ink);background:var(--surface);
  border:1px solid var(--line-strong);border-radius:2px;font-variant-numeric:tabular-nums;letter-spacing:.01em}

/* ═══════════════════════ THE LANDING SPLASH (owner 2026-08-31) ═══════════════════════
   *"fit in 1 page no scrolling … kinda like the live site's version but way cleaner and nicer."*

   ⭐ THE WHOLE RULE OF THIS BLOCK: NOTHING HERE MAY SCROLL. Live's gate lets its copy column
   scroll (`.sp-copy{overflow:auto}`) and on a 13" laptop that is exactly what happens — which is
   the thing being fixed. So every size below is clamped against the VIEWPORT HEIGHT as well as
   the width, and a short window shrinks the type rather than hiding the button.
   ⚠ `100dvh`, NOT `100vh`. On iOS Safari `100vh` counts the area BEHIND the URL bar, so a `vh`
   panel is taller than what you can see and its last row — here, the two buttons — sits below the
   fold on the one screen that is supposed to be the whole thing. `dvh` is the visible height.
   ⚠ `min-height:0` on the grid child: a grid item defaults to `min-height:auto`, which refuses to
   shrink below its content and would push the panel taller than the screen instead of scaling.

   ⭐ SPACE-BETWEEN, NOT CENTRE (owner 2026-08-31, revision 2): *"put things further away from each
   other to create space instead of sticking everything together in the middle to create
   surrounding space. Have the space be more throughout rather than surrounded."*
   The first version was `justify-content:center` with tight clamped gaps — a dense block floating
   in a wide margin, which is precisely the shape he described. The column is now FOUR bands
   (wordmark · the title group · benefits · skin tones · the ask) spread with `space-between`, so
   the empty space is DIVIDED BETWEEN them instead of pooled around the outside. The padding came
   down at the same time, because outer margin is the space he did not want.
   ⚠ THAT IS WHY EVERY GAP BELOW IS A `%`-OF-HEIGHT CLAMP AND NOT A FIXED px: space-between only
   distributes what is left over, so on a short window there is nothing to distribute and the
   bands must still be able to sit close without overlapping. */
.splash{position:fixed;inset:0;z-index:200;background:var(--paper);
  transition:opacity .7s var(--ease-rise),visibility .7s var(--ease-rise)}
.splash.is-gone{opacity:0;visibility:hidden;pointer-events:none}
body.is-gated{overflow:hidden}
/* ⏸ TEMPORARY — owner 2026-09-01, SPLASH_SECS = 0 in build/build.mjs.
   The gate no longer walks off by itself, so the palette launcher has to be reachable THROUGH
   it or the reviewer cannot change a single option without first dismissing the screen he came
   to review. .splash is z-index:200; 201 is the smallest number that clears it.
   ⚠ SCOPED TO .is-gated on purpose. The launcher's normal 80 puts it UNDER the booking and
   Azur sheets, which is correct once the gate is down — a permanent 201 would float it over
   an open sheet. body.is-gated is removed on dismissal, so this reverts itself.
   ⚠ The PANEL needs no z-index of its own: .palette is position:fixed and therefore its own
   stacking context, so the panel rides up with the launcher. */
body.is-gated .palette{z-index:201}
.sp-in{display:grid;grid-template-columns:minmax(0,1.05fr) minmax(0,1fr);height:100dvh;min-height:0}
/* ⭐ CENTRED, WITH THE BANDS SLIGHTLY APART — owner 2026-09-01 (revision 4, and the last word):
   *"Make the landing page info more centered in the middle not all the way on top. And then make
   things slightly spaces out from each other. Placed nicely."*

   THE THREE RULINGS ON THIS ONE PROPERTY, IN ORDER, because the next reader will otherwise undo one:
     1. 2026-08-31  space-between   — « put things further away from each other »
     2. 2026-09-01  flex-start + half the gap — « the gate's text closer together »
     3. 2026-09-01  centred, with the gap back up to roughly the middle of those two.
     4. 2026-09-02  « slightly higher » + « AROUND 15% » more space — the revision-3 gaps × 1.15
        and a lift by bottom padding, gated behind a min-height so short windows stayed centred.
     5. 2026-09-02  THIS: « make the lift work on small screen too ». The gate on the padding is
        gone. Two flex spacers (::before / ::after, 1 : 2) now hold the free space, so the column
        sits at the upper third on EVERY window and the spacers are the first thing to collapse
        when a window is short — there is no longer a height at which the lift switches off, and
        no height at which it can clip. `gap` is 0 and the rhythm rides on each child's own
        margin-top, because `gap` would also wrap the two spacers and cost two steps on the
        tightest phone. The rules live at the END of this file, under « THE GATE, ÉPURÉE ».
   Revision 2 was right about the gap and wrong about the anchor. Measured on the deployed page
   before this change: the skin-tone scale sat **11px** off the Réserver button at 1440x900 (9px at
   1280x720) while **272px** of the window sat empty underneath — the tightening pulled every band
   up and left the space it freed all in one place at the bottom. Centring spends that space on both
   ends instead, and the gap goes 6/1.2vh/14 → 12/2.2vh/24 so the bands read as separate again
   without returning to five islands.

   ⛔ WHY `flex-start` AND NOT `safe center` ANY MORE: the spacers distribute the free space
   themselves, and their basis is 0 — when content outgrows the window they are already gone and
   the column overflows at the BOTTOM only, wordmark still on screen, which is the same protection
   `safe` gave. A centred column with spacers would double-distribute and fight.
   ⚠ The step stays a `%`-of-height clamp for the original reason: on a short window there is
   nothing to distribute and the bands must still be able to sit close without overlapping. */
.sp-copy{display:flex;flex-direction:column;justify-content:flex-start;gap:0;--sp-step:clamp(14px,2.53vh,28px);
  min-height:0;padding:clamp(22px,4.2vh,46px) clamp(24px,3.6vw,58px);max-width:760px;margin-left:auto;width:100%}
.sp-copy>*{flex:0 0 auto}
/* the lift itself is at the END of this file (« THE GATE, ÉPURÉE », part 1) — ruling 5 above. */
/* the wordmark — royal blue, the one place the clinic's name is set in the accent (owner) */
.sp-brand{margin:0;font-family:var(--serif);font-weight:600;color:var(--royal);
  font-size:clamp(17px,min(1.5vw,2.4vh),22px);letter-spacing:.2em;text-transform:uppercase}
.sp-mid{display:flex;flex-direction:column}
.sp-eyebrow{margin:0;font-size:clamp(11px,1.3vh,12.5px);letter-spacing:.18em;text-transform:uppercase;
  color:var(--brass-text);font-weight:500}
.sp-title{margin:clamp(2px,.58vh,7px) 0 0;font-family:var(--serif);font-weight:600;line-height:1.02;
  font-size:clamp(34px,min(6.4vw,8.6vh),84px);color:var(--ink);letter-spacing:-.012em}
.sp-title em{font-style:normal;color:var(--royal)}
.sp-ed{margin:clamp(1px,.35vh,3.5px) 0 0;font-family:var(--serif);font-style:italic;font-weight:500;
  font-size:clamp(18px,min(2.2vw,3.1vh),29px);color:var(--brass-text)}
.sp-sub{margin:clamp(5px,.92vh,10px) 0 0;font-size:clamp(14.5px,min(1.2vw,1.9vh),17.5px);line-height:1.55;
  color:var(--ink-2);max-width:50ch}
/* FOUR benefits in 2×2 — two rows instead of four lines is what paid for the gaps above. On a
   narrow column they fall back to one per row, which is the right order to read them in anyway. */
.sp-benefits{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(auto-fit,minmax(255px,1fr));
  gap:clamp(8px,1.5vh,17px) clamp(14px,2vw,30px)}   /* ROW gap +15%; the column gap is horizontal and unchanged */
/* the tick is DRAWN, not a ✓ glyph — that character lands as a coloured emoji on some Androids */
.sp-benefits li{position:relative;padding-left:29px;font-size:clamp(14px,min(1.1vw,1.85vh),16.5px);
  line-height:1.4;color:var(--ink)}
.sp-benefits li::before{content:"";position:absolute;left:0;top:.1em;width:19px;height:19px;border-radius:50%;
  background:rgba(212,175,55,.16);border:1px solid var(--brass)}
.sp-benefits li::after{content:"";position:absolute;left:6.3px;top:calc(.1em + 4.6px);width:5px;height:9px;
  border:solid var(--brass-text);border-width:0 2px 2px 0;transform:rotate(42deg)}
/* ---- the Fitzpatrick tones, as tones (owner 2026-08-31) ----
   Six real swatches with a gold tick on each. The numeral flips to light ink from IV down, where
   the tone is dark enough that dark-on-dark stops being readable — the same break live uses. */
.sp-fitz{display:flex;flex-direction:column;gap:clamp(6px,1.04vh,11.5px)}
.sp-fitz-lab{margin:0;font-size:clamp(10.5px,1.25vh,12px);letter-spacing:.16em;text-transform:uppercase;
  color:var(--muted);font-weight:500}
.sp-fitz-scale{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:clamp(7px,1.1vw,12px)}
.sp-fitz-scale li{position:relative;display:grid;place-items:center;background:var(--tone);
  width:clamp(34px,min(3.1vw,4.6vh),44px);height:clamp(34px,min(3.1vw,4.6vh),44px);border-radius:50%;
  border:1px solid rgba(20,27,61,.14);box-shadow:0 1px 4px rgba(20,27,61,.10)}
.sp-fitz-scale li span{font-size:clamp(10.5px,1.25vh,12px);font-weight:600;letter-spacing:.04em;color:rgba(20,27,61,.66)}
.sp-fitz-scale li:nth-child(n+5) span{color:rgba(255,255,255,.88)}
.sp-fitz-scale li::after{content:"";position:absolute;right:-1px;bottom:-1px;width:16px;height:16px;border-radius:50%;
  background:var(--brass);border:1.5px solid var(--paper)}
.sp-fitz-scale li::before{content:"";position:absolute;right:4.5px;bottom:5px;width:3.5px;height:6.5px;z-index:1;
  border:solid #2C2408;border-width:0 1.6px 1.6px 0;transform:rotate(42deg)}
.sp-end{display:flex;flex-direction:column;gap:clamp(8px,1.27vh,15px)}
/* ⭐ TWO EQUAL BUTTONS (owner 2026-09-01). A flex row sizes each button to its own words, so
   « Réserver » came out ~90px narrower than « Découvrir la clinique » and the pair read as a
   primary and an afterthought. `1fr 1fr` makes both take the wider one's width, and the cap
   keeps the pair from stretching the width of a 760px column.
   ⚠ .sp-cta is a <p> and both buttons are its DIRECT children — a wrapper between them would
   make the grid one column of one item and the equalising would silently stop. */
.sp-cta{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin:0;max-width:520px}
.sp-cta .btn{min-height:clamp(46px,5.6vh,54px)}
.btn-ghost{background:none;color:var(--royal);border:1px solid var(--royal);box-shadow:none;text-shadow:none}
.btn-ghost:hover{background:var(--royal);color:var(--paper)}
.sp-photo{margin:0;min-height:0;overflow:hidden;background:var(--paper-2)}
.sp-photo img{width:100%;height:100%;object-fit:cover;display:block}
/* the 25 seconds, made visible. An overlay that vanishes by itself with no warning reads as a
   fault; a thin line running out reads as an invitation that is about to end. */
.sp-timer{position:absolute;left:0;right:0;bottom:0;height:3px;background:rgba(20,27,61,.10)}
.sp-timer i{display:block;height:100%;width:100%;background:var(--brass);transform-origin:left center;
  transform:scaleX(1);transition:transform linear}
.splash.is-running .sp-timer i{transform:scaleX(0)}
@media (prefers-reduced-motion:reduce){.sp-timer i{transition:none}}
/* PHONE: the photo becomes the ground and the copy sits on it. Two stacked halves would put the
   buttons below the fold, which is the one thing this screen may not do. */
@media (max-width:900px){
  .sp-in{grid-template-columns:1fr}
  .sp-photo{position:absolute;inset:0;z-index:0}
  .sp-photo img{opacity:.16;filter:grayscale(.2)}
  .sp-copy{position:relative;z-index:1;margin:0;max-width:none}
  .sp-benefits{grid-template-columns:1fr}
  /* the stack is at 900px, NOT 600 — that is the breakpoint this gate has always used, and
     it is where the photo goes behind the copy. Grid form of the same rule. */
  .sp-cta{grid-template-columns:1fr;max-width:none}
  .sp-cta .btn{width:100%}
}
/* a genuinely short window (a laptop in landscape, a phone on its side). The order things go in
   is the order of what they are worth: the paragraph first (the benefits say the same thing in
   fewer words), then the tone LABEL — never the tones themselves, which are the proof he asked
   for. (The foot line used to sit between those two steps; it was deleted outright on 2026-09-03.) */
@media (max-height:620px){ .sp-sub{display:none} }
@media (max-height:520px){ .sp-fitz-lab{display:none} }
@media (max-height:430px){ .sp-brand{display:none} }
/* ---- the sound control: on/off + volume (owner 2026-08-31) ----
   ⚠ THE SLIDER IS 44px TALL EVEN THOUGH THE TRACK IS 4px. A range input's HIT AREA is the element,
   not the line drawn inside it, and WCAG 2.5.8 counts the target — a 4px-tall control is a control
   nobody can grab on a laptop trackpad, let alone a touchscreen. The track is painted small; the
   box that catches the pointer is not. */
.sea-ctl{position:fixed;left:16px;bottom:74px;z-index:80;display:flex;align-items:center;gap:10px}
.sea-btn{display:inline-flex;align-items:center;gap:8px;
  min-height:44px;padding:0 14px;border-radius:2px;background:var(--surface);border:1px solid var(--line-strong);
  font:500 12.5px/1 var(--sans);letter-spacing:.1em;text-transform:uppercase;color:var(--ink);cursor:pointer;
  box-shadow:0 10px 30px rgba(20,27,61,.10)}
.sea-btn[aria-pressed="false"]{color:var(--muted)}
.sea-btn i{width:9px;height:9px;border-radius:50%;background:#2F8F5B}
.sea-btn[aria-pressed="false"] i{background:var(--line-strong)}
.sea-vol{-webkit-appearance:none;appearance:none;width:104px;height:44px;padding:0;margin:0;cursor:pointer;
  background:var(--line-strong);background-clip:content-box;border:0;border-radius:2px;
  border-top:20px solid transparent;border-bottom:20px solid transparent;box-shadow:0 10px 30px rgba(20,27,61,.10)}
.sea-vol::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:16px;height:16px;border-radius:50%;
  background:var(--surface);border:1px solid var(--night);box-shadow:0 1px 3px rgba(20,27,61,.28);margin-top:-6px}
.sea-vol::-moz-range-thumb{width:16px;height:16px;border-radius:50%;background:var(--surface);
  border:1px solid var(--night);box-shadow:0 1px 3px rgba(20,27,61,.28)}
.sea-vol:focus-visible{outline:2px solid var(--royal);outline-offset:2px}
@media (max-width:900px){ .sea-ctl{display:none} }

/* ---- /tarifs without its headline (owner 2026-08-31) ----
   The page's h1 is now the small « Tarifs » kicker, so it needs the eyebrow's look while staying a
   heading element. `.thero-lean` takes the vertical padding down to match: the old height was
   sized for a 72px display line that is no longer there, and leaving it would trade a big headline
   for a big empty space — which is the same screenful he asked to get back. */
h1.eyebrow-h1{font-family:var(--sans);font-weight:500;line-height:1.2;margin:0}
.thero-lean{padding-top:clamp(26px,4vw,52px);padding-bottom:clamp(22px,3vw,40px)}
.thero-lean .sec-head{margin-bottom:clamp(14px,2vw,22px)}


/* ══════ THREE COLOUR ROWS — ?liens= · ?titres= · ?porte= (owner 2026-09-01) ══════
   Three independent rows in the palette panel, sharing one set of three values:
     encre  #141B3D  ← THE DEFAULT, and therefore the attribute-less state. There is deliberately
                       nothing written for it: Reset removes the attribute, and a rule keyed to
                       the default value would leave the last pick on screen after a Reset.
     royal  #1A23BA
     or     var(--gold-opt) #5A450B  (see the token, with its measurements, near the top)

   ⚠ #1A23BA AND var(--gold-opt) ARE USED INSTEAD OF var(--royal) / var(--brass-text).
   Version B redefines --royal to #175A85 — which this same panel calls « Azur » one row up — and
   --brass-text to #2B6384, a BLUE. A button labelled « Royal » that paints Azur, and one labelled
   « Or » that paints blue, would be a switch that lies about itself. The chips in the panel show
   these exact two values, so the ink and the swatch agree in every version.

   ⚠ PLACED AT THE END OF THE FILE ON PURPOSE. Every rule below overrides an earlier one, and a
   @media query adds no specificity — later wins on a tie. Moving this block up silently disables
   parts of it.

   Contrast, computed (not estimated) against --paper / --paper-2 / --paper-3 / --surface in
   versions L, A and B — the worst of the twelve grounds for each value:
     encre  11.22:1   ·   royal  7.06:1   ·   or  6.14:1
   and against the darkest real pixel under the gate's own text at 375px (see --gold-opt):
     encre   8.83:1   ·   royal  5.56:1   ·   or  4.84:1
   All three clear 4.5:1, which is the floor for the smallest text any of these rows touches
   (.link at 13.5px, .benefits-sm .b-t at 14.5px, .sp-benefits li at 14px). */

/* ---- ?liens= : the arrow links ---- */
/* ⭐ GOLD IS BAKED (owner 2026-09-05, "for arrow links, keep as is" — as is meant the gold he
   had selected, not the ink the code defaulted to). Gold is now the attribute-less state and
   therefore has NO rule; encre and royal are the two that need one. */
.link{color:var(--gold-opt)}
html[data-liens="encre"] .link{color:var(--ink)}
html[data-liens="royal"] .link{color:#1A23BA}

/* ---- ?titres= : the section titles ----
   .title is the h2 the h2() helper emits (264 of them across the 50 pages, including the four
   `.title.seg-h2` « Épilation laser pour femmes / pour hommes » headings and the price h2).
   Then the h3 sub-titles that live inside those same sections:
     · bare <h3>        176 — 1.35–1.75rem depending on the section (.concerns 21.6px through
                              .steps 28px), including .answer h3 whose child <a> inherits it
     · .b-t              36 — 1.28rem, and 14.5px under .benefits-sm
     · .pt-h3             6 — 1.3rem
   NOT covered, and each on purpose: .display (the hero — it has its own ?h= row), running text
   (the ?texte= row owns it), and the two <h4>s on the package cards (.pk-n, .pkg-gt) — .pk-n is
   light text on a dark card, where a dark ink or a gold would disappear. */
/* ⭐ ROYAL IS BAKED (owner 2026-09-05: "for section titles make it royal"). The row is gone, so
   this is unconditional — and the dark bands are what makes that non-trivial: royal #1A23BA on
   --night #141B3D measures 1.13:1, text that is simply gone. The band rules that used to rescue
   it keyed on html[data-titres], an attribute that no longer exists. They are not needed, and
   that is checked rather than assumed: `.ground-night .display,.ground-night .title,
   .ground-night h3` (0,2,0) and `.ground-night .benefits .b-t` (0,3,0) already sit in the base
   and outrank every selector below. */
.title,h3,.pt-h3,.benefits .b-t{color:#1A23BA}
/* ⛔ THE OWNER'S ONE EXCEPTION, and it is deliberate: "on the overview of the laser page … make
   the six main points there to be ink, not royal". `.thero-benefits` is the treatment-page hero
   block, so this covers the laser page he pointed at AND its eleven siblings — six royal points
   on one treatment page and ink on the next would read as a bug, not a choice.
   Same specificity as `.benefits .b-t` (0,2,0), so it wins on source order. Keep it after. */
.thero-benefits .b-t{color:var(--ink)}

/* ---- ?porte= : the words on the entry gate ----
   ⛔ .sp-brand — the DERMAZUR wordmark — is absent from these selectors and must stay absent.
   Ruled 2026-08-31: the wordmark is the one place the clinic's name is set in royal, and it holds
   at every value of this row. .sp-eyebrow, .sp-ed and the Fitzpatrick tones are left alone too;
   the row colours the sentence a reader actually reads. */
/* ⭐ GOLD IS BAKED (owner 2026-09-05: "for the gate text, leave it gold"). Unconditional now.
   --gold-opt, not the metal #D4AF37: as WORDS the metal measures 2.3-3.4:1 on these grounds. */
.sp-title,.sp-title em,.sp-sub,.sp-benefits li{color:var(--gold-opt)}

/* ═══════ ?soustitre= — THE HOME HERO'S BOTTOM LINE (owner 2026-09-04) ═══════
   *"make that bottom text another color to be selectable."*
   The line is now TWO elements — the lede and the place eyebrow — so the row colours BOTH, or it
   would tint half a sentence and leave the other half behind.
   ⚠ THE DEFAULT IS A REAL CHANGE AND IT IS DELIBERATE. Before this the two halves were different
   colours (lede --ink-2, eyebrow --royal) because they sat at opposite ends of the hero. Now that
   they share one line, two inks read as a mistake, so `encre` — the attribute-less default — puts
   both in --ink-2. The eyebrow keeps everything else that makes it an eyebrow: uppercase, the
   .16em letterspacing and its gold ::before rule.
   ⛔ NO RULE FOR `encre`. It is the data-default, so Reset removes the attribute and lands here —
   a rule for the default is how Reset ends up looking broken. The base .lede colour IS the default.
   ⚠ Values are the three already-measured inks (see --gold-opt: the gold chip is #5A450B, not the
   metal #D4AF37, because #D4AF37 AS WORDS measures 2.3–3.4:1 on these grounds). */
/* --ink, NOT --ink-2, AND THIS WAS A REGRESSION CAUGHT BY MEASURING. The deleted `.hero-in .lede`
   rule carried `color:var(--ink)` alongside its text-shadow; removing the rule for the halo
   silently handed the hero lede back to the base `.lede{color:var(--ink-2)}` - a LIGHTER ink, on
   a photograph, measuring 3.92:1 at 1280 where 17px body text needs 4.5:1. Both halves of the
   bottom line are --ink here, which also makes them match - the point of the row below. */
/* ⭐ ROYAL IS BAKED (owner 2026-09-05: "for home hero bottom line, leave it royal").
   ⚠ THIS LINE SITS ON A PHOTOGRAPH, so the ink is not a free choice — it was re-measured on
   composited pixels after this change, not assumed. See the run note in the commit. */
.hero-sub .lede,
.hero-sub .eyebrow{color:#1A23BA}

/* ═══════ ?entete= — THE TREATMENT-PAGE HEADLINE (owner 2026-09-04) ═══════
   *"I want those treatment page headers to be editable for the color"* — pointing at the H1 he
   highlighted in his screenshot, the one that reads « Laser hair removal, virtually painless. »
   on /epilation-laser-montreal and its eleven siblings.
   ⚠ `.thero-copy .display` IS THE TREATMENT PAGES' H1 AND NOT THE HOME PAGE'S. Home is
   `.hero-in .display`, which is decided blue and has no row any more. The two never overlap, so
   this row cannot reach back into the hero the owner just settled.
   ⚠ The ads landing page (`.ads-hero .display`) is deliberately NOT included: it is a bought
   landing page with its own hero rules declared after these, and quietly recolouring an ad's
   headline from a row labelled "treatment pages" is the kind of surprise this file avoids.
   ⛔ NO RULE FOR `encre` — same reason as above; --ink is already what .display computes to. */
/* ⭐ ROYAL IS THE DEFAULT (owner 2026-09-05: "for treatment page headline, take out the ink
   option"). Royal is now the attribute-less state and so has NO rule; gold is the one alternate
   left in the row. */
.thero-copy .display{color:#1A23BA}
html[data-entete="or"] .thero-copy .display{color:var(--gold-opt)}

/* ---- THE NIGHT BANDS ARE PUT BACK ----
   ⚠ This is the trap the rows above walk into, and it is invisible in the source. Royal #1A23BA
   on --night #141B3D measures 1.13:1 — text that is simply gone. `.ground-night .title` is
   (0,1,1) and `html[data-titres="royal"] .title` is (0,2,1), so the row's rule was winning on
   every dark band. These restore the band's own colours at (0,3,1)/(0,2,2), which beats the row
   and loses to nothing.
   ⚠ `color:inherit` for .link, NOT var(--paper): a .link inside a night <p> is #D7D9E6, and
   forcing paper would silently brighten it. inherit reproduces whatever the band already gives it.
   ⚠ The attribute selector is bare — html[data-titres] with no value — so one rule covers royal
   and or, and the attribute-less default never reaches it at all. */
/* ⚠ REWRITTEN 2026-09-05 WHEN THE ROWS WERE BAKED. These used to hang off html[data-titres] /
   html[data-liens]; those attributes are gone, so the rules would never have fired again and the
   dark bands would have shipped royal-on-night (1.13:1) and gold-on-night links.
   `.ground-night .title` / `h3` / `.benefits .b-t` are already restored in the base blocks above.
   The LINK is not, so it is restored here — and the `html` prefix is load-bearing: it makes this
   (0,2,1), which beats `html[data-liens="royal"] .link` (0,2,1) on source order. Drop the `html`
   and royal wins on the night bands. */
html .ground-night .link{color:inherit}

/* ═══════ ?accueil= — THE HOME HEADLINE'S OWN FACE ═══════
   ⭐ OWNER 2026-09-04: *"for that headline home page text 'Laser Hair removal, Virtually painless'
   make it EB garamond … Give me 4 other new options next to EB garamond."*

   EB GARAMOND IS NOW THE PIN, NOT A ROW VALUE. body[data-page="home"] (top of this file) declares
   --serif:'EB Garamond' directly, exactly as it declared Bodoni before — so EB Garamond is the
   attribute-less state, gets NO rule here, and Reset (which removes the attribute) lands on it.
   ⛔ A RULE FOR THE DEFAULT IS HOW RESET ENDS UP LOOKING BROKEN. That is why there are four rules
   below for five options.

   🗑 GONE IN THE SAME EDIT: Bodoni · Newsreader · Instrument Serif · Libre Caslon Text · Spectral.
   "4 other NEW options" is a replacement, not an addition. Their .woff2 files were deleted and
   their entries came out of build/fonts.mjs, which is the whole truth for that folder. Bodoni Moda
   SURVIVES in the font list because the ?font= row still offers it for every other page.

   🗑 AND EVERY --plate-t / --plate-b / --plate-lh WENT WITH THEM. Those three numbers per face
   existed only to fit the ?h=blanc ivory plate around each font's ink, and blue-with-no-plate is
   now decided, so the plate block above this one is deleted. The four faces below therefore need
   no per-face measurement at all — which is also why adding a fifth is now cheap.

   ⚠ EACH RULE TARGETS BOTH `html[data-accueil=…]` AND `body[data-page="home"]`, and that pairing
   is the whole reason these work. --serif is declared on <html> by the ?font= rules and INHERITED
   by <body> — but body[data-page="home"] declares it ON <body>, and a value declared on an element
   always beats one it inherited, no matter what the selector specificity is. A rule written as
   `html[data-accueil="x"]{--serif:…}` would therefore compute correctly on every page except the
   one it is for. Naming <body> in the selector puts the new value on the same element as the pin
   and lets specificity settle it (0,2,1 beats 0,1,1).

   ⭐ ALL FIVE ARE VARIABLE FAMILIES WITH A REAL ITALIC, and both halves of that matter:
     · variable → one file covers 400..700, so `font-weight:500` is a real weight, never a
       synthesised fake bold. That is why every rule below can say 500 and none has to say 400,
       unlike the static Instrument Serif and Libre Caslon Text they replace.
     · real italic → the headline sets its second half in <em>; a face without one gets a slanted
       fake. It is what rules out Marcellus, Prata, Italiana, Gilda Display and Abril Fatface.
   ⚠ `.display em` / `.title em` therefore need NO separate naming here — that was only required
   for the two 400-only faces, and both are gone.
   ⚠ Only EB Garamond is preloaded on the home page (build.mjs `dispFont`). The four alternates
   load normally, so the first switch to one in a session shows a one-frame flash of the fallback
   serif. That is a reviewer's cost; a visitor never leaves the default. */
html[data-accueil="crimsonpro"] body[data-page="home"]{--serif:'Crimson Pro',Georgia,serif}
html[data-accueil] body[data-page="home"] .display,
html[data-accueil] body[data-page="home"] .title,
html[data-accueil] body[data-page="home"] h3,
html[data-accueil] body[data-page="home"] .tlist-name{font-weight:500}

/* ================= THE AZUR HELLO CARD (owner 2026-09-01) =================
   A small ivory card above the launcher, once per session, gone in 20 s or on ×.

   ⚠ IT LIVES AT THE END OF THE FILE ON PURPOSE. Its offsets are derived from .cart-pill's and
   .chat-fab's own phone rules, which are declared ~900 lines above; a rule written up beside
   them would be overridden by nothing, but a rule written up there and later re-tuned would be
   — that is the .cart-pill lesson this file records twice. Everything the card owns is here.

   ⛔ `.azur-hi[hidden]{display:none}` IS REQUIRED, not tidying. `display:flex` below is an
   author rule and outranks the UA's `[hidden]{display:none}` — without this line the card
   paints on first load of every page, which is precisely the flash the markup's `hidden`
   attribute exists to prevent. The pill and the sheets each carry the identical guard.

   ⛔ The two `body` guards are the ONLY thing standing between this card and the entry gate /
   an open sheet. site.js also refuses to show it in those states; the CSS is the guard that
   still holds if a state changes while the card is already up. */
.azur-hi{position:fixed;right:16px;bottom:82px;z-index:79;display:flex;align-items:flex-start;gap:6px;
  max-width:min(320px,calc(100vw - 32px));padding:12px 6px 12px 16px;
  background:var(--surface);border:1px solid var(--line-strong);border-top:2px solid var(--royal);
  border-radius:2px;box-shadow:0 18px 40px -18px rgba(20,27,61,.45)}
.azur-hi[hidden]{display:none}
body.is-gated .azur-hi,body.sheet-open .azur-hi{display:none}
.azur-hi-t{text-align:left;font:400 14.5px/1.45 var(--sans);color:var(--ink);padding:5px 0}
.azur-hi-t:hover{text-decoration:underline;text-underline-offset:3px;text-decoration-color:var(--royal)}
/* 44×44 exactly — WCAG 2.5.5 enhanced, and the smallest thing on this page a thumb has to hit */
.azur-hi-x{flex:none;width:44px;height:44px;display:grid;place-items:center;font-size:19px;line-height:1;color:var(--muted)}
.azur-hi-x:hover{color:var(--ink)}
.azur-hi-x:focus-visible,.azur-hi-t:focus-visible{outline:2px solid var(--royal);outline-offset:2px}
/* ⚠ THE LIFT IS DRIVEN BY body.cart-has, SET ON THE SAME STATEMENT THAT UNHIDES THE PILL
   (site.js cartRender). A separate check of the cart's own storage would be a second source of
   truth for "is the pill on screen", and the two would eventually disagree. */
@media (max-width:600px){
  /* launcher: bottom 8 + 56 tall = 64 off the floor, + 10 of air = 74 */
  /* ⚠ NO max-width OVERRIDE HERE. It carried `calc(100vw - 16px)` for one draft, which is
     359px of a 375px screen — at that width the card stops reading as a card and starts
     reading as a banner the layout could not fit, which is the same mistake the pill's own
     « was » figure was removed for. The base `min(320px, …)` is the right shape here. */
  .azur-hi{right:8px;bottom:74px}
  /* pill: bottom 66 + 48 tall = 114 off the floor, + 8 of air = 122 */
  body.cart-has .azur-hi{bottom:122px}
}
/* the 601–959 band lifts the pill to bottom:82 (see the cart block above), so the card has to
   follow it: 82 + 48 tall = 130 off the floor, + 8 of air = 138. */
@media (min-width:601px) and (max-width:959.98px){ body.cart-has .azur-hi{bottom:138px} }

/* ══════════ THE EDUCATION SECTIONS: HEADINGS USE WIDTH, NOT HEIGHT (owner 2026-09-02) ══════════
   *"Huge two-line headings with empty width beside them, paragraphs floated far apart, and on the
   longer pages a wall of text with a blank column."*

   MEASURED BEFORE THE CHANGE, at 1440 (build/…/measure.mjs, 12 treatment pages):
     `.sec-story .title` — 63.36px type in a 665px box (`max-width:22ch`) inside a 1280px parent.
     615px of the row sat empty. 39 of the 52 education headings wrapped to two lines (132px) and
     four to three lines (198px), so eight words were costing a quarter of a screen of height
     before a single sentence of the explanation began.

   THE FIX IS HORIZONTAL, NOT A CUT. Nothing is removed: the heading moves into its own column and
   the text moves beside it, so the same words occupy width that was already paid for. Every
   sentence of the education is still on the page — that is checked mechanically, by diffing the
   extracted text of all twelve pages before and after.

   ⚠ THIS BLOCK IS LAST IN THE FILE AND HAS TO BE. `.sec-story .title{max-width:22ch}` (~L925),
   `.story-p` (~L928), `.story-grid` (~L926) and the shared responsive block (~L1624-1656) are all
   declared above; a @media query adds no specificity, so anything written earlier would beat these
   rules on source order and silently do nothing. Same lesson this file already records for
   `.cart-pill`, twice. Nothing about `.sec-story` goes below here. */

/* ---- 1. THE VERTICAL RHYTHM, TIGHTENED — every value is scoped to `.sec-story` ----
   ⛔ `--sec` IS NOT EDITED AT THE ROOT. `:root{--sec:clamp(72px,9vw,136px)}` feeds `.sec{padding:
   var(--sec) 0}` for EVERY section on the site — home, concerns, Tarifs, the clinic page. Editing
   it there would move all of them. Redeclaring the custom property on `.sec-story` re-resolves
   var(--sec) for the education sections alone, because a custom property is resolved against the
   element it lands on. One line, no new selector, no leak.
   ⚠ 72px IS THE FLOOR ON PURPOSE, so the phone is bit-identical: at 375px the old clamp already
   floored at 72px and so does this one. Only 800px and up actually tighten. */
.sec-story{--sec:clamp(72px,6.4vw,92px)}            /* 1440: 129.6px → 92px  ·  375: 72px → 72px */
/* ⭐⭐ THE PARAGRAPH GAP WAS NEVER `.story-p`'s MARGIN — IT WAS A GRID ROW-GAP, AND THAT IS THE
   WHOLE FINDING. Measured on the untouched build at 1440: two consecutive paragraphs inside a
   one-column education block stood **87-88px apart = 3.20-3.44 × the 27.225px line**, while
   `.story-p{margin-bottom}` said 16px. Both numbers were true.
   Cause: `.story-grid-1` sets only `grid-template-columns`, so it inherits `.story-grid`'s
   `gap:clamp(32px,5vw,80px)` as a ROW-gap — and in those blocks the `<p>`s are DIRECT GRID ITEMS
   (`<div class="story-grid story-grid-1">${paras}</div>`, no wrapper). Grid applies the row-gap
   BETWEEN them and never collapses their margins, so the real gap was 72 + 16.
   ⛔ This is why tuning `margin-bottom` alone barely moved the page, and why "tighten the gap
   between paragraphs" and "land inside the measured range" looked like they contradicted each
   other. They never did: at 3.2 lines the site was ~4× LOOSER than every reference (0.80-1.11),
   not tighter. Zeroing the row-gap hands the rhythm back to the paragraph's own margin — which is
   how all six references do it — and then both instructions point the same way.
   ⚠ SCOPED to `.sec-story` so nothing else that reuses `.story-grid-1` changes.
   ⚠ NO `:last-child{margin-bottom:0}` HERE, deliberately: the last paragraph's 22px plus a
   device's own 6px margin-top is what produces the 28px (1.03-line) seam before a card list,
   a step list or a timeline. Zeroing it would collapse that seam to 6px. */
.sec-story .story-p{margin-bottom:22px}             /* 22 ÷ 27.225 = 0.808 × the line — inside the
                                                       0.80-0.83 prose band measured on the three
                                                       references. Real gap 87-88px → 22px, -75%. */
.sec-story .story-items{margin-top:6px}             /* 8px → 6px */
/* ⭐ « Is this for you? » READS AS A LIST, NOT AS A PARAGRAPH GRID (owner 2026-09-04: "it's good
   right now, but it just doesn't stand out as text… maybe more space in-between the sentences").
   Three changes, all on the same list: more air per row (9 → 15px, so the rule under each line
   belongs to that line instead of floating between two), the body ink instead of the muted grey so
   a sentence someone is supposed to recognise themselves in is not set like a caption, and a
   slightly larger size. The hairline stays — it is what makes the six read as six. */
.sec-story .story-items li{padding:14px 0;font-size:15.5px;color:var(--ink);line-height:1.5}
/* ⭐ « Is this for you? » IS ONE COLUMN, SO EVERY LINE IS ONE LINE (owner 2026-09-04: "it just
   looks ugly now… Put everything in 1 line").
   Measured at 1440: the longest item needs 575px and the two-column track is 547px, so at 16.5px
   it wrapped — that was my regression. But going back to 15.5px only bought 7px of spare, and
   BELOW 1280 the track drops to 473px and the line wrapped even at the old size. A layout that
   holds by 7px at one width and fails at the next is not fixed, it is lucky.
   One full-width column gives every item 1152px against a 575px longest line: no item can wrap at
   any width this rule applies to. It costs three rows of height (~135px) and buys six clean lines.
   ⚠ The size goes back to 15.5px too. The "make it stand out" from the previous round now comes
   entirely from things that do not change width — the body ink instead of the muted grey, and the
   roomier row padding. Type size was the one lever that could break the line, so it is not used. */
.sec-story .story-items-2{grid-template-columns:minmax(0,1fr)}
.sec-story .cards{margin-top:6px}                   /* 8px → 6px */
.sec-story .cards li{padding:14px 0 16px}           /* 20/22 → 14/16 */
.sec-story .punch{margin:6px 0}                     /* 8px → 6px */
.sec-story .steps{padding-top:22px}                 /* 32px → 22px */
.sec-story .timeline{padding-top:16px;margin-top:8px}   /* 22/12 → 16/8 */
.sec-story .story-photos{margin-top:28px}           /* 40px → 28px */
.sec-story .story-grid{gap:clamp(28px,3.6vw,52px)}  /* 1440: 72px → 51.8px */
/* ⛔ AND IT HAS TO SIT AFTER THAT LINE, NOT BEFORE IT. `gap` is a SHORTHAND — it writes both
   row-gap and column-gap — so the rule above resets any row-gap declared earlier, even from a
   selector with higher specificity, because it is a later declaration of the same property on the
   same element. Written above it this line read as correct CSS, computed to 51.84px, and changed
   nothing. That is the third time this file has recorded the same lesson (see .cart-pill, twice). */
.sec-story .story-grid-1{row-gap:0}

/* ---- 2. THE TWO-COLUMN LAYOUT — >=960px only. Phone and tablet stack, exactly as before ---- */
@media (min-width:960px){
  /* 4/12 and 8/12 of the content column, as the brief asks. At 1440 the content column is 1152px,
     so with a 64px gutter the heading gets ~363px and the text ~725px. */
  .sec-story .story-split{display:grid;grid-template-columns:minmax(0,4fr) minmax(0,8fr);
    gap:0 clamp(36px,4.4vw,64px);align-items:start}
  .sec-story .story-head{min-width:0}
  .sec-story .story-body{min-width:0}
  /* the 22ch cap is what forced two lines; in its own column the heading may use the whole column
     and the margin under it is gone, because there is nothing under it any more. */
  .sec-story .title{max-width:none;margin-bottom:0}
  .sec-story .story-head .eyebrow{margin-bottom:12px}
  /* the text column is wider than 62ch, so the measure is raised a little rather than leaving the
     paragraphs short of their own column. It stays inside the comfortable range for body text. */
  .sec-story .story-p{max-width:66ch}
}

/* ---- 2b. WHEN THE HEADING COLUMN CARRIES A SECOND ELEMENT, THE PROSE STARTS LEVEL WITH IT ----
   Owner, 2026-09-03, on the microneedling « pourquoi une cure » block: *"the text … could be put
   lower to made the design look better placement-wise."*
   What he was looking at: heading + pull-quote filled the left column to ~480px while two short
   paragraphs sat at the very top of the right one, so the prose ended a third of the way down and
   the rest of the row was a hole beside a quote.
   ⭐ THE FIX IS A ROW, NOT A NUDGE. `display:contents` dissolves `.story-head` so its three children
   become grid items of the split itself; the eyebrow and the heading take rows 1 and 2 of column 1,
   and the pull-quote (or the education photo) and the BODY share row 3. The prose therefore begins
   exactly where the quote begins — no magic number, and it stays correct when the heading wraps to
   a different number of lines in the other language.
   ⚠ STAMPED BY THE BUILD (`story-split-x`), because CSS cannot ask "does the heading column have a
   second child". A block whose heading stands alone keeps the old top-alignment and is untouched.
   ⚠ THIS IS THE SAME `display:contents` TRICK `?mise=dessus` USES BELOW, and that block must keep
   winning — it is declared later and more specifically, and both were re-measured together.
   ⛔ It is not a contradiction of the top-alignment rule (design-rulebook Part 1 §4): the prose is
   still flush with the top of the thing it SHARES A ROW WITH. When a column holds two stacked
   elements, the row partner is the one at the same level, not the topmost one in the column. */
@media (min-width:960px){
  /* ---- 2c. THE HEADING TAKES THE WHOLE ROW, SO IT NEVER WRAPS (owner 2026-09-03) ----
     *"Where you can, make the titles longer. i don't [want] them on two lines. It makes things
     look unprofessional."*
     Measured, all 52 education headings at 1440: in a 363px column, 48 of them wrapped — to two
     lines mostly, to three or four on /microneedling. What each needs on ONE line ranges from
     387px to 949px, and the widest of all is « Ce que le microneedling atténue, et ce qu'il
     n'efface pas » at 941px (its English twin, 949px).
     ⭐ THE FULL ROW IS 1152px, so spanning both columns fits EVERY ONE with 203px to spare. There
     is no partial answer here: widening the column to 5.5/12 gives 499px and still leaves nine
     wrapping, and shrinking the type to fit 949px into 363px means 17px — a caption, not a
     heading. The heading needed the width, so it takes the width.
     ⛔ THE SIZE STAYS AT 3.2vw (44.8px) AND MUST NOT GO BACK UP. Round 2 stepped it down from
     63.36px for the narrow column, and it is tempting to restore it now the column is gone — but
     63.36px scales that widest heading to 1342px, which is 190px MORE than the row has, and the
     wrapping starts again. The step-down is now load-bearing for a different reason than it was
     written for.
     ⚠ Rows 1 and 2 of column 2 are empty by construction, which is what makes the span free: the
     body already starts on row 3 (see 2b). A block with no pull-quote and no photo has nothing in
     column 1 of row 3, so its body takes the whole row instead — `.story-p{max-width:66ch}` keeps
     the measure right, exactly as it does in the `?mise=dessus` variant. */
  .sec-story .story-split .story-head{display:contents}
  .sec-story .story-split .story-head .eyebrow{grid-area:1/1/2/2}
  .sec-story .story-split .story-head .title{grid-area:2/1/3/-1}
  .sec-story .story-split:not(.story-split-x):not(.story-split-lead) .story-body{grid-area:3/1/4/-1}
  /* ⭐ A PULL-QUOTE OVER A LONG BODY SPANS THE ROW instead of standing in a column beside it —
     see the 420-character rule in build.mjs. It takes row 3 full width, the body drops to row 4,
     and the 303px hole beside it (72 % of the row on 6 pages) goes to zero. The quote keeps its
     brass rule and its italic; only its measure changes, so it is the same device, wider. */
  /* ⛔ THE `:not()` ABOVE HAD TO GAIN `:not(.story-split-lead)`, AND THAT IS WHY THE QUOTE PRINTED
     ON TOP OF THE PROSE. `:not(.story-split-x)` carries the specificity of its own argument, so the
     generic body rule scored (0,4,1) against this block's (0,3,1) and won — the body was placed in
     row 3 with the pull-quote instead of row 4 under it, and the two painted over each other.
     Nothing overflowed and no checker moved; it was visible only by looking. Second time this file
     has been bitten by two rules on the same property (see the `gap`/`row-gap` note above): when a
     later rule "does not take", read the SPECIFICITY before doubting the value. */
  .sec-story .story-split-lead .story-head .punch-wide{grid-area:3/1/4/-1;max-width:52ch;margin-top:22px}
  .sec-story .story-split-lead .story-body{grid-area:4/1/5/-1;margin-top:26px}
  /* ⭐ 6/6 WHEN THE LEFT COLUMN IS A STACK OF CARDS, NOT A PHOTO OR A QUOTE. Measured on the
     microneedling aftercare block at 1440, sweeping the ratio:
         4/8 (the default) → the cards run 555px and the prose 207px: a 348px VOID beside them
         6/6              → 459 vs 262: void 197px, and the prose sits at ~66 characters a line
         7/5              → void 143px, but the prose drops to ~55 characters
         8/4              → void 64px, prose ~44 characters
     6/6 is the pick: it removes 43 % of the void and lands the measure inside the 65–75 character
     band the rulebook asks for. 7/5 closes more of the hole but buys it with a measure the owner
     has already rejected once as too narrow (2026-08-26), so the emptier column is the cheaper of
     the two costs. Four stacked cards will always out-run two paragraphs; this makes it small, it
     does not pretend to make it zero. */
  .sec-story .story-split-even{grid-template-columns:minmax(0,6fr) minmax(0,6fr)}
  /* ⚠ AND THE HEADING GETS ITS BOTTOM MARGIN BACK IN THIS CASE ONLY. `.sec-story .title` is set to
     `margin-bottom:0` at >=960 with the note "there is nothing under it any more" — which was true
     while the heading sat alone in its own column. Now that it spans the row, a block with no
     pull-quote and no photo has its PROSE directly underneath, and measured, the gap was 0px: the
     first line of body text was welded to the heading. 22px is the same step the pull-quote and the
     education photo already take, so all three kinds of block open on one rhythm — measured 22 / 22
     / 22 afterwards.
     ⛔ NOT on `.story-split-x`: there the next row already carries `margin-top:22px`, and margins do
     not collapse across grid rows, so adding this would make that gap 44px. */
  .sec-story .story-split:not(.story-split-x) .story-head .title{margin-bottom:22px}
  .sec-story .story-split-x .story-head .punch,
  .sec-story .story-split-x .story-head .cards-stack,
  .sec-story .story-split-x .story-head .story-fig{grid-area:3/1/4/2}
  /* ⚠ THE SAME 22px THE QUOTE AND THE PHOTO CARRY. Without it the body sat 22px ABOVE its row
     partner — measured, on every one of the eleven blocks — because `.story-head .punch` and
     `.story-head .story-fig` each have `margin-top:22px` inside the row and the body had none.
     "Level with" has to be measured, not assumed from sharing a grid row. */
  .sec-story .story-split-x .story-body{grid-area:3/2/4/3;margin-top:22px}
}

/* ---- 3. ?mise=dessus — THE STACKED ALTERNATIVE, for comparison only ----
   The old arrangement, with the two things that made it expensive removed: the 22ch cap is gone
   (so a heading uses the width it has instead of wrapping at eight words) and the type is one step
   smaller. ⭐ There is deliberately NO rule for the default value: `cote` is the attribute-less
   state, so Reset lands on the side-by-side layout and the default can never drift into a rule. */
@media (min-width:960px){
  /* ⚠ `display:block` WAS WRONG AND A SCREENSHOT CAUGHT IT. The photo now lives in `.story-head`,
     so simply un-gridding the split stacked eyebrow → heading → PHOTO → text and left the whole
     right half of the row empty — a worse layout than the one this option is supposed to preserve
     for comparison. The old arrangement put the photo BESIDE the paragraphs (`.story-grid`, 7fr/5fr).
     `display:contents` on the head dissolves the wrapper so its three children become grid items of
     the split itself, and the four areas are then placed by hand: eyebrow and heading run the full
     width, the text takes the left column, the photo the right. Same DOM, both layouts. */
  html[data-mise="dessus"] .sec-story .story-split{display:grid;
    grid-template-columns:minmax(0,7fr) minmax(0,5fr);gap:0 clamp(28px,3.6vw,52px);align-items:start}
  html[data-mise="dessus"] .sec-story .story-head{display:contents}
  html[data-mise="dessus"] .sec-story .story-head .eyebrow{grid-area:1/1/2/-1;margin-bottom:14px}
  html[data-mise="dessus"] .sec-story .story-head .title{grid-area:2/1/3/-1;
    max-width:none;margin-bottom:20px;font-size:clamp(2rem,3.6vw,3.4rem)}   /* 1440: 63.36px → 51.84px */
  /* ⚠ `margin-top:0` UNDOES THE 22px `.story-split-x` GIVES THE BODY. That rule exists to level the
     body with a pull-quote SITTING BESIDE IT; in this variant the body is under the heading and the
     figure is beside the body, so the same 22px just pushed the prose 22px below its own figure —
     measured at 22 on all eleven blocks, 0 before. A rule written for one layout has to be undone
     in the other; it does not stop applying just because the areas changed. */
  html[data-mise="dessus"] .sec-story .story-body{grid-area:3/1/4/2;min-width:0;margin-top:0}
  html[data-mise="dessus"] .sec-story .story-head .story-fig{grid-area:3/2/4/3;margin-top:0}
  /* ⚠ EVERY CHILD OF `.story-head` MUST BE PLACED HERE, because `display:contents` dissolves the
     wrapper and anything without a grid-area is AUTO-PLACED. Measured before this line existed:
     the pull-quote landed at x=837.6 on /microneedling?mise=dessus — in the right column beside
     the body text — and on a page whose figure already owns 3/2/4/3 it fell into row 4, BELOW the
     body. No overflow, no error, just a scrambled layout that only a screenshot catches.
     It takes the same cell the figure takes: in this variant the quote goes back beside the
     prose, which is where it sat before 2026-09-03 and what this variant exists to preserve. */
  html[data-mise="dessus"] .sec-story .story-head .punch{grid-area:3/2/4/3;margin-top:0}
  html[data-mise="dessus"] .sec-story .story-head .story-fig img{aspect-ratio:4/5}
  html[data-mise="dessus"] .sec-story .story-p{max-width:62ch}
  /* the body column is 7/12 here, not 8/12, so the four-up grids stay two-up for the same reason */
  /* same as above: the timeline is no longer a child of .story-body in either layout */
  html[data-mise="dessus"] .sec-story .story-body .cards-4{grid-template-columns:repeat(2,minmax(0,1fr))}
}

/* ---- 4. the education photo, now a child of the heading column ---- */
.sec-story .story-head .story-fig{margin-top:22px}
/* ⭐ A CARD LIST CAN RIDE THERE TOO, STOOD UP (owner 2026-09-03, the microneedling aftercare).
   One column, not the 2×2 `.cards-4` used to be: he asked for them "vertically … and then the
   paragraph on the right", and stacked is also what makes the four steps read as one sequence of
   times — 24 hours, 5 days, 2 weeks, ongoing — instead of a grid the eye has to order itself. */
.cards-stack{grid-template-columns:minmax(0,1fr)}
.sec-story .story-head .cards-stack{margin-top:22px}
/* ---- 4b. AND SO DOES THE PULL-QUOTE (owner 2026-09-03) ----
   `.sec-story .story-head .title` sets `margin-bottom:0` ("there is nothing under it any more"),
   and `.sec-story .punch` sets `margin:6px 0`. Measured, a punch dropped into the heading column
   therefore sat 6px under the heading where the figure sits 22px under it — close enough to read
   as a stuck element rather than a placed one. Same rule for both children.
   ⚠ THE 22px IS THE >=960 NUMBER. Below that the title keeps `margin-bottom:28px` and margins
   collapse to max(28,22) = 28px — measured at 375 and 959. The education figure lands on 28px
   there too, so "both children of the heading column get the same gap" holds at every width; only
   the number changes.
   ⚠ At 1440 the 30ch cap (381.4px) EXCEEDS the 4/12 column (362.9px), so it stops binding and the
   quote fills its column — measured, not hoped: docOverflow 0.0 at 1440, 1280 and 960.
   ⚠ BUT THAT IS ONLY TRUE ABOVE ~1170. Measured `.story-head .punch` width: 959 → 317.8 ·
   960 → 273.9 · 1024 → 292.2 · 1100 → 313.9 · 1170 → 333.8 · 1440 → 362.9. Between 960 and ~1170
   the binding constraint is the COLUMN, not the cap, and `max-width:none` does nothing there.
   It changes no line count for today's strings; it is written so the intent is on the page and
   nobody re-derives it from a number that happens to be larger this month. */
.sec-story .story-head .punch{margin-top:22px}
@media (min-width:960px){ .sec-story .story-head .punch{max-width:none} }
/* ⚠ the >=960 crop is set once, further down, in the block that also narrows the heading —
   the two were derived from the same measurement and drift apart if they live in two places. */

/* ---- 5. THE DEVICES INSIDE THE NARROWER BODY COLUMN ----
   Measured after the two-column layout landed: the body column is 726px at 1440, and the grids
   inside it were still asking for four columns. `.timeline` was giving each of its four entries
   151px and `.cards-4` 149px — narrower than the words in them. A four-column grid is a decision
   about a 1152px row; inside a 726px column the same decision is a defect. Both become 2×2, which
   at 726px is 339px a cell. Only inside `.story-body`, so /epilation-laser's own full-width
   `.cards-4` elsewhere on the site is untouched. */
@media (min-width:960px){
  /* ⚠ THE `.timeline` RULE THAT USED TO SIT HERE IS GONE (2026-09-03). The timeline left
     `.story-body` for the full-width `.seq-band` when the chronological row shipped, so a rule
     scoped to `.story-body .timeline` matched nothing — dead CSS that still read like a live
     decision. `.cards-4` DID stay in the body column and still needs this. */
  .sec-story .story-body .cards-4{grid-template-columns:repeat(2,minmax(0,1fr))}
  /* ⭐ THE HEADING COMES DOWN ONE STEP IN ITS OWN COLUMN — and this is what makes the layout pay.
     63.36px in a 363px column wraps « Ce que le microneedling atténue, et ce qu'il n'efface pas »
     to FIVE lines (360px), taller than the 227px of text beside it: the section then measures the
     heading again, which is the exact failure this round exists to end. At 3.4vw (49px at 1440)
     the same heading is three lines and the text drives the height. It is still a display
     heading — 49px against 16.5px body — just no longer the tallest thing on the row. */
  .sec-story .story-head .title{font-size:clamp(2rem,3.2vw,2.8rem)}
  /* ⚠ A PORTRAIT CROP OUT-RUNS EVERY HEADING BESIDE IT. At 4/5 the photo stood 454px in a 363px
     column and the about section became photo-driven (head 679px vs body 584px). 5/4 is 290px and
     the two columns land within 20px of each other. */
  /* ⭐ 2/3, NOT 5/4 (owner 2026-09-04: "'What is laser hair removal' feels awkward due to the empty
     space on the bottom left"). Measured across the ten « Qu'est-ce que X ? » blocks at 1440: a
     290px photo beside 548px of prose left a 265px void under it — 47 % of the row. A taller crop
     fills the column the prose sets: void 265.5 → 11.5px (−96 %) at 1440, 263.6 → 8 at 1280.
     ⛔ THE OLD 5/4 WAS WRITTEN FOR A DIFFERENT ROW and could not serve both. It was chosen on
     2026-09-02 when the photo stood beside a HEADING (4/5 portrait made the head column 454px in a
     363px slot); it now stands beside PROSE, which is taller, so the same rule under-fills.
     ⚠ It is safe to make it tall only because the one block where a tall photo made things WORSE —
     microneedling's « three stages », where a 290px photo faced 54px of prose — no longer has a
     photo at all. Injecting 2/3 there measured 235.9 → 489.9px, i.e. 90 % void. If that photo ever
     comes back, this rule must be stamped per block, not shared. */
  .sec-story .story-head .story-fig img{aspect-ratio:2/3}
}
/* ---- 6. the long callout ----
   `.punch` is set for a one-sentence aphorism: 1.8rem italic serif at 30ch. The pigmented-lesion
   rule on /thermocoagulation is two sentences and 330 characters — at 30ch it stood eleven lines
   tall and stopped reading as a callout. Same materials (italic serif, the brass rule down the
   left), a smaller size and a longer measure. */
.punch-rule{font-size:clamp(1.15rem,1.35vw,1.4rem);line-height:1.45;max-width:48ch}

/* ══════════════ THE CHRONOLOGICAL ROW — LEFT TO RIGHT, WITH ARROWS ══════════════
   Owner, 2026-09-03: *"i would prefer things to be left to right in chronological with arrows."*
   Applies to every numbered or dated run on the site: `.steps` (01/02/03 — the home page, the
   clinic, Tarifs, the ads page and both education `steps-n` runs) and `.timeline` (Séance 1 ·
   4 à 6 semaines · …). All seven kinds are genuine sequences, checked one by one before an arrow —
   which asserts "then" — was put between them.

   ⛔ WHAT WAS ACTUALLY WRONG, from his screenshot: `.steps` was `repeat(3,1fr)`, so a FOUR-step run
   put « 04 Soins après » on a second row underneath « 01 », and the five-step laser run broke 3+2.
   A sequence that wraps has stopped being a sequence — the eye reads a grid, not an order.

   ⭐ ONE ROW IS BOUGHT BY LEAVING THE 8/12 COLUMN, NOT BY SHRINKING THE TYPE. build.mjs now prints
   the education runs in their own full-width `.seq-band` after `.story-split`. Measured per step
   at 1440:
       inside .story-body (726px)   4 steps → 160px   5 steps → 123px    ← unusable
       full width      (1152px)     4 steps → 267px   5 steps → 208px    ← what ships
   The prose above keeps its whole column; nothing was made narrower to make this fit.

   ⚠ THE ARROW IS DECORATION AND IS NOT ANNOUNCED — it is `content` on a pseudo-element. The order
   is already carried by `<ol>` and by the printed 01/02/03, so nothing is lost. It sits on the
   number's own line vertically and in the middle of the gutter horizontally, so it can never
   collide with a heading however long that heading wraps. */
/* ⭐ STACKED IS THE DEFAULT; THE ROW IS OPTED INTO PER COUNT, AT ITS OWN MEASURED WIDTH.
   A single `min-width:900px` was written first and it was wrong for the long runs: at 900 the page
   column is 810px, so five steps got 133px each — the very width this whole change exists to avoid,
   and narrower than the 160px the old wrapping layout gave them. A breakpoint that is right for
   three columns is not right for five.
   Measured column width per step, at each count's threshold:
       seq-2 / seq-3  →  900px viewport → 246px   ✅
       seq-4          → 1100px viewport → 214px   ✅
       seq-5          → 1240px viewport → 183px   ✅
   Below its own threshold a run STACKS and the arrow turns down. It never goes 2-up: two columns
   of a four-step sequence is exactly the "04 sits under 01" defect that started this. */
.seq{display:grid;grid-auto-flow:row;grid-template-columns:minmax(0,1fr)}
.seq li{position:relative}
/* ⚠ THE ARROW SITS IN THE GRID'S OWN GAP — IT ADDS NO PADDING, AND THE FIRST VERSION DID BOTH AND
   LANDED ON THE TEXT. It gave the item a `padding-bottom` AND then translated the glyph back UP by
   the same amount, which put the arrow's centre exactly on the last line's baseline: on a 375px
   phone the ↓ printed on top of « premier peeling. ». Caught in a screenshot, not by a number —
   nothing overflowed and nothing was clipped, the two things were simply on top of each other.
   `top:100%` is the item's bottom edge; a HALF-GAP push down centres the glyph in the gap that is
   already there. Same arithmetic as the horizontal arrow, on the other axis. */
.seq li:not(:last-child)::after{content:"\2193";position:absolute;left:0;top:100%;
  transform:translateY(calc(var(--seq-gap) / 2)) translateY(-50%);
  font-size:15px;line-height:1.25;color:var(--brass-text);pointer-events:none}
.ground-night .seq li:not(:last-child)::after{color:#CDB577}

/* ⚠ `--seq-gap` MUST MATCH THE GRID'S OWN `gap`, and it is declared beside it for that reason.
   `.steps` and `.timeline` use different clamps; one shared value would put the arrow off-centre
   on one of them. */
.steps.seq{--seq-gap:clamp(28px,4vw,56px)}
.timeline.seq{--seq-gap:clamp(20px,3vw,40px)}
.seq-band{margin-top:clamp(26px,3vw,40px)}

/* the row, per count — same three declarations each time, at three different widths */
@media (min-width:900px){
  .seq-2,.seq-3{grid-auto-flow:column;grid-auto-columns:minmax(0,1fr);grid-template-columns:none}
  .seq-2 li:not(:last-child)::after,.seq-3 li:not(:last-child)::after{content:"\2192";left:100%;top:0;
    transform:translateX(calc(var(--seq-gap) / 2)) translateX(-50%)}
}
@media (min-width:1100px){
  .seq-4{grid-auto-flow:column;grid-auto-columns:minmax(0,1fr);grid-template-columns:none}
  .seq-4 li:not(:last-child)::after{content:"\2192";left:100%;top:0;
    transform:translateX(calc(var(--seq-gap) / 2)) translateX(-50%)}
  /* ⭐ TYPE SIZED TO THE COLUMN, and only once the run IS a row. A 28px display heading is right in
     a 347px column and wrong in a 246px one. Stacked, the step has the full width and keeps the
     full size — which is why these live inside the row query, not outside it. */
  .steps.seq-4 h3{font-size:1.5rem}
  .steps.seq-4 p{font-size:15px;max-width:none}
  .timeline.seq-4 h3{font-size:1.3rem}
}
@media (min-width:1240px){
  .seq-5{grid-auto-flow:column;grid-auto-columns:minmax(0,1fr);grid-template-columns:none}
  .seq-5 li:not(:last-child)::after{content:"\2192";left:100%;top:0;
    transform:translateX(calc(var(--seq-gap) / 2)) translateX(-50%)}
  .steps.seq-5 h3{font-size:1.35rem}
  .steps.seq-5 p{font-size:15px;max-width:none}
}

/* ⭐ EACH STEP GETS ITS OWN TOP RULE IN ROW MODE (owner 2026-09-04: "if there is a way to make the
   'How a course goes step by step' section nicer"). The run hung five columns of very unequal text
   off ONE hairline across the whole top, so the ragged bottoms read as text that failed to line up.
   Giving every step its own rule turns them into five bounded cells — the `.cards` device this site
   already uses — and unequal lengths then read as columns of a table instead of as a mistake. The
   rules also sit on the same line as the arrows, so the top of the run reads as one band.
   ⚠ ROW MODE ONLY, and each count inside its own threshold: stacked on a phone the single top rule
   is right, and per-item rules there would collide with the ↓ arrows sitting in the same gaps. */
@media (min-width:900px){
  .steps.seq-2,.steps.seq-3{border-top:none;padding-top:0}
  .steps.seq-2 > li,.steps.seq-3 > li{border-top:1px solid var(--line);padding-top:20px}
}
@media (min-width:1100px){
  .steps.seq-4{border-top:none;padding-top:0}
  .steps.seq-4 > li{border-top:1px solid var(--line);padding-top:20px}
}
@media (min-width:1240px){
  .steps.seq-5{border-top:none;padding-top:0}
  .steps.seq-5 > li{border-top:1px solid var(--line);padding-top:20px}
}

/* ══════════════ A PARAGRAPH BESIDE SOMETHING TALLER FOLLOWS THE SCROLL ══════════════
   Owner, 2026-09-04: *"in all sections where you see there is a paragraph next to something, make
   it so that when I scroll down, the paragraph scrolls down with the thing next to it. So the
   paragraph can always be read whenever… Make this a rule for all future websites."*

   ⭐ BOTH COLUMNS ARE STICKY, AND THAT IS THE WHOLE TRICK. CSS cannot ask which column is shorter,
   and it does not need to: a sticky element can only travel inside space its own row gives it, so
   the SHORT column follows the scroll and the tall one — which already fills the row — cannot move
   and behaves exactly as before. The rule selects itself, per row, in both directions, and it
   keeps working when the clinic adds a paragraph and flips which side is longer.

   ⛔ AND HERE IS THE HONEST PART, MEASURED BEFORE IT SHIPPED: on THIS site today it changes almost
   nothing. Of 39 two-column rows, only 3 are taller than a 900px viewport — and 8 at 700px, 10 at
   640px — and every single one of them is a `.faq-grid` or `.tlist-grid`, both of which have had
   `position:sticky` since long before this. Not one `.story-split`, `.prices-grid`, `.who-grid`,
   `.cta-grid`, `.azur-grid`, `.ladder-grid`, `.tech-grid` or `.dev-grid` row reaches even 640px.
   **A row shorter than the window has nothing to scroll past, so sticky is a no-op there** — this
   is the same measured non-fix the empty-space audit already recorded when it tried to solve the
   `.prices-grid` hole this way. What ships here is the rule made permanent, armed for the day a
   row does grow, not a change he will see this afternoon.

   ⚠ `top:96px` is the site's own existing value (`.faq-grid .sec-head`, `.tlist-fig`,
   `.concern-fig`): the 84px nav plus 12px of air. One number, not a fourth one.
   ⚠ ≥960px ONLY. Below that the columns stack into one, and a pinned paragraph on a phone would
   sit on top of the content underneath it rather than beside it.
   ⚠ `position:sticky` dies silently under an ancestor with `overflow` other than `visible`, and it
   needs `align-items:start` on the row — with `stretch` the item fills the track and has nowhere
   to travel. Both hold here; the story rows were already `align-items:start`. */
@media (min-width:960px){
  /* ⛔ THE PAIRED ROW ONLY — NOT THE EYEBROW AND NOT THE HEADING. A first pass said
     `.story-split-x > *` and, because `.story-head` is `display:contents`, that made the eyebrow
     and the title grid items too: measured, the heading then travelled 505px down the page,
     detaching from its own eyebrow and heading for the same 96px line the eyebrow was already
     pinned to. A heading that follows you is a different feature, and a worse one. Only the two
     things that actually sit SIDE BY SIDE stick — the body column and whatever shares its row. */
  .sec-story .story-split-x .story-body,
  .sec-story .story-split-x .story-head .punch,
  .sec-story .story-split-x .story-head .story-fig,
  .sec-story .story-split-x .story-head .cards-stack,
  .prices-grid > *, .who-grid > *, .who-grid-rev > *,
  .azur-grid > *, .ladder-grid > *, .tech-grid > *, .dev-grid > *{position:sticky;top:96px}
}

/* ══════════ THE GATE, « ÉPURÉE » (owner 2026-09-02: "make the page nicer, cleaner and easier on
   the eyes — I want to see the best way you'd do it for design and readability") ══════════
   ⭐ SUPERSEDED 2026-09-05 — the owner picked « classic », so the rules that used to carry
   `html[data-entree="classique"]` are unprefixed now and the row is gone. See the note below.
   Base rules = the refined gate. The classic rules below override them, and it puts
   the morning's gate back so the two can be compared on one screen. Every choice below is either
   a rule this site already wrote for itself (DESIGN.md) or a defect measured on the page.
   ⚠ LAST IN THE FILE AND IT HAS TO BE: every `.sp-*` rule it overrides is declared above. */

/* ---- 1. THE LIFT, ON EVERY SCREEN (ruling 5) ----
   Two spacers share the free space 1 : 2, so the column sits at the upper third — the optical
   centre, where a reader expects a page's weight. Basis 0 means they are the first thing to give
   way on a short window: nothing here can push the buttons off the screen. */
.sp-copy::before,.sp-copy::after{content:"";display:block;flex:1 1 0;min-height:0}
.sp-copy::after{flex-grow:2}
.sp-copy>*+*{margin-top:var(--sp-step)}
/* the rhythm is grouped, not uniform: pitch (eyebrow → sub) · proof (facts + tones) · ask
   (buttons). The breaks BETWEEN groups are 1.3 steps; inside a group, one. Five equal gaps read as
   five islands; three groups read as one composition. */
.sp-benefits,.sp-end{margin-top:calc(var(--sp-step) * 1.3)}

/* ---- 2. ONE PRIMARY, ONE LINK (DESIGN.md §2) ----
   The outlined twin of Réserver was a second primary. The way in is now the house link with its
   arrow, sitting beside the one gold button. */
.sp-cta{display:flex;flex-wrap:wrap;align-items:center;gap:12px clamp(18px,2.4vw,32px);max-width:none}
/* the primary keeps a floor width so « Book » (4 letters) has the same presence as « Réserver »
   (8) — a caps word in a gold box shrinks to its letters otherwise, and the EN gate read as a
   small tab beside a long link. The phone block below stretches it full-width regardless. */
.sp-cta .btn{min-width:clamp(180px,15vw,216px)}
.sp-enter{min-height:44px;padding:0;border:0;background:none;cursor:pointer;color:var(--ink);
  font-size:clamp(14px,min(1.1vw,1.85vh),16px);text-decoration-color:var(--brass)}
.sp-enter:hover{color:var(--royal)}
.sp-enter:focus-visible{outline:2px solid var(--royal);outline-offset:4px}

/* ---- 3. FOUR FACTS UNDER HAIRLINES, NOT FOUR TICKS ----
   DESIGN.md §0 names "check-mark bullet lists" as one of the tells being removed from this site,
   and the gate had four of them. A hairline above each fact is the `.cards` device the rest of
   the site already uses: a small table of facts, not a feature checklist. */
.sp-benefits{gap:clamp(6px,1.1vh,12px) clamp(18px,2.4vw,36px)}
.sp-benefits li{padding:clamp(8px,1.2vh,12px) 0 0;border-top:1px solid var(--line-strong)}
.sp-benefits li::before,.sp-benefits li::after{content:none}

/* ---- 4. THE SKIN TONES WITHOUT SIX BADGES ----
   The label already says « sûr sur tous les phototypes »; six gold ticks said it six more times.
   The tones themselves stay — they are the claim and its evidence in one glance. */
.sp-fitz-scale li::before,.sp-fitz-scale li::after{content:none}

/* ---- 5. THE EYEBROW GETS THE HOUSE HAIRLINE (DESIGN.md §2: "a 24px brass hairline to its left") —
   every other eyebrow on the site has it; the gate's did not. */
.sp-eyebrow{display:flex;align-items:center;gap:12px}
/* the gate's own hairline went with the section eyebrows' — see the note at .eyebrow::before */

/* ---- 6. the dialog now takes focus on load (site.js); a full-screen dialog must not draw a ring ---- */
.splash:focus{outline:none}

@media (max-width:900px){
  .sp-cta{flex-direction:column;align-items:stretch}
  .sp-enter{justify-content:center}
  /* ⛔ MEASURED, NOT TASTE: on a phone the four facts stack into FOUR hairlines and four paddings,
     and at 375x667 the refined gate ran 38px taller than the classic one and clipped its last
     line by 10px — the one thing this screen may never do. The hairline already separates each
     fact, so the phone pays less padding and a smaller row gap, and the group breaks soften from
     1.3 to 1.15 steps. Desktop (2×2, two hairlines) keeps the fuller values above. */
  .sp-benefits{gap:clamp(4px,.8vh,8px) 0}
  .sp-benefits li{padding-top:clamp(5px,.8vh,8px)}
  .sp-benefits,.sp-end{margin-top:calc(var(--sp-step) * 1.15)}
}

/* ---- ?entree=classique — the gate exactly as it stood on the morning of 2026-09-02 ---- */
.sp-benefits,.sp-end{margin-top:var(--sp-step)}
.sp-benefits{gap:clamp(8px,1.5vh,17px) clamp(14px,2vw,30px)}
.sp-benefits li{padding:0 0 0 29px;border-top:0}
.sp-benefits li::before,.sp-benefits li::after,
.sp-fitz-scale li::before,.sp-fitz-scale li::after{content:""}
.sp-eyebrow::before{content:none}
.sp-cta{display:grid;grid-template-columns:1fr 1fr;gap:10px;max-width:520px}
.sp-enter{display:inline-flex;align-items:center;justify-content:center;
  min-height:clamp(46px,5.6vh,54px);padding:0 28px;font-weight:500;font-size:12.5px;letter-spacing:.14em;
  text-transform:uppercase;text-decoration:none;color:var(--royal);border:1px solid var(--royal);
  border-radius:var(--btn-radius);background:none}
.sp-enter:hover{background:var(--royal);color:var(--paper)}
.sp-enter span{display:none}
@media (max-width:900px){
  .sp-cta{grid-template-columns:1fr;max-width:none}
  .sp-enter{width:100%}
}

/* ⭐ « CLASSIQUE » IS THE GATE NOW (owner 2026-09-05: "for front door, make it classic").
   The 15 selectors above used to be prefixed html[data-entree="classique"]; the prefix is simply
   gone, which drops each from (0,2,1) to (0,1,1) — still later in the file than the épurée base
   rules they override, so source order keeps them winning. Nothing else moved.
   ⛔ The épurée declarations are LEFT IN PLACE above rather than deleted: they are what these
   rules override, so removing them would change what "classic" actually renders. */

/* ⭐ THE ?halo= BLOCK IS GONE — NO WHITE BEHIND THE WORDS (owner 2026-09-05).
   The default was already "none", so nothing had to be folded into the base: the 12 % paper veil
   on `.hero-media::after` that buys the headline its contrast was never part of this block's
   selectors, and stays exactly where it was. Deleted with it: the row, its boot segment, its QP
   entry and its four C.UI labels. Measured before deleting — headline 3.49:1 (3:1 bar at this
   size), bottom line 6.62:1 — which is why the veil is the thing that had to survive, not this. */

/* ═══════ ?ponct= — THE HEADLINES' PUNCTUATION, THREE STATES (Badia via the owner, 2026-09-04) ═══════
   *"remove the ',' and the '.' in the header … in french there shouldn't be those in the titles."*
   She is right about the PERIOD: French typography does not end a title with one. She is not right
   about the comma — a comma inside a title is ordinary punctuation, not a terminal mark, and her own
   live site uses them freely in its h2s while its h1 ends in a period.

   ⭐ AND THE OWNER RULED THE SAME DAY: *"Set it to no punctuation"*. So « rien » is the DEFAULT.

     default (attribute-less) .. « L'épilation au laser pratiquement sans douleur »
     ?ponct=sanspoint .......... « L'épilation au laser, pratiquement sans douleur »
     ?ponct=phrase ............. « L'épilation au laser, pratiquement sans douleur. »

   ⛔ THE POLARITY WAS INVERTED, NOT A THIRD SELECTOR ADDED. The default has to be the ATTRIBUTE-LESS
   state, because Reset works by removing the attribute — so whatever the bare `.hp-*` rules say is
   what Reset lands on. Writing a `html[data-ponct="rien"]` rule and leaving the base showing the
   marks would have made Reset put the punctuation back, which is the "a rule for the default" trap
   this file records elsewhere. « rien » therefore has no selector of its own; the other two do.
   ⚠ `display:none` and not `visibility` or a transparent colour: it must remove the glyph from the
   ACCESSIBILITY TREE too, so a screen reader hears the same sentence the page shows. A comma read
   aloud that is not on screen is the same defect as one on screen that is not in the markup.
   ⚠ `display:inline` and not `initial` — these are <span>s inside a heading, and `initial` computes
   to `inline` for a span but says something weaker; naming it keeps the pair symmetrical.
   ⚠ THE MARKS ARE STILL IN THE HTML, just hidden. Nothing is stripped at build time, so all three
   states stay one click apart and Badia's ruling stays reversible.
   ⚠ The comma span exists only on headlines with EXACTLY ONE comma — see head() in build.mjs. Seven
   headlines are lists (« Un appel, un texto, un courriel : … ») and keep theirs in every state,
   because removing those is a grammar error, not a style. */
.hp-p,.hp-c{display:none}
/* ⭐ NO PUNCTUATION, BAKED (owner 2026-09-05). The row is gone and the two show-rules with it.
   ⛔ `.hp-p,.hp-c{display:none}` above STAYS. head() still wraps every final period and single
   comma, so deleting that line would print all 468 periods back onto the page. The spans are
   now inert markup — and they are the seam if the decision is ever revisited. */

/* ═══════ THE LASER CONFIGURATOR (owner 2026-09-04) ═══════
   *"Make the body and face nice. Clean to match the look. Whatever selected turns gold and whatever
   isn't selected is blue."*

   ⭐ THE TWO COLOURS ARE THE SITE'S OWN, NOT NEW ONES. Blue is --royal (#1A23BA, read off
   dermazur.ca's own hero with getComputedStyle) and gold is --gold-dk / #D4AF37, the CTA metal the
   owner ruled on 2026-08-30. Inventing a third pair for one page is how a demo stops looking like
   one site.
   ⚠ THE UNSELECTED STATE IS A TINT, NOT A FILL. At 15 % alpha a zone reads as available without
   turning the figure into a blue blob — the drawing underneath is the thing being pointed at. The
   selected state goes opaque gold with a darker rim, so "chosen" is legible at a glance and, more
   to the point, is not carried by colour ALONE: it also gains a rim and a size step, which is what
   makes it usable to a colour-blind visitor. Colour as the only signal is the accessibility defect
   this file avoids elsewhere too.
   ⚠ `vector-effect:non-scaling-stroke` — the SVG scales to its container, so a plain stroke-width
   would thicken on a phone and hairline on a desktop. */
.cfg{margin-top:clamp(28px,3.4vw,44px)}
.cfg-controls{display:flex;flex-wrap:wrap;gap:10px 14px;margin-bottom:clamp(18px,2.2vw,28px)}
.cfg-switch{display:inline-flex;border:1px solid var(--line-strong);border-radius:2px;overflow:hidden;background:var(--surface)}
.cfg-switch button{appearance:none;border:0;background:none;cursor:pointer;
  font:500 13px/1 var(--sans);letter-spacing:.04em;color:var(--ink-2);
  padding:11px 16px;min-height:44px}
.cfg-switch button+button{border-left:1px solid var(--line)}
.cfg-switch button:hover{color:var(--ink);background:var(--paper-2)}
/* ⚠ THERE ARE TWO WOMEN/MEN SWITCHES ON THIS SITE AND THE FIRST VERSION OF THIS ROW ONLY
   REACHED ONE. `.cfg-switch` is the configurator's; `.seg-btn` is the one on the treatment and
   pricing pages, above « Laser hair removal for women » — a different class, a different
   default (--ink, not --cta-bg), and the one the owner was actually looking at when he said
   the row did nothing. Both are listed here now, and BOTH defaults are left alone: the row
   changes the SELECTED colour, it does not unify two controls that were never the same. */
/* ⭐ INK IS DECIDED (owner 2026-09-05), row deleted. .seg-btn.is-on was already --ink, so the
   only thing that had to change for the CONFIGURATOR switch was its default: it was painting
   --cta-bg (gold) while the treatment pages painted ink, which is the mismatch the row made
   visible. Both are ink now and the two switches finally agree. */
.cfg-switch button[aria-pressed="true"]{background:var(--ink);color:var(--paper)}
.cfg-switch[hidden]{display:none}

.cfg-grid{display:grid;grid-template-columns:minmax(0,7fr) minmax(0,5fr);gap:clamp(28px,4vw,56px);align-items:start}
.cfg-stage{background:var(--surface);border:1px solid var(--line);padding:clamp(14px,2vw,26px);position:relative}
/* the turn-the-figure button, top right of the stage. 40px, so 2.5.8's 24 is clear. */
.cfg-rotate{position:absolute;top:10px;right:10px;z-index:2;width:40px;height:40px;
  display:inline-flex;align-items:center;justify-content:center;padding:0;
  background:var(--paper);border:1px solid var(--line-strong);border-radius:50%;
  color:var(--royal);cursor:pointer;transition:background .16s,color .16s,border-color .16s}
/* ⛔ AND THIS LINE IS NOT OPTIONAL. `display:inline-flex` on the class BEATS the browser's own
   `[hidden]{display:none}`, so without this the button stayed on screen over the FACE figure —
   which has no back to turn to. Caught in a screenshot, not in the source. */
.cfg-rotate[hidden]{display:none}
.cfg-rotate svg{width:20px;height:20px;display:block}
.cfg-rotate:hover{background:#D4AF37;border-color:#8A6F14;color:var(--ink)}
.cfg-rotate:focus-visible{outline:2px solid var(--ink);outline-offset:2px}
.cfg-rotate[aria-pressed="true"]{background:var(--royal);border-color:var(--royal);color:var(--paper)}
.cfg-fig{display:block}
.cfg-fig[hidden]{display:none}
.cfg-fig svg{display:block;width:100%;height:auto;max-height:min(62vh,620px);margin:0 auto}
/* ⛔ THE FOUR .fig-* RULES CAME WITH THE DRAWINGS, AND WITHOUT THEM THE FIGURE IS A BLACK BLOB.
   The lifted SVG paths carry `class="fig-skin"` / `fig-line` / `fig-line-soft` / `fig-shade` and
   nothing else — no inline fills — so with no rules to match, every path falls back to the SVG
   default of solid black. That is exactly what shipped in the first build of this page, and only a
   screenshot showed it: the markup was perfect, the extraction assertion passed, the price was
   right, and the picture was a silhouette.
   ⚠ `--line-2` DOES NOT EXIST IN v2 — it is a live token. Mapped to `--line-strong`, which is this
   site's equivalent weight; copying the rule verbatim would have given the skin a `stroke` of
   nothing and lost the outline. That is the one edit made to these four rules. */
.cfg-fig .fig-skin{fill:url(#cfgSkin);stroke:var(--line-strong);stroke-width:1.4}
.cfg-fig .fig-line{fill:none;stroke:var(--royal);stroke-width:1.3;opacity:.34;stroke-linecap:round;stroke-linejoin:round}
.cfg-fig .fig-line-soft{fill:none;stroke:var(--ink-2);stroke-width:1;opacity:.22;stroke-linecap:round}
.cfg-fig .fig-shade{fill:var(--royal);opacity:.05}

/* the hotspots — blue when available, gold when chosen */
.cfg-z{cursor:pointer}
/* ⭐ QUIETER AT REST, LOUDER UNDER THE POINTER (owner 2026-09-05: "don't make it so obvious …
   a very light shade of blue so people know it's selectable … more noticeable once people hover
   on it, and when they hover on it, have it be light gold").
   Rest went 15% fill / 55% stroke → 6% / 20%: enough to read as a live area over the drawing,
   not enough to compete with it. Hover is the loud state now, and it is GOLD, which also means
   the pointer previews the colour a chosen area turns — the hover and the selected state are the
   same hue at two strengths, so the picker teaches itself.
   ⛔ HOVER AND SELECTED MUST NOT LOOK ALIKE. Selected stays SOLID #D4AF37 with a dark stroke and
   the 1.06 scale; hover is a 26% wash with no scale. Different enough to tell apart at a glance
   on a figure where several may be on at once. */
.cfg-z ellipse{fill:color-mix(in srgb,var(--royal) 6%,transparent);
  stroke:color-mix(in srgb,var(--royal) 10%,transparent);stroke-width:1;
  vector-effect:non-scaling-stroke;transition:fill .16s,stroke .16s,transform .16s;
  transform-box:fill-box;transform-origin:center}
.cfg-z:hover ellipse{fill:color-mix(in srgb,#D4AF37 26%,transparent);stroke:#B8912A;stroke-width:2}
.cfg-z.is-on ellipse{fill:#D4AF37;stroke:#8A6F14;stroke-width:2;transform:scale(1.06)}
.cfg-z:focus{outline:none}
.cfg-z:focus-visible ellipse{stroke:var(--ink);stroke-width:3}

.cfg-legend{display:flex;align-items:center;gap:8px;margin:14px 0 0;font-size:13px;color:var(--muted)}
.cfg-ask{margin:12px 0 0}
/* the hover preview, floating over the figure beside whatever the pointer is on. Gold ink on
   paper with a gold hairline: gold is what a chosen area turns, so the label and the colour say
   the same thing. pointer-events:none is load-bearing — a label under the cursor would steal the
   hover from the very hotspot it is describing and flicker. */
.cfg-prev{position:absolute;z-index:3;pointer-events:none;white-space:nowrap;
  padding:6px 10px;border-radius:3px;background:var(--paper);border:1px solid var(--gold-opt);
  box-shadow:0 2px 10px rgba(20,27,61,.10);font-size:13px;line-height:1.2;
  color:var(--gold-opt);font-weight:500;transform:translate(-50%,-100%)}
.cfg-prev b{font-weight:700}
/* the text-a-photo route inside the shave note — inline, so it reads as part of the sentence */
.bk-photo{display:inline;font-size:inherit;letter-spacing:inherit}
.cfg-key{width:13px;height:13px;border-radius:50%;display:inline-block}
.cfg-key-on{background:#D4AF37;border:1px solid #8A6F14}
.cfg-key-off{background:color-mix(in srgb,var(--royal) 15%,transparent);border:1px solid color-mix(in srgb,var(--royal) 55%,transparent);margin-left:12px}

.cfg-panel{border:1px solid var(--line);background:var(--surface);padding:clamp(18px,2.2vw,26px);position:sticky;top:calc(var(--nav-h) + 18px)}
.cfg-list{list-style:none;margin:12px 0 0;padding:0}
.cfg-list li{display:flex;align-items:center;gap:10px;padding:9px 0;border-top:1px solid var(--line);font-size:15px}
.cfg-list li:first-child{border-top:0}
.cfg-list b{margin-left:auto;font-weight:500;font-variant-numeric:tabular-nums}
.cfg-x{appearance:none;border:1px solid var(--line-strong);background:none;cursor:pointer;
  width:26px;height:26px;min-width:26px;border-radius:50%;line-height:1;color:var(--muted);font-size:15px}
.cfg-x:hover{border-color:var(--ink);color:var(--ink)}
.cfg-empty{margin:12px 0 0;color:var(--muted);font-size:14.5px;line-height:1.55}
.cfg-tot{display:grid;grid-template-columns:1fr auto;gap:6px 14px;margin:18px 0 0;padding-top:14px;border-top:1px solid var(--line-strong)}
.cfg-tot dt{color:var(--ink-2);font-size:14px}
.cfg-tot dd{margin:0;text-align:right;font-variant-numeric:tabular-nums;color:var(--brass-text);font-size:14px}
.cfg-tot .cfg-tot-k{color:var(--ink);font-weight:500;font-size:16px;align-self:end}
.cfg-tot .cfg-tot-v{color:var(--ink);font-size:16px}
.cfg-tot .cfg-tot-v b{font-size:24px;font-weight:500}
.cfg-tot[hidden]{display:none}
.cfg-clear{margin-left:16px}
.cfg-note,.cfg-ask{margin:14px 0 0;font-size:13px;color:var(--muted);line-height:1.55}

@media (max-width:900px){
  .cfg-grid{grid-template-columns:minmax(0,1fr)}
  /* ⚠ the panel unsticks on a phone — a sticky sidebar that is now a full-width block below the
     figure would pin itself over the figure the visitor is trying to tap. */
  .cfg-panel{position:static}
  .cfg-fig svg{max-height:none}
}

/* the equivalent control — see the note in build.mjs. Same gold/blue rule as the figure, at a size
   a thumb can actually hit: 44px high, which is above 2.5.8's 24px and at Apple's own target size. */
.cfg-chiprow{margin-top:20px;padding-top:18px;border-top:1px solid var(--line)}
/* ⭐ GROUPED (owner 2026-09-04: *"Just organize it better"*). 25 body chips in one flex wrap is a
   wall — which is why he read it as zones being MISSING when all 65 were already there. Six short
   shelves with a quiet label each, and inside every shelf the biggest zone comes first so the
   cheaper-together option is read before its parts. */
.cfg-group + .cfg-group{margin-top:18px}
.cfg-group-t{margin:0 0 9px;font-family:var(--sans);font-weight:500;font-size:11px;
  letter-spacing:.16em;text-transform:uppercase;color:var(--muted)}
.cfg-group-row{display:flex;flex-wrap:wrap;gap:8px}
.cfg-chip{appearance:none;cursor:pointer;min-height:44px;padding:8px 13px;
  display:grid;grid-template-columns:1fr auto;gap:0 12px;align-items:baseline;
  font:400 13.5px/1.25 var(--sans);text-align:left;
  border:1px solid color-mix(in srgb,var(--royal) 38%,transparent);
  background:color-mix(in srgb,var(--royal) 7%,transparent);color:var(--ink);border-radius:2px;
  transition:background .16s,border-color .16s,color .16s}
.cfg-chip-n{grid-column:1}
.cfg-chip-p{grid-column:2;font-variant-numeric:tabular-nums;color:var(--ink-2)}
/* ⚠ the minutes sit UNDER the name, not beside the price — they are the second-most useful thing
   on the chip and the least urgent, and a third column would push the row to wrap on a phone. */
.cfg-chip-m{grid-column:1/-1;font-size:11.5px;color:var(--muted);letter-spacing:.04em}
.cfg-chip:hover{background:color-mix(in srgb,var(--royal) 16%,transparent);border-color:var(--royal)}
.cfg-chip.is-on{background:#D4AF37;border-color:#8A6F14;color:#2C2408;font-weight:500}
.cfg-chip.is-on .cfg-chip-p,.cfg-chip.is-on .cfg-chip-m{color:#4A3B08}
.cfg-chip:focus-visible{outline:2px solid var(--royal);outline-offset:2px}

