/* ─── Graham Piano Tuning — component styles ─────────────────────────────────
   Every color, font, radius and spacing value comes from tokens.css. This
   file is the layout and the components; a rebrand is a tokens.css change.

   Design notes (from the Claude Design mockups, "Classical" system):
   · Navy ink on a near-white ground, slate as the accent, tan and sand as
     fills. Only navy and slate may carry small text on a light ground.
   · Cards are white and lifted, shadows tinted navy. Sections alternate
     ground and surface, because five identical sections in a row read as
     one long sheet however good the contents are.
   · Libre Baskerville headings, Libre Franklin body at 18px and labels.
   ·   Both are named in tokens.css and nowhere else. Many customers are
     older or on a phone: 16px minimum anywhere, 48px tap targets.
   · Two dark bands (stats, final CTA) are the only large fills on the site.
   ────────────────────────────────────────────────────────────────────────── */

*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
body {
  margin: 0; background: var(--color-bg); color: var(--color-text);
  font-family: var(--font-body); font-size: var(--font-size-body); line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
/* ── Why the text is drawn in its own layer ─────────────────────────────────
   WHAT THE PROBLEM WAS. Windows renders text with ClearType: instead of one
   grey pixel at the edge of a letter it lights the red, green and blue
   subpixels separately to fake extra resolution. That is why a column of
   navy text on white measured about 30% warm pixels on Michael's machine,
   an orange fringe down one side of every glyph and a blue one down the
   other, which at reading size the eye blends into pink.

   -webkit-font-smoothing above is the macOS switch for this and does nothing
   at all on Windows. There is no CSS property that turns ClearType off. The
   one thing that does is putting the text in its own compositing layer,
   because a layer is composited without knowing what is behind it, so the
   browser has to fall back to plain grey antialiasing.

   WHERE IT IS SAFE TO DO THAT. A transform makes the element the containing
   block for any position:fixed descendant, which would break the mobile call
   bar and the add-on dialog, so this is applied to page-level blocks that
   hold no fixed children rather than to body. The blocks are listed rather
   than inherited for that reason: adding one is a decision, not an accident.

   THE COST, honestly: text in a layer is a shade lighter, and at some zoom
   levels a shade softer. Michael compared the two side by side on Windows
   and picked this. */
main, .topbar, .nav, .footer, .mobile-bar, .booking-footer, .modal { transform: translateZ(0); }
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; }
}
img { max-width: 100%; height: auto; display: block; }
a { color: var(--color-accent-700); text-decoration-thickness: 1px; text-underline-offset: 3px; }
a:hover { color: var(--color-accent-600); }
:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
::selection { background: var(--color-accent-200); }

/* LIBRE BASKERVILLE IS WIDE AND HAS A BIG X-HEIGHT, so it reads larger than
   most serifs at the same pixel size and headings run to more lines. The
   scale below is cut about 10% from where it sat, and the tracking is barely
   negative rather than properly tight: squeezing a Baskerville closes up the
   counters and it stops being readable, which is the whole reason to pick
   one. Line height is looser for the same reason. */
h1, h2, h3, h4 {
  font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  line-height: 1.18; margin: 0 0 var(--space-3); letter-spacing: -0.004em;
  text-wrap: balance;
}
/* The gap between levels stays wide: a page reads as flat when every heading
   is roughly the size of the last. */
h1 { font-size: clamp(33px, 5.6vw, 52px); }
h2 { font-size: clamp(24px, 3.5vw, 33px); }
h3 { font-size: clamp(19px, 2.7vw, 23px); }
h4 { font-size: 18px; }
p { margin: 0 0 var(--space-4); }
/* SECONDARY TEXT IS BLUE, NOT GREY, and that is a rendering decision as much
   as a design one. Windows draws text with ClearType, which lights the red
   and blue subpixels at the edge of every glyph; on a neutral grey, which
   has no hue of its own, those fringes have nothing to sink into and the
   whole column reads pink. A slate with real colour in it absorbs them. It
   is also 6.2:1 on the page ground, so it carries small text honestly. */
.text-muted { color: var(--color-muted); }
.small { font-size: 16px; }
.tnum { font-variant-numeric: tabular-nums lining-nums; font-feature-settings: "tnum" 1, "lnum" 1; }

/* The small uppercase kicker above headings ("Services", "Why us") */
.kicker {
  display: flex; align-items: center; gap: var(--space-2); margin: 0 0 var(--space-3);
  font-family: var(--font-label); font-size: 12px; letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--color-accent-700); font-weight: var(--font-label-weight);
}
.kicker::before { content: ""; width: 32px; height: 1px; background: var(--color-accent); flex: none; }
.kicker.center { justify-content: center; }

/* ─── Layout ─────────────────────────────────────────────────────────────── */
.wrap { max-width: 1080px; width: 100%; margin: 0 auto; padding-inline: var(--space-4); }
.wrap-narrow { max-width: 760px; }
/* SECTIONS ALTERNATE GROUND NOW. The home page ran five consecutive sections
   on the same ivory, which the eye reads as one long sheet of paper however
   good the contents are. Ground, surface, ground, navy: the rhythm is what
   creates depth, not the colours themselves. A band that sets its own
   background drops the divider, because a colour change is already a
   stronger boundary than a hairline. */
.section { padding-block: clamp(40px, 7vw, 72px); border-top: 1px solid var(--color-divider); }
.section.first { border-top: 0; }
.section.band-surface { background: var(--color-surface); border-top: 0; }
.section.band-surface + .section { border-top: 0; }
.grid { display: grid; gap: var(--space-6); }
.grid-2 { grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }
.grid-3 { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
.hr { border: 0; border-top: 1px solid var(--color-divider); margin: var(--space-6) 0; }
.stack > * + * { margin-top: var(--space-3); }
.row { display: flex; gap: var(--space-3); flex-wrap: wrap; align-items: center; }
.between { justify-content: space-between; }

/* ─── Top bar + nav ──────────────────────────────────────────────────────── */
.topbar {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); flex-wrap: wrap;
  padding: var(--space-2) var(--space-4); border-bottom: 1px solid var(--color-divider); font-size: 15px;
}
.topbar .links { display: flex; gap: var(--space-4); }
.topbar a { font-weight: 500; text-decoration: none; }
.topbar a:hover { text-decoration: underline; }
/* The tagline's link reads as part of the sentence, not as navigation: it is
   underlined rather than coloured, so the bar stays a strip of text. */
.topbar .tagline a { color: inherit; font-weight: inherit; text-decoration: underline; text-underline-offset: 2px; }

/* A link that moves you down the page rather than to another one. Carried on
   a line that is already there, so it adds no element and no row. */
.jump-link { white-space: nowrap; }
/* A contents row for a long page. Quiet enough to skip, close enough to the
   top that nobody has to scroll a price list to find out what else is on it. */
.page-jump { display: flex; flex-wrap: wrap; gap: 8px 20px; margin: var(--space-4) 0 0; font-size: 16px; }
.page-jump a { text-decoration: none; border-bottom: 1px solid var(--color-divider); padding-bottom: 2px; }
.page-jump a:hover { border-bottom-color: currentColor; }
.page-jump a::after { content: " \2193"; opacity: 0.55; }
/* Same reason as #area on the home page: a heading pinned to the very top
   edge after a jump reads as a broken page. */
#cleaning, #repairs, #assessments, #gift, #tax-exempt { scroll-margin-top: var(--space-4); }
.jump-link::after { content: " \2193"; }          /* a down arrow: it says "further down this page" */
/* Landing on a heading pinned to the very top edge reads as a broken page. */
#area { scroll-margin-top: var(--space-4); }

.nav {
  display: flex; align-items: center; gap: var(--space-4); flex-wrap: wrap; row-gap: var(--space-2);
  padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--color-divider);
  position: relative; z-index: 20; background: var(--color-bg);
}
.nav-brand {
  display: inline-flex; align-items: center; gap: 10px; text-decoration: none; color: var(--color-text);
  font-family: var(--font-heading); font-weight: var(--font-heading-weight); font-size: 21px; line-height: 1;
}
.nav-brand img { width: 42px; height: 42px; }
.nav-links { display: contents; }
.nav-links a { font-size: 16px; text-decoration: none; color: var(--color-text); padding: 6px 2px; position: relative; }
.nav-links a:not(.btn):hover, .nav-links a.active:not(.btn) { color: var(--color-accent-700); }
.nav-links a.active:not(.btn)::after { content: ""; position: absolute; left: 2px; right: 2px; bottom: 0; height: 1px; background: var(--color-accent); }
.nav-spacer { margin-left: auto; }
.nav-toggle {
  display: none; align-items: center; justify-content: center; min-height: 44px; min-width: 44px; margin-left: auto;
  border: 1px solid var(--color-divider); border-radius: var(--radius-md); background: transparent;
  font-family: var(--font-body); font-size: 16px; cursor: pointer; color: var(--color-text); padding: 0 14px;
}
@media (max-width: 860px) {
  .nav-toggle { display: inline-flex; }
  .nav-links {
    display: none; width: 100%; flex-direction: column; align-items: stretch; gap: 2px;
    border-top: 1px solid var(--color-divider); padding-top: var(--space-2); margin-top: var(--space-2);
  }
  .nav-links.open { display: flex; }
  .nav-links a { padding: 12px 8px; font-size: 18px; }
  .nav-links .btn { margin-top: var(--space-2); justify-content: center; }
  .nav-spacer { display: none; }
  .topbar .tagline { display: none; }
}

