/* ==========================================================================
   Furniture Magicians — shared stylesheet
   Phase 3. Single source of truth for tokens and components.

   Every colour pairing here was measured with docs/tools/contrast.ps1.
   Ratios are noted inline. Do not introduce a colour that is not a token.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Fonts — system stacks. No webfont download, no FOUT, no CDN.
   (A CDN would also repeat an existing production bug: /testimonials loads
   Google Fonts over http on an https page and is already blocked.)

   Georgia leads the display stack deliberately. It is a screen-designed serif
   with a large x-height and real warmth, and — importantly — it is NOT a
   high-contrast Didone, so it does not compete with the logo wordmark.
   Swapping in a webfont later means changing these two lines and nothing else.
   -------------------------------------------------------------------------- */

/* --------------------------------------------------------------------------
   2. Tokens
   -------------------------------------------------------------------------- */
:root {
  /* Colour — sampled from Images/FurnitureMagiciansLogo_2026.png, an in-house
     study rather than Susan's logo. The sample is still valid: the burgundy is
     identical in both, and only the chair differs. See docs/BRAND-ASSETS.md. */
  --paper:          #FFFFFF;
  --ground:         #F4F2F1;  /* warm grey. NOT cream (#FDF8EE) */
  --ink:            #1C1614;  /* 17.88:1 on paper */
  --ink-soft:       #5B5350;  /*  7.51:1 on paper */
  --burgundy:       #800C19;  /* 10.58:1 on paper — logo colour, unmodified */
  --burgundy-deep:  #5A0810;  /* 14.22:1 under white */
  --hairline:       #E2DDDB;

  /* Gold — DECORATIVE ONLY, and only on dark grounds.
     Measured: 2.10:1 on paper and 1.88:1 on ground, so it can NEVER carry text
     or a boundary on a light surface. It performs where it is actually used:
     8.50:1 on the ink image frame, 5.03:1 on a burgundy band.
     Golds dark enough to pass on white (#8C6D1F and below) stop reading as
     gold and turn olive, so the constraint is kept rather than the colour
     compromised. See docs/PHASE3-DESIGN-PLAN.md §3a. */
  --gold:           #D4AF37;
  --gold-hi:        #E8CC72;  /* sheen highlight */
  --gold-deep:      #A67C00;  /* sheen shadow */
  --gold-sheen:     linear-gradient(160deg,
                      var(--gold-hi)  0%,
                      var(--gold)    28%,
                      var(--gold-deep) 52%,
                      var(--gold)    74%,
                      var(--gold-hi) 100%);
  /* The same idea run left-to-right, for a band only a few pixels tall where a
     160deg sweep would read as a flat colour. Dark at the ends, bright across
     the middle - a gilded moulding catching the light. */
  --gilt-band:      linear-gradient(to right,
                      var(--gold-deep)  0%,
                      var(--gold)      30%,
                      var(--gold-hi)   50%,
                      var(--gold)      70%,
                      var(--gold-deep) 100%);

  /* Type */
  --font-display: Georgia, 'Iowan Old Style', 'Palatino Linotype', Palatino, 'Times New Roman', serif;
  --font-body:    system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;

  --text-xs:   0.8125rem;
  --text-sm:   0.9375rem;
  --text-base: 1.0625rem;   /* 17px — set for reading, not for density */
  --text-lg:   1.1875rem;
  --text-xl:   clamp(1.35rem, 1.1rem + 0.9vw, 1.6rem);
  --text-2xl:  clamp(1.7rem, 1.3rem + 1.6vw, 2.15rem);
  --text-3xl:  clamp(2.1rem, 1.5rem + 2.6vw, 3rem);

  --leading-tight: 1.2;
  --leading-body:  1.65;

  /* Space — 4px base */
  --s-1: 0.25rem;  --s-2: 0.5rem;   --s-3: 0.75rem;  --s-4: 1rem;
  --s-5: 1.5rem;   --s-6: 2rem;     --s-7: 3rem;     --s-8: 4rem;   --s-9: 6rem;

  /* Form */
  --radius:    5px;
  --radius-lg: 8px;
  /* `ch` is the advance of the digit ZERO, not of an average character. At
     --text-base in this stack 1ch measures 9.16px while real prose averages
     about 6.2px, so the old 68ch was 623px wide and held roughly 100
     characters a line — /privacy and /terms counted at 102 and 100 against a
     45–85 comfort band.
         48ch x 9.16px = 440px;  440px / 6.2px = ~71 characters.
     If you widen this, COUNT characters on a rendered page. The unit will lie
     to you: a Range over the text node, watching the line-box y change, is
     the only honest measurement. */
  --measure:   52ch;
  --container: 74rem;

  --shadow-sm: 0 1px 2px rgba(28, 22, 20, 0.07);
  --shadow-md: 0 2px 8px rgba(28, 22, 20, 0.09);

  --transition: 140ms ease;
}

/* No dark-mode variant is defined, deliberately. The brand burgundy measures
   1.98:1 against black — it cannot survive a dark ground without abandoning
   the logo colour. See docs/PHASE3-DESIGN-PLAN.md §3. */

/* --------------------------------------------------------------------------
   3. Base
   -------------------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }

body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: var(--text-base);
  line-height: var(--leading-body);
  -webkit-font-smoothing: antialiased;
}

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

/* Skip link — first focusable element on every page */
.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  background: var(--burgundy);
  color: var(--paper);
  padding: var(--s-3) var(--s-4);
  z-index: 100;
  font-weight: 600;
}
.skip-link:focus { left: var(--s-4); top: var(--s-4); }

/* --------------------------------------------------------------------------
   4. Focus — never suppressed. Colour alone is never the only state cue.
   -------------------------------------------------------------------------- */
:focus-visible {
  outline: 2px solid var(--ink);      /* 17.88:1 on paper */
  outline-offset: 2px;
  border-radius: 2px;
}
.band--burgundy :focus-visible,
.band--hero :focus-visible,
.site-footer :focus-visible {
  outline-color: var(--paper);        /* 10.58:1 on burgundy */
}

/* The one exception, and the only place an outline is suppressed anywhere in
   this stylesheet. The skip link focuses <main> so that the next Tab carries
   on into the content instead of returning to the skip link; without
   tabindex="-1" there the link scrolls but never moves focus, and the visitor
   cannot actually bypass the nav.

   That focus is a landing point, not a control. Ringing the entire content
   region in 2px says nothing about what will receive the next keystroke, and
   the page having scrolled is the real feedback. WCAG 2.4.7 governs keyboard
   operable components; a skip target is not one. Every actual control keeps
   its outline. */
main[tabindex="-1"]:focus,
main[tabindex="-1"]:focus-visible { outline: none; }

/* --------------------------------------------------------------------------
   5. Typography
   -------------------------------------------------------------------------- */
h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 600;
  line-height: var(--leading-tight);
  color: var(--ink);
  margin: 0 0 var(--s-4);
  text-wrap: balance;
}
h1 { font-size: var(--text-3xl); letter-spacing: -0.015em; }
h2 { font-size: var(--text-2xl); margin-top: var(--s-7); }
h3 { font-size: var(--text-xl);  margin-top: var(--s-6); }
h4 { font-size: var(--text-lg);  margin-top: var(--s-5); }

p { margin: 0 0 var(--s-4); }
p:last-child { margin-bottom: 0; }

a {
  color: var(--burgundy);             /* 10.58:1 on paper, 9.48:1 on ground */
  text-decoration-thickness: 1px;
  text-underline-offset: 0.15em;
}
a:hover { color: var(--burgundy-deep); text-decoration-thickness: 2px; }

.lede { font-size: var(--text-lg); color: var(--ink); }
.muted { color: var(--ink-soft); }     /* 7.51:1 — safe for body copy */

.eyebrow {
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--burgundy);
  margin: 0 0 var(--s-2);
}

/* --------------------------------------------------------------------------
   6. Layout
   -------------------------------------------------------------------------- */
.container {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--s-5);
}
.measure { max-width: var(--measure); }

.section { padding-block: var(--s-8); }

.band { padding-block: var(--s-8); }

/* The first band sits directly under the header, which already draws its own
   rule and supplies the separation. A full band's worth of padding on top of
   that left every page opening a long way down. Only the top is trimmed - the
   rhythm BETWEEN bands is untouched. Specificity (0,2,1) clears
   .band--hero's own padding-block. */
main > .band:first-child { padding-top: var(--s-6); }
@media (min-width: 900px) { main > .band:first-child { padding-top: var(--s-7); } }

/* Text-led pages get a little less than the hero does, because the two are not
   equivalent at the same number. The hero opens with an image, whose top edge
   is exactly where its box starts. A page title opens with type, which carries
   leading above the cap height, so an identical padding value puts the letters
   visibly lower and the page reads as though it starts further down. Taking a
   step off the text pages lands both at about the same optical height.
   :not() rather than source order, so this cannot be undone by a later edit. */
main > .band:first-child:not(.band--hero) { padding-top: var(--s-5); }
@media (min-width: 900px) {
  main > .band:first-child:not(.band--hero) { padding-top: var(--s-6); }
}
/* The closing call to action, shared by every page from MasterPage.master.
   It used to be a single left column whose ink stopped at 744px of an 1184px
   container — 35% of the most-repeated composition on the site was empty, on
   fifteen pages at once. Two columns: the words keep the measure they need,
   the buttons take the space that was doing nothing. Below 780px it stacks
   back to exactly what it was. */
.cta-band__grid {
  display: grid;
  gap: var(--s-5);
  align-items: center;
}
@media (min-width: 780px) {
  .cta-band__grid {
    grid-template-columns: minmax(0, 1fr) auto;
    gap: var(--s-7);
  }
  .cta-band__actions { justify-content: flex-end; }
}
.cta-band__grid > * > :first-child { margin-top: 0; }
.cta-band__grid > * > :last-child  { margin-bottom: 0; }
/* The buttons sit as a column on wide screens so two long labels do not force
   the text column narrow; as a row when stacked, where width is free. */
@media (min-width: 780px) {
  .cta-band__actions { flex-direction: column; align-items: stretch; }
  .cta-band__actions .btn { text-align: center; justify-content: center; }
}
/* ...except where the two labels are short enough to sit beside each other.
   On the About mission band the stacked pair left a hole down the middle of
   the burgundy: text stopping at its measure on the left, two buttons hugging
   the right, and nothing in between. Side by side the pair is 335px wide and
   reaches back into that gap.

   Not below 1000px, though. The pair takes its width out of the text column,
   and measured at 860px that column fell to 414px and at 780px to 334px -
   under the 477px measure, which is the thing the column narrowing was
   supposed to protect. Above 1000px the text still has 554px or more. Below
   780px both the base rule and this one are out of the way and .cluster wraps
   the buttons on its own, exactly as it did before. */
@media (min-width: 1000px) {
  .cta-band__actions--row { flex-direction: row; align-items: center; }
  .cta-band__actions--row .btn { flex: 0 0 auto; }
}

.band--paper    { background: var(--paper); }
.band--ground   { background: var(--ground); }
.band--burgundy { background: var(--burgundy); color: var(--paper); }
.band--burgundy h1,
.band--burgundy h2,
.band--burgundy h3 { color: var(--paper); }
.band--burgundy a  { color: var(--paper); }
/* .lede sets --ink explicitly, so it overrides the band's inherited white and
   renders near-black on burgundy (about 1.5:1 - illegible). Needs restating
   here for the same reason .band--hero does it. */
