/* ---------------------------------------------------------------------------
   SELF-HOSTED TYPE, added 30 Aug 2026.
   Figtree was fetched from fonts.googleapis.com and fonts.gstatic.com on every
   visit, which announced every reader of a dental clinic's website to Google
   before they had read a word. One variable file, 19 KB, now sits in this
   folder and the page contacts nobody. One file covers 400 to 800 because
   Google serves a single variable font and points every weight at it.
   --------------------------------------------------------------------------- */
@font-face{
  font-family:'Figtree';
  font-style:normal;
  font-weight:300 900;
  font-display:swap;
  src:url('fonts/figtree-latin-var.woff2') format('woff2');
}

/* ==================================================================
   QUILLFIELD DENTAL
   9 Juniper Row, Kolkata

   PALETTE, taken from the clinic itself.
   The navy is the signboard's own lettering. The teal accent is the
   treatment room: mint walls and a teal-blue chair, both visible in
   the photographs. The clinic's signboard red is deliberately NOT an
   accent here; it appears on the sign only as opening-hours text, and
   red on a dental page reads as blood.

   Deliberately unlike the other builds in this folder: another build is
   warm cream with green and marigold, another build is oxblood and gold,
   another build is deep ultramarine on cool grey. This one is high-key
   and near-white, with a single cool accent.

   SHAPE RULE, applied everywhere with no exceptions:
     buttons and pills  999px
     cards and panels   16px
   There is no third tier because there are no inputs on this site.

   TYPE
   Figtree throughout. Hierarchy comes from weight, size and tracking
   rather than a second family, which is what keeps a clinical page
   calm. System stack behind it so the page is still correct if the
   font request fails.

   Every colour is declared in the two token blocks below and nowhere
   else, so both themes stay in step.
   ================================================================== */

:root{
  --paper:      #0B1620;
  --paper-2:    #111E29;
  --paper-3:    #16242F;
  --card:       #101C27;

  --ink:        #EAF2F6;
  --ink-2:      #CBDCE5;
  --body:       #A9BCC8;
  --muted:      #8EA3B1;

  --accent:     #45BDB6;
  --accent-deep:#6BD3CC;
  --accent-wash:#0E2A2B;
  --on-accent:  #04211F;

  --deep:       #17395C;
  --on-deep:    #EAF2F6;
  --on-deep-dim:#B3C7D4;

  --line:       #21313D;
  --line-soft:  #1A2833;

  --shadow-sm: 0 2px 8px rgba(0,0,0,.5);
  --shadow-md: 0 18px 40px -24px rgba(0,0,0,.7);
  --shadow-lg: 0 40px 80px -40px rgba(0,0,0,.8);

  --sans: "Figtree", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;

  --ease:     cubic-bezier(.22,1,.36,1);
  --ease-out: cubic-bezier(.16,1,.3,1);

  --nav-h: 70px;
  --shell: 1180px;
  --gut:   32px;

  /* ---- VERTICAL SCALE, viewport-HEIGHT aware -----------------------
     This page was the worst of the three for it. Measured at 1280x610,
     the working area a Windows laptop actually has at 150% display
     scaling, SIX of eight sections were taller than the whole screen and
     one gallery photograph alone was 1.41 screens tall. Nothing could be
     seen entire, which is why the page read as permanently zoomed in and
     only composed at about 35% browser zoom.

     Every vertical value is now the SMALLER of a width-based and a
     height-based measure. The plain vh line before each svh line is the
     fallback for engines that do not parse svh. */
  --section-y: clamp(44px, 7vw, 104px);
  --section-y: min(clamp(44px, 7vw, 104px), 12svh);

  --media-max: min(58vh, 520px);
  --media-max: min(58svh, 520px);

  --dur-press: 150ms;
  --dur-ui:    220ms;

  --r-pill: 999px;
  --r-card: 16px;

  /* Used on photographic panels where the text sits on a fixed dark
     scrim, so these stay the same value in both themes on purpose. */
  --on-photo: #FFFFFF;
  color-scheme: dark;
}



*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
/* scroll-behavior is NOT smooth: Lenis owns the scrolling now, and two
   smooth implementations fight each other. Under reduced motion Lenis
   never starts and the browser's own instant jump is the correct result. */
html{-webkit-text-size-adjust:100%}
@media (prefers-reduced-motion:reduce){ html{scroll-behavior:auto} }

body{
  font-family:var(--sans);
  background:var(--paper);
  color:var(--body);
  font-size:17px;
  line-height:1.62;
  overflow-x:hidden;
  -webkit-font-smoothing:antialiased;
  text-rendering:optimizeLegibility;
}

/* height:auto is load-bearing. The width/height attributes on each
   <img> are presentational hints that otherwise pin the box to the
   file's own pixel height and silently override every aspect-ratio
   rule below. */
img{max-width:100%;display:block;height:auto}
a{color:inherit}
:focus-visible{outline:3px solid var(--accent);outline-offset:3px;border-radius:4px}

.shell{max-width:var(--shell);margin:0 auto;padding:0 var(--gut)}
section{padding:var(--section-y) 0}