/* Sticky thumb-reach bar on phones */
.mobile-bar { display: none; }
@media (max-width: 640px) {
  body.has-mobile-bar { padding-bottom: 72px; }
  .mobile-bar {
    display: flex; position: fixed; left: 0; right: 0; bottom: 0; z-index: 500;
    background: var(--color-bg); border-top: 1px solid var(--color-divider);
    padding: 8px 12px calc(8px + env(safe-area-inset-bottom)); gap: 8px;
    box-shadow: 0 -3px 10px rgba(45,43,43,0.10);
  }
  .mobile-bar .btn { flex: 1; justify-content: center; min-height: 48px; }
  .mobile-bar .btn-primary { flex: 2; }
}

/* ─── Buttons ────────────────────────────────────────────────────────────── */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  min-height: var(--tap-target-min); padding: 10px 22px; border-radius: var(--radius-md);
  border: 1px solid transparent; background: transparent; cursor: pointer; text-decoration: none;
  font-family: var(--font-body); font-size: 17px; font-weight: 500; line-height: 1.2; color: var(--color-text);
  transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
.btn:disabled { opacity: 0.45; cursor: not-allowed; }
/* A button that has to read as a link: it changes the page rather than
   navigating, but "send it as a gift" is a sentence, not a control, and a
   third button on the review screen would compete with the one that pays. */
.btn-link {
  border: 0; background: none; padding: 0; font: inherit; cursor: pointer;
  color: var(--color-accent-700); text-decoration: underline; text-underline-offset: 3px;
}
.btn-link:hover { color: var(--color-accent-600); }
.btn-primary { color: var(--color-accent-800); border-color: var(--color-accent); }
/* THE ONE BUTTON THAT SHOULD WIN. Everywhere else a primary button is an
   outline, which is right on a page full of them and wrong in a nav bar that
   is otherwise plain links: the thing you most want clicked was the quietest
   thing up there. Filled in navy, which at 16:1 on the ground is the one
   colour nothing else can outshout. */
.nav-links .btn-primary,
.mobile-bar .btn-primary {
  background: var(--color-cta); color: #fff; border-color: var(--color-cta); font-weight: 600;
  /* Wider than an ordinary button on purpose. A filled button with tight
     side padding reads as cramped next to a row of plain links, and this one
     is meant to look like the thing to press. */
  padding-inline: 30px;
}
.nav-links .btn-primary:hover,
.mobile-bar .btn-primary:hover { background: var(--color-cta-hover); border-color: var(--color-cta-hover); }
.btn-primary:hover { background: color-mix(in srgb, var(--color-accent) 12%, transparent); color: var(--color-accent-800); }
.btn-primary:active { background: color-mix(in srgb, var(--color-accent) 22%, transparent); }
.btn-secondary { border-color: var(--color-divider); }
.btn-secondary:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); color: var(--color-text); }
.btn-ghost { color: var(--color-accent-700); padding-inline: var(--space-2); min-height: 40px; }
.btn-ghost:hover { background: color-mix(in srgb, var(--color-accent) 10%, transparent); color: var(--color-accent-700); }
.btn-icon { width: 44px; height: 44px; padding: 0; }
.btn-block { width: 100%; }
.btn-lg { min-height: 54px; font-size: 18px; padding-inline: var(--space-6); }
/* On dark bands */
.on-dark .btn-primary { color: var(--color-navy); background: var(--color-sand); border-color: var(--color-sand); font-weight: 600; }
.on-dark .btn-primary:hover { background: var(--color-warm-300); border-color: var(--color-warm-300); }
.on-dark a { color: var(--color-sand); }

/* ─── Tags, cards, plates, tables ────────────────────────────────────────── */
.tag {
  display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: 999px;
  font-size: 14px; font-weight: 500; line-height: 1.3; white-space: nowrap;
}
.tag-accent { background: var(--color-accent-100); color: var(--color-accent-800); }
.tag-neutral { background: var(--color-neutral-200); color: var(--color-neutral-800); }
.tag-outline { border: 1px solid var(--color-accent); color: var(--color-accent-700); }

/* NOTHING WAS ABOVE ANYTHING ELSE. A card was a hairline box on the same
   ground as the page, which is the definition of flat. It is a white surface
   with a real lift now, and the shadow is navy-tinted so it belongs to this
   palette rather than sitting on top of it. */
.card {
  border: 1px solid var(--color-divider); border-radius: var(--radius-lg); padding: var(--space-6);
  background: #fff; box-shadow: var(--shadow-sm);
  display: flex; flex-direction: column; gap: var(--space-2);
  transition: box-shadow .18s ease, transform .18s ease;
}
/* On a surface-coloured band a white card still reads as lifted; on white it
   would vanish, so the border carries it there. */