.band--burgundy .lede { color: var(--paper); opacity: 0.94; }
.band--burgundy .muted { color: rgba(255,255,255,0.78); }
/* Gold on burgundy measures 5.03:1 — comfortably above the 4.5 text threshold,
   and the one place gold may carry type. Sized up from --text-xs on dark
   bands: at 13px a gold kicker read as a caption rather than as the label for
   the section under it, which is the job it is actually doing there. */
.band--burgundy .eyebrow,
.band--hero .eyebrow {
  color: var(--gold);
  opacity: 1;
  font-size: var(--text-sm);
  letter-spacing: 0.14em;
}

/* Where the kicker IS the section's title rather than a label above one - the
   mission statement, for instance - it needs heading weight and heading
   semantics, not a sized-up paragraph. */
.section-kicker {
  font-family: var(--font-body);
  font-size: var(--text-lg);
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  margin: 0 0 var(--s-4);
}
/* Two classes deep on purpose. `.band--burgundy h2` is a class plus an element
   and so outranks a lone `.section-kicker`, which would silently repaint this
   white on the very bands it exists for. */
.band--burgundy .section-kicker,
.band--hero .section-kicker { color: var(--gold); }
.band--burgundy .rule-split { background: rgba(255,255,255,0.28); }
.band--burgundy .rule-split::after { background: var(--gold); }

/* The split rule as a section divider — an echo of the restoration figure. */
.rule-split {
  border: 0;
  height: 2px;
  background: var(--hairline);
  position: relative;
  margin: var(--s-7) 0;
  overflow: visible;
}
.rule-split::after {
  content: '';
  position: absolute;
  left: 50%;
  top: -5px;
  width: 3px;
  height: 12px;
  background: var(--burgundy);
  transform: translateX(-50%);
}

/* Two-column content pairs. Mobile first: stacked, side by side from 820px. */
.split { display: grid; grid-template-columns: 1fr; gap: var(--s-6); align-items: start; }
@media (min-width: 820px) { .split { grid-template-columns: 1fr 1fr; gap: var(--s-7); } }

/* Page titles sit inside the split's first column so the facing image starts at
   the top of the band (see the comment in any service page). Inside .stack the
   heading picks up the stack's s-4 rhythm, which is far too much between a
   kicker and the heading it labels - the eyebrow belongs TO the h1, it is not a
   separate block. Specificity (0,3,1) clears .stack > * + *. */
.split .stack > .eyebrow + h1 { margin-top: 0; }

/* Several stacked text blocks facing a single image in the other column.
   h2 carries a top margin so consecutive headings breathe, but on the FIRST
   block that margin pushes the heading below the top of the facing image and
   the two columns visibly fail to line up. Zeroing it on the first block
   only keeps the alignment without collapsing the spacing between blocks. */
.split__stack > .split__block + .split__block { margin-top: var(--s-7); }
.split__stack > .split__block:first-child > :first-child { margin-top: 0; }

/* The same failure one level up. A heading leading a PLAIN split column - no
   .stack, no .split__block - keeps its --s-7 top margin, so the column boxes
   line up while the visible text starts 48px below the picture facing it.
   align-items:start aligns the boxes; it cannot know the ink inside one of
   them begins lower.

   /about's service-area heading had already been patched inline with
   margin-top:0, and the home page's "You can rely on our expertise" split had
   not - measured out by exactly --s-7. Making it a rule stops the next one
   from depending on somebody noticing. */
.split > * > :first-child { margin-top: 0; }

/* Tight by default so the trunk photo is visible on a phone without
   scrolling - the proof of the work matters more than generous air above it.
   The roomier spacing comes back once there is width to spare. */
.hero { display: grid; grid-template-columns: 1fr; gap: var(--s-4); align-items: center; }
@media (min-width: 900px) { .hero { grid-template-columns: 0.95fr 1.25fr; gap: var(--s-7); } }
.hero__copy { max-width: 34rem; }
.hero__figure { margin: 0; }

/* The hero's own call to action. Was an inline margin-top of 2rem, which on a
   phone pushed the image most of a screen further down. */
.hero__cta { margin-top: var(--s-4); }
@media (min-width: 900px) { .hero__cta { margin-top: var(--s-6); } }

/* --------------------------------------------------------------------------
   Hero on burgundy — the entrance to the site gets the brand's own colour
   rather than sitting flat white like every other band. White/gold text is
   AAA (10.58:1 / 5.03:1). Bookends the page with the shared burgundy CTA
   band at the bottom, separated by white content between, so it reads as a
   deliberate rhythm rather than overuse of one colour.
   -------------------------------------------------------------------------- */
.band--hero {
  background: var(--burgundy);
  color: var(--paper);
  padding-block: var(--s-6);
}
@media (min-width: 900px) { .band--hero { padding-block: var(--s-8); } }
.band--hero h1,
.band--hero h2,
.band--hero h3 { color: var(--paper); }
.band--hero .eyebrow { color: var(--gold); }          /* 5.03:1 on burgundy */
.band--hero .lede { color: var(--paper); opacity: 0.94; }
.band--hero a:not(.btn) { color: var(--gold); }
.band--hero a:not(.btn):hover { color: var(--gold-hi); }

.hero-rule {
  width: 3.5rem;
  height: 3px;
  background: var(--gold-sheen);
  border: 0;
  margin: var(--s-3) 0 var(--s-4);
}
@media (min-width: 900px) { .hero-rule { margin: var(--s-4) 0 var(--s-5); } }

/* The hero image gets the same gilt frame as the restoration catalogue -
   ties the very first thing a visitor sees to the site's one signature
   device, instead of introducing a second, unrelated framing idea. */
.hero__figure .restoration__frame--single {
  padding: 4px;
  background: var(--gold-sheen);
  box-shadow: 0 8px 28px rgba(0,0,0,0.35);
}
.hero__figure .restoration__frame--single img {
  border-radius: calc(var(--radius-lg) - 1px);
}
/* Compound selectors, not .hero__figure alone: .restoration figcaption in
   §10 below sets the dark ink-soft grey figure captions use elsewhere, and
   at equal specificity a single-class selector loses that tie by source
   order - which is exactly what made this caption unreadable on burgundy. */
.band--hero .hero__figure figcaption {
  color: var(--paper);
  opacity: 0.85;
}

.portrait { width: 100%; height: auto; border-radius: var(--radius-lg); box-shadow: var(--shadow-md); }

/* Gilt frame for the service page hero images - the same device as the
   restoration figures, which is the site's signature motif. Gold is legitimate
   here: the token notes rule it out for text and for boundaries on a light
   ground, but measure it at 8.50:1 as an image frame, which is what this is.

   The frame also gives the image a visible box before the picture arrives, so
   the right-hand column no longer reads as empty space while it loads. */
.portrait-frame {
  padding: 3px;
  background: var(--gold-sheen);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
  overflow: hidden;
  line-height: 0;
}
.portrait-frame .portrait {
  border-radius: calc(var(--radius-lg) - 2px);
  box-shadow: none;
  display: block;
}

/* Line art, not a photograph: no frame or drop shadow, since those read as a
   photo's edge and an illustration has none. */
.illustration { width: 100%; height: auto; display: block; }

.pullquote {
  margin: 0;
  max-width: 46rem;
  border-left: 4px solid var(--burgundy);
  padding-left: var(--s-5);
}
.pullquote p {
  font-family: var(--font-display);
  font-size: var(--text-xl);
  line-height: 1.45;
  margin-bottom: var(--s-3);
}
.pullquote footer { color: var(--ink-soft); font-size: var(--text-sm); font-weight: 600; }

/* --------------------------------------------------------------------------
   7. Buttons — real <button>/<a>, never an image
   -------------------------------------------------------------------------- */
/* <button> and <a> must render identically — normalise the UA styles that
   otherwise leak in (system font, buttonface background, buttontext colour). */
.btn {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  appearance: none;
  -webkit-appearance: none;
  font-family: var(--font-body);
  font-size: var(--text-base);
  font-weight: 600;
  line-height: 1;
  margin: 0;
  padding: var(--s-3) var(--s-5);
  border-radius: var(--radius);
  border: 2px solid transparent;
  cursor: pointer;
  text-decoration: none;
  text-align: center;
  transition: background-color var(--transition), border-color var(--transition),
              background-size var(--transition),
              box-shadow var(--transition), transform var(--transition);
}

/* Text colour is fixed across every state. It never shifts on hover —
   a lightened label reads as "disabled", not "interactive". */
.btn--primary,
.btn--primary:hover,
.btn--primary:focus,
.btn--primary:active { color: var(--paper); }      /* 10.58:1 / 14.22:1 */

/* A gilded bottom edge, present at rest rather than only on hover.
   Two reasons. The resting button was a plain rectangle that could have
   belonged to any business, and this is the one place the brand's own material
   — the gilt edge on a restored piece — can do the work instead of a generic
   UI flourish. And it moves the hover cue off a full ring, which is the shape
   convention for "selected" and sat awkwardly close to the ink focus outline.

   It is the border, not an inset shadow: the button already carries a 2px
   transparent border, so colouring one side costs no layout, whereas an inset
   shadow is clipped to the padding box and would leave a burgundy lip below
   the gilt.

   The constraint that shaped the old design still holds — burgundy-deep on
   burgundy is 1.34:1, so a hover that only darkens is invisible. Three things
   now change together: the fill deepens, the gilt brightens from --gold-deep
   to --gold, and the button lifts onto a warmer shadow. */
.btn--primary {
  background-color: var(--burgundy);
  /* The gilt is a background band, not a border-bottom. A coloured bottom
     border meeting transparent side borders is mitered at 45 degrees, which
     tapered the band over 7px at each end - a flat run of 160px on a 174px
     button - and left it running straight under the 5px rounded corners, so
     the button appeared to sit on a slightly wrong-shaped plinth. A background
     is clipped to the border radius, so the band follows the curve and cannot
     miter.

     background-origin and background-repeat are both required. The default
     origin is the padding box while the paint area is the border box, so the
     band tiles and its bottom edge reappears inside the 2px transparent top
     border - a gold line across the top of the button. */
  background-image: var(--gilt-band);
  background-size: 100% 3px;
  background-position: bottom;
  background-repeat: no-repeat;
  background-origin: border-box;
}
/* Same band, thicker. Keeping one image and animating only its size means the
   gilt grows smoothly; swapping between two gradients would cut. */
.btn--primary:hover {
  background-color: var(--burgundy-deep);
  background-size: 100% 5px;
  box-shadow: 0 4px 14px rgba(90, 8, 16, 0.3);
  transform: translateY(-2px);
}
.btn--primary:active {
  transform: translateY(0);
  background-size: 100% 5px;
  box-shadow: none;
}

.btn--secondary {
  background-color: transparent;
  color: var(--burgundy);
  border-color: var(--burgundy);
}
/* Once it fills burgundy it is wearing the primary's clothes, so it picks up
   the same gilt band rather than inventing a second hover language. */
.btn--secondary:hover {
  background-color: var(--burgundy);
  background-image: var(--gilt-band);
  background-size: 100% 3px;
  background-position: bottom;
  background-repeat: no-repeat;
  background-origin: border-box;
  color: var(--paper);                /* 10.58:1 — full white, not tinted */
  border-color: var(--burgundy);
  transform: translateY(-1px);
}
.btn--secondary:active { transform: translateY(0); }