h1,h2,h3{color:var(--ink);font-weight:800;letter-spacing:-.03em;line-height:1.08}
h2{font-size:clamp(30px,4vw,46px);margin-bottom:18px}
h3{font-size:20px;letter-spacing:-.015em;line-height:1.25}
p{max-width:64ch}
.lede{font-size:19px;color:var(--body);max-width:56ch}

/* ---------- motion -------------------------------------------------
   The hidden state is scoped under .js, set by an inline script in the
   head. If JavaScript never runs, nothing is hidden and the page still
   reads. A reveal that starts at opacity:0 unscoped will blank an
   entire page the day a script fails. */
/* NO OPACITY IN THE REVEAL. The reveal is a slide and nothing else.

   This was fought three times in one session. Text that is part-way
   through a fade genuinely fails a contrast check, and Lighthouse
   snapshots whenever it likes: an h2 was caught at 2.47:1, then at 1.89:1
   against a required 4.5. Shortening the fade from .28s to .16s only
   narrowed the window it could be caught in - it did not close it. Any
   opacity animation on text is a race, and the only way to win it is not
   to run it.

   Movement changes no colour, so it cannot fail. The element is hidden
   before it reveals by visibility, which is binary and has no
   intermediate state for a checker to sample. */
.js .rise{visibility:hidden;transform:translateY(22px)}
.js .rise.in{
  visibility:visible;transform:none;
  transition:transform 1.05s var(--ease-out);
}
.js .rise.in.d1{transition-delay:.10s}
.js .rise.in.d2{transition-delay:.20s}
.js .rise.in.d3{transition-delay:.30s}
/* Already on screen when the page first painted: show it, do not
   animate it. Set by main.js. */
.js .rise.instant{transition:none;transition-delay:0s}
@media (prefers-reduced-motion:reduce){
  .js .rise,.js .rise.in{visibility:visible;transform:none;transition:none}
}

/* ---------- nav ---------------------------------------------------- */
.nav{
  position:sticky;top:0;z-index:40;height:var(--nav-h);
  display:flex;align-items:center;
  background:color-mix(in srgb,var(--paper) 88%,transparent);
  backdrop-filter:saturate(180%) blur(14px);
  -webkit-backdrop-filter:saturate(180%) blur(14px);
  border-bottom:1px solid var(--line-soft);
}
@supports not (backdrop-filter:blur(1px)){ .nav{background:var(--paper)} }

/* Over the hero the bar carries no background at all, so the dark runs
   right to the top of the window and the page opens as one image. It
   turns back into the ordinary light bar the moment the hero's top edge
   leaves the viewport - main.js toggles the class off a sentinel rather
   than off a scroll handler, so nothing runs per frame.

   .js scopes it: with no JavaScript the class is never added, and the
   bar stays solid and readable, which is the correct failure. */