.band-surface .card { background: #fff; }
.card.elev-sm { box-shadow: var(--shadow-md); }

/* ── A colour per tile ────────────────────────────────────────────
   A coloured hairline on a white box is the cheapest possible way to say
   "these are different", and it reads as exactly that. The whole tile is the
   colour now: a gradient from the tile hue down into a near-black cut on the
   same hue, the way the printed card does it, so the three prices are three
   objects rather than three paragraphs.

   Every gradient bottoms out at the same lightness as the site navy, so five
   different hues still read as one family. White text clears 8.9:1 on the
   lightest point of any of them and 15.8:1 at the foot; the sand kicker
   clears 5.3:1 at the lightest point. A card with no tile- class is
   untouched and still white.

   The button inverts: white fill, tile text. On a saturated ground a filled
   white button is the only thing that outranks the tile itself, which is
   what a Book button has to do. */
.card[class*="tile-"] {
  background: linear-gradient(160deg, var(--tile) 0%, var(--tile-deep) 55%, var(--tile-900) 100%);
  border-color: var(--tile-900);
  color: #fff;
}
.card[class*="tile-"] .card-title,
.card[class*="tile-"] .card-body,
.card[class*="tile-"] .price,
.card[class*="tile-"] li,
.card[class*="tile-"] p { color: #fff; }
.card[class*="tile-"] .card-kicker { color: var(--color-sand); }
/* "paid at your visit" sits beside the price: present, not competing. */
.card[class*="tile-"] .text-muted { color: rgba(255,255,255,0.78); }
.card[class*="tile-"] li { border-top-color: rgba(255,255,255,0.22); }
.card[class*="tile-"] li::before { color: var(--color-sand); }
.card[class*="tile-"] a:not(.btn) { color: var(--color-sand); }
.card[class*="tile-"] .btn-primary {
  background: #fff; border-color: #fff; color: var(--tile-900); font-weight: 600;
}
.card[class*="tile-"] .btn-primary:hover { background: var(--color-warm-100, #f6ece0); border-color: var(--color-warm-100, #f6ece0); color: var(--tile-900); }
.card[class*="tile-"] .btn-secondary { border-color: rgba(255,255,255,0.55); color: #fff; }
.card[class*="tile-"] .btn-secondary:hover { background: rgba(255,255,255,0.14); color: #fff; }
/* A white card lifts off the page with a shadow; a dark card lifts by getting
   its own shadow in its own colour, or it looks pasted on. */
.card[class*="tile-"] { box-shadow: 0 1px 2px rgba(7,23,57,0.16), 0 10px 22px -12px rgba(7,23,57,0.45); }
.card[class*="tile-"].featured, .card[class*="tile-"]:hover {
  box-shadow: 0 2px 4px rgba(7,23,57,0.2), 0 18px 34px -14px rgba(7,23,57,0.55);
}
/* The season ribbon rode on the old top bar. On a filled tile it needs its
   own ground to stay legible. */
.card[class*="tile-"] .season-flag {
  background: var(--color-sand); color: var(--tile-900); border-color: var(--color-sand);
}
.card[class*="tile-"].featured { border-color: var(--color-sand); }
/* The surface band gave white cards their contrast. Filled tiles supply their
   own, so don't repaint them white there. */
.band-surface .card[class*="tile-"] { background: linear-gradient(160deg, var(--tile) 0%, var(--tile-deep) 55%, var(--tile-900) 100%); }
/* ── The capped tile ────────────────────────────────────────────────────────
   Option B. Twelve filled tiles down one page is more weight than a page
   read top to bottom can carry, and it spends the colour so evenly that the
   three tunings stop leading. So the tunings keep the full fill and every
   other family gets a white tile with a cap in its own colour: teal is a
   cleaning, clay is a repair, plum is an assessment. The code is learned
   once and the page can then be scanned by colour alone.

   These rules come after the filled ones and undo them, so a card carries
   `tile-3 capped` and reads as capped. Colour still comes from the same five
   tokens; only how much of it is used changes. */
.card.capped {
  background: #fff;
  color: var(--color-text);
  border: 1px solid var(--color-divider);
  border-top: 4px solid var(--tile);
  box-shadow: var(--shadow-sm);
}
.card.capped .card-title, .card.capped .card-body, .card.capped li, .card.capped p { color: var(--color-text); }
.card.capped .card-kicker { color: var(--tile-deep); }
.card.capped .price { color: var(--tile-deep); }
.card.capped .text-muted { color: var(--color-muted); }
.card.capped li { border-top-color: var(--color-divider); }
.card.capped li::before { color: var(--tile); }
.card.capped .btn-primary { background: var(--tile-deep); border-color: var(--tile-deep); color: #fff; font-weight: 600; }
.card.capped .btn-primary:hover { background: var(--tile); border-color: var(--tile); color: #fff; }
.card.capped .btn-secondary { border-color: var(--tile); color: var(--tile-deep); background: transparent; }
.card.capped .btn-secondary:hover { background: var(--tile-tint); color: var(--tile-deep); }
.band-surface .card.capped { background: #fff; }
/* The price's second line: what the same job costs added to a tuning. */
.card .price-note { margin: 2px 0 0; font-size: 15px; }
.card.capped.featured { border-color: var(--color-accent); border-top-color: var(--tile); }

.card.tile-1 { --tile: var(--tile-1); --tile-deep: var(--tile-1-deep); --tile-tint: var(--tile-1-tint); --tile-900: var(--tile-1-900); }
.card.tile-2 { --tile: var(--tile-2); --tile-deep: var(--tile-2-deep); --tile-tint: var(--tile-2-tint); --tile-900: var(--tile-2-900); }
.card.tile-3 { --tile: var(--tile-3); --tile-deep: var(--tile-3-deep); --tile-tint: var(--tile-3-tint); --tile-900: var(--tile-3-900); }
.card.tile-4 { --tile: var(--tile-4); --tile-deep: var(--tile-4-deep); --tile-tint: var(--tile-4-tint); --tile-900: var(--tile-4-900); }
.card.tile-5 { --tile: var(--tile-5); --tile-deep: var(--tile-5-deep); --tile-tint: var(--tile-5-tint); --tile-900: var(--tile-5-900); }
@media (prefers-reduced-motion: no-preference) {
  .card:hover { box-shadow: var(--shadow-md); transform: translateY(-2px); }
}
.card-kicker { font-family: var(--font-label); font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-accent-700); font-weight: var(--font-label-weight); }
.card-title { margin: 0; font-size: 24px; }
/* Takes the slack, so everything under it starts at the same height in every
   card of a row. Three services whose descriptions run to one, two and three
   lines used to put three prices at three different heights, and a row of
   prices that does not line up reads as three unrelated offers rather than
   one price list. Only index.html uses .card-body, and only for that row. */
.card-body { margin: 0; color: var(--color-neutral-800); flex: 1 1 auto; }
/* Baskerville is wide, so a 34px price ran long beside its "paid at your
   visit" label. Tabular lining figures asked for both ways, because a price
   column that does not line up is the one type fault a customer notices. */
.price { font-family: var(--font-heading); font-size: 31px; font-weight: var(--font-heading-weight);
  font-variant-numeric: tabular-nums lining-nums; font-feature-settings: "tnum" 1, "lnum" 1; line-height: 1; }
.price-row { display: flex; align-items: baseline; gap: var(--space-2); margin-top: var(--space-2); }
.card ul, .checklist { margin: var(--space-2) 0 var(--space-3); padding-left: 0; list-style: none; }
.card li, .checklist li { padding: 6px 0 6px 26px; position: relative; border-top: 1px solid var(--color-divider); }
.card li:first-child, .checklist li:first-child { border-top: 0; }
.card li::before, .checklist li::before { content: "✓"; position: absolute; left: 4px; color: var(--color-accent-600); font-weight: 600; }
.card .btn { margin-top: auto; }

.plate {
  border: 6px solid var(--color-surface); border-radius: var(--radius-sm); box-shadow: var(--shadow-sm);
  overflow: hidden; background: var(--color-neutral-200); position: relative;
  /* An empty slot sets both a shape and a floor height, and between about
     650 and 720px wide the floor won the argument: 220px tall at 4:3 wants
     293px of width, the grid column was 211px, and the plate quietly pushed
     the whole page sideways. Nothing may grow past its column. */
  max-width: 100%;
}
.plate img { width: 100%; height: 100%; object-fit: cover; }
.plate.ratio-4x3 { aspect-ratio: 4 / 3; }
.plate.ratio-3x4 { aspect-ratio: 3 / 4; }
.plate.ratio-1x1 { aspect-ratio: 1 / 1; }
.plate.placeholder { display: grid; place-items: center; text-align: center; padding: var(--space-4); color: var(--color-neutral-700); font-size: 15px; min-height: 220px; }
.plate .credit { position: absolute; right: 6px; bottom: 6px; font-size: 11px; background: rgba(243,242,242,0.85); padding: 2px 6px; border-radius: 2px; }

.table { width: 100%; border-collapse: collapse; font-size: 17px; }
.table th { text-align: left; font-weight: 500; font-size: 14px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--color-neutral-700); padding: 10px 12px 10px 0; border-bottom: 1px solid var(--color-text); }
.table td { padding: 12px 12px 12px 0; border-bottom: 1px solid var(--color-divider); vertical-align: top; }
.table td:last-child, .table th:last-child { text-align: right; padding-right: 0; white-space: nowrap; font-variant-numeric: tabular-nums; }
.table .price-cell { font-family: var(--font-heading); font-size: 22px; font-weight: 600; }
.table-wrap { overflow-x: auto; }
/* ── A table that has to live in half a page ────────────────────────────────
   .table is built for a full-width column: uppercase headings, a 22px
   Baskerville price, and a last column that never wraps. Put that in one
   half of a two-column section and the headings stack, the last one runs off
   the edge and the whole thing grows a sideways scrollbar. Compact mode
   keeps the same table and lets it fit: the headings wrap like words, the
   price steps down a size, and the row's own detail sits under its name
   rather than claiming a column of its own. */
/* A HEADING IS NOT A PRICE. .price-cell carries the Baskerville that makes a
   number look like a number, and putting that class on a th to line the
   column up dragged the serif along with it: "CLEANING ONLY" came out in
   caps in the heading face, which is why the table read as borrowed from
   another site. Headings take the label face here whatever else they carry. */
.table-compact th, .table-compact th.price-cell {
  font-family: var(--font-label); font-weight: var(--font-label-weight);
  white-space: normal; font-size: 13px; letter-spacing: 0.06em; vertical-align: bottom;
}
.table-compact td:last-child, .table-compact th:last-child { white-space: normal; }
/* Both money columns behave the same way. .table right-aligns only the last
   column, which left one price left-aligned under a left-aligned heading and
   the other right-aligned, so the two numbers a customer is comparing did
   not sit above each other. */
.table-compact .price-cell { font-size: 20px; text-align: right; width: 23%; }
.table-compact td .small { display: inline-block; line-height: 1.35; }
.table-compact td:first-child { padding-right: var(--space-3); }

/* ── The services band on the home page ────────────────────────────────────
   Centred end to end: the kicker, the heading, what is inside each of the
   three tiles, and the two lines under them. The tiles themselves already
   share a row, so centring their contents is what makes the block read as
   one object on the page rather than three left-aligned columns with a
   left-aligned heading above them.

   The comparison chart is the exception and stays left: a table whose rows
   are centred is a table nobody can read down. It is centred as a block
   instead, which is a different thing. */
.services-band { text-align: center; }
.services-band .card { align-items: center; }
.services-band .card .price-row { justify-content: center; }
.services-band .card .btn { width: 100%; }
/* A heading centred on a 66ch measure still hangs left if the measure is
   wider than the words; the measure itself has to be centred too. */
.services-band h2, .services-band > .wrap > p { margin-inline: auto; }
.services-band .compare-wrap { text-align: left; }

/* ── The comparison chart ───────────────────────────────────────────────────
   Three cards say what each service is; this says which one to pick. The
   first column is the question, the other three are the answers, and the
   chips repeat the card colours so a reader coming down the page knows which
   column belongs to which card without a label saying so.

   .table's last-child rule right-aligns the final column, which would leave
   one of three answer columns hanging off on its own; the chart overrides it
   so all three read as one set. */
.compare th[scope="row"] { font-weight: 500; text-transform: none; letter-spacing: 0; font-size: 16px;
  color: var(--color-neutral-800); border-bottom: 1px solid var(--color-divider); vertical-align: top;
  padding: 12px 20px 12px 0; white-space: nowrap; }
.compare thead th { vertical-align: bottom; white-space: nowrap; }
/* Equal answer columns on a desktop. With the table sizing itself to its
   content, one long answer stole the width and left the other two columns
   narrow and unaligned, which is the one thing a comparison chart must not
   do. Fixed only above the phone breakpoint: below it the table is wider
   than the screen on purpose and scrolls, and a fixed layout would squeeze
   it back into the viewport instead. */
@media (min-width: 721px) {
  .compare { table-layout: fixed; }
  .compare th[scope="row"] { width: 25%; }
}
/* The answers are centred under their own column heading; only the row
   labels stay left, because a column of left-aligned questions is what the
   eye runs down to find its row. .table's own last-child rule would right
   align the final column on its own, which would leave one of three answers
   behaving differently from the other two. */
.compare td, .compare thead th + th { text-align: center; }
.compare td:last-child, .compare th:last-child { text-align: center; padding-right: 12px; white-space: normal; }
.compare .price-cell { color: var(--color-text); }
.compare .swatch { display: inline-block; width: 10px; height: 10px; border-radius: 3px; margin-right: 7px;
  vertical-align: 1px; background: var(--swatch); }
.compare .swatch-1 { --swatch: var(--tile-1); }
.compare .swatch-2 { --swatch: var(--tile-2); }
.compare .swatch-3 { --swatch: var(--tile-3); }
/* A tick and a dash carry the row faster than the words do, but the words
   stay for anyone listening to the page rather than looking at it. */
.compare .yes, .compare .no { font-size: 0; }
.compare .yes::before { content: "\2713"; font-size: 19px; color: var(--color-accent-600); font-weight: 600; }
.compare .no::before { content: "\2014"; font-size: 19px; color: var(--color-neutral-600); }
/* ── The chart on a phone ───────────────────────────────────────────────────
   A four-column table does not fit a phone, and the two usual answers are
   both bad. Scrolling it sideways hides two of the three services behind an
   edge most people never discover, and stacking it into one card per service
   just repeats the three tiles sitting directly above it, which is the one
   thing the chart exists NOT to be.

   So the table keeps its four columns of meaning and loses its shape: the
   question moves onto its own line, and its three answers sit side by side
   underneath it, in the same order and the same colours as the tiles. Every
   comparison stays on screen, nothing scrolls, and the markup does not
   change, so a screen reader still reads a table and the desktop layout is
   untouched.

   The head row sticks, because once the questions are inside the rows the
   only thing naming the three columns is up there. It can stick because the
   wrapper stops being a scroll container down here; a scroll container is
   what breaks position:sticky against the viewport. */
@media (max-width: 720px) {
  .compare-wrap { overflow: visible; }
  .compare { font-size: 15px; }
  .compare thead, .compare tbody, .compare tr, .compare th, .compare td { display: block; }
  .compare thead {
    position: sticky; top: 0; z-index: 2;
    background: var(--color-surface); box-shadow: 0 1px 0 var(--color-text);
  }
  .compare thead tr, .compare tbody tr { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0 8px; }
  /* The empty corner cell has nothing to label down here. */
  .compare thead th:first-child { display: none; }
  .compare thead th {
    font-size: 11px; letter-spacing: 0.04em; padding: 8px 0; text-align: center;
    white-space: normal; border-bottom: 0;
  }
  .compare .swatch { display: block; margin: 0 auto 3px; }
  /* The question, on its own line, in the label type the rest of the site
     uses for exactly this job. */
  .compare th[scope="row"] {
    grid-column: 1 / -1; white-space: normal; border-bottom: 0;
    font-family: var(--font-label); font-size: 11px; font-weight: var(--font-label-weight);
    letter-spacing: 0.14em; text-transform: uppercase; color: var(--color-neutral-700);
    padding: 14px 0 6px;
  }
  .compare tbody tr { border-bottom: 1px solid var(--color-divider); padding-bottom: 10px; }
  .compare tbody tr:last-child { border-bottom: 0; }
  .compare td { padding: 0 0 2px; border-bottom: 0; text-align: center; hyphens: auto; }
  .compare .price-cell { font-size: 24px; }
  /* One row carries a sentence rather than an answer, and a sentence in a
     third of a phone is a very tall column. It gets its own size rather than
     shortening the sentence, which is a real qualifier customers need. */
  .compare tbody tr:last-child td { font-size: 13px; line-height: 1.35; }
}

/* ─── Forms ──────────────────────────────────────────────────────────────── */
.field { display: flex; flex-direction: column; gap: 8px; margin-bottom: var(--space-4); }
.field > label, .field-label { font-weight: 500; font-size: 17px; }
.field .hint { font-size: 15px; color: var(--color-neutral-700); margin: 0; }
.input {
  width: 100%; min-height: var(--tap-target-min); padding: 10px 14px; font: inherit; font-size: 18px; color: var(--color-text);
  background: var(--color-neutral-100); border: 1px solid color-mix(in srgb, var(--color-text) 30%, transparent); border-radius: var(--radius-md);
}
.input:hover { border-color: color-mix(in srgb, var(--color-text) 50%, transparent); }
.input:focus-visible { border-color: var(--color-accent); outline-offset: 0; }
.input[aria-invalid="true"] { border-color: #b3261e; }
textarea.input { min-height: 100px; resize: vertical; }
select.input { appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='none' stroke='%23201f1d' stroke-width='1.5'%3E%3Cpath d='M4 6l4 4 4-4'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 12px center; padding-right: 40px; }
.two-col { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-3); }

/* Consent checkbox: big enough to hit with a thumb, and the disclosure right
   under it where a carrier reviewer (and the customer) will actually see it. */
.checkbox-label { display: flex; align-items: flex-start; gap: 12px; cursor: pointer; font-weight: 500; line-height: 1.45; }
.checkbox-label input[type="checkbox"] { width: 24px; height: 24px; margin: 0; flex: none; accent-color: var(--color-accent-700); cursor: pointer; }
.field.consent { border: 1px solid var(--color-divider); border-radius: var(--radius-md); padding: var(--space-3) var(--space-4); background: var(--color-neutral-100); }
.field.consent .hint { margin-top: 8px; }
@media (max-width: 520px) { .two-col { grid-template-columns: 1fr; } }

/* Chips: the tappable options in the booking flow */
.chips { display: flex; flex-wrap: wrap; gap: 8px; }
.chip {
  min-height: var(--tap-target-min); padding: 10px 16px; font-family: var(--font-body); font-size: 16px; cursor: pointer;
  border-radius: var(--radius-md); border: 1px solid var(--color-divider); color: var(--color-text); background: transparent;
  transition: background 0.12s ease, border-color 0.12s ease;
}
.chip:hover { background: color-mix(in srgb, var(--color-text) 5%, transparent); }
.chip[aria-pressed="true"], .chip.selected { border-color: var(--color-accent); color: var(--color-accent-800); background: var(--color-accent-100); font-weight: 500; }

/* Selectable option cards (services, payment) */
.option {
  display: block; width: 100%; text-align: left; cursor: pointer; padding: var(--space-3) var(--space-4);
  font-family: var(--font-body); font-size: 17px; color: var(--color-text); border-radius: var(--radius-md);
  border: 1px solid var(--color-divider); background: transparent; transition: background 0.12s ease, border-color 0.12s ease;
}
.option:hover { background: color-mix(in srgb, var(--color-text) 4%, transparent); }
.option[aria-pressed="true"] { border-color: var(--color-accent); background: var(--color-accent-100); }
.option .opt-kicker { display: block; font-size: 13px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-accent-700); margin-bottom: 4px; }
.option .opt-head { display: flex; justify-content: space-between; gap: var(--space-3); align-items: baseline; }
.option .opt-name { font-weight: 500; font-size: 18px; }
.option .opt-price { font-family: var(--font-heading); font-size: 22px; font-weight: 600; white-space: nowrap; }
.option .opt-desc { display: block; margin-top: 4px; color: var(--color-neutral-700); font-size: 16px; }
.option-list { display: grid; gap: 10px; }
/* Price and length stacked at the right, because they are the two things
   being compared down the column and they should line up with each other. */
.option .opt-figures { display: flex; flex-direction: column; align-items: flex-end; gap: 1px; }
.option .opt-meta { font-size: 13px; color: var(--color-neutral-700); white-space: nowrap; }
/* A chosen option used to differ from its neighbours by a border colour, on
   a screen where the border colours are already doing other work. A tick
   says it in the one place nothing else is using. */
.option { position: relative; }
.option[aria-pressed="true"]::after {
  content: "\2713"; position: absolute; top: 8px; right: 10px;
  font-size: 15px; font-weight: 700; color: var(--color-accent-700); line-height: 1;
}
.option[aria-pressed="true"] .opt-head { padding-right: 18px; }
/* The three tunings, in the colours they wear on the home page and the
   services page. A 4px rail and nothing else: enough for the eye to connect
   the two pages, not enough to compete with the tick. */
.option[class*="opt-tone-"] { border-left-width: 4px; border-left-color: var(--tone); }
.option.opt-tone-1 { --tone: var(--tile-1); }
.option.opt-tone-2 { --tone: var(--tile-2); }
.option.opt-tone-3 { --tone: var(--tile-3); }
.opt-footnote { margin-top: var(--space-4); }
/* The one option on a tab that is there to be compared against the rest. */
.option.option-feature { border-color: var(--color-accent); border-left-width: 4px; border-left-color: var(--color-accent); }
/* A line of explanation above a list of choices, quieter than a .notice. */
.notice-inline {
  margin: 0 0 var(--space-3); padding: 10px var(--space-3); font-size: 16px;
  border-left: 3px solid var(--color-accent); background: var(--color-neutral-100);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0; color: var(--color-neutral-800);
}

/* Piano type picker */
.piano-types { display: grid; grid-template-columns: repeat(auto-fill, minmax(118px, 1fr)); gap: 8px; }
.piano-type {
  display: flex; flex-direction: column; align-items: center; gap: 4px; text-align: center; padding: 12px 6px; cursor: pointer;
  font-family: var(--font-body); color: var(--color-text); border-radius: var(--radius-md); border: 1px solid var(--color-divider); background: transparent;
}
.piano-type svg { color: var(--color-neutral-700); }
/* A real photo of that size of piano, when one has been uploaded. It fills
   the top of the tile edge to edge rather than floating in the middle like
   the drawing does, because a photograph inset with a gap around it reads as
   a picture of a picture. The drawing stays the fallback, so a half-filled
   set of slots still looks deliberate. */
.piano-type.has-photo { padding: 0 0 10px; overflow: hidden; }
.piano-type .pt-photo { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; display: block; margin-bottom: 8px; }
.piano-type.has-photo .pt-name, .piano-type.has-photo .pt-sub { padding-inline: 6px; }
.piano-type .pt-name { font-size: 16px; font-weight: 500; }
.piano-type .pt-sub { font-size: 13px; color: var(--color-neutral-700); }
.piano-type[aria-pressed="true"] { border-color: var(--color-accent); background: var(--color-accent-100); }
.piano-type[aria-pressed="true"] svg { color: var(--color-accent-700); }

/* ─── Notices ────────────────────────────────────────────────────────────── */
.notice {
  border: 1px solid var(--color-accent); border-left-width: 4px; border-radius: var(--radius-md);
  padding: var(--space-3) var(--space-4); background: var(--color-accent-100); margin-bottom: var(--space-4); font-size: 17px;
}
.notice p:last-child { margin-bottom: 0; }
.notice.neutral { border-color: var(--color-divider); background: var(--color-neutral-100); }
.notice.error { border-color: #b3261e; background: #fbeae8; }
.notice .row { margin-top: var(--space-3); }

/* ─── Details / FAQ ──────────────────────────────────────────────────────── */
.faq details { border-top: 1px solid var(--color-divider); }
.faq details:last-child { border-bottom: 1px solid var(--color-divider); }
.faq summary {
  cursor: pointer; list-style: none; padding: 16px 40px 16px 0; font-size: 19px; font-weight: 500; position: relative; min-height: var(--tap-target-min);
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after { content: "+"; position: absolute; right: 4px; top: 12px; font-size: 26px; color: var(--color-accent-700); font-family: var(--font-heading); }
.faq details[open] summary::after { content: "–"; }
.faq details p, .faq details ul { margin: 0 0 18px; color: var(--color-neutral-800); max-width: 70ch; }
details.plain summary { cursor: pointer; font-weight: 500; padding: 8px 0; }

/* ─── Home sections ──────────────────────────────────────────────────────── */
.hero { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: var(--space-8); align-items: center; padding-block: clamp(32px, 6vw, 64px); }
.hero h1 { font-size: clamp(34px, 6vw, 54px); line-height: 1.12; }
.hero .lede { font-size: 20px; max-width: 40ch; }
.hero .plate { max-height: 520px; }
/* The dark bands carry most of the liveliness in this palette. Navy and
   tan are not loud colours, so the energy has to come from contrast: a deep
   band against a near-white ground, with sand as the warm note inside it. */
.band-dark { background: var(--color-navy); color: var(--color-fog); }
.band-darker { background: #050f27; color: var(--color-fog); }
/* A DARK AREA IS WHERE FLATNESS SHOWS MOST. Two soft off-centre glows, well
   under the threshold where anyone would call it a gradient, so the navy
   reads as a surface with light falling on it rather than as a fill. Pure
   CSS, no image, nothing to load. */
.band-dark, .band-darker { position: relative; isolation: isolate; }
.band-dark::before, .band-darker::before {
  content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none;
  background:
    radial-gradient(120% 80% at 18% -10%, color-mix(in srgb, var(--color-slate) 42%, transparent), transparent 60%),
    radial-gradient(90% 70% at 92% 115%, color-mix(in srgb, var(--color-tan) 16%, transparent), transparent 62%);
}
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: var(--space-4); text-align: center; padding-block: var(--space-6); }
.stat-n { font-family: var(--font-heading); font-size: clamp(30px, 4vw, 40px); font-weight: 600; color: var(--color-sand); font-variant-numeric: tabular-nums; line-height: 1.1; }
.stat-l { font-size: 16px; color: var(--color-mist); margin-top: 4px; }
.stats-light .stat-n { color: var(--color-text); }
.stats-light .stat-l { color: var(--color-neutral-700); }
/* The three home photos. align-items:start so a photo set to a different
   shape from its neighbours sits at its own height rather than stretching,
   which keeps the tops in line even when the shapes are mixed. */
.photo-strip { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: var(--space-3); margin-top: var(--space-6); align-items: start; }
/* Brands band. Centred: the kicker, the line under it, the photographs and
   the closing note all share one centre axis. The list of brand names that
   used to sit here is gone, and so is the faded picture behind it; the
   moving rows of Michael's own work make the same point without either. */
.brands { position: relative; overflow: hidden; text-align: center; }
/* Photographs want the quietest ground on the page, so this one band is
   white rather than the site's ivory. It is a section modifier rather than a
   rule on .brands, because the ground a section sits on is part of the page's
   rhythm and belongs with the other two. */
.section.band-white { background: #fff; }
/* One line, at the size of a section heading, because it is one. It carries
   the band on its own now that the kicker, the sentence under it and the
   list of names are gone. */
.brands-title { margin: 0 0 var(--space-6); }
.brands .tail { font-size: 15px; margin: var(--space-4) 0 0; }
/* The closing note sits under the photographs, so it needs the gap a caption
   needs, not the gap a paragraph needs. */
.strip + .wrap .tail { margin-top: var(--space-6); }

/* ── The moving strip ───────────────────────────────────────────────────────
   Two rows of photographs crossing the brands band, the same direction, at
   different speeds. Its markup is duplicated exactly once by site.js and the
   row travels to -50%, so the loop has no seam whatever is in it.

   Speeds are far apart on purpose. Two rows within a few seconds of each
   other read as one row that has gone wrong; 48s against 78s reads as two
   things at different distances, which is the whole point. Slow enough that
   nothing demands to be watched: it is the evidence, sitting still enough to
   be glanced at, not a carousel with something to say.

   The band already clips (overflow:hidden), so nothing escapes it. */
.strip { margin-top: var(--space-6); display: grid; gap: 10px; }
.strip-row { display: flex; gap: 10px; width: max-content; will-change: transform; }
.strip-row img {
  height: 116px; width: auto; max-width: none; border-radius: var(--radius-md);
  object-fit: cover; display: block; box-shadow: var(--shadow-sm);
}
@media (prefers-reduced-motion: no-preference) {
  .strip-slow { animation: strip-drift 78s linear infinite; }
  .strip-fast { animation: strip-drift 48s linear infinite; }
  @keyframes strip-drift { to { transform: translateX(-50%); } }
}
/* Without motion it is still two rows of photographs, just sitting still and
   clipped at the edge. Nothing is announced by it, so nothing is lost. */
@media (max-width: 600px) {
  .strip-row img { height: 86px; }
  .strip { gap: 8px; }
  .strip-row { gap: 8px; }
}
/* The second row starts part-way along, so the two are never briefly showing
   the same picture at the same x. */
.strip-fast { transform: translateX(-9%); }
@media (prefers-reduced-motion: no-preference) {
  .strip-fast { animation-delay: -11s; }
}
.cta-final { text-align: center; padding-block: clamp(48px, 8vw, 88px); position: relative; overflow: hidden; }
.cta-final .note { position: absolute; font-size: 320px; line-height: 1; color: color-mix(in srgb, var(--color-sand) 12%, transparent); left: -20px; top: -60px; pointer-events: none; font-family: var(--font-heading); }
.cta-final h2 { color: var(--color-fog); font-size: clamp(30px, 5vw, 44px); }
/* The service-area map. The frame drives the crop: the map inside reads its
   own container and cuts a window to match (js/area-map.js), so the shape
   chosen here is the shape the map is composed for. A 5:4 plate keeps the
   coverage rings generous on a wide column and stays tall enough to hold
   Lehi-to-Payson when the column narrows on a phone. */
.map-frame { width: 100%; height: 380px; border: 0; display: block; }
.map-plate { margin-top: var(--space-3); }
.map-plate .map-frame { height: auto; aspect-ratio: 5 / 4; max-height: 440px; }
/* Full-width home-page band. Left to run the whole column it becomes a 3:1
   letterbox, and a circular service area in a letterbox is mostly empty
   background with the top and bottom of the area cut off. Capped and
   centred, it keeps Taylorsville and Payson in frame. */
.map-plate-wide { max-width: 760px; margin-inline: auto; }
.map-plate-wide .map-frame { aspect-ratio: 16 / 10; max-height: 480px; }
@media (max-width: 900px) {
  .map-plate .map-frame { aspect-ratio: 4 / 3; max-height: 400px; }
  .map-plate-wide .map-frame { aspect-ratio: 4 / 3; }
}
@media (max-width: 520px) {
  .map-plate .map-frame, .map-plate-wide .map-frame { aspect-ratio: 1 / 1; max-height: none; }
}
.reviews-wrap { min-height: 120px; }
/* With scripts off nothing can ever reveal the fallback, so it is not
   hidden in the first place. The !important is not decoration: browsers now
   set [hidden] { display: none !important } in the user-agent sheet, so a
   plain rule here silently loses and the no-script visitor gets a section
   with nothing in it. */
.no-js #reviews-fallback[hidden] { display: block !important; }
/* THE PINK TEXT. Every other piece of type on the site is smoothed on the
   body rule above, which is what keeps dark text on white looking navy. The
   widget draws in a shadow root that does not pick that up, so its text fell
   back to subpixel rendering: each glyph edge gets an orange fringe on one
   side and a blue one on the other, and at reading size a column of it reads
   as pink. Set here as well as inside the shadow root by site.js, because
   font smoothing is an inherited property and setting it on the host reaches
   the widget even if its shadow root is closed to us. */
.reviews-wrap .elfsight-app-85cb8555-fe5e-4a73-b3ed-f86f907d421e,
.reviews-wrap .elfsight-app-85cb8555-fe5e-4a73-b3ed-f86f907d421e * {
  font-family: var(--font-body) !important;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* ─── Footer ─────────────────────────────────────────────────────────────── */
.footer { border-top: 1px solid var(--color-divider); padding: var(--space-8) var(--space-4); font-size: 16px; }
.footer .wrap { display: flex; justify-content: space-between; gap: var(--space-6); flex-wrap: wrap; }
.footer .brand { font-family: var(--font-heading); font-size: 22px; font-weight: 600; margin-bottom: 4px; }
.footer .links { display: flex; flex-direction: column; gap: 6px; }
.footer .links a { text-decoration: none; }
.footer .links a:hover { text-decoration: underline; }
.footer .fine { font-size: 14px; color: var(--color-neutral-600); margin-top: var(--space-4); width: 100%; }

/* ─── Booking flow ───────────────────────────────────────────────────────── */
.booking-main { max-width: 640px; margin: 0 auto; padding: var(--space-6) var(--space-4) var(--space-8); }
.steps { display: flex; align-items: center; gap: 8px; margin-bottom: var(--space-6); flex-wrap: wrap; }
.step-dot {
  width: 30px; height: 30px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; font-size: 14px;
  border: 1px solid var(--color-divider); color: var(--color-neutral-600); font-variant-numeric: tabular-nums;
}
.step-dot.done { border-color: var(--color-accent-700); background: var(--color-accent-700); color: var(--color-accent-100); }
.step-dot.current { border-color: var(--color-accent-700); color: var(--color-accent-700); font-weight: 600; }
.step-name { font-size: 15px; margin-left: 6px; }

/* ── The running total on the booking form ──────────────────────────────────
   Sits under the step dots on every screen after the first, so the answer to
   "what am I buying and what does it cost" is never more than a glance away.
   Quiet by design: it is a reminder, not the thing being filled in, so it
   takes the surface ground and small type rather than a card's lift. */
.cart {
  background: var(--color-surface); border: 1px solid var(--color-divider);
  border-radius: var(--radius-md); padding: var(--space-3) var(--space-4);
  margin-bottom: var(--space-6); font-size: 16px;
}
.cart-head { font-family: var(--font-label); font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--color-neutral-700); font-weight: var(--font-label-weight); margin: 0 0 6px; }
.cart-line { display: flex; justify-content: space-between; gap: var(--space-3); padding: 3px 0; }
.cart-when { color: var(--color-neutral-800); }
.cart-amt { font-variant-numeric: tabular-nums lining-nums; white-space: nowrap; }
.cart-total { display: flex; justify-content: space-between; gap: var(--space-3);
  margin-top: 6px; padding-top: 7px; border-top: 1px solid var(--color-divider); font-weight: 600; }

/* ── The add-on offer ───────────────────────────────────────────────────────
   One question, two answers, and a way out. Centred on a desktop and sitting
   on the bottom edge on a phone, where the thumb already is. */
.modal-backdrop { position: fixed; inset: 0; background: rgba(7,23,57,0.55); z-index: 60; }
.modal {
  position: fixed; z-index: 61; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: min(460px, calc(100vw - 32px)); background: #fff; color: var(--color-text);
  border-radius: var(--radius-lg); padding: var(--space-6); box-shadow: var(--shadow-lg, var(--shadow-md));
}
.modal h2 { margin: 2px 0 var(--space-3); font-size: 26px; }
.modal p { margin: 0 0 var(--space-3); }
.modal-actions { display: flex; flex-direction: column; gap: var(--space-2); margin-top: var(--space-4); }
.modal-actions .btn { width: 100%; }
/* Same reasoning as the nav bar: an outlined primary is right on a page full
   of options and wrong in a dialog asking one question, where two outlines
   read as two equal choices and the reader has to stop and pick a button
   rather than answer a question. */
.modal-actions .btn-primary { background: var(--color-cta); color: #fff; border-color: var(--color-cta); font-weight: 600; }
.modal-actions .btn-primary:hover { background: var(--color-cta-hover); border-color: var(--color-cta-hover); color: #fff; }
.modal-x { position: absolute; top: 8px; right: 10px; background: none; border: 0; font-size: 26px; line-height: 1;
  color: var(--color-neutral-700); cursor: pointer; width: 40px; height: 40px; border-radius: var(--radius-sm); }
.modal-x:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }
.modal-open { overflow: hidden; }
@media (max-width: 560px) {
  .modal { top: auto; bottom: 0; left: 0; transform: none; width: 100%;
    border-radius: var(--radius-lg) var(--radius-lg) 0 0; padding-bottom: calc(var(--space-6) + env(safe-area-inset-bottom, 0px)); }
}
@media (prefers-reduced-motion: no-preference) {
  .modal { animation: modal-in .16s ease-out; }
  @keyframes modal-in { from { opacity: 0; transform: translate(-50%, calc(-50% + 8px)); } }
  @media (max-width: 560px) {
    @keyframes modal-in { from { opacity: 0; transform: translateY(10px); } }
  }
}

/* The returning customer's "anything changed?" screen: three blocks, each
   showing what Michael already has before asking whether it is still true. */
.check-block { border-top: 1px solid var(--color-divider); padding: var(--space-4) 0; }
.check-block:first-of-type { border-top: 0; }
.check-have { margin: 4px 0 10px; font-size: 18px; }
.check-block .piano-types, .check-block .field { margin-top: var(--space-3); }

.booking-main h1 { font-size: clamp(30px, 5vw, 40px); }
.booking-main .lede { color: var(--color-neutral-800); margin-bottom: var(--space-6); }
.step-nav { display: flex; justify-content: space-between; gap: var(--space-3); margin-top: var(--space-6); padding-top: var(--space-4); border-top: 1px solid var(--color-divider); }
.step-nav .btn { min-width: 140px; }
.step-error { color: #8f1e15; font-weight: 500; margin-top: var(--space-3); }
.svc-tabs { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: var(--space-4); }
.review-rows { border-top: 1px solid var(--color-divider); margin-bottom: var(--space-4); }
.review-row { display: grid; grid-template-columns: 110px 1fr auto; gap: var(--space-3); align-items: center; padding: 12px 0; border-bottom: 1px solid var(--color-divider); font-size: 17px; }
.review-row .text-muted { font-size: 15px; }
@media (max-width: 520px) { .review-row { grid-template-columns: 1fr auto; } .review-row .text-muted { grid-column: 1 / -1; } }
.review-total { display: flex; justify-content: space-between; align-items: baseline; padding: 14px 0; font-size: 18px; }
.review-total .price { font-size: 30px; }
.confirm-mark { width: 64px; height: 64px; border-radius: 50%; border: 1px solid var(--color-accent); color: var(--color-accent-700); display: grid; place-items: center; font-size: 30px; margin-bottom: var(--space-4); }
.booking-footer { text-align: center; font-size: 16px; color: var(--color-neutral-700); padding: var(--space-6) var(--space-4); border-top: 1px solid var(--color-divider); }

/* Calendar */
.cal-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); margin-bottom: var(--space-3); }
.cal-month { font-family: var(--font-heading); font-size: 22px; font-weight: 600; }
.cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; }
.cal-dow { text-align: center; font-size: 13px; color: var(--color-neutral-600); padding-bottom: 4px; letter-spacing: 0.06em; }
.cal-day {
  min-height: 48px; padding: 4px 0; font-size: 16px; font-family: var(--font-body); font-variant-numeric: tabular-nums; border-radius: var(--radius-md);
  border: 1px solid var(--color-divider); background: transparent; color: var(--color-text); cursor: pointer; position: relative;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px;
}
.cal-day:hover:not(:disabled) { background: color-mix(in srgb, var(--color-text) 5%, transparent); }
.cal-day:disabled { border-color: transparent; color: var(--color-neutral-400); cursor: default; }
.cal-day.empty { border: 0; background: transparent; }
.cal-day.selected { border-color: var(--color-accent); background: var(--color-accent-100); color: var(--color-accent-800); font-weight: 600; }

/* DAYS MICHAEL IS ALREADY IN YOUR AREA. These used to be marked with a
   6px dot, which is a footnote, not a signal. They are the days that route
   well for him and get a booking sooner for the customer, so they are worth
   seeing across the whole month at a glance. Green, filled, with the dot
   kept as the second cue so it still works without colour.
   .selected wins over this on purpose: the day you picked has to look picked
   whether it was a near-you day or not, so this rule sits above it. */
.cal-day.near { border-color: var(--color-near); background: var(--color-near-100); color: var(--color-near-800); font-weight: 600; }
.cal-day.near:hover:not(:disabled) { background: color-mix(in srgb, var(--color-near) 16%, transparent); }
.cal-day .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--color-near); }
.cal-day.selected .dot { background: var(--color-accent-700); }
.cal-day.overflow-only { border-style: dashed; }
.cal-legend { display: flex; gap: var(--space-4); flex-wrap: wrap; font-size: 15px; color: var(--color-neutral-700); margin-top: var(--space-3); }
/* One rule for the legend dot, in the same green as the near-you days. There
   used to be two, and the later one (the old brass accent) won. */
.cal-legend .dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; background: var(--color-near); margin-right: 6px; vertical-align: middle; }
.tag.tag-near { background: var(--color-near-100); color: var(--color-near-800); }
.slots { margin-top: var(--space-4); }
.slots .head { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; margin-bottom: var(--space-2); font-weight: 500; }
.loading { padding: var(--space-6); text-align: center; color: var(--color-neutral-700); }

/* ─── Misc pages ─────────────────────────────────────────────────────────── */
.pairs { display: grid; gap: var(--space-8); }
.pair { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: var(--space-3); }
.pair .lbl { font-size: 14px; letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 6px; color: var(--color-neutral-700); }
.pair .lbl.after { color: var(--color-accent-700); }
figure { margin: 0; }
figcaption { margin-top: var(--space-2); font-size: 16px; color: var(--color-neutral-800); }
.callout { border: 1px solid var(--color-divider); border-radius: var(--radius-lg); padding: var(--space-4) var(--space-6); display: flex; justify-content: space-between; align-items: center; gap: var(--space-4); flex-wrap: wrap; margin: var(--space-6) 0; }
.callout h4 { margin: 0 0 4px; }
.callout p { margin: 0; color: var(--color-neutral-700); }
.fine-print { display: flex; flex-wrap: wrap; gap: 8px 24px; font-size: 15px; }
.fine-print span::before { content: "·"; margin-right: 8px; color: var(--color-accent); }
/* ── Four facts, laid out as four facts ─────────────────────────────────────
   These used to run on in one wrapping line separated by middots, which is a
   paragraph pretending to be a list: nothing lines up, and the eye cannot
   tell where one fact ends and the next begins. A grid gives each one a
   column and a rule of its own, and the rule is on top so a fact that runs
   to two lines still starts level with its neighbours. */
/* ── Who a plan is for ──────────────────────────────────────────────────────
   Six short answers to "is this for me?", in a grid so each one can be found
   by its bold first words without reading the rest. */
.fit-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: var(--space-4); margin-top: var(--space-4); }
.fit { border-top: 2px solid var(--color-accent); padding-top: 10px; display: flex; flex-direction: column; gap: 4px; font-size: 16px; }
.fit strong { font-size: 18px; font-weight: 600; }
.fit span { color: var(--color-muted); line-height: 1.5; }
/* The same list, compressed onto the plan's own tile. */
.recommend { background: var(--color-surface); border-radius: var(--radius-md); padding: 10px 14px; margin: 4px 0 8px; }
.recommend .card-kicker { display: block; margin-bottom: 2px; }
.recommend p { margin: 0; font-size: 16px; line-height: 1.5; }

.facts {
  list-style: none; margin: var(--space-6) 0 0; padding: 0;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: var(--space-4);
  font-size: 16px;
}
.facts li { border-top: 2px solid var(--color-accent); padding-top: 10px; }
.facts strong { font-weight: 600; }
.facts .small { display: inline-block; margin-top: 4px; line-height: 1.45; }

/* Reveal on scroll: opt-in via .reveal; harmless when JS is off */
.reveal { opacity: 0; transform: translateY(14px); transition: opacity 0.5s ease, transform 0.5s ease; }
.reveal.in-view { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) { .reveal { opacity: 1; transform: none; } }
.no-js .reveal { opacity: 1; transform: none; }

/* :not([hidden]) guards: an unconditional display beats the UA [hidden] rule */
.row:not([hidden]) { display: flex; }
.grid:not([hidden]) { display: grid; }
[hidden] { display: none !important; }
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }

/* ─── Address autocomplete ──────────────────────────────────────────────────
   Shared by the booking form and the admin panel. The list sits over the
   page, so it needs to be above everything the form draws under it. */
.ac-wrap { position: relative; }
.ac-list {
  position: absolute; top: 100%; left: 0; right: 0; z-index: 50;
  margin: 4px 0 0; padding: 4px; list-style: none;
  max-height: 300px; overflow-y: auto;
  background: var(--color-bg); border: 1px solid var(--color-text);
  border-radius: var(--radius-md); box-shadow: var(--shadow-lg);
}
.ac-list li {
  display: flex; flex-direction: column; gap: 1px;
  padding: 10px 12px; cursor: pointer; border-radius: var(--radius-sm);
  /* 48px of tappable height on a phone, same as every other control. */
  min-height: 48px; justify-content: center;
}
.ac-list li.active, .ac-list li:hover { background: var(--color-accent-100); }
.ac-list .ac-street { font-size: 16px; }
.ac-list .ac-place { font-size: 13px; color: var(--color-neutral-700); }
.ac-list mark { background: transparent; font-weight: 600; color: inherit; }

/* ─── Quick links card (/links.html) ─────────────────────────────────────────
   Michael's own page: a QR beside each link, big enough for a customer to
   scan off his phone from a step away. Not in the nav, noindex, no data. */
.link-list { display: grid; gap: var(--space-4); }
.link-card {
  display: grid; grid-template-columns: auto 1fr; gap: var(--space-4);
  align-items: start; padding: var(--space-4);
  border: 1px solid var(--color-divider); border-radius: var(--radius-md);
  background: var(--color-neutral-100);
}
.link-qr img { width: 130px; height: 130px; background: #fff; border-radius: var(--radius-sm); }
.link-body h2 { font-size: 22px; margin: 0 0 4px; }
.link-body p { margin: 0 0 6px; }
.link-url { font-size: 14px; color: var(--color-neutral-700); word-break: break-all; }
.link-card .row { margin-top: var(--space-3); }
.link-card .btn { min-height: 44px; font-size: 16px; padding-inline: var(--space-3); }
@media (max-width: 520px) {
  .link-card { grid-template-columns: 1fr; justify-items: center; text-align: center; }
  .link-card .row { justify-content: center; }
  .link-url { text-align: center; }
}

/* ─── Season bar ─────────────────────────────────────────────────────────────
   One strip above everything, only present while a season is running. It is
   inserted by site.js rather than sitting in every page's HTML, so adding a
   season never means editing ten files. Colours come from the accent tokens,
   which the season has already swapped, so this matches whatever it is
   dressed as without a rule per season. */
.season-bar {
  display: flex; align-items: center; justify-content: center;
  gap: var(--space-3); flex-wrap: wrap;
  padding: 10px var(--space-4);
  background: var(--color-accent-100);
  color: var(--color-accent-800);
  border-bottom: 1px solid var(--color-accent);
  font-size: 16px; text-align: center;
}
.season-bar a {
  color: inherit; font-weight: 600; white-space: nowrap;
  text-decoration-thickness: 2px; text-underline-offset: 3px;
  /* A tap target even on the narrowest phone. */
  min-height: 44px; display: inline-flex; align-items: center;
}
.season-bar em {
  font-style: normal; font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase;
  border: 1px solid currentColor; border-radius: 999px; padding: 1px 8px; opacity: 0.75;
}
@media (max-width: 520px) {
  .season-bar { font-size: 15px; gap: var(--space-2); padding: 8px var(--space-3); }
}

/* The one service a season pushes. A quiet ribbon, not a shout. */
.card.featured { border-color: var(--color-accent); box-shadow: var(--shadow-md); position: relative; }
.card .season-flag {
  position: absolute; top: 0; right: var(--space-4); transform: translateY(-50%);
  background: var(--color-accent-100); color: var(--color-accent-800);
  border: 1px solid var(--color-accent); border-radius: 999px;
  font-size: 13px; padding: 2px 12px; white-space: nowrap;
}
.callout.featured { border-color: var(--color-accent); box-shadow: var(--shadow-md); position: relative; }
tr.featured { background: var(--color-accent-100); }
tr.featured .season-flag { position: static; transform: none; margin-left: 8px; display: inline-block; }

/* A season's second colour, when it has one. Christmas wears red through the
   site and green on the bar: both present, each with its own job, and the
   ivory ground underneath keeps the pair from tipping into a shop window. */
:root.has-second-accent .season-bar {
  background: color-mix(in srgb, var(--color-accent-second) 12%, var(--color-bg));
  border-bottom-color: var(--color-accent-second);
  color: color-mix(in srgb, var(--color-accent-second) 70%, #000);
}

/* ─── Jump nav, and links that leave the site ────────────────────────────── */
/* Jumping down a page and leaving it are different acts, so they are no
   longer dressed as the same button. */
.jump { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px 18px; margin: 0 0 var(--space-5); }
.jump .jump-label { font-size: 13px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-neutral-700); }
.jump a { font-size: 17px; text-decoration: underline; text-underline-offset: 3px; text-decoration-thickness: 1px; }
.jump a:hover { text-decoration-thickness: 2px; }

/* The "this opens Google in a new tab" box. Built to match the add-on offer
   on the booking form: white, the same padding, the same navy backdrop, and
   the button that answers the question filled so it reads as the answer. */
.leave-dialog {
  border: 0; border-radius: var(--radius-lg); padding: var(--space-6);
  width: min(440px, calc(100vw - 32px)); max-width: none;
  background: #fff; color: var(--color-text); box-shadow: var(--shadow-lg);
}
.leave-dialog::backdrop { background: rgba(7, 23, 57, 0.55); }
.leave-dialog .leave-title { margin: 0 0 var(--space-2); font-size: 22px; line-height: 1.25; }
.leave-dialog .leave-body { margin: 0 0 var(--space-4); color: var(--color-muted); font-size: 17px; }
.leave-dialog .leave-actions { display: flex; flex-direction: column; gap: var(--space-2); }
.leave-dialog .leave-actions .btn { width: 100%; }
.leave-dialog .btn-primary { background: var(--color-cta); border-color: var(--color-cta); color: #fff; font-weight: 600; }
.leave-dialog .btn-primary:hover { background: var(--color-cta-hover); border-color: var(--color-cta-hover); color: #fff; }

/* ─── Assessment report and invoice, on the private link ─────────────────── */
.doc-sections, .doc-findings { display: grid; gap: 0; margin-bottom: var(--space-5); }
.doc-section, .doc-finding { padding: 14px 0; border-bottom: 1px solid var(--color-divider); }
.doc-section:first-child, .doc-finding:first-child { border-top: 1px solid var(--color-divider); }
.doc-section-head, .doc-finding-head { display: flex; justify-content: space-between; gap: var(--space-3); align-items: baseline; flex-wrap: wrap; }
.doc-finding-name { display: block; font-size: 18px; }
.doc-estimate { font-family: var(--font-heading); font-size: 20px; font-weight: 600; white-space: nowrap; }
/* The ratings carry meaning, so they are not decorative colour: each one is
   also spelled out in words, which is what a printed copy and a colour-blind
   reader both rely on. */
.rating, .pri { font-size: 13px; letter-spacing: 0.08em; text-transform: uppercase; white-space: nowrap; }
.rating-good { color: #2f6b3a; }
.rating-fair { color: #8a6a2f; }
.rating-poor { color: #9b2f26; }
.rating-na   { color: var(--color-neutral-700); }
.pri { display: block; margin-bottom: 2px; }
.pri-now      { color: #9b2f26; }
.pri-soon     { color: #8a6a2f; }
.pri-eventual, .pri-optional { color: var(--color-neutral-700); }
.doc-totals { display: grid; gap: 6px; margin: 0 0 10px; }
.doc-totals div { display: flex; justify-content: space-between; gap: var(--space-3); align-items: baseline; font-size: 18px; }
.doc-totals strong { font-family: var(--font-heading); font-size: 22px; }

/* The returning customer's "has anything changed?" screen. One row per thing
   we already know: the value stated plainly, and one button to correct it.
   Designed so the common answer is to touch nothing. */
.known-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px;
  padding: 14px 0; border-bottom: 1px solid var(--color-rule, rgba(32,31,29,0.14)); }
.known-row .field-label { display: block; }
.known-value { margin: 2px 0 0; font-size: 17px; }
.known-edit { padding: 4px 0 18px; border-bottom: 1px solid var(--color-rule, rgba(32,31,29,0.14)); }
.known-row + .field, .known-edit + .field { margin-top: 20px; }
@media (max-width: 520px) {
  .known-row { flex-direction: column; gap: 8px; }
}

/* The optional half of the piano step, folded away. A <details> rather than a
   toggle script, because the content policy forbids inline JS and this is not
   worth a handler. */
.more-fields { margin-top: 8px; }
.more-fields > summary { cursor: pointer; padding: 10px 0; font-size: 16px;
  color: var(--color-accent-700, #425e82); }
.more-fields > summary::marker { color: var(--color-neutral-700, #575d62); }
.more-fields[open] > summary { margin-bottom: 6px; }

/* The service note sits under the option list now rather than above it, so it
   needs room to read as a separate thing rather than a fifth option crowding
   the fourth. Slightly roomier inside, too: it is a paragraph someone actually
   reads, not a one-line warning. */
.option-list + .notice-inline {
  margin-top: var(--space-4);
  padding: var(--space-3) var(--space-3);
  line-height: 1.55;
}
.option-list + .notice-inline .text-muted { display: inline-block; margin-top: 6px; }

/* PAYMENT OPTIONS. Two groups, because the only thing a customer needs from
   this is "which of these costs me extra". Logos are the card networks' own
   marks, used to say what is accepted and nothing else. */
.pay-options { margin: var(--space-4) 0 var(--space-3); }
.pay-group + .pay-group { margin-top: var(--space-3); }
.pay-label { margin: 0 0 8px; }
.pay-badge { display: inline-block; font-size: 13px; font-weight: 600; letter-spacing: .03em;
  text-transform: uppercase; padding: 4px 10px; border-radius: 999px; }
.pay-free { background: var(--color-near-100); color: var(--color-near-800); }
.pay-fee { background: var(--color-accent-100); color: var(--color-accent-800); }
.pay-row { display: flex; flex-wrap: wrap; gap: 10px; list-style: none; margin: 0; padding: 0; }
.pay-chip { height: 52px; min-width: 92px; padding: 0 14px; display: flex; align-items: center;
  justify-content: center; background: #fff; border: 1px solid var(--color-neutral-200);
  border-radius: var(--radius-lg); }
.pay-chip img { max-height: 26px; max-width: 110px; width: auto; height: auto; object-fit: contain; }
.pay-chip img[alt="Mastercard"], .pay-chip img[alt="American Express"] { max-height: 34px; }
.pay-word { font-weight: 600; font-size: 15px; color: var(--color-text); }
@media (max-width: 600px) {
  .pay-chip { height: 46px; min-width: 76px; padding: 0 10px; }
  .pay-chip img { max-height: 22px; }
}

/* The second and third piano in the same house. Indented and quieter than
   the first one's picker: they are the same question asked again, not a new
   section of the form. */
.extra-piano { border-left: 3px solid var(--color-neutral-200); padding-left: var(--space-3); margin: var(--space-3) 0; }
.extra-piano .field-label { display: block; margin-bottom: 6px; }