/* Every burgundy surface, not just .band--burgundy. The hero is .band--hero,
   and while it was missing from this selector its two call-to-action buttons
   were burgundy-on-burgundy - a 1:1 contrast ratio, i.e. completely
   invisible, while still occupying their full height. That empty-looking
   band under the hero copy was the buttons all along. */
.band--burgundy .btn--secondary,
.band--hero .btn--secondary { color: var(--paper); border-color: var(--paper); }
/* This one fills WHITE on hover, and the gold ring it used to carry broke the
   stylesheet's own rule two sections up: gold is 2.10:1 on paper and can never
   carry a boundary on a light surface. It was decorative rather than
   load-bearing, so nothing was unreadable — but it was invisible, and it was
   the rule being contradicted in the one place nobody looked. Burgundy carries
   the edge here instead, at 10.58:1, mirroring the gilt edge the burgundy
   buttons wear. */
.band--burgundy .btn--secondary:hover,
.band--hero .btn--secondary:hover {
  background-color: var(--paper);
  background-image: linear-gradient(var(--burgundy), var(--burgundy));
  background-size: 100% 3px;
  background-position: bottom;
  background-repeat: no-repeat;
  background-origin: border-box;
  color: var(--burgundy);
  transform: translateY(-2px);
  box-shadow: 0 4px 14px rgba(28, 22, 20, 0.28);
}

/* --------------------------------------------------------------------------
   8. Header & navigation
   -------------------------------------------------------------------------- */
.site-header { background: var(--paper); border-bottom: 1px solid var(--hairline); }
.site-header__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-5);
  padding-block: var(--s-4);
}
.site-logo { display: block; flex: 0 1 auto; min-width: 0; }
.site-logo img { width: 250px; max-width: 100%; height: auto; }
@media (min-width: 860px) { .site-logo img { width: 330px; } }

/* On a phone the primary action is calling, so the number is a real tap target
   and sits beside the menu button rather than being buried in the nav. */
/* Georgia sets old-style (text) figures by default, so 3 4 7 9 hang below the
   baseline and 6 8 rise above it. That is handsome in running prose and wrong
   here: a phone number is data to be scanned and tapped, not read as a
   sentence, and the bouncing baseline makes it harder to take in at a glance.
   lining-nums asks the face for its aligned figures instead; tabular-nums
   keeps the digits on a common width so the number reads as one block. */
.site-tel {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  font-family: var(--font-display);
  font-size: var(--text-lg);
  font-weight: 700;
  font-variant-numeric: lining-nums tabular-nums;
  letter-spacing: 0.01em;
  color: var(--burgundy);          /* 10.58:1 on paper */
  text-decoration: none;
  white-space: nowrap;
}
.site-tel:hover { color: var(--burgundy-deep); text-decoration: underline; }

/* The phone number and the estimate button share the right end of the bar. Up
   to 1080px only the number is here, so this is a one-item row and behaves
   exactly as the bare link did. Above it, see the column rule further down. */
.site-actions { display: flex; align-items: center; gap: var(--s-3); }

/* MOBILE FIRST: the collapsed nav is the base state, and the horizontal bar is
   the enhancement at >=860px. Written this way round deliberately — most of
   this site's traffic is phones. */
.nav { display: flex; align-items: center; gap: var(--s-2); }

.nav__list {
  display: none;
  list-style: none;
  margin: 0; padding: 0;
}
/* The panel used to be exactly as tall as its six links, and its bottom edge
   landed in the middle of a line of the hero paragraph — the top half of that
   line hidden behind white, the bottom half showing on burgundy. It read as a
   rendering fault. (It was never a stacking bug: hit tests at three points
   inside the panel all returned .nav__link.) Running it to the bottom of the
   viewport means there is no seam to land badly. */
.nav__list.is-open {
  display: flex;
  flex-direction: column;
  position: fixed;
  left: 0; right: 0;
  top: var(--header-h, 101px);
  bottom: 0;
  background: var(--paper);
  border-top: 1px solid var(--hairline);
  padding: var(--s-4) var(--s-4) var(--s-8);
  box-shadow: var(--shadow-md);
  z-index: 20;
  overflow-y: auto;
  overscroll-behavior: contain;
}
/* The page behind must not scroll under the open panel. */
body.nav-open { overflow: hidden; }

.nav__link {
  display: block;
  /* 44px minimum tap target — WCAG 2.5.5 / Apple HIG */
  min-height: 44px;
  display: flex;
  align-items: center;
  padding: var(--s-2) var(--s-3);
  font-size: var(--text-base);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink);
  text-decoration: none;
  border-radius: var(--radius);
  border-bottom: 3px solid transparent;
}
.nav__link:hover { color: var(--burgundy); background: var(--ground); }
.nav__link[aria-current="page"] { color: var(--burgundy); border-bottom-color: var(--burgundy); }

.nav__toggle { display: inline-flex; min-height: 44px; }
.site-header__inner { flex-wrap: wrap; position: relative; }

/* The call to action exists twice, and only ever one of them is on screen.
   In the header bar it is a filled button; inside the collapsed menu it is
   another menu row. Trying to serve both with one element meant either a
   button crowding a 390px bar that is already full, or no call to action at
   all on the phones that are most of this site's traffic. */
.site-cta { display: none; }
.nav__cta .nav__link { color: var(--burgundy); font-weight: 700; }

@media (min-width: 860px) {
  .nav__list { display: flex; gap: var(--s-1); }
  .nav__link { font-size: var(--text-sm); min-height: 0; }
  .nav__toggle { display: none; }
  .site-header__inner { flex-wrap: nowrap; }
  /* Once the menu is a horizontal bar, "Request an estimate" is too long to sit
     in it: at 880px it pushed the logo - which shrinks silently - down to
     nothing. Between 860 and 1080 the header carries no call to action beyond
     the phone number and the Contact link, which is how it has always been. */
  .nav__cta { display: none; }
}

/* The filled button needs more room than the nav breakpoint has. At 880px it
   took its width out of the logo - which is `flex: 0 1 auto; min-width: 0` and
   so shrinks silently - and crushed the wordmark to nothing. Between 860 and
   1080 the call to action is an ordinary nav item, which costs a third of the
   width; the button appears only once there is genuinely space for it. */
@media (min-width: 1080px) {
  .nav__cta { display: none; }
  /* Only needed once the button is in the bar. Without it "Our Work" wrapped to
     two lines and stood the header 25px taller. Below 1080 the links are left
     free to wrap, because down there the logo needs the width more than the
     nav does. */
  .nav__link { white-space: nowrap; }
  /* Sized down from the page buttons: this one sits in a 1px-ruled bar beside
     type at --text-sm, and at full button scale it dominated the logo. */
  .site-cta {
    display: inline-flex;
    flex: 0 0 auto;
    white-space: nowrap;
    min-height: 44px;              /* the tap target this site uses everywhere else */
    font-size: var(--text-sm);
    padding: var(--s-2) var(--s-4);
  }

  /* Stack them: button on top, number beneath it. The pair now occupies the
     button's width instead of the button's width plus the number's plus a gap,
     which is where the bar's crowding came from. It costs no header height -
     the 330px wordmark is 68px tall and already sets the bar's height, so a
     44px button over a ~22px number fits inside what the logo was reserving.

     The number drops from --text-lg to --text-base here. At display size,
     directly under a filled burgundy button, it competed with the button
     instead of supporting it; one size down it reads as the button's quieter
     alternative, which is what it is. The 44px tap-target floor is dropped
     with it - this branch only ever applies at >=1080px, where the pointer is
     a mouse. Below 860px, where the number IS the tap target, it keeps the
     full 44px from the base rule above. */
  .site-actions {
    flex-direction: column;
    align-items: stretch;
    /* 2px, not --s-1. The budget here is the wordmark's 68px: a 44px button
       plus this gap plus the number's line box has to fit inside it, or the
       stack starts setting the header's height and the bar grows taller in
       exchange for the width it just saved. At --s-1 and the inherited 1.6
       line-height the stack came to 76px and the header went from 102px to
       109px. At 2px and 1.15 it comes to 67px and the header is unchanged. */
    gap: 2px;
  }
  .site-tel {
    justify-content: center;
    min-height: 0;
    font-size: var(--text-base);
    line-height: 1.15;
  }
}

/* --------------------------------------------------------------------------
   9. (removed) Accordion sections
   The .accordion/.acc rules lived here to support the About page, which used
   to carry four former pages behind collapsible <details>. Three of the four
   were closed on arrival, so most of the page's content was invisible until
   clicked - including the offers, which are the only content on the site with
   direct commercial intent. Coupons became the standalone /deals page and the
   reviews became /testimonials; mission and service area are now ordinary
   visible sections on /about, keeping their #mission and #service-area
   anchors for the 301s. Nothing used these rules afterwards, so they went
   rather than lingering as dead CSS.
   -------------------------------------------------------------------------- */

/* --------------------------------------------------------------------------
   10. Restoration figure — the signature element
   Gallery images are 978x550 composites with the before/after seam at the
   exact midpoint (x=489), verified across all 45. The rule sits on the seam.
   -------------------------------------------------------------------------- */
.restoration { margin: 0 0 var(--s-6); }

/* Gilt frame. The gradient gives the edge a metallic read rather than a flat
   mustard line — gold is a surface, not a hue, so it needs a sheen to be
   legible as gold. Padding-as-border via a gradient background, because a
   plain `border` cannot carry a gradient. Gold on ink measures 8.50:1.

   MOBILE FIRST: the two halves STACK by default and sit side by side only at
   >=620px. On a 390px phone a side-by-side split gives each half about 195px,
   which is too small to read damage in. Stacked, each half gets the full
   width. Both panes point at the same source file, so it is one download. */
.restoration__frame {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 3px;
  background: var(--gold-sheen);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
  overflow: hidden;
}

/* The frame reserves its full size from the image's width/height attributes,
   so until the image arrives the gold sheen painted as a solid bright slab -
   most obvious on the hero, where it is the first thing on screen. While the
   image is still downloading the frame holds a quiet neutral instead and the
   gold arrives with the picture it is framing.

   The class is added by script, so the default state here is the gold: with
   no JavaScript the frame simply looks the way it always did. */
.restoration__frame.is-loading {
  background: rgba(28, 22, 20, 0.15);   /* reads as soft grey on paper, deeper burgundy on the hero */
  box-shadow: none;
  transition: none;
}

/* NO flex-basis here. The frame is a COLUMN flex container until 620px, so
   the main axis is vertical and `flex: 1 1 0` would set flex-basis 0 on the
   HEIGHT. The frame has no height of its own, so there is no free space for
   flex-grow to distribute and every pane collapsed to zero - which silently
   hid every image on the home carousel and the whole 45-image gallery on
   phones, leaving just the gold frame as a thin strip. aspect-ratio cannot
   rescue it, because flex-basis wins on the main axis. Stacked, the pane
   takes full width from align-items:stretch and derives its height from
   aspect-ratio; the flex-basis is reinstated below only once the panes sit
   side by side and the main axis is horizontal again. */
.restoration__pane {
  position: relative;
  overflow: hidden;
  aspect-ratio: 489 / 550;   /* one half of the 978x550 composite */
  background: var(--ink);
  line-height: 0;
}
.restoration__pane img {
  position: absolute;
  top: 0; left: 0;
  width: 200%;               /* full composite, twice the pane width */
  height: 100%;
  max-width: none;
  object-fit: cover;
}
.restoration__pane--after img { left: -100%; }   /* slide to the right half */