.js .nav{transition:background-color .35s var(--ease), border-color .35s var(--ease)}
.js .nav.on-hero{
  background:transparent;border-bottom-color:transparent;
  backdrop-filter:none;-webkit-backdrop-filter:none;
}
.js .nav.on-hero .brand{color:#fff}
.js .nav.on-hero .nav-links a{color:#C3D6E2}
.js .nav.on-hero .nav-links a:hover{color:#fff}
.js .nav.on-hero .brand svg rect{fill:#fff}
.js .nav.on-hero .brand svg path{fill:#0E2740}
.js .nav.on-hero .nav-call{background:#fff;color:var(--deep)}
@media (prefers-reduced-motion:reduce){ .js .nav{transition:none} }
.nav .shell{display:flex;align-items:center;justify-content:space-between;gap:20px;width:100%}
.brand{display:flex;align-items:center;gap:11px;font-weight:800;color:var(--ink);
  letter-spacing:-.02em;font-size:17px;text-decoration:none;white-space:nowrap}
.brand svg{width:30px;height:30px;flex:none}
.nav-links{display:flex;gap:28px;font-size:15px;font-weight:500}
.nav-links a{text-decoration:none;color:var(--body);transition:color var(--dur-ui) var(--ease)}
@media (max-width:940px){ .nav-links{display:none} }
/* On a narrow phone the brand plus a full call button do not both fit.
   The label collapses to the icon; the accessible name lives on the
   anchor's aria-label so nothing is lost to a screen reader. */
@media (max-width:460px){
  .nav-call .lbl{display:none}
  .nav-call{padding:11px 13px;min-width:44px;min-height:44px}
}

/* ---------- buttons ------------------------------------------------ */
.btn{
  display:inline-flex;align-items:center;justify-content:center;gap:9px;
  border-radius:var(--r-pill);border:1px solid transparent;
  font-family:inherit;font-size:15.5px;font-weight:700;letter-spacing:-.005em;
  padding:13px 24px;text-decoration:none;cursor:pointer;white-space:nowrap;
  transition:transform var(--dur-press) var(--ease-out),background var(--dur-ui) var(--ease),border-color var(--dur-ui) var(--ease);
}
.btn svg{width:17px;height:17px;flex:none}
.btn:active{transform:scale(.975)}
.btn-solid{background:var(--accent);color:var(--on-accent)}
.btn-line{background:transparent;color:var(--ink);border-color:var(--line)}
.btn-onDeep{background:var(--on-deep);color:var(--deep)}
.btn-onDeepLine{background:transparent;color:var(--on-deep);border-color:rgba(255,255,255,.32)}
.btn-sm{padding:10px 18px;font-size:14.5px}

/* ---------- hero: dark, cinematic, object bleeding off the frame ------

   Reworked 28 Aug 2026 after he sent sharplink.com as the feeling he
   wanted. What was taken from it: a dark stage, the object big enough to
   run off the edge of its own frame rather than sitting politely in a
   box, hard raking light, and display type at a size that carries the
   page. What was NOT taken: its identity. That site sells credibility to
   institutional investors and is square-cornered, industrial and dark
   from top to bottom. This one has to reassure somebody with toothache,
   so the shape rule (pills, 16px cards) stands, and the dark runs for the
   hero only. Everything below it stays light and readable.

   The navy is not borrowed either: --deep is already this clinic's own
   signboard lettering, which is why a dark hero sits on this page at all.

   ONE theme transition, at the very top, which is the exception the
   theme-lock rule allows. In dark mode there is no transition at all
   because the page below is dark too. */
.hero{
  position:relative;
  /* Pulled up under the sticky bar and padded back out by the same
     amount, so the dark actually runs to the top of the window and the
     bar has something to be transparent OVER. Without this the bar sits
     on the page background above the hero, and a white brand mark on
     white paper is invisible - which is exactly what a first pass did. */
  margin-top:calc(-1 * var(--nav-h));
  padding:calc(var(--nav-h) + clamp(30px,4svh,58px)) 0 clamp(34px,5svh,62px);
  background:
    radial-gradient(120% 90% at 78% 18%,
      color-mix(in srgb, var(--accent) 26%, transparent), transparent 62%),
    linear-gradient(168deg, #0B1620 0%, var(--deep) 46%, #16324C 100%);
  color:var(--on-deep);
  overflow:hidden;
  isolation:isolate;
}
/* No fade at the bottom. A first pass blended the navy into the hours
   strip and produced a muddy grey band across the full width. A clean
   edge is tidier, and it crops the roots against the section boundary
   rather than dissolving them, which is the effect that was wanted. */

.hero .grid{display:grid;grid-template-columns:1.02fr .98fr;gap:40px;
  align-items:center;position:relative;z-index:1}
.hero h1{
  font-size:clamp(42px,6.6vw,84px);
  letter-spacing:-.045em;line-height:.98;
  color:#fff;margin-bottom:24px;
  text-wrap:balance;
}
/* The hero is dark in BOTH themes, so its text cannot use the accent
   tokens: those flip with the theme and --accent-deep is the DARK teal
   meant for light paper. On navy it measured close to invisible. These
   two are fixed values chosen against the hero's own background. */
.hero h1 em{font-style:normal;color:#6BD3CC}
.hero .lede{color:#C3D6E2;margin-bottom:34px;font-size:20px;max-width:44ch}
.hero-cta{display:flex;flex-wrap:wrap;gap:13px}

/* An uppercase micro-label, the one place on this page that gets one.
   It earns its place by carrying today's opening, which is the single
   most asked question a clinic gets. */
.hero-note{margin-top:38px;display:flex;align-items:baseline;gap:14px;flex-wrap:wrap}
.hero-note b{font-size:11.5px;letter-spacing:.19em;text-transform:uppercase;
  font-weight:700;color:#6BD3CC}
.hero-note span{font-size:15.5px;color:#C3D6E2}

/* The stage runs wider than its column and is allowed to overflow to the
   right, which is the whole trick: the tooth is cropped by the edge of
   the page instead of floating inside a neat square. */
.tooth-stage{position:relative;width:100%;
  max-width:min(720px,78svh);
  aspect-ratio:1/1;margin:0;justify-self:end;
  margin-right:clamp(-140px,-7vw,0px);
}
.tooth-still,.tooth-canvas{position:absolute;inset:0;width:100%;height:100%;
  display:block}
.tooth-still{object-fit:contain}
/* The still is what ships and what paints first. The canvas fades over it
   only once WebGL is running and the model has arrived, so any failure
   anywhere leaves a complete hero rather than a hole in it. */
.tooth-canvas{opacity:0;transition:opacity .6s var(--ease)}
.tooth-stage.ready .tooth-canvas{opacity:1}
.tooth-stage.ready .tooth-still{opacity:0;transition:opacity .6s var(--ease)}
/* The glow the object sits in. inset:0, never a negative inset: bleeding
   it outside the stage pushed the document wider than a 420px viewport
   and gave the whole page a sideways scroll. */
.tooth-stage::before{content:"";position:absolute;inset:0;
  border-radius:50%;pointer-events:none;z-index:-1;
  background:radial-gradient(closest-side,
    color-mix(in srgb, var(--accent) 34%, transparent),
    transparent 68%)}

@media (max-width:960px){
  .hero .grid{grid-template-columns:1fr;gap:30px}
  .hero-cta{justify-content:flex-start}
  .tooth-stage{justify-self:center;margin-right:0;max-width:min(400px,52svh)}
}
@media (max-width:900px){
  /* Below this width neither three.js nor the model is fetched at all and
     the still render is the hero image. */
  .tooth-stage{max-width:min(340px,46svh)}
}
@media (prefers-reduced-motion:reduce){
  .tooth-canvas{display:none}
  .tooth-stage.ready .tooth-still{opacity:1}
}

/* ---------- facts strip -------------------------------------------- */
.facts{background:var(--paper-2);border-top:1px solid var(--line-soft);
  border-bottom:1px solid var(--line-soft);padding:44px 0}
.facts .grid{display:grid;grid-template-columns:repeat(3,1fr);gap:40px}
.fact dt{font-size:13px;font-weight:700;letter-spacing:.03em;color:var(--muted);margin-bottom:9px}
.fact dd{font-size:17px;color:var(--ink);font-weight:600;line-height:1.5}
.fact dd span{display:block;font-weight:500;color:var(--body);font-size:15.5px}
@media (max-width:820px){ .facts .grid{grid-template-columns:1fr;gap:26px} }

/* ---------- treatments: an index, not four boxes --------------------
   The previous version was a six-column grid of tinted cards, and three
   things were wrong with it.

   1. The card widths were set by span, not by content, and the spans
      ran opposite to the content. "Rebuilding a tooth" holds two items
      and got two columns, so its pills stacked vertically; "Making a
      smile look right" holds three and got four columns, so it sat in a
      card half full of nothing. The group with the fewest items had the
      widest box.
   2. One card of the four carried an accent tint for no reason a reader
      could infer, and in dark mode that tint reads as green against a
      navy page.
   3. Four identical boxes, each a heading over a bag of pills, give the
      eye nothing to rank. That is what made it feel bland.

   It is now an index: four hairline-separated rows, each with a number,
   the group name and how many it holds on the left, and the treatments
   on the right. Rows size themselves to their content, so the inversion
   cannot come back, and the counts add the hierarchy the boxes lacked.
   The pill shape rule still holds; there is just one shape of pill and
   no tinted special case. */
.treat{margin-top:clamp(28px,4vh,48px);border-top:1px solid var(--line)}
.treat-row{display:grid;grid-template-columns:minmax(210px,.34fr) 1fr;
  gap:18px 44px;padding:clamp(20px,2.6vh,28px) 0;border-bottom:1px solid var(--line);
  align-items:start}
.treat-key{display:flex;align-items:baseline;flex-wrap:wrap;gap:0 12px}
.treat-n{font-size:12.5px;font-weight:800;letter-spacing:.09em;color:var(--accent);
  font-variant-numeric:tabular-nums}
.treat-key h3{font-size:19px;letter-spacing:-.022em;line-height:1.25}
.treat-c{font-size:13px;font-weight:600;color:var(--muted);flex-basis:100%;margin-top:5px}
.treat-items{list-style:none;display:flex;flex-wrap:wrap;gap:9px}
.treat-items li{
  font-size:14.5px;font-weight:600;color:var(--ink-2);
  background:var(--paper-2);border:1px solid var(--line-soft);
  border-radius:var(--r-pill);padding:7px 15px}

/* The pills arrive one after another rather than the whole row landing
   at once, which is the difference between a section that appears and a
   section that resolves. 45ms apart: short enough that the last pill of
   a four-item row is only 135ms behind the first, so nothing is ever
   waiting on the animation. Opacity resolves in 260ms because a slower
   fade genuinely fails a contrast check while it is in flight, which an
   audit caught on the sister site. */
.js .treat-row .treat-items li{visibility:hidden;transform:translateY(7px);
  transition:transform 380ms var(--ease-out);
  transition-delay:calc(var(--i, 0) * 45ms)}
.js .treat-row.in .treat-items li{visibility:visible;transform:none}
/* Anything already on screen at first paint does not animate at all. */
.js .treat-row.instant .treat-items li{transition:none;transition-delay:0s;opacity:1;transform:none}
@media (prefers-reduced-motion:reduce){
  .js .treat-row .treat-items li,
  .js .treat-row.in .treat-items li{visibility:visible;opacity:1;transform:none;transition:none;transition-delay:0s}
}

@media (max-width:760px){
  .treat-row{grid-template-columns:1fr;gap:14px}
  .treat-c{flex-basis:auto;margin-top:0}
}

/* ---------- dentist: type only --------------------------------------
   There is no photograph of Dr. Sen, and there was never going to be
   one. The previous version filled the space with a picture of the
   treatment chair, which beside a person's name reads as a stand-in for
   that person: it is the reason the section looked unprofessional.
   Substituting a different photograph would be the same mistake with a
   different picture, so this section is set in type and given room
   rather than filled. */
.dentist{background:var(--paper-2);border-top:1px solid var(--line-soft);border-bottom:1px solid var(--line-soft)}
.dentist-head{max-width:62ch}
.eyebrow{font-size:13px;font-weight:700;letter-spacing:.10em;text-transform:uppercase;
  color:var(--accent);margin-bottom:14px}
.dentist-head h2{margin-bottom:16px}

.dentist-facts{display:grid;grid-template-columns:repeat(4,1fr);gap:0;
  margin-top:clamp(30px,4.5vh,46px);border-top:1px solid var(--line)}
.dentist-facts > div{padding:22px 26px 24px 0;border-right:1px solid var(--line)}
.dentist-facts > div:last-child{border-right:none;padding-right:0}
.dentist-facts dt{font-size:12.5px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;
  color:var(--muted);margin-bottom:10px}
.dentist-facts dd{font-size:18px;font-weight:600;color:var(--ink);line-height:1.4;letter-spacing:-.012em}

.dentist-note{margin-top:clamp(26px,3.6vh,38px);max-width:64ch;color:var(--body)}

@media (max-width:900px){
  .dentist-facts{grid-template-columns:1fr 1fr}
  .dentist-facts > div{padding:20px 22px 20px 0}
  .dentist-facts > div:nth-child(2n){border-right:none;padding-right:0}
  .dentist-facts > div:nth-child(-n+2){border-bottom:1px solid var(--line)}
}
@media (max-width:520px){
  .dentist-facts{grid-template-columns:1fr}
  .dentist-facts > div{border-right:none;border-bottom:1px solid var(--line);padding:18px 0}
  .dentist-facts > div:last-child{border-bottom:none}
}

/* ---------- first visit: stacked rows ------------------------------- */
.visit-list{margin-top:44px;border-top:1px solid var(--line)}
.vrow{display:grid;grid-template-columns:auto 1fr;gap:26px;align-items:start;
  padding:28px 0;border-bottom:1px solid var(--line)}
.vrow .ic{width:44px;height:44px;border-radius:var(--r-pill);flex:none;
  background:var(--accent-wash);color:var(--accent);
  display:flex;align-items:center;justify-content:center}
.vrow .ic svg{width:21px;height:21px}
.vrow h3{margin-bottom:5px}
.vrow p{color:var(--body);font-size:16px}
@media (max-width:640px){ .vrow{grid-template-columns:1fr;gap:14px} }

/* ---------- gallery: two columns, three cells ------------------------
   Three cells in a 2x2 with the first spanning both rows, so every slot
   is filled. An earlier version used three columns and left the
   bottom-right slot empty, which reads as a layout that ran out of
   photographs. There is deliberately no hover transform here: these are
   figures, not links, and lifting or zooming something that cannot be
   clicked is a promise the page does not keep. */
.gal{display:grid;grid-template-columns:repeat(3,1fr);gap:18px;
  margin-top:clamp(28px,4vh,44px)}
.gal figure{margin:0;display:flex;flex-direction:column}
/* One governor only. Pairing aspect-ratio with max-height and cover is the
   same fault the hero comment above describes: the cap shortens the box, the
   column keeps its width, and cover crops the difference off the top and
   bottom by an amount that changes with viewport height. Here the column is
   549px and Reviewed on Google wants 686px against a 520px cap, so it was cropping about
   29% off two portrait photographs on essentially every desktop. The ratio
   governs; the cap is gone. */
.gal img{width:100%;aspect-ratio:1/1;object-fit:cover;object-position:center 38%;
  border-radius:var(--r-card);background:var(--paper-2)}
.gal figcaption{padding-top:12px;font-size:14.5px;color:var(--muted);letter-spacing:.005em}
@media (max-width:980px){ .gal{grid-template-columns:1fr 1fr} }
@media (max-width:640px){
  .gal{grid-template-columns:1fr;gap:22px}
  .gal img{aspect-ratio:4/3}
}

/* ---------- page-wide motion ----------------------------------------

   Asked for on 28 Aug 2026: the page felt static. Three things were
   added, all of them cheap, all of them off under reduced motion.

   1. A longer, softer reveal curve on .rise, so sections arrive rather
      than appear. The transition itself already existed; only the timing
      changed.
   2. A drift on the gallery figures, driven by the browser's own
      scroll-driven animation timeline. This costs NO JavaScript: the
      animation-timeline property is native, runs on the compositor, and
      simply does nothing in an engine that has not implemented it.
   3. Buttons and gallery images that respond to the pointer.

   Nothing here animates anything but transform and opacity, which are
   the two properties the compositor can handle without a repaint. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    @keyframes drift { from { transform: translateY(16px) } to { transform: translateY(-16px) } }
    .gal figure{
      animation: drift linear both;
      animation-timeline: view();
      animation-range: entry 10% exit 90%;
    }
    /* Staggered ranges, so the three do not move in lockstep, which is
       what separates a parallax from a lift. */
    .gal .g2{animation-range: entry 4% exit 96%}
    .gal .g3{animation-range: entry 16% exit 84%}
    /* TRANSFORM ONLY. No opacity on this one, deliberately. A fade tied to
       a scroll timeline holds text at a partial opacity for as long as the
       reader stays put, and half-faded text genuinely fails a contrast
       check - which is the exact fault the note on .rise above records
       being caught by an audit once already. A first pass here animated
       opacity from .55 and dropped Lighthouse accessibility from 100 to
       96, with the hours strip measured at 2.88:1 against a required 4.5.
       Movement is safe because it changes no colour. */
    @keyframes settle { from { transform: translateY(26px) } to { transform: none } }
    .facts .fact, .vrow, .treat-row{
      animation: settle linear both;
      animation-timeline: view();
      animation-range: entry 6% cover 26%;
    }
  }
}

/* The pointer responses, kept to devices that actually have a pointer. */
@media (hover:hover) and (pointer:fine){
  /* No hover transform on the gallery figures. The note above them says
     why, and it still holds: they are figures, not links, and lifting
     something that cannot be clicked promises an interaction that is not
     there. Only the buttons, which ARE clickable, respond to a pointer. */
  .btn{transition:transform var(--dur-ui) var(--ease), box-shadow var(--dur-ui) var(--ease), background-color var(--dur-ui) var(--ease), color var(--dur-ui) var(--ease)}
  .btn:hover{transform:translateY(-2px)}
  .btn:active{transform:translateY(0) scale(.985)}
}
@media (prefers-reduced-motion:reduce){
  .btn{transition:none}
  .btn:hover,.btn:active{transform:none}
}

/* ---------- appointment composer ------------------------------------
   Quillfield's shape rule holds here with no exceptions: pill buttons,
   16px cards, 10px inputs. The segmented control is built from real
   radio inputs with the input visually hidden and the label styled, so
   it is a native radio group for a keyboard and a screen reader and only
   looks like a segmented control. */
.book .grid{display:grid;grid-template-columns:1fr 1fr;gap:56px;align-items:start}
/* A grid item's automatic minimum size is min-content, not zero, so a
   child whose min-content exceeds the column pushes the whole track
   wider than its container and the page gains a horizontal scrollbar.
   Measured at 375px: a 360px grid box with a 373px column. This is the
   same fault the another build order form hit through its <textarea>, and
   the fix is the same one. */
.book .grid > *{min-width:0}

.book-steps{list-style:none;counter-reset:bs;margin-top:30px;display:flex;flex-direction:column;gap:18px}
.book-steps li{counter-increment:bs;position:relative;padding-left:44px;color:var(--body);font-size:16px;line-height:1.6}
.book-steps li::before{content:counter(bs);position:absolute;left:0;top:1px;
  width:28px;height:28px;border-radius:100px;background:var(--accent-wash);color:var(--accent);
  font-size:13.5px;font-weight:800;display:grid;place-items:center}
.book-steps li b{display:block;color:var(--ink);font-weight:700;letter-spacing:-.012em;margin-bottom:3px}

.book-form{background:var(--card);border:1px solid var(--line);border-radius:var(--r-card);
  padding:clamp(24px,3.4vh,32px);box-shadow:var(--shadow-sm)}
.field{margin-bottom:18px}
.field label{display:block;font-size:14px;font-weight:700;color:var(--ink);margin-bottom:8px;letter-spacing:-.005em}
.field textarea{width:100%;min-width:0;min-height:104px;resize:vertical;
  font-family:inherit;font-size:16px;color:var(--ink);line-height:1.55;
  background:var(--paper);border:1px solid var(--line);border-radius:10px;padding:12px 14px;
  transition:border-color var(--dur-ui) var(--ease)}
.field textarea:focus{outline:none;border-color:var(--accent)}
.field textarea:focus-visible{outline:3px solid var(--accent);outline-offset:2px}

.sitting{border:0;padding:0;margin:0 0 20px}
.sitting legend{font-size:14px;font-weight:700;color:var(--ink);margin-bottom:8px;padding:0;letter-spacing:-.005em}
.seg{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}
.seg input{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none}
.seg label{display:block;text-align:center;cursor:pointer;
  background:var(--paper);border:1px solid var(--line);border-radius:10px;padding:11px 8px;
  font-size:14.5px;font-weight:700;color:var(--ink);line-height:1.25;
  transition:border-color var(--dur-ui) var(--ease),background var(--dur-ui) var(--ease),color var(--dur-ui) var(--ease)}
.seg label span{display:block;font-size:12.5px;font-weight:500;color:var(--muted);margin-top:3px}
.seg input:checked + label{background:var(--accent-wash);border-color:var(--accent);color:var(--accent)}
.seg input:checked + label span{color:var(--accent)}
/* The input is visually hidden, so focus has to be drawn on the label or
   a keyboard user cannot see where they are (WCAG 2.4.7). */
.seg input:focus-visible + label{outline:3px solid var(--accent);outline-offset:2px}

.book .note{margin-top:16px;font-size:13.5px;color:var(--muted);line-height:1.6}

@media (max-width:900px){ .book .grid{grid-template-columns:1fr;gap:34px} }
@media (max-width:420px){ .seg{grid-template-columns:1fr} }

/* ---------- FAQ ----------------------------------------------------- */
.faq{max-width:820px;margin-top:40px}
.faq details{border-bottom:1px solid var(--line)}
.faq summary{
  list-style:none;cursor:pointer;padding:22px 44px 22px 0;position:relative;
  font-size:18px;font-weight:700;color:var(--ink);letter-spacing:-.015em;
}
.faq summary::-webkit-details-marker{display:none}
.faq summary::after{
  content:"";position:absolute;right:6px;top:50%;width:11px;height:11px;
  border-right:2.2px solid var(--accent);border-bottom:2.2px solid var(--accent);
  transform:translateY(-70%) rotate(45deg);transition:transform .28s var(--ease);
}
.faq details[open] summary::after{transform:translateY(-25%) rotate(-135deg)}
.faq .ans{padding:0 0 24px;color:var(--body);font-size:16.5px}
.faq .ans p + p{margin-top:12px}

/* ---------- contact band -------------------------------------------- */
.band{background:var(--deep);color:var(--on-deep)}
.band h2{color:var(--on-deep)}
.band .grid{display:grid;grid-template-columns:1.05fr .95fr;gap:60px;align-items:center}
.band p{color:var(--on-deep-dim)}
.band-cta{display:flex;flex-wrap:wrap;gap:13px;margin-top:32px}
.band-list{display:grid;gap:2px}
.band-list div{display:flex;justify-content:space-between;gap:24px;
  padding:15px 0;border-bottom:1px solid rgba(255,255,255,.14);font-size:16px}
.band-list dt{color:var(--on-deep-dim)}
.band-list dd{color:var(--on-deep);font-weight:600;text-align:right}
.band-list a{color:var(--on-deep);text-decoration:none;border-bottom:1px solid rgba(255,255,255,.35)}
@media (max-width:900px){ .band .grid{grid-template-columns:1fr;gap:36px} }

/* ---------- footer --------------------------------------------------- */
footer{background:var(--paper-2);border-top:1px solid var(--line);padding:60px 0 40px}
.foot{display:grid;grid-template-columns:1.4fr 1fr 1fr;gap:40px}
/* h3 rather than h4: the preceding section heading is an h2, and
   jumping to h4 skips a level, which a screen reader reports as a
   broken outline. Lighthouse flagged exactly this. */
.foot h3{font-size:13px;font-weight:700;letter-spacing:.03em;
  color:var(--muted);margin-bottom:14px;line-height:1.4}
.foot p,.foot a{font-size:15.5px;color:var(--body);text-decoration:none;line-height:1.75}
.foot ul{list-style:none}
.foot .fbrand{font-size:19px;font-weight:800;color:var(--ink);letter-spacing:-.025em;margin-bottom:10px}
.foot-end{margin-top:44px;padding-top:24px;border-top:1px solid var(--line);
  display:flex;flex-wrap:wrap;gap:14px;justify-content:space-between;
  font-size:14px;color:var(--muted)}
.foot-end a{color:var(--muted);text-decoration:underline;text-underline-offset:3px}
@media (max-width:820px){ .foot{grid-template-columns:1fr;gap:30px} }

/* ---------- legal page ---------------------------------------------- */
.doc{max-width:760px;padding:56px 0 96px}
.doc h1{font-size:clamp(32px,4.6vw,46px);margin-bottom:14px}
.doc h2{font-size:22px;margin:44px 0 12px}
.doc p,.doc li{font-size:16.5px;color:var(--body)}
.doc ul{margin:12px 0 0 20px}
.doc li{margin-bottom:8px}
.doc .back{display:inline-flex;align-items:center;gap:8px;margin-bottom:30px;
  font-size:15px;font-weight:600;color:var(--accent);text-decoration:none}
.doc .updated{font-size:14.5px;color:var(--muted)}

/* ---------- hover, gated ---------------------------------------------
   A touch device fires :hover on tap and leaves it applied until the
   next tap elsewhere, so an ungated hover state reads as a stuck
   selection to the person using it. Fine pointers only. Same treatment
   as the two pharmacy builds in this folder. */
@media (hover:hover) and (pointer:fine){
  .nav-links a:hover{color:var(--accent)}
  .btn-solid:hover{background:var(--accent-deep)}
  .btn-line:hover{border-color:var(--accent);color:var(--accent)}
  .btn-onDeep:hover{background:var(--on-photo)}
  .btn-onDeepLine:hover{border-color:var(--on-deep)}
  .band-list a:hover{border-bottom-color:var(--on-deep)}
  .foot a:hover{color:var(--accent)}
  .foot-end a:hover{color:var(--accent)}
  .doc .back:hover{text-decoration:underline}
  .seg label:hover{border-color:var(--accent)}
}

/* ===========================================================================
   COOKIE NOTICE, added 30 Aug 2026. Built by main.js so the wording cannot
   drift between index.html and legal.html. This site sets nothing of its own
   and embeds no map, so the notice says exactly that rather than inventing a
   choice. Uses this build's own tokens.
   =========================================================================== */
.cookiebar{
  position:fixed; z-index:60;
  left:clamp(.5rem,2vw,1.25rem); right:clamp(.5rem,2vw,1.25rem);
  bottom:clamp(.5rem,2vw,1.25rem);
  max-width:46rem; margin-inline:auto;
  display:flex; flex-wrap:wrap; align-items:center; gap:.85rem 1.15rem;
  padding:1rem 1.15rem;
  background:var(--card,#fff);
  border:1px solid var(--line,#dfe3ea);
  border-left:3px solid var(--accent,#1b7f79);
  border-radius:12px;
  box-shadow:0 30px 64px -30px rgba(12,22,40,.45);
  transform:translateY(130%); opacity:0;
  transition:transform .42s cubic-bezier(.16,1,.3,1), opacity .42s cubic-bezier(.16,1,.3,1);
}
.cookiebar--in{transform:none; opacity:1}
.cookiebar--out{transform:translateY(130%); opacity:0}
.cookiebar__text{
  flex:1 1 20rem; margin:0;
  font-family:inherit; font-size:.88rem; line-height:1.55; color:var(--body,#41506a);
}
.cookiebar__text strong{color:var(--ink,#0f1b2e); font-weight:700}
.cookiebar__text a{color:var(--accent,#1b7f79); font-weight:600}
.cookiebar__btn{
  flex:none; display:inline-flex; align-items:center; justify-content:center;
  min-height:44px; min-width:44px; padding:.6rem 1.4rem;
  border:0; border-radius:999px; cursor:pointer;
  background:var(--accent,#0B6E70); color:var(--on-accent,#fff);
  font-family:inherit; font-size:.82rem; font-weight:700;
  transition:transform .16s ease-out, filter .2s ease-out;
}
.cookiebar__btn:hover{filter:brightness(1.08)}
.cookiebar__btn:active{transform:scale(.97)}
.cookiebar__btn:focus-visible{outline:2px solid var(--ink,#0f1b2e); outline-offset:3px}
@media (prefers-reduced-motion:reduce){
  .cookiebar{transition:none; transform:none; opacity:1}
  .cookiebar--out{display:none}
}

/* Credit line, added 30 Aug 2026, to match the other four personal builds. */
.byline{
  margin:18px 0 0; text-align:center;
  font-size:13px; font-weight:500; letter-spacing:.02em;
  color:var(--on-deep-dim,#A9C0CE);
}

/* The map in the visit band, added 30 Aug 2026. */
.mapwrap{
  margin-top:34px;
  position:relative; width:100%;
  aspect-ratio:16/7; min-height:260px;
  border-radius:16px; overflow:hidden;
  border:1px solid rgba(255,255,255,.14);
  background:var(--deep);
}
.mapwrap iframe{position:absolute; inset:0; width:100%; height:100%; border:0}
@media (max-width:640px){ .mapwrap{aspect-ratio:4/3; min-height:280px} }

/* ===========================================================================
   SUB-PAGES: about.html and faq.html, added 30 Aug 2026.
   Built from the same tokens as the home page so the two new pages read as
   part of the same site rather than as bolt-ons.
   =========================================================================== */
.pagehead{padding:calc(var(--nav-h,72px) + 54px) 0 34px; background:var(--paper-2); border-bottom:1px solid var(--line)}
.pagehead h1{margin:0; font-size:clamp(30px,4.4vw,50px); line-height:1.08; letter-spacing:-.02em; color:var(--ink)}
.pagehead .lede{margin:14px 0 0; max-width:56ch; color:var(--body); font-size:clamp(15.5px,1.15vw,17.5px); line-height:1.65}

.pageprose{padding:44px 0 10px}
.pageprose h2{margin:38px 0 12px; font-size:clamp(20px,2.1vw,27px); letter-spacing:-.012em; color:var(--ink)}
.pageprose h2:first-child{margin-top:0}
.pageprose p{margin:0 0 15px; max-width:66ch; color:var(--body); line-height:1.7}
.pageprose a{color:var(--accent); font-weight:600}

.factlist{margin:22px 0 6px; display:grid; gap:0; max-width:44rem}
.factlist > div{display:grid; grid-template-columns:minmax(9rem,auto) 1fr; gap:1rem;
  padding:13px 0; border-bottom:1px solid var(--line)}
.factlist dt{margin:0; color:var(--muted); font-size:13px; font-weight:600; letter-spacing:.04em; text-transform:uppercase}
.factlist dd{margin:0; color:var(--ink); font-weight:600}

.faq{margin-top:0}   /* the sub-page supplies its own top spacing */
.pagecta{margin:46px 0 0; padding:44px 0 54px; background:var(--deep); color:var(--on-deep); text-align:center}
.pagecta h2{margin:0 0 10px; font-size:clamp(22px,2.6vw,32px); letter-spacing:-.015em; color:var(--on-deep)}
.pagecta p{margin:0 auto 22px; max-width:44ch; color:var(--on-deep-dim)}
.pagecta .btn-solid{background:var(--on-deep); color:var(--deep)}
@media (max-width:640px){
  .factlist > div{grid-template-columns:1fr; gap:.2rem}
}

/* Skip link, added 30 Aug 2026. Off-screen until it takes keyboard focus,
   which is the whole point: the first Tab press on any page should offer a
   way past the navigation. It was added unstyled with the sub-pages and
   rendered as a visible link in the corner until this rule landed. */
.skip{
  position:absolute; left:10px; top:-64px; z-index:200;
  padding:11px 16px; border-radius:0 0 10px 10px;
  background:var(--accent); color:var(--on-accent);
  font-family:inherit; font-size:14px; font-weight:700; text-decoration:none;
  transition:top .18s cubic-bezier(.16,1,.3,1);
}
.skip:focus{top:0}

/* ---------------------------------------------------------------------------
   SINGLE MODE, committed 30 Aug 2026.
   This site used to carry a full light palette AND a dark override, while its
   hero stayed dark in both. In light mode a reader scrolled from a dark,
   art-directed hero straight into a near-white page - two sites stapled
   together. His words: "in light mode it's completely garbage".
   The dark palette is now the only palette, the way another build has always been.
   Do not reintroduce a prefers-color-scheme block here.
   --------------------------------------------------------------------------- */

/* Concept-site additions. */
.map-stub{padding:28px 24px;border:1px solid currentColor;opacity:.75;font-size:.95em}