.restoration__label {
  position: absolute;
  left: var(--s-3);
  bottom: var(--s-3);
  z-index: 2;
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 600;
  line-height: 1.4;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--paper);
  background: rgba(28, 22, 20, 0.82);
  padding: var(--s-1) var(--s-3);
  border-radius: 3px;
}
.restoration__label--after { background: var(--burgundy); }

@media (min-width: 620px) {
  .restoration__frame { flex-direction: row; }
  /* the split rule only exists when the halves are actually side by side */
  .restoration__frame { gap: 0; }
  /* Safe here and only here: the main axis is horizontal, so this shares the
     WIDTH between the two panes and aspect-ratio still sets the height. */
  .restoration__pane { flex: 1 1 0; }
  .restoration__pane--after { border-left: 3px solid var(--burgundy); }
  .restoration__label--after { left: calc(var(--s-3) + 3px); }
}

/* Single-image variant — a finished piece with no before/after pair,
   e.g. the home page hero. Same gilt frame, no split. */
.restoration__frame--single { display: block; }
.restoration__frame--single img {
  width: 100%;
  border-radius: calc(var(--radius-lg) - 2px);
  display: block;
}

/* The caption used to open with "No. 07" in a burgundy column of its own, which
   told a visitor nothing and, across 45 entries, was 45 repetitions of an index
   nobody could use. With it gone the two-column grid had nothing to put in its
   first track, so this is now a plain stack: what the piece is, then what was
   wrong with it, in the owner's own words.

   Nothing replaced the number - no dot, no rule, no mark. The gilt frame
   directly above is the figure's one graphic device, and a second marker
   immediately under it competes with the frame rather than supporting it. The
   site also already uses dots to mean carousel POSITION (§10c/§10d), so a
   decorative one here would read as if it did something. Hierarchy is carried
   by weight and colour alone, the same split .tile__title/.tile__summary uses
   on the services grid. */
.restoration figcaption {
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
  margin-top: var(--s-3);
  font-size: var(--text-sm);
  color: var(--ink-soft);
}
.restoration__title {
  font-weight: 600;
  color: var(--ink);            /* 17.88:1 on paper */
  line-height: 1.35;
}
.restoration__problem {
  line-height: 1.35;
}
.restoration__problem::first-letter { text-transform: uppercase; }

/* Catalogue grid */
.catalogue {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
  gap: var(--s-6);
  list-style: none;
  margin: 0; padding: 0;
}

/* --------------------------------------------------------------------------
   10c. Restoration carousel — home page "featured work" strip.
   One slide visible at a time, auto-advancing, pausable, keyboard- and
   touch-reachable. Every slide stays in the DOM (display:none on the
   inactive ones, not removed), so this degrades to "the first restoration,
   plainly" if JS never runs rather than an empty section.
   -------------------------------------------------------------------------- */
.fm-carousel { max-width: 900px; margin-inline: auto; }
.fm-carousel__track { position: relative; }
.fm-carousel__slide { display: none; }
.fm-carousel__slide.is-active { display: block; animation: fm-fade 500ms ease; }

.fm-carousel__nav {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-4);
  margin-top: var(--s-5);
}
.fm-carousel__arrow {
  appearance: none;
  border: 2px solid var(--burgundy);
  background: var(--paper);
  color: var(--burgundy);
  width: 2.75rem; height: 2.75rem;
  min-width: 44px; min-height: 44px;   /* tap target */
  border-radius: 999px;
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
  transition: background var(--transition), color var(--transition);
}
.fm-carousel__arrow:hover { background: var(--burgundy); color: var(--paper); }

.fm-carousel__dots { display: flex; gap: var(--s-2); }
.fm-carousel__dot {
  appearance: none;
  border: 2px solid var(--burgundy);
  background: transparent;
  width: 0.7rem; height: 0.7rem;
  padding: 0;
  border-radius: 999px;
  cursor: pointer;
}
.fm-carousel__dot.is-active { background: var(--burgundy); }
/* Real tap target without a visually oversized dot.
   -14px around an 11.2px dot gives 39.2px, which is under the 44px minimum
   this file already cites for .nav__link. -17px lands at 45.2px. The dot
   still LOOKS 11px; only the target changed. */
.fm-carousel__dot { position: relative; }
.fm-carousel__dot::after {
  content: '';
  position: absolute; inset: -17px;
}
/* ...and the row needs the gap to match, or two 45px targets 8px apart
   overlap and the wrong dot answers the tap. */
.fm-carousel__dots { gap: var(--s-6); }

@keyframes fm-fade { from { opacity: 0; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  .fm-carousel__slide.is-active { animation: none; }
}

/* --------------------------------------------------------------------------
   10d. Testimonial fader — home page. Same reduced-motion respect as the
   carousel; auto-advance pauses on hover/focus so a reader mid-quote is
   never interrupted.
   -------------------------------------------------------------------------- */
.fm-fader { position: relative; max-width: 46rem; min-height: 11rem; }
.fm-fader__item {
  display: none;
  border-left: 4px solid var(--burgundy);
  padding-left: var(--s-5);
}
.fm-fader__item.is-active { display: block; animation: fm-fade 600ms ease; }
.fm-fader__item p {
  font-family: var(--font-display);
  font-size: var(--text-xl);
  line-height: 1.45;
  margin-bottom: var(--s-3);
}
.fm-fader__item footer { color: var(--ink-soft); font-size: var(--text-sm); font-weight: 600; }

.fm-fader__dots { display: flex; gap: var(--s-2); margin-top: var(--s-5); }
.fm-fader__dot {
  appearance: none;
  border: 2px solid var(--burgundy);
  background: transparent;
  width: 0.7rem; height: 0.7rem;
  padding: 0;
  border-radius: 999px;
  cursor: pointer;
  position: relative;
}
.fm-fader__dot.is-active { background: var(--burgundy); }
.fm-fader__dot::after {
  content: '';
  position: absolute; inset: -14px;
}

/* Closing section of the home page, where a left-aligned quote leaves the
   right half of the band empty. The rule stays on the left of the quote -
   it marks where the quotation starts, so centring it would strand it. */
.fm-fader--centred { margin-inline: auto; }
.fm-fader--centred .fm-fader__dots { justify-content: center; }

@media (prefers-reduced-motion: reduce) {
  .fm-fader__item.is-active { animation: none; }
}

/* --------------------------------------------------------------------------
   10e. Gallery lightbox — opens a catalogue entry larger, with prev/next
   through all 45. The thumbnail grid remains the primary, fully-indexable
   way to browse (see PHASE3-DESIGN-PLAN.md on why modals were avoided
   elsewhere); this is the one place a modal was always the plan, since it
   supplements rather than replaces the underlying content.
   -------------------------------------------------------------------------- */
.fm-lightbox {
  position: fixed; inset: 0;
  z-index: 100;
  display: none;
  /* A tall picture plus a three-line caption can exceed a laptop viewport once
     the browser chrome is open. Without this the excess was simply clipped with
     no way to reach it. */
  overflow-y: auto;
  padding: var(--s-5);
  background: rgba(28, 22, 20, 0.92);
}
.fm-lightbox.is-open { display: flex; }

/* margin:auto rather than justify-content/align-items:center on the parent.
   The two centre identically while the content fits, but when it does not,
   auto margins collapse to zero and the top of the picture stays reachable at
   scroll position 0 - whereas centring would push equal amounts off BOTH ends
   and leave the overflow at the top permanently unscrollable. */
.fm-lightbox__body {
  margin: auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  max-width: 1100px;
  width: 100%;
}

.fm-lightbox__frame {
  position: relative;
  width: 100%;                  /* max-width now lives on __body, which shares it with the caption */
  padding: 4px;
  background: var(--gold-sheen);
  border-radius: var(--radius-lg);
  box-shadow: 0 20px 60px rgba(0,0,0,0.5);
}
.fm-lightbox__img { width: 100%; height: auto; display: block; border-radius: calc(var(--radius-lg) - 2px); }

.fm-lightbox__label {
  position: absolute;
  bottom: calc(var(--s-4) + 4px);
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--paper);
  background: rgba(28, 22, 20, 0.82);
  padding: var(--s-1) var(--s-3);
  border-radius: 3px;
}
.fm-lightbox__label--before { left: calc(var(--s-4) + 4px); }
.fm-lightbox__label--after  { right: calc(var(--s-4) + 4px); background: var(--burgundy); }

/* Space for four lines is RESERVED whether the caption needs them or not.
   Arrowing from a short caption to a long one would otherwise change the height
   of __body, and since __body is centred, that moves the PICTURE up and down
   between clicks. Reserving the tallest case pins the picture, which is the
   whole point of putting the caption underneath.

   FOUR, not three: the longest caption in the catalogue (No. 44, 142 chars)
   takes two lines at the desktop width but four on a 390px phone, where the box
   is only 342px wide. Three was measured as one line short there and the
   picture moved 11px. If captions ever grow past this, the phone case in
   verify-gallery-layout.mjs fails - it compares the frame's box between the
   longest and shortest captions at three viewports.

   34rem caps the line length: the frame runs to 1100px and centred body text
   that wide is unreadable. Kept left-to-right agnostic with padding-inline. */
.fm-lightbox__caption {
  width: 100%;
  max-width: 34rem;
  min-height: calc(1.5em * 4);
  margin-top: var(--s-4);
  padding-inline: var(--s-3);
  color: var(--paper);
  text-align: center;
  font-size: var(--text-sm);
  line-height: 1.5;
}

.fm-lightbox__close {
  position: absolute;
  top: var(--s-5); right: var(--s-5);
  width: 44px; height: 44px;
  border-radius: 999px;
  border: 2px solid var(--paper);
  background: rgba(0,0,0,0.3);
  color: var(--paper);
  font-size: 1.25rem;
  line-height: 1;
  cursor: pointer;
}
.fm-lightbox__close:hover { background: var(--paper); color: var(--ink); }

.fm-lightbox__arrow {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 3rem; height: 3rem;
  min-width: 44px; min-height: 44px;
  border-radius: 999px;
  border: 2px solid var(--paper);
  background: rgba(0,0,0,0.3);
  color: var(--paper);
  font-size: 1.4rem;
  line-height: 1;
  cursor: pointer;
}
.fm-lightbox__arrow:hover { background: var(--paper); color: var(--ink); }
.fm-lightbox__arrow--prev { left: var(--s-4); }
.fm-lightbox__arrow--next { right: var(--s-4); }

.catalogue button.restoration-trigger {
  appearance: none;
  border: 0;
  background: transparent;
  padding: 0;
  width: 100%;
  text-align: left;
  cursor: zoom-in;
  font: inherit;
  color: inherit;
}
.catalogue button.restoration-trigger:hover .restoration__frame { box-shadow: var(--shadow-md), 0 0 0 3px var(--gold); }

@media (max-width: 700px) {
  .fm-lightbox__arrow { width: 2.5rem; height: 2.5rem; font-size: 1.1rem; }
  .fm-lightbox__close { top: var(--s-3); right: var(--s-3); }
}

/* --------------------------------------------------------------------------
   10a. Service tiles — the /services index
   The whole tile is one link. No nested interactive elements, so there is a
   single tab stop per tile and screen readers announce one target.
   -------------------------------------------------------------------------- */
.tile-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
  gap: var(--s-5);
  list-style: none;
  margin: 0; padding: 0;
}

.tile {
  display: flex;
  flex-direction: column;
  height: 100%;
  background: var(--paper);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-lg);
  overflow: hidden;
  text-decoration: none;
  color: var(--ink);
  box-shadow: var(--shadow-sm);
  transition: border-color var(--transition), box-shadow var(--transition),
              transform var(--transition);
}
.tile:hover {
  border-color: var(--burgundy);
  box-shadow: var(--shadow-md);
  transform: translateY(-2px);
}
/* Gold is not used here — on a white tile it measures 2.10:1 and would be
   an invisible boundary. Burgundy carries the hover instead, at 10.58:1. */

.tile__media {
  aspect-ratio: 4 / 3;
  background: var(--ground);
  overflow: hidden;
}
.tile__media img {
  width: 100%; height: 100%;
  object-fit: cover;
  transition: transform 200ms ease;
}
.tile:hover .tile__media img { transform: scale(1.03); }

.tile__body {
  padding: var(--s-4) var(--s-5) var(--s-5);
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  flex: 1;
}
.tile__title {
  font-family: var(--font-display);
  font-size: var(--text-lg);
  font-weight: 600;
  margin: 0;
  line-height: 1.25;
}
.tile:hover .tile__title { color: var(--burgundy); }
.tile__summary { margin: 0; font-size: var(--text-sm); color: var(--ink-soft); }
.tile__more {
  margin-top: auto;
  padding-top: var(--s-2);
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--burgundy);
}
.tile__more::after { content: ' \2192'; }

@media (prefers-reduced-motion: reduce) {
  .tile:hover { transform: none; }
  .tile:hover .tile__media img { transform: none; }
}

/* --------------------------------------------------------------------------
   10b. Testimonials — 8 real customer quotes, on /testimonials

   The quotes range from one line ("Thank you so much. It's beautiful!") to a
   full paragraph. A grid stretches every cell to the tallest in its row, so
   the short quotes rendered as mostly-empty boxes beside the long ones.
   CSS columns instead: each card takes only the height it needs and the wall
   packs naturally. break-inside keeps a card from splitting across columns.
   -------------------------------------------------------------------------- */
.quote-wall {
  columns: 1;
  column-gap: var(--s-5);
  list-style: none;
  margin: 0; padding: 0;
}
@media (min-width: 620px) { .quote-wall { columns: 2; } }
@media (min-width: 980px) { .quote-wall { columns: 3; } }

.quote-wall > .testimonial {
  break-inside: avoid;
  -webkit-column-break-inside: avoid;
  page-break-inside: avoid;   /* older WebKit/Blink honours only this one */
  margin-bottom: var(--s-5);
}

/* A single quote used as social proof inside a service page. */
.testimonial--inline { max-width: var(--measure); }

.testimonial {
  /* Browsers give <blockquote> a default `margin: 1em 40px`, and nothing here
     ever overrode it. The result: on /about, /kitchen, /repair, /refinishing
     and /upholstery the quote card sat inset 40px from the photograph directly
     above it, on both sides - so the burgundy rule never lined up with the
     image edge it was meant to sit under. The reset does not affect the
     testimonials wall, where these are <li> and the grid gap does the spacing. */
  margin-inline: 0;
  background: var(--paper);
  border-left: 3px solid var(--burgundy);
  border-radius: 0 var(--radius-lg) var(--radius-lg) 0;
  padding: var(--s-5);
  box-shadow: var(--shadow-sm);
}
.testimonial p {
  font-family: var(--font-display);
  font-size: var(--text-base);
  line-height: 1.55;
  margin: 0 0 var(--s-3);
}
.testimonial footer {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--ink-soft);
}

/* --------------------------------------------------------------------------
   11. Ticket — replaces the starburst coupon JPEGs.
   Offer text becomes real, selectable, indexable copy.
   -------------------------------------------------------------------------- */
.ticket {
  /* As a flex ITEM this governs width in the parent's horizontal axis; the
     column direction below is this card's own internal layout and the two do
     not interact. */
  flex: 1 1 300px;
  max-width: 26rem;
  background: var(--paper);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-lg);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  box-shadow: var(--shadow-sm);
}
/* Fixed height, centred content. The offers are not the same length, so
   "FREE Chair Stabilization" wrapped to two lines while its neighbours sat on
   one, and the burgundy headers came out at different depths across a row.
   Centring inside a set height means a wrapped offer and a short one occupy
   exactly the same band, and the type is a step smaller so most do not wrap
   at all. */
.ticket__head {
  background: var(--burgundy);
  color: var(--paper);
  padding: var(--s-4) var(--s-5);
  text-align: center;
  display: grid;
  place-items: center;
  min-height: 6.25rem;
}
.ticket__offer {
  font-family: var(--font-display);
  font-size: var(--text-xl);
  font-weight: 700;
  line-height: 1.15;
  margin: 0;
  color: var(--paper);
  text-wrap: balance;
}

/* A flex column, so the action below can push itself to the bottom. As a plain
   block container the child's `margin-top: auto` resolved to zero and the
   button simply followed the terms text, landing at a different height in
   every ticket. */
.ticket__body {
  padding: var(--s-5);
  flex: 1;
  display: flex;
  flex-direction: column;
}
.ticket__terms {
  margin: 0;
  border-top: 2px dashed var(--hairline);
  padding-top: var(--s-3);
  font-size: var(--text-xs);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

/* Heading and intro sit over a centred row of tickets, so they centre too -
   a left-aligned heading above centred cards reads as a mistake. */
.offer-head { text-align: center; margin-bottom: var(--s-6); }
.offer-head h2 { margin-top: 0; }
.offer-head p { margin-inline: auto; max-width: var(--measure); }

/* Flex, not grid. `repeat(auto-fill, ...)` lays down as many tracks as fit and
   leaves the surplus ones empty, so a group of two tickets sat in a
   three-track row with a column of dead space on the right, and the lone
   hotel offer sat with two. Wrapping flex items only create the tracks they
   need, and centring the row keeps a short group balanced under its heading.
   The cap stops a single ticket stretching the full width of the band. */
.ticket-grid {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--s-5);
  list-style: none;
  margin: 0; padding: 0;
}

/* Responsive embed frame — used for the contact page map */
.map-frame {
  position: relative;
  width: 100%;
  aspect-ratio: 4 / 3;
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
  border: 1px solid var(--hairline);
}
.map-frame iframe {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  border: 0;
}
@media (min-width: 820px) { .map-frame { aspect-ratio: 1 / 1; } }

/* On the contact page the map is orientation, not the point of the page, so
   it stays a wide strip instead of growing into a square that competed with
   the form for attention. */
.map-frame--compact { aspect-ratio: 3 / 2; }
@media (min-width: 820px) { .map-frame--compact { aspect-ratio: 3 / 2; } }

/* --------------------------------------------------------------------------
   12b. Contact page layout — the form leads, everything else supports
   -------------------------------------------------------------------------- */
.contact-layout { display: grid; grid-template-columns: 1fr; gap: var(--s-6); align-items: start; }
@media (min-width: 900px) {
  .contact-layout { grid-template-columns: 1.55fr 1fr; gap: var(--s-7); }
}
.contact-aside { display: flex; flex-direction: column; gap: var(--s-5); }

.contact-card {
  background: var(--ground);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-lg);
  padding: var(--s-5);
}
.contact-card__head {
  font-size: var(--text-lg);
  margin: 0 0 var(--s-4);
}
.contact-card__row {
  display: flex;
  gap: var(--s-4);
  align-items: baseline;
  margin: 0 0 var(--s-3);
  font-size: var(--text-sm);
}
.contact-card__label {
  flex: none;
  width: 4.75rem;
  font-weight: 600;
  color: var(--ink-soft);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: var(--text-xs);
}
.contact-card__value { font-size: var(--text-base); }

/* --------------------------------------------------------------------------
   Spec strip — the service pages' at-a-glance facts.

   These were a muted grey card in the right-hand margin, which put the two
   things a customer actually arrived for (what it costs, how long it takes)
   in the least important-looking element on the page, and left a 300-600px
   hole under the left column while the right column ran on. Full width under
   the hero fixes both at once.

   The rules are burgundy, not gold: gold measures 2.10:1 on paper and cannot
   carry a boundary on a light ground. See the token notes in section 2.
   -------------------------------------------------------------------------- */
.spec-strip {
  margin-top: var(--s-7);
  padding-top: var(--s-5);
  border-top: 1px solid var(--hairline);
}
.spec-strip__head {
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--burgundy);
  margin: 0 0 var(--s-5);
}
.spec-strip__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: var(--s-5) var(--s-6);
  margin: 0;
}

/* Five items, not three or four. Every "At a glance" strip on the service
   pages holds three or four, which auto-fit lays out as one full row. The
   About credentials strip holds five: at the default 13rem minimum four fit
   across, which strands the fifth alone in a quarter-width row under three
   siblings and reads as an accident. A 18rem minimum forces three across
   (4 x 18rem + gaps exceeds the 1136px content width), so five falls as
   3 + 2 - still uneven, but deliberately so, and the orphan is half the row
   rather than a quarter of it. Still auto-fit, so it collapses to 2 and then
   1 on narrower screens exactly as the others do. */
.spec-strip__grid--three {
  grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
}
.spec-strip__item {
  border-top: 3px solid var(--burgundy);   /* 10.58:1 on paper */
  padding-top: var(--s-3);
}
.spec-strip__item dt {
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-soft);                  /* 7.51:1 */
  margin-bottom: var(--s-2);
}
.spec-strip__item dd {
  margin: 0;
  font-size: var(--text-base);
  line-height: 1.5;
}
/* Full width, at the owner's call - the same call she made on the "What we do"
   and "Beautiful restorations" intros.

   This was constrained to --measure because unconstrained it could run the
   full 1136px of the container. The concern was real in the abstract and wrong
   in practice here: these notes are one or two short sentences, so at measure
   width they wrapped to three ragged lines under a tidy three-column grid and
   left most of the row empty, while at full width they set as a single line
   that reads as a caption belonging to the grid above it.

   text-wrap: pretty so that where a note IS long enough to wrap, it does not
   leave one orphaned word on the second line. If a note is ever written long
   enough to run past about 100 characters a line, shorten the note rather than
   putting the cap back. */
.spec-strip__note {
  max-width: none;
  text-wrap: pretty;
  margin: var(--s-5) 0 0;
  font-size: var(--text-sm);
  color: var(--ink-soft);                  /* 7.51:1 on paper */
}

/* --------------------------------------------------------------------------
   Service detail columns.

   These were three stacked full-width bands, each holding one narrow column
   of prose - so 45% of every band was empty and the pages ran to 3,600px for
   under 600 words. Side by side they occupy the width they already had, and
   nothing is hidden behind an interaction: no tabs, no modals, every section
   visible, linkable and printable.

   auto-fit collapses this to two columns on a tablet and one on a phone,
   where vertical scrolling is the right answer anyway.
   -------------------------------------------------------------------------- */
.service-detail {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
  gap: var(--s-7) var(--s-6);
}
/* REVERTED to three columns on 2026-08-31, at the owner's call.

   This was briefly two columns with the job story full width beneath, because
   three equal columns held three unequal blocks and left a notch of empty
   space at the bottom right - 136 to 247px between the shortest and longest
   column on /military, /caning, /insurance and /mosaics.

   She looked at it and saw the opposite problem: two columns plus a full-width
   row underneath does not read as one section, it reads as a section that
   broke. The story's own measure cap made it worse, leaving a half-empty band
   under two full ones.

   Ragged column bottoms are the cost, and they are the smaller cost. Same
   judgement as the two-column section head and the /deals coupons: a
   dead-space measurement is a candidate, not a verdict. Do not re-suggest
   this.

   The modifier classes are gone from the markup as well as from here, so
   nothing is left half-wired. */
.service-detail__col > h2 {
  font-size: var(--text-xl);
  margin: 0 0 var(--s-4);
  padding-top: var(--s-4);
  border-top: 3px solid var(--burgundy);   /* 9.48:1 on ground */
  text-wrap: pretty;
}
.service-detail__col > * + * { margin-top: var(--s-4); }
.service-detail__col p:last-child { margin-bottom: 0; }
.contact-card__note {
  margin: var(--s-4) 0 0;
  padding-top: var(--s-4);
  border-top: 1px solid var(--hairline);
  font-size: var(--text-sm);
  color: var(--ink-soft);
}

/* --------------------------------------------------------------------------
   12c. Photo uploader
   The native file input is visually replaced by a large tap target, but it is
   still a real focusable <input> tied to a <label> rather than a div driven by
   JS - so keyboard, screen reader and no-JS all keep working, and the phone's
   own Take Photo / Photo Library chooser is what opens.
   -------------------------------------------------------------------------- */
.uploader { position: relative; }

/* Not display:none - that would make the input unfocusable and unreachable by
   keyboard. Clipped instead, so it stays in the accessibility tree. */
.uploader__input {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.uploader__drop {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  padding: var(--s-5);
  border: 2px dashed var(--hairline);
  border-radius: var(--radius-lg);
  background: var(--paper);
  cursor: pointer;
  transition: border-color var(--transition), background var(--transition), box-shadow var(--transition);
}
.uploader__drop:hover { border-color: var(--burgundy); background: var(--ground); }
.uploader__input:focus-visible + .uploader__drop {
  border-color: var(--burgundy);
  box-shadow: 0 0 0 3px rgba(128, 12, 25, 0.15);
}
.uploader.is-dragover .uploader__drop {
  border-color: var(--burgundy);
  border-style: solid;
  background: rgba(128, 12, 25, 0.05);
}

.uploader__icon {
  flex: none;
  display: grid;
  place-items: center;
  width: 3rem; height: 3rem;
  border-radius: 50%;
  background: var(--burgundy);
  color: var(--paper);
}
.uploader__text { display: block; }
.uploader__title {
  display: block;
  font-weight: 600;
  color: var(--ink);
  margin-bottom: 0.15rem;
}
.uploader__sub {
  display: block;
  font-size: var(--text-sm);
  color: var(--ink-soft);
}
/* Dragging is a pointer-only affordance; on touch it is just noise. */
.uploader__drag { display: none; }
@media (hover: hover) and (pointer: fine) {
  .uploader__drag { display: inline; }
}

/* The camera button, and the mirror image of the rule above: taking a photo is
   a touch-only affordance. On a desktop this control opens an ordinary file
   dialog and means nothing, so it is not shown there.

   It exists because the OS chooser can no longer be relied on to offer both
   routes. Android 13+ sends accept="image/*" to the system Photo Picker, a
   photo grid with no camera in it - so without this button there is no way to
   reach the camera from this form on a current Android phone. */
.uploader__camera { display: none; }
@media (hover: none) and (pointer: coarse) {
  .uploader__camera {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--s-2);
    width: 100%;
    margin-top: var(--s-3);
    padding: var(--s-3) var(--s-4);
    border: 1px solid var(--burgundy);
    border-radius: var(--radius);
    background: var(--paper);
    color: var(--burgundy);
    font-weight: 600;
    cursor: pointer;
  }
  /* Same focus treatment the drop zone gets, for the same reason: the real
     input is clipped, so the label has to show the focus ring on its behalf. */
  #cameraUpload:focus-visible + .uploader__camera {
    border-color: var(--burgundy);
    box-shadow: 0 0 0 3px rgba(128, 12, 25, 0.15);
  }
}

/* --------------------------------------------------------------------------
   12. Forms
   -------------------------------------------------------------------------- */
.field { margin-bottom: var(--s-5); }
.field__label {
  display: block;
  font-weight: 600;
  font-size: var(--text-sm);
  margin-bottom: var(--s-2);
}
.field__req { color: var(--burgundy); }   /* 10.58:1 — not colour-only, has * glyph */

.input, .textarea {
  width: 100%;
  font-family: var(--font-body);
  font-size: var(--text-base);
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--ink-soft);      /* 7.51:1 boundary */
  border-radius: var(--radius);
  padding: var(--s-3);
  transition: border-color var(--transition), box-shadow var(--transition);
}
.input:hover, .textarea:hover { border-color: var(--ink); }
.input:focus, .textarea:focus {
  border-color: var(--burgundy);
  box-shadow: 0 0 0 3px rgba(128, 12, 25, 0.15);
}
.textarea { min-height: 9rem; resize: vertical; }

.field__error {
  display: block;
  margin-top: var(--s-2);
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--burgundy);
}
.field--invalid .input,
.field--invalid .textarea { border-color: var(--burgundy); border-width: 2px; }

.field__hint { margin-top: var(--s-2); font-size: var(--text-sm); color: var(--ink-soft); }

/* --------------------------------------------------------------------------
   12d. Deal carried in from the Deals page via ?deal=
   Gold-edged rather than burgundy: it is good news the visitor chose, not a
   warning, and the error notice below already owns burgundy on this page.
   -------------------------------------------------------------------------- */
.applied-deal {
  display: flex;
  align-items: flex-start;
  gap: var(--s-4);
  padding: var(--s-4) var(--s-5);
  margin-bottom: var(--s-5);
  background: var(--ground);
  /* --gold-deep (3.42:1 on ground), not --gold (1.88:1). The token notes in
     section 2 rule out gold carrying a boundary on a light surface, and this
     border was the one place in the stylesheet that broke its own rule. It
     also now matches the left accent bar, which was always gold-deep. */
  border: 1px solid var(--gold-deep);
  border-left: 4px solid var(--gold-deep);
  border-radius: var(--radius-lg);
}
.applied-deal__tag { flex: none; color: var(--gold-deep); line-height: 0; padding-top: 0.15rem; }
.applied-deal__text { display: block; flex: 1 1 auto; }
.applied-deal__label {
  display: block;
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.applied-deal__offer { display: block; font-size: var(--text-lg); margin-top: 0.1rem; }
.applied-deal__detail { display: block; font-size: var(--text-sm); color: var(--ink-soft); margin-top: 0.2rem; }
.applied-deal__remove {
  flex: none;
  font-size: var(--text-sm);
  color: var(--burgundy);
  text-decoration: underline;
  background: none;
  border: 0;
  cursor: pointer;
  padding: 0;
}
.applied-deal__remove:hover { color: var(--burgundy-deep); }

/* Sits inside a ticket, where the full-size button is heavier than the card. */
.btn--sm { padding: 0.5rem 0.9rem; font-size: var(--text-sm); }

/* Pinned to the foot of the card and centred, so every ticket in a row shows
   its button at the same height regardless of how much terms copy sits above
   it. Needs .ticket__body to be a flex column for the auto margin to resolve. */
.ticket__action {
  margin-top: auto;
  padding-top: var(--s-5);
  text-align: center;
}

/* Honeypot - off-screen, not display:none, so a bot's basic HTML parser
   still sees and fills the field. Real visitors never encounter it. */
.field--honeypot {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* Server-rendered submit result. Stays in-palette rather than borrowing an
   unrelated green - gold reads as an affirmative here, burgundy as the one
   colour on the page already doing double duty for "pay attention." */
.form-notice {
  padding: var(--s-4) var(--s-5);
  border-radius: var(--radius-lg);
  font-weight: 600;
}
.form-notice--success {
  background: var(--ground);
  border: 2px solid var(--gold);
  color: var(--ink);
}
.form-notice--error {
  background: rgba(128, 12, 25, 0.06);
  border: 2px solid var(--burgundy);
  color: var(--burgundy-deep);
}

.file-input {
  display: block;
  width: 100%;
  font-family: var(--font-body);
  font-size: var(--text-sm);
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--ink-soft);
  border-radius: var(--radius);
  padding: var(--s-3);
}

.file-preview {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3);
  margin-top: var(--s-3);
}
.file-preview:empty { display: none; }
.file-preview__item {
  position: relative;
  width: 5.5rem;
}
.file-preview__thumb {
  display: block;
  width: 5.5rem;
  height: 5.5rem;
  object-fit: cover;
  border-radius: var(--radius);
  border: 1px solid var(--ink-soft);
  background: var(--ground);
}
.file-preview__name {
  display: block;
  margin-top: var(--s-1);
  font-size: 0.7rem;
  color: var(--ink-soft);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.file-preview__remove {
  position: absolute;
  top: -0.5rem;
  right: -0.5rem;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 50%;
  border: 1px solid var(--burgundy);
  background: var(--paper);
  color: var(--burgundy);
  font-size: 0.7rem;
  line-height: 1;
  cursor: pointer;
}
.file-preview__remove:hover { background: var(--burgundy); color: var(--paper); }

/* --------------------------------------------------------------------------
   12a. Site map tree — asp:TreeView output on /sitemap
   -------------------------------------------------------------------------- */
.site-tree table { border-collapse: collapse; }
.site-tree .site-tree__node a,
.site-tree .site-tree__leaf a {
  font-family: var(--font-body);
  font-size: var(--text-base);
  color: var(--ink);
  text-decoration: none;
  padding: var(--s-1) var(--s-2);
  border-radius: var(--radius);
  display: inline-block;
  min-height: 32px;
  line-height: 32px;
}
.site-tree .site-tree__node a { font-weight: 700; color: var(--burgundy); }
.site-tree .site-tree__leaf a:hover,
.site-tree .site-tree__node a:hover { background: var(--ground); color: var(--burgundy); }

/* --------------------------------------------------------------------------
   13. Footer
   -------------------------------------------------------------------------- */
/* Gold appears only where the ground is dark enough to carry it.
   On burgundy-deep it measures 6.76:1. */
.site-footer {
  background: var(--burgundy-deep);
  color: var(--paper);                    /* 14.22:1 */
  padding-block: var(--s-7);
  border-top: 3px solid transparent;
  border-image: var(--gold-sheen) 1;
}
.site-footer a { color: var(--paper); }

.site-footer__grid {
  display: grid;
  grid-template-columns: 1fr;      /* mobile first: one column */
  gap: var(--s-6);
}
@media (min-width: 700px) { .site-footer__grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1000px) { .site-footer__grid { grid-template-columns: 1.2fr 1fr 1fr 1fr; } }

/* Text wordmark, not the logo image. The PNG has an opaque white background
   (confirmed - no alpha channel), so it can't sit on this burgundy-deep band
   without an ugly white box around it, and there is no separate light
   variant of it. A styled text lockup avoids the problem entirely rather
   than working around it. */
/* The light logo variant carries the brand in the footer. The header PNG has
   no transparency so it cannot sit on burgundy; this white-fill SVG can, and
   reads as the actual mark rather than a plain-text stand-in. */
.site-footer__logo { width: 240px; max-width: 100%; height: auto; display: block; }
.site-footer__swam {
  width: 110px; height: auto;
  margin-top: var(--s-4);
  background: var(--paper);
  padding: var(--s-2);
  border-radius: var(--radius);
}
/* Footer links measured 22–32px tall on a phone. The type size is right for a
   footer; the target was not. Padding rather than a bigger font. */
@media (max-width: 859px) {
  .site-footer a { display: inline-block; padding-block: var(--s-2); }
  .site-footer .cluster { gap: var(--s-4); }
}

.site-footer__head {
  font-family: var(--font-display);
  font-size: var(--text-base);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--gold);              /* 6.76:1 on burgundy-deep */
  margin: 0 0 var(--s-3);
}
.site-footer__links { list-style: none; margin: 0; padding: 0; }
.site-footer__links li { margin-bottom: var(--s-2); }
.site-footer__links a { min-height: 32px; display: inline-flex; align-items: center; }

.site-footer__meta {
  margin-top: var(--s-6);
  padding-top: var(--s-4);
  border-top: 1px solid rgba(255,255,255,0.22);
  font-size: var(--text-sm);
  opacity: 0.92;
}

/* --------------------------------------------------------------------------
   14. Utilities
   -------------------------------------------------------------------------- */
.visually-hidden {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.stack > * + * { margin-top: var(--s-4); }
.cluster { display: flex; flex-wrap: wrap; gap: var(--s-3); align-items: center; }

/* Signposts on the error page. A visitor who mistyped an address has nothing
   to go on, so these carry a little more weight than body copy. */
.marked-list { list-style: none; padding: 0; margin: 0; }
.marked-list > li + li { margin-top: var(--s-3); }
.marked-list > li {
  padding-left: var(--s-4);
  border-left: 3px solid var(--gold);
  line-height: 1.5;
}
.marked-list a { font-weight: 700; color: var(--burgundy); }
.marked-list a:hover { color: var(--burgundy-deep); }

/* --------------------------------------------------------------------------
   15. Motion
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  .btn:hover { transform: none; }
}

@media print {
  .site-header, .site-footer, .nav, .btn { display: none; }
  .acc__body { display: block !important; }
  body { background: #fff; }
}

/* --------------------------------------------------------------------------
   Footer social marks — glyph plus word. The SVGs inherit currentColor so
   they pick up the footer link colour and its hover without a second rule.
   -------------------------------------------------------------------------- */
.site-footer__social a { display: inline-flex; align-items: center; gap: var(--s-2); }
.site-footer__social svg { width: 1.05em; height: 1.05em; flex: none; }

/* --------------------------------------------------------------------------
   Primary button on a dark ground.

   The hero previously ran two outlined buttons of identical weight, so the
   most important click on the site looked exactly like the second most
   important. --primary cannot be used as-is here: its fill IS the burgundy
   the hero is already painted in. Inverted instead — paper fill, burgundy
   label (10.58:1) — with the gilt band swapped for a burgundy one so the
   edge treatment survives the inversion. This is the same construction the
   secondary button already uses when it is hovered on a burgundy band.
   -------------------------------------------------------------------------- */
.btn--on-dark {
  background-color: var(--paper);
  color: var(--burgundy);
  border-color: var(--paper);
  background-image: linear-gradient(var(--burgundy), var(--burgundy));
  background-size: 100% 3px;
  background-position: bottom;
  background-repeat: no-repeat;
  background-origin: border-box;
}
.btn--on-dark:hover {
  background-color: var(--paper);
  color: var(--burgundy-deep);
  background-size: 100% 5px;
  box-shadow: 0 4px 14px rgba(28, 22, 20, 0.28);
  transform: translateY(-2px);
}
.btn--on-dark:active { transform: translateY(0); box-shadow: none; }

/* On a phone the two hero buttons sat left-aligned at different widths and
   read as ragged. Full width below 480px, where there is no cost to it. */
@media (max-width: 479px) {
  .hero__cta { flex-direction: column; align-items: stretch; }
  .hero__cta .btn { width: 100%; justify-content: center; text-align: center; }
}


/* --------------------------------------------------------------------------
   Gallery filter.

   Fifty pieces in one flat grid ran to 46,000px on a phone. The buttons are
   real buttons with aria-pressed, and the count is a live region so a screen
   reader is told how many pieces are left rather than being dropped into a
   silently changed grid.
   -------------------------------------------------------------------------- */
.gallery-filter {
  margin-top: var(--s-6);
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s-3) var(--s-4);
}
.gallery-filter[hidden] { display: none; }
.gallery-filter__label {
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.gallery-filter__set { display: flex; flex-wrap: wrap; gap: var(--s-2); }
.gallery-filter__btn {
  appearance: none;
  min-height: 44px;
  padding: var(--s-2) var(--s-4);
  font: inherit;
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--hairline);
  border-radius: 999px;
  cursor: pointer;
  transition: background var(--transition), border-color var(--transition), color var(--transition);
}
.gallery-filter__btn:hover { border-color: var(--burgundy); color: var(--burgundy); }
/* Burgundy fill at 10.58:1, not a gold one - gold on a light control is
   2.10:1 and would be an invisible selected state. */
.gallery-filter__btn.is-active {
  background: var(--burgundy);
  border-color: var(--burgundy);
  color: var(--paper);
}
.gallery-filter__count {
  margin: 0;
  font-size: var(--text-sm);
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}
.catalogue li[hidden] { display: none; }

/* --------------------------------------------------------------------------
   Site map.

   Replaces an <asp:TreeView>, which used 29% of the page width in a single
   narrow column with WebResource.axd toggle images beside the branches. Three
   columns of plain links, matching the footer, so the page finally looks like
   the rest of the site.
   -------------------------------------------------------------------------- */
.sitemap-list {
  list-style: none;
  margin: var(--s-7) 0 0;
  padding: 0;
  display: grid;
  gap: var(--s-6) var(--s-7);
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  align-items: start;
}
.sitemap-list__group { break-inside: avoid; }
.sitemap-list__top {
  display: flex;
  align-items: center;
  min-height: 44px;
  font-family: var(--font-display);
  font-size: var(--text-lg);
  font-weight: 600;
  color: var(--ink);
  text-decoration: none;
  padding-bottom: var(--s-2);
  border-bottom: 2px solid var(--burgundy);   /* 10.58:1 on paper */
}
.sitemap-list__top:hover { color: var(--burgundy); }
.sitemap-list__sub { list-style: none; margin: var(--s-3) 0 0; padding: 0; }
.sitemap-list__sub li + li { margin-top: var(--s-1); }
.sitemap-list__sub a {
  display: block;
  /* 40px was the TreeView's height and it was under the 44px this file sets
     for every other target. */
  min-height: 44px;
  display: flex;
  align-items: center;
  padding: var(--s-2) var(--s-3);
  margin-left: calc(var(--s-3) * -1);
  border-radius: var(--radius);
  color: var(--ink);
  text-decoration-color: var(--hairline);
}
.sitemap-list__sub a:hover { background: var(--ground); color: var(--burgundy); }


/* --------------------------------------------------------------------------
   /deals keeps its own composition.

   This block briefly left-aligned the offer heads and the ticket actions to
   match every other section head on the site. Consistency was the wrong goal
   here: a coupon is a centred object, and the tickets are the one place on
   the site where that reads as deliberate rather than as a stray default.
   Left-aligning the heads pulled them off the row they belong to, and the
   "Add to request" buttons ended up hanging in the bottom-left corner of
   cards that had grown too wide for their one line of copy.

   The rules that used to be here are gone; the originals higher up this file
   stand. Only the phone-width button survives, because that one is about the
   size of a tap target and not about where things sit.

   "Add to request" is the only thing anyone is on this page to do, and it
   measured 135x35 on a phone. Full width below 560px.
   -------------------------------------------------------------------------- */
@media (max-width: 559px) {
  .ticket__action .btn { display: flex; width: 100%; justify-content: center; }
}

/* The home page testimonial: the eyebrow, dots and link were centred while
   the quote itself was a left-aligned block with a rule down its edge, so the
   quote read as sitting off-centre. All left now. */
.fm-fader__dots { justify-content: flex-start; }


/* --------------------------------------------------------------------------
   Testimonials — the substantial four, then the short ones.

   Every quote used to get an identical card, so a four-word thank-you carried
   the same visual weight as a paragraph explaining why the work mattered.
   The lead quotes get room and a larger face; the rest keep the existing
   compact card and sit under their own heading.
   -------------------------------------------------------------------------- */
.quote-lead {
  list-style: none;
  margin: 0; padding: 0;
  display: grid;
  gap: var(--s-5);
  grid-template-columns: repeat(auto-fit, minmax(22rem, 1fr));
}
.testimonial--lead {
  background: var(--paper);
  border-left: 3px solid var(--burgundy);
  border-radius: var(--radius);
  padding: var(--s-5) var(--s-6);
  box-shadow: var(--shadow-sm);
}
.testimonial--lead p {
  font-family: var(--font-display);
  font-size: var(--text-lg);
  line-height: 1.5;
  margin: 0;
}
.testimonial--lead footer {
  margin-top: var(--s-4);
  font-family: var(--font-body);
  font-size: var(--text-sm);
  color: var(--ink-soft);          /* 7.51:1 on paper */
}
.quote-more__head {
  font-size: var(--text-lg);
  margin: var(--s-8) 0 var(--s-5);
  padding-bottom: var(--s-2);
  border-bottom: 1px solid var(--hairline);
}


/* --------------------------------------------------------------------------
   Home service index — picture and name, no summary.

   Smaller minimum than .tile-grid so the index reads as an index rather than
   as the catalogue it used to duplicate, and so a phone shows two across
   instead of one. That alone removes roughly a third of the home page's
   height on a 390px screen.
   -------------------------------------------------------------------------- */
.tile-grid--index { grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: var(--s-4); }
.tile-grid--index .tile__body { padding: var(--s-3) var(--s-4) var(--s-4); }
.tile-grid--index .tile__title { font-size: var(--text-base); }
@media (min-width: 700px) {
  .tile-grid--index { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); }
  .tile-grid--index .tile__title { font-size: var(--text-lg); }
}

/* --------------------------------------------------------------------------
   Resolving the tension between two fixes.

   Narrowing --measure from 68ch to 48ch made the LINES right and the
   COMPOSITION worse: any lone measure-width column sitting in a full-width
   band now stops even further short of the container. Six bands regressed.
   The answer is not to widen the text back, it is to stop putting a narrow
   column in a wide band.
   -------------------------------------------------------------------------- */

/* Prose pages — /privacy and /terms are one column of text and nothing else.
   With a 74rem container and a 48ch measure, 61% of the band was empty. Narrow
   the CONTAINER so the band is the width of its text and the page reads as a
   document rather than as a column that failed to fill. */
/* Size the CONTAINER to the measure, rather than removing the measure and
   letting the text fill a wide container - the first attempt did the latter
   and ran /privacy and /terms out to 832px, about 134 characters a line,
   which is worse than the 100 the review started from. 33rem holds the 477px
   measure plus the container padding and almost nothing else, so the lines
   stay right AND the band has no empty half. */
.band--prose .container { max-width: 33rem; }

/* The home testimonial. A quote is not body copy: short lines at a larger size
   read fine well past the prose measure, and holding it to 48ch left 485px of
   the band empty once the block was left-aligned. */
.fm-fader { max-width: 46rem; }
.fm-fader__item p {
  font-family: var(--font-display);
  font-size: var(--text-xl);
  line-height: 1.45;
}

/* The coupons were briefly a grid with `minmax(19rem, 1fr)` tracks and the
   26rem cap removed, to close the 304px the centred flex row left empty in an
   1184px container. It closed the gap and moved it inside the cards: an offer
   is one short sentence, so a 470px-wide ticket is mostly white, and the lone
   hotel offer stretched the full width of the band around a single line. The
   dead-space number went down and the page looked worse - the same mistake as
   the two-column section head, in a place where the empty space either side of
   a centred row is the composition and not a defect.
   The flex row and the 26rem cap higher up this file stand. */

/* --------------------------------------------------------------------------
   Remaining tap targets.

   The carousel dots were fixed by widening their ::after; the testimonial
   fader has its own dots with the same -14px inset and the same 39.2px
   result. And "Add to request" went full width without getting any taller —
   292x35 is wide enough to hit and still too short.
   -------------------------------------------------------------------------- */
.fm-fader__dot::after { inset: -17px; }
.fm-fader__dots { gap: var(--s-6); }

.btn--sm { min-height: 44px; }

/* Footer links landed at 41px with --s-2 either side; --s-3 clears 44. */
@media (max-width: 859px) {
  .site-footer a { padding-block: var(--s-3); }
}

/* Nine services in an auto-fill grid landed as 5 + 4, leaving an empty cell at
   the end of the second row — the same orphan the About credentials strip was
   already fighting. Nine is 3 x 3. */
@media (min-width: 900px) {
  .tile-grid--index { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (min-width: 620px) and (max-width: 899px) {
  .tile-grid--index { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* The carousel was centred (margin-inline: auto) inside a container whose
   heading and button are ranged left, so its left edge sat 119px inboard of
   the words above it — the sort of misalignment that is invisible in a
   measurement and obvious the moment you look at the page. Left, like
   everything else in the band; the controls stay centred under the picture
   they operate. */
.fm-carousel { margin-inline: 0; }


/* --------------------------------------------------------------------------
   Section head — heading, then its intro underneath.

   This was briefly two columns, heading left and intro right, to fill the
   band. It filled the band and broke the thing that mattered: a heading and
   its own sentence stopped reading as a pair. Bottom-aligning them made it
   worse still - where the intro ran five lines against a two-line heading,
   the heading sank to the foot of the row and left a diagonal hole.

   The "42% unused width" the dead-space sweep reported here is not the same
   defect as the 35% on the call-to-action band, where two buttons could fill
   it, or the 71% on the sitemap. A left-ranged heading with its intro beneath
   and air to the right is just a section head. The sweep was right about the
   CTA and the sitemap and wrong about this one.
   -------------------------------------------------------------------------- */
.section-head { display: block; }
.section-head__lead > :first-child { margin-top: 0; }
.section-head__lead > :last-child { margin-bottom: 0; }
.section-head__intro { margin: var(--s-4) 0 0; max-width: var(--measure); }

/* The eyebrow was --text-xs (13px). Above a 34px display heading it read as a
   stray caption rather than as the label of the section. --text-sm with a
   little more tracking gives it enough presence to belong to the heading,
   and the gap between the two comes down so they read as one unit. */
.eyebrow {
  font-size: var(--text-sm);
  letter-spacing: 0.12em;
  margin-bottom: var(--s-1);
}

/* The carousel goes back to centred. It was moved left because it sat 119px
   inboard of a left-ranged heading and the two edges disagreed; with the
   section head now spanning the full width, a centred picture underneath is
   balanced by both edges instead of arguing with one. */
.fm-carousel { margin-inline: auto; }

/* --------------------------------------------------------------------------
   Service tiles — a burgundy rule between the picture and its name.

   The tiles were the only cards on the site with no brand mark on them at
   all: a white box, a hairline, and a photograph. This is the same 3px
   burgundy top border the "At a glance" strip and the service-detail headings
   already use, so it is a treatment the site owns rather than a new one.

   Not gold: on a white card gold measures 2.10:1 and would be an invisible
   rule. Burgundy is 10.58:1.
   -------------------------------------------------------------------------- */
.tile__body { border-top: 3px solid var(--burgundy); }
.tile:hover .tile__body { border-top-color: var(--burgundy-deep); }

/* Moving the filter into the grid band left it 24px above the first row of
   tiles while carrying 100px of band padding above it - it read as belonging
   to the pictures rather than governing them. Even it up, and give it a rule
   so it is visibly a control strip and not a stray line of chips. */
.gallery-filter {
  margin-top: 0;
  margin-bottom: var(--s-7);
  padding-bottom: var(--s-5);
  border-bottom: 1px solid var(--hairline);
}

/* --------------------------------------------------------------------------
   Testimonial fader — stop it resizing the page every time it advances.

   The quotes run from four words to five lines, and only the active one was
   laid out (display:none on the rest). So every auto-advance changed the
   container's height and shoved everything below it: measured at 0.57 CLS on
   main and 0.63 here, over a window long enough to catch three rotations.
   (The earlier "CLS 0" readings were honest for a page LOAD and wrong as a
   general claim — the shift only appears once the fader turns over.)

   All the quotes now occupy the same grid cell, so the container is always as
   tall as the tallest one and nothing moves. visibility rather than display,
   because visibility:hidden still reserves the box AND still takes the
   inactive quotes out of the accessibility tree.
   -------------------------------------------------------------------------- */
.fm-fader {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  min-height: 0;                      /* the tallest quote sets it now */
}
.fm-fader__item {
  display: block;
  visibility: hidden;
  grid-row: 1;
  grid-column: 1;
}
.fm-fader__item.is-active { visibility: visible; }
.fm-fader__dots { grid-row: 2; grid-column: 1; }

/* --------------------------------------------------------------------------
   SWaM certification mark.

   110px wide from a 336px source: three times more file than box, and the
   words inside it were too small to read, which defeats the point of showing
   a credential. 200px sits under the 240px footer wordmark without crowding
   it, and puts the source to better use (1.7x rather than 3.05x).
   -------------------------------------------------------------------------- */
.site-footer__swam {
  width: 200px;
  max-width: 100%;
  padding: var(--s-3);
}
/* Below the footer's three-column breakpoint the column is full width, so the
   mark can be larger still without competing with anything. */
@media (max-width: 599px) {
  .site-footer__swam { width: 240px; }
}

/* The two certification marks sit on one row and wrap together.
   The eVA file is 1.53:1 against SWaM's 2.56:1, so matching them on WIDTH
   would make eVA tower over the badge beside it. They are matched on HEIGHT
   instead, which is what the eye actually compares when two marks sit side by
   side, and each keeps its own proportions. */
.site-footer__marks {
  align-items: center;
  gap: var(--s-3);
  margin-top: var(--s-4);
}
.site-footer__marks .site-footer__swam { margin-top: 0; }
/* Sized so the two chips finish the same height: the SWaM chip measures 93px
   overall at desktop and 108px below 600px, and these are those numbers.
   `height` here is the WHOLE chip, padding included - box-sizing is border-box
   site-wide - which is why it is not 93 minus the padding. Set from a
   measurement of the badge beside it, not by eye. */
.site-footer__eva {
  height: 93px; width: auto;
  background: var(--paper);
  padding: var(--s-2);
  border-radius: var(--radius);
}
@media (max-width: 599px) {
  .site-footer__eva { height: 108px; }
}


/* --------------------------------------------------------------------------
   Full-width section intro.

   Deliberately outside --measure, at the owner's call: on these two sections
   the empty space beside a measure-width intro bothered them more than the
   line length does, and both sentences are short enough to set as a single
   line at container width rather than as a block of long lines. That is the
   part that makes it work - one line has no return sweep to lose - so if
   either sentence is ever lengthened, check it still fits on one line at
   1440px before leaving this class on it.
   -------------------------------------------------------------------------- */
.section-head__intro--full { max-width: none; }

/* The caption under the Our Work photograph, matching the hero and the
   catalogue captions rather than inventing a third style. */
.portrait-caption {
  margin: var(--s-3) 0 0;
  font-size: var(--text-sm);
  color: var(--ink-soft);          /* 7.51:1 on paper */
}
.portrait-frame--figure { margin: 0; }

/* A captioned photograph. The caption must sit OUTSIDE .portrait-frame, not
   inside it: that frame is `overflow: hidden` with `line-height: 0` for the
   gold edge, so a figcaption placed within it renders clipped and collapsed
   across the bottom border. Seen happening on /repair before this existed.
   So the <figure> is the outer element, the frame is a plain div inside it,
   and the caption is a sibling of the frame - which is also the correct
   nesting for figcaption. */
.portrait-figure { margin: 0; }

/* At container width the shorter of the two intros sets as one clean line, but
   the longer one broke as a 192-character line plus a two-word widow - worse
   than either full width or measure width. text-wrap: pretty tells the browser
   to avoid a stubby last line, so it balances the two lines instead. */
.section-head__intro--full { text-wrap: pretty; }

/* --------------------------------------------------------------------------
   Our Work intro — the photograph matches the height of the text beside it.

   A fixed crop can only match at one viewport: the text column grows taller as
   it wraps, and the picture does not follow, so a gap opens under one or the
   other. Stretching the row and letting the image cover its box means the two
   columns finish level at EVERY width, with no whitespace to trim.

   Only above the split's own breakpoint - stacked on a phone, the picture keeps
   its own proportions.
   -------------------------------------------------------------------------- */
@media (min-width: 900px) {
  .split--flush { align-items: stretch; }
  .split--flush > div:last-child { display: flex; flex-direction: column; }
  .split--flush .portrait-frame--figure { flex: 1 1 auto; min-height: 0; display: flex; }
  .split--flush .portrait-frame--figure picture { display: block; width: 100%; }
  .split--flush .portrait-frame--figure img {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }
}

/* --------------------------------------------------------------------------
   Our Work intro, revised.

   Pinning the picture to the text's height forced a 2:1 letterbox and, since
   the crop already used the full width of the source, there was no way to show
   more of the sofa. The picture keeps its own 1.6:1 proportions now and is
   allowed to be the taller of the two columns.

   The text is centred against it rather than sitting at the top, so the extra
   height reads as the two columns being balanced instead of as a hole under
   the paragraph — which is what an aligned-to-top version would look like.
   -------------------------------------------------------------------------- */
@media (min-width: 900px) {
  .split--flush .portrait-frame--figure img {
    height: auto;
    object-fit: fill;              /* no cover crop: show the whole 1.6:1 cut */
  }
  .split--flush > div:first-child {
    justify-content: center;       /* the column is already display:flex */
    display: flex;
    flex-direction: column;
  }
  .split--flush .portrait-frame--figure { flex: 0 0 auto; }
}

/* --------------------------------------------------------------------------
   Home carousel — a real cross-fade.

   The slides were display:none / display:block with a fade-IN keyframe on the
   incoming one. The outgoing slide therefore vanished on the same frame the
   new one began, which is why it read as a hard switch rather than a fade:
   there was never a moment where both were on screen.

   Stacked in one grid cell and cross-faded on opacity instead. visibility is
   delayed by the length of the fade on the way out, so the outgoing slide is
   still painted while it fades but is still removed from the accessibility
   tree once it is gone.
   -------------------------------------------------------------------------- */
.fm-carousel__track { display: grid; grid-template-columns: minmax(0, 1fr); }
.fm-carousel__slide {
  display: block;
  grid-row: 1;
  grid-column: 1;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  animation: none;                 /* the old keyframe would fight the transition */
  transition: opacity 650ms ease, visibility 0s linear 650ms;
}
.fm-carousel__slide.is-active {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  animation: none;
  transition: opacity 650ms ease, visibility 0s linear 0s;
}

@media (prefers-reduced-motion: reduce) {
  .fm-carousel__slide,
  .fm-carousel__slide.is-active { transition: none; }
}
