/* =====================================================================
   VANTAGE POINT CREATIVE — public site shell
   Real brand colours sampled from the actual logo files (navy + grey),
   not invented. Plain and restrained on purpose — this is a framework
   to build on, not a finished design; every placeholder is labelled.
   ===================================================================== */

:root {
  --navy:   #1c2c58;
  --navy-2: #16234a;
  --grey:   #6f6f6f;
  --bg:     #fbfaf7;
  --surface:#ffffff;
  --border: #e2e0da;
  --text:   #1c1c1c;
  --text-muted: #666666;
  /* Light blue-grey, for text sitting on the navy where pure white would
     be too flat (eyebrows, ticker dots) — no third hue, just a tint of
     the brand navy itself. Replaces an earlier brass accent that tested
     as "out there and not on theme". */
  --sky: #8fa8d6;
  --font-head: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
  --font-body: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
  /* Display face, used only on the page banners and the homepage hero.
     Space Grotesk has near-uniform stroke weight, which is clean at 14px
     and flat at 58px. This has real thick/thin, and it picks up the serif
     already sitting in the VPC logo, which nothing else on the site did.
     One weight only: DM Serif Display ships regular and italic. */
  --font-display: 'DM Serif Display', Georgia, 'Times New Roman', serif;

  /* One elevation scale, used everywhere something needs to lift off the
     page, so depth is consistent instead of decided per component. Tinted
     with the brand navy rather than pure black: a neutral-black shadow on
     a warm cream ground reads as grey dirt. */
  --e1: 0 1px 2px rgba(28,44,88,.06);
  --e2: 0 2px 6px rgba(28,44,88,.08);
  --e3: 0 10px 24px -8px rgba(28,44,88,.18);
  --e4: 0 22px 48px -16px rgba(28,44,88,.28);
}

* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
  margin: 0; background: var(--bg); color: var(--text);
  font-family: var(--font-body); line-height: 1.55;
}
img { max-width: 100%; display: block; }
a { color: inherit; }
/* Keyboard focus was inheriting the browser default, which the site's own
   outline colours were washing out on the navy. :focus-visible rather than
   :focus so it shows for keyboard users without ringing every mouse click. */
:focus-visible { outline: 2px solid var(--navy); outline-offset: 2px; }
.page-head :focus-visible, .hero-diag :focus-visible, .site-footer :focus-visible { outline-color: currentColor; }
.container { max-width: 1080px; margin: 0 auto; padding: 0 24px; }
h1, h2, h3 { font-family: var(--font-head); font-weight: 600; margin: 0 0 10px; color: var(--navy); }
/* Form controls don't take the page's font by default, so every <button>
   came out in Arial, and a .btn button sat 6px shorter than the same .btn
   as a link beside it. */
button, input, select, textarea { font-family: inherit; }
/* Headings break into even lines rather than leaving one word (or half of
   "in-house") on a line of its own; paragraphs avoid a lone last word.
   Browsers that don't know these simply wrap as before. */
h1, h2, h3, .rot-phrase { text-wrap: balance; }
p, li, dd { text-wrap: pretty; }
/* A phone number never splits across two lines, and nor does a short
   link inside a sentence ("frame builder" was breaking in two). */
a[href^="tel:"], .inline-link { white-space: nowrap; }
p { margin: 0 0 14px; }

/* --- placeholder marker, used everywhere content isn't real yet ----- */
.placeholder {
  display: inline-block; font-size: 11.5px; font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase; color: #a05a00; background: #fff2df;
  border: 1px solid #f0d3a3; padding: 2px 9px; border-radius: 3px; margin-bottom: 8px;
}

/* --- header ---------------------------------------------------------- */
/* Sticky, so it needs to sit above the page rather than sharing its
   plane. The shadow only appears once the page has actually scrolled
   under it (script.js sets .is-stuck) — a floating bar at the very top of
   an unscrolled page is shadow for its own sake. */
.site-header {
  background: rgba(255,255,255,.88); backdrop-filter: saturate(160%) blur(10px);
  border-bottom: 1px solid var(--border);
  position: sticky; top: 0; z-index: 50;
  transition: box-shadow .25s ease, background-color .25s ease;
}
.site-header.is-stuck { background: rgba(255,255,255,.94); box-shadow: var(--e2); }
@supports not (backdrop-filter: blur(10px)) { .site-header { background: var(--surface); } }
.header-inner { display: flex; align-items: center; justify-content: space-between; padding: 12px 24px; gap: 20px; }
/* The full lockup rather than the square mark alone. It is 2.6:1, and the
   wordmark stacks three lines inside that height, so it needs more height
   than the mark did or "Vantage Point Creative" turns to mush. */
.logo img { height: 52px; width: auto; }
@media (max-width: 760px) { .logo img { height: 44px; } }
/* The current page was marked with a filled beige block, which read as a
   button that had wandered into a nav bar and gave the header two
   competing rectangles. A rule under the word instead, and the inactive
   links drop to muted, so "you are here" is carried by colour as well as
   the mark rather than by a slab of background. */
.main-nav { display: flex; gap: 6px; }
.main-nav a {
  position: relative; font-size: 14px; font-weight: 600; text-decoration: none;
  color: var(--text-muted); padding: 8px 12px; transition: color .2s ease;
}
.main-nav a::after {
  content: ''; position: absolute; left: 12px; right: 12px; bottom: 0; height: 2px;
  background: var(--border); transform: scaleX(0);
  transition: transform .2s ease, background-color .2s ease;
}
.main-nav a:hover { color: var(--navy); }
.main-nav a:hover::after { transform: scaleX(1); }
/* After the hover rules on purpose: same specificity, so source order is
   what keeps the current page's rule navy while it is hovered. */
.main-nav a[aria-current] { color: var(--navy); }
.main-nav a[aria-current]::after { background: var(--navy); transform: scaleX(1); }
@media (prefers-reduced-motion: reduce) {
  .main-nav a, .main-nav a::after { transition: none; }
}
.nav-toggle {
  display: none; width: 38px; height: 38px; border: 1px solid var(--border); background: #fff;
  flex-direction: column; align-items: center; justify-content: center; gap: 4px; cursor: pointer;
}
.nav-toggle span { width: 18px; height: 2px; background: var(--navy); }
@media (max-width: 760px) {
  .nav-toggle { display: flex; }
  /* Closed, this has to collapse to nothing at all. box-sizing is
     border-box everywhere, so max-height:0 measures the border box and
     cannot squash the padding or the bottom border out of existence —
     the panel floored at 13px and left a strip of itself showing under
     the header, with the current page's 2px marker as a tick in the
     corner of it. So the padding and the border belong to the open
     state, not to the panel. */
  .main-nav {
    position: absolute; top: 100%; left: 0; right: 0; flex-direction: column;
    background: var(--surface);
    max-height: 0; overflow: hidden;
  }
  .main-nav.open { max-height: 360px; border-bottom: 1px solid var(--border); padding: 6px; }
  .main-nav a { padding: 12px 14px; }
  /* Stacked full-width, an underline reads as a divider between rows
     rather than a marker on one. A bar down the leading edge instead. */
  .main-nav a::after { display: none; }
  .main-nav a[aria-current] { background: rgba(28,44,88,.05); box-shadow: inset 2px 0 0 var(--navy); }
}

/* --- hero / page head ------------------------------------------------- */
.hero {
  background: var(--navy); color: #fff; padding: 56px 0; text-align: center;
}
.hero h1 { color: #fff; font-size: clamp(26px, 4vw, 38px); }
.hero p { color: rgba(255,255,255,.78); max-width: 560px; margin: 0 auto 22px; font-size: 15px; }
/* The banner every page except Home opens on. It was one flat navy fill,
   which is what made the pages feel like a template: no light in it, no
   depth, and the same tone edge to edge.
   Three layers now, all navy, no new hue. A diagonal base so the colour
   shifts across the width; a soft light thrown from the top right, which
   is where the homepage hero's light already comes from; and a wash
   lifting off the bottom edge so the band does not die into the page. */
.page-head {
  position: relative; overflow: hidden; color: #fff; padding: 54px 0 58px; text-align: center;
  background:
    radial-gradient(115% 130% at 82% -25%, rgba(143,168,214,.24) 0%, rgba(143,168,214,0) 55%),
    linear-gradient(152deg, #22356a 0%, var(--navy) 45%, var(--navy-2) 100%);
  box-shadow: var(--e3);
}
/* A lighter tint than --sky. --sky is sized for text on flat navy, where
   it measures 5.6; against the lightest corner of the banner gradient it
   drops to 2.83. The eyebrow is centred, so in practice it would sit in
   the safe part of the sweep, but that is luck rather than design. This
   clears 4.5 at every point of the gradient. */
.page-head-eyebrow {
  display: block; font-size: 12px; font-weight: 700; letter-spacing: .22em;
  text-transform: uppercase; color: #d5dff4; margin-bottom: 12px;
}
.page-head h1 {
  font-family: var(--font-display); font-weight: 400;
  color: #fff; font-size: clamp(34px, 5.4vw, 58px); line-height: 1.04;
  margin-bottom: 14px; letter-spacing: -.01em;
}
/* .88 rather than the .78 this used to be. Against a flat navy that was
   comfortable; against the lightest corner of the gradient, with the
   highlight and the wedge stacked on it, it measured 4.53 against a 4.5
   requirement. Passing by 0.03 is not passing. Now 5.7. */
.page-head p { color: rgba(255,255,255,.88); max-width: 520px; margin: 0 auto; font-size: 15px; }
/* The same diagonal-cut language as the homepage hero, kept as a wedge
   rather than a repeat of the whole treatment. */
.page-head::after {
  content: ''; position: absolute; right: -4%; top: 0; bottom: 0; width: 34%;
  background: rgba(255,255,255,.05); clip-path: polygon(32% 0, 100% 0, 68% 100%, 0% 100%);
}
/* A hairline of the light blue-grey along the bottom, so the band ends on
   an edge instead of a hard stop against the cream. */
.page-head::before {
  content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 1px; z-index: 3;
  background: linear-gradient(90deg, transparent, rgba(143,168,214,.55), transparent);
}
.page-head .container { position: relative; z-index: 2; }

/* --- hero: diagonal cut + rotating headline + full-bleed photo -----------
   Three taglines rotate every 12s. Everything to the right of the
   diagonal is one photo run full-bleed to the edges rather than a small
   floating card. Stacking order does the work: photo at 0, navy wedge at
   1, text at 2, so the navy's clip-path is what reveals the photo rather
   than the photo being clipped itself.
   Which photo is set in admin -> Photo Hub; see home-photos.js. */
/* The global `section { padding: 56px 0 }` applies here too, and was
   stacking on top of the inner box's own padding — 56px of navy above the
   text and 56px below, on every screen, that nothing asked for. The hero
   sets its own spacing on .hero-diag-inner instead. */
.hero-diag { position: relative; overflow: hidden; padding: 0; }
.hero-diag-photo { position: absolute; inset: 0; z-index: 0; background: var(--bg); }
/* The fallback here belongs to the photo that ships in index.html, not to
   the slot — the hero is far wider than any photo, so cover crops the
   height hard and which part to keep is a property of the image. Anything
   set in admin overrides it. */
.hero-diag-photo img { width: 100%; height: 100%; object-fit: cover; object-position: center var(--hero-focus, 28%); }
.hero-diag-bg { position: absolute; inset: -2px; z-index: 1; background: var(--navy); clip-path: polygon(0 0, 62% 0, 42% 100%, 0% 100%); }
.hero-diag-inner { position: relative; z-index: 2; display: flex; align-items: center; justify-content: flex-start; gap: 40px; padding: 64px 24px; min-height: 520px; }

.hero-eyebrow {
  display: inline-block; font-size: 13px; font-weight: 700; letter-spacing: .16em; text-transform: uppercase;
  color: var(--sky); margin-bottom: 16px;
}

.hero-diag-text { flex: 0 1 460px; max-width: 460px; color: #fff; }
/* The box reserves room for the tallest phrase so the buttons below it
   don't jump on every rotation. Expressed as two lines rather than a
   pixel height: the pixel values were measured against Space Grotesk,
   and the serif is narrower, so the same phrases wrap to fewer lines and
   the old heights left up to 58px of dead space under the headline.
   2 x 1.12 line-height = 2.24em, and it now follows the font size. */
.rot-box {
  position: relative; display: block; height: 2.24em; margin: 0 0 10px; font-size: clamp(30px, 4.4vw, 42px);
  line-height: 1.12; font-family: var(--font-display); font-weight: 400;
  /* the global `h1 { color: var(--navy) }` rule otherwise wins over
     inheriting white from .hero-diag-text — confirmed live, navy text
     was landing invisible against the navy backdrop. */
  color: #fff;
}
.rot-phrase {
  position: absolute; left: 0; top: 0; right: 0; display: block;
  opacity: 0; transform: translateY(14px);
  animation: rotatePhrase 12s infinite;
}
@keyframes rotatePhrase {
  0%   { opacity:0; transform:translateY(14px); }
  4%   { opacity:1; transform:translateY(0); }
  27%  { opacity:1; transform:translateY(0); }
  31%  { opacity:0; transform:translateY(-14px); }
  100% { opacity:0; transform:translateY(-14px); }
}
.hero-diag-text p { color: rgba(255,255,255,.85); font-size: 15px; margin-bottom: 22px; }
.accent-bar { width: 46px; height: 4px; background: #fff; margin-bottom: 22px; animation: barPulse 4s ease-in-out infinite; }
@keyframes barPulse { 0%, 100% { opacity:.55; width:32px; } 50% { opacity:1; width:52px; } }
@media (prefers-reduced-motion: reduce) {
  .rot-phrase { animation: none; opacity: 0; }
  .rot-phrase:first-child { opacity: 1; transform: none; }
  .accent-bar { animation: none; width: 46px; opacity: 1; }
}

.hero-ctas { display: flex; gap: 12px; flex-wrap: wrap; }
.hero-link {
  display: inline-block; margin-top: 16px; font-size: 14px; font-weight: 600;
  color: rgba(255,255,255,.88); text-decoration: none;
  border-bottom: 1px solid rgba(255,255,255,.35); padding-bottom: 2px;
  transition: color .2s ease, border-color .2s ease;
}
.hero-link:hover { color: #fff; border-bottom-color: #fff; }
.hero-link span { display: inline-block; transition: transform .2s ease; }
.hero-link:hover span { transform: translateX(3px); }
@media (prefers-reduced-motion: reduce) { .hero-link span { transition: none; } .hero-link:hover span { transform: none; } }
.btn-accent { background: #fff; color: var(--navy); border-color: #fff; }
.btn-accent:hover { background: #f1efe9; border-color: #f1efe9; }

/* Below ~900px there isn't width for a side-by-side split, so the cut
   turns horizontal: navy over the text, the photo as a band across the
   bottom. The bottom padding is what reserves that band — the inner box
   still sets the hero's height, so without it the photo would have
   nothing to show through.
   The navy is kept to roughly what the words need and no more: the
   padding is tight, and the cut starts just below the buttons. It used
   to sit far lower, which left a slab of empty navy between the buttons
   and the photo and made the whole hero read as a blue block. */
@media (max-width: 900px) {
  .hero-diag-inner { flex-direction: column; align-items: center; text-align: center; padding: 26px 24px 280px; min-height: 0; }
  /* Measured from the bottom, not as a percentage of the hero. As a
     percentage the gap between the buttons and the cut moved with the
     hero's height, so on a narrow phone where the buttons wrap onto two
     rows the cut ran straight through them. From the bottom, the
     clearance is the same whatever the text does. */
  .hero-diag-bg { clip-path: polygon(0 0, 100% 0, 100% calc(100% - 240px), 0 calc(100% - 140px)); }
  /* On a phone the photo only shows as a band along the bottom, so the
     layer is pinned to that band rather than filling the hero — otherwise
     cover fits the photo to the full height and the band lands on
     whatever happens to be at the bottom of it. */
  .hero-diag-photo { top: auto; bottom: 0; height: 300px; }
  .hero-diag-photo img { object-position: center 50%; }
  .hero-diag-text { flex: 1 1 auto; max-width: 100%; }
  .rot-box { font-size: clamp(24px, 6vw, 30px); }
  .accent-bar { margin: 0 auto 16px; }
  /* Centred, this would otherwise set the full width of the phone and
     read as a block of small print rather than a line under the
     headline. */
  .hero-diag-text p { max-width: 42ch; margin-left: auto; margin-right: auto; }
  .hero-ctas { justify-content: center; }
}


/* --- recent-work scroller: continuous slow marquee, kept in deliberately
   (not decoration) — pauses on hover/focus, sits still for anyone who
   prefers reduced motion. Faded edges hint that it keeps going. */

/* --- how it works (Home) ------------------------------------------------
   Heading and its note as two columns. Stacked they made a third
   paragraph in a column that already had three below it, which is what
   made this look busy — side by side the whole preamble is two lines
   deep. */
.how-to-head {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 40px; align-items: end; margin-bottom: 34px;
}
.how-to-head h2 { margin: 0; }
.how-to-head .section-sub { margin: 0; max-width: none; }
@media (max-width: 760px) {
  .how-to-head { grid-template-columns: 1fr; gap: 12px; align-items: start; margin-bottom: 26px; }
}

/* --- workshop intro (Home) ---------------------------------------------- */
.workshop-intro { background: var(--surface); border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); }
.workshop-inner { display: flex; align-items: center; gap: 44px; }
.workshop-photo { flex: 0 0 44%; width: 44%; aspect-ratio: 4/3; object-fit: cover; border: 1px solid var(--border); }
.workshop-inner > div { flex: 1; }
/* .section-sub carries a 30px bottom margin for the single-paragraph case
   it was written for; as a run of paragraphs that reads as three separate
   blocks rather than one piece of writing. */
.workshop-inner .section-sub { margin-bottom: 14px; max-width: none; }
.workshop-inner .section-sub:last-child { margin-bottom: 0; }
.workshop-inner h2 { margin-top: 4px; }
@media (max-width: 760px) {
  .workshop-inner { flex-direction: column; gap: 22px; }
  .workshop-photo { flex: none; width: 100%; }
}

/* --- reviews (Home) ------------------------------------------------------
   Three real Google reviews in a row, square like every other card on
   the site. The stars are the brand navy rather than the usual gold: gold
   is nowhere else in the palette. */
.reviews-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 20px; }
.review-card {
  margin: 0; padding: 24px 24px 22px;
  display: flex; flex-direction: column; gap: 14px;
  background: var(--surface); border: 1px solid var(--border); box-shadow: var(--e1);
}
.review-stars { color: var(--navy); font-size: 16px; letter-spacing: 3px; line-height: 1; }
.review-card blockquote { margin: 0; flex: 1; font-size: 15px; line-height: 1.6; color: var(--text); }
.review-card figcaption { font-size: 13px; color: var(--text-muted); }
.review-card figcaption b { color: var(--text); font-weight: 600; }
@media (max-width: 900px) { .reviews-grid { grid-template-columns: 1fr; gap: 14px; } }

/* --- L-frames (canvas frames) ---------------------------------------------
   Shown to customers, but always labelled: a canvas sits inside an L-frame
   with a gap all round, with no mount and no glass, so it's the wrong
   choice for a paper print and people need telling before they pick it. */
.lframe-tag {
  display: inline-block; margin: -2px 0 10px; padding: 2px 8px;
  font-size: 11.5px; font-weight: 700; letter-spacing: .02em;
  color: var(--navy); background: #eef2fb; border: 1px solid #c9d4ee;
}
.lframe-note {
  display: flex; gap: 10px; align-items: flex-start;
  margin: 0 0 18px; padding: 12px 14px;
  font-size: 13.5px; line-height: 1.5; color: var(--text);
  background: #eef2fb; border: 1px solid #c9d4ee; border-left: 3px solid var(--navy);
}
.lframe-note[hidden] { display: none; }
.lframe-note i { color: var(--navy); font-size: 18px; flex: none; margin-top: 1px; }
.b-field .lframe-note { margin: 10px 0 0; font-size: 12.5px; }
.chip-lframe {
  position: absolute; top: 3px; right: 3px; min-width: 16px; height: 16px; padding: 0 3px;
  font-style: normal; font-size: 10px; font-weight: 800; line-height: 16px; text-align: center;
  color: #fff; background: var(--navy);
}

/* Builder in L-frame (canvas) mode: the mount and glass steps keep their
   number and heading but swap their controls for a one-line note, so the
   panel keeps its shape and nothing further down jumps. */
.lframe-na { display: none; }
#builder-steps.is-lframe .lframe-na { display: block; margin-top: 4px; }
/* The canvas's shadow gap, only asked for with an L-frame. */
.lframe-only { display: none; }
#builder-steps.is-lframe .lframe-only { display: block; }
.b-field label.b-sub-label { display: block; margin: 0 0 6px; font-size: 13px; font-weight: 600; color: var(--text); }
#builder-steps.is-lframe #mount-field .b-toggle,
#builder-steps.is-lframe #mount-field > .hint:not(.lframe-na),
#builder-steps.is-lframe #mount-fields,
#builder-steps.is-lframe #glass-field > :not(label):not(.lframe-na) { display: none !important; }
#builder-steps.is-lframe #mount-field .b-toggle-row { pointer-events: none; gap: 0; }

/* --- buttons ----------------------------------------------------------- */
.btn {
  display: inline-flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 700; line-height: 1.55;
  padding: 11px 22px; border-radius: 3px; text-decoration: none; border: 1px solid transparent; cursor: pointer;
  transition: background-color .2s ease, border-color .2s ease, box-shadow .2s ease, transform .2s ease;
}
.btn:hover { box-shadow: var(--e2); }
.btn:active { transform: translateY(1px); box-shadow: none; }
@media (prefers-reduced-motion: reduce) { .btn:active { transform: none; } }
.btn-primary { background: #fff; color: var(--navy); }
.btn-primary:hover { background: #f1efe9; }
.btn-ghost { border-color: rgba(255,255,255,.5); color: #fff; }
.btn-ghost:hover { background: rgba(255,255,255,.1); }
.btn-dark { background: var(--navy); color: #fff; }
.btn-dark:hover { background: var(--navy-2); }
/* .btn-ghost assumes a dark backdrop (it's white-on-transparent, built
   for the hero) — this is the equivalent for a plain light card. */
.btn-outline { background: #fff; border-color: var(--border); color: var(--navy); }
.btn-outline:hover { border-color: var(--navy); background: var(--bg); }
.btn-sm { padding: 7px 14px; font-size: 13px; }
/* .btn's display would otherwise beat the hidden attribute (frame.js shows
   "See all in the range" only when there are more). */
.btn[hidden] { display: none; }

/* --- sections ------------------------------------------------------- */
section { padding: 56px 0; }
.section-eyebrow { font-size: 12px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--grey); margin-bottom: 8px; }
.section-sub { color: var(--text-muted); max-width: 560px; margin-bottom: 30px; font-size: 14.5px; }
.center { text-align: center; margin-left: auto; margin-right: auto; }

/* --- photo placeholder tiles ------------------------------------------ */
.photo-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
.photo-tile {
  aspect-ratio: 4/3; background: #eeece5; border: 1px solid var(--border);
  display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 6px;
  color: var(--text-muted); font-size: 12.5px; text-align: center; padding: 10px;
}
.photo-tile i { font-size: 22px; opacity: .5; }
@media (max-width: 760px) { .photo-grid { grid-template-columns: 1fr 1fr; } }

/* --- service cards (Home) -------------------------------------------- */
.svc-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.svc-card {
  border: 1px solid var(--border); background: var(--surface); padding: 26px; text-decoration: none;
  display: flex; flex-direction: column; gap: 8px; box-shadow: var(--e1);
  transition: border-color .2s ease, box-shadow .2s ease, transform .2s ease;
}
.svc-card:hover { border-color: var(--navy); box-shadow: var(--e3); transform: translateY(-3px); }
@media (prefers-reduced-motion: reduce) { .svc-card:hover { transform: none; } }

/* Entry points for the frame builder, which was previously reachable only
   from a single moulding's product page. */
.builder-callout {
  display: flex; align-items: center; justify-content: space-between; gap: 26px; flex-wrap: wrap;
  margin-top: 20px; padding: 24px 28px;
  background: var(--surface); border: 1px solid var(--border); box-shadow: var(--e1);
}
.builder-callout h3 { font-size: 18px; margin-bottom: 4px; }
.builder-callout p { font-size: 14.5px; color: var(--text-muted); margin: 0; max-width: 52ch; }
.builder-callout .btn { flex: none; }
@media (max-width: 620px) { .builder-callout { padding: 20px; } .builder-callout .btn { width: 100%; justify-content: center; } }

/* Sits under the intro line in the navy banner, so the builder is offered
   in the same place and the same shape as it is in the home page hero.
   The banner is centred, so this is too. */
.svc-card h3 { margin-bottom: 2px; }
.svc-card p { color: var(--text-muted); font-size: 14px; margin-bottom: 4px; }
.svc-card span { font-size: 13px; font-weight: 700; color: var(--navy); }
@media (max-width: 700px) { .svc-grid { grid-template-columns: 1fr; } }

/* --- moulding catalogue (Frames) -------------------------------------- */
.frames-layout { display: flex; align-items: flex-start; gap: 28px; }

.filters { flex: 0 0 220px; display: flex; flex-direction: column; gap: 22px; }
.filter-group { display: flex; flex-direction: column; gap: 8px; }
.filter-group label, .filter-label {
  font-size: 12px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: var(--grey);
}
/* .filter-label is a <button> so it can collapse its own group in the
   mobile sheet. Here it does nothing, so the browser's button chrome is
   stripped back and it reads as the plain heading it always was. */
button.filter-label {
  display: block; width: 100%; padding: 0; text-align: left; cursor: default;
  background: none; border: none; font-family: inherit;
}
#filter-search {
  padding: 9px 11px; border: 1px solid var(--border); font-size: 13.5px; font-family: inherit; background: var(--surface);
}
#filter-search:focus { outline: 1px solid var(--navy); }
.filter-checks { display: flex; flex-direction: column; gap: 7px; }
.filter-checks label {
  display: flex; align-items: center; gap: 7px; font-size: 13.5px; font-weight: 400;
  text-transform: none; letter-spacing: 0; color: var(--text); cursor: pointer;
}

/* --- filter pills + colour swatches ------------------------------------
   Both keep a real (visually hidden) checkbox and skin it via
   `:checked ~`, so keyboard focus, space-to-toggle and screen readers
   behave exactly as they would with a plain tick box. The .filters
   group's uppercase label styling is reset here, since these sit inside
   a <label> that would otherwise inherit it. */
.filter-pills { display: flex; flex-wrap: wrap; gap: 6px; }
.filter-pills label, .filter-swatches label {
  text-transform: none; letter-spacing: 0; font-weight: 400; color: var(--text);
}
.filter-pill { position: relative; cursor: pointer; }
.filter-pill input, .filter-swatch input {
  position: absolute; opacity: 0; width: 0; height: 0; margin: 0;
}
.filter-pill span {
  display: inline-block; padding: 6px 12px; font-size: 13px;
  border: 1px solid var(--border); background: var(--surface); color: var(--text);
  transition: background .12s ease, border-color .12s ease, color .12s ease;
}
.filter-pill:hover span { border-color: var(--navy); }
.filter-pill input:checked ~ span {
  background: var(--navy); border-color: var(--navy); color: #fff; font-weight: 600;
}
.filter-pill input:focus-visible ~ span { box-shadow: 0 0 0 3px rgba(28,44,88,.28); }

.filter-swatches { display: grid; grid-template-columns: 1fr 1fr; gap: 8px 6px; }
.filter-swatch {
  position: relative; display: flex; align-items: center; gap: 7px;
  cursor: pointer; font-size: 12.5px; min-width: 0;
}
.filter-swatch .dot {
  width: 17px; height: 17px; border-radius: 50%; flex-shrink: 0;
  /* Inner hairline keeps pale swatches (White, Cream) visible on the
     off-white page background. */
  box-shadow: inset 0 0 0 1px rgba(0,0,0,.18);
  transition: box-shadow .12s ease;
}
.filter-swatch .name {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-muted);
  transition: color .12s ease;
}
.filter-swatch:hover .name { color: var(--text); }
.filter-swatch input:checked ~ .dot {
  box-shadow: inset 0 0 0 1px rgba(0,0,0,.18), 0 0 0 2px var(--surface), 0 0 0 4px var(--navy);
}
.filter-swatch input:checked ~ .name { color: var(--navy); font-weight: 700; }
.filter-swatch input:focus-visible ~ .dot {
  box-shadow: inset 0 0 0 1px rgba(0,0,0,.18), 0 0 0 2px var(--surface), 0 0 0 4px rgba(28,44,88,.45);
}
.filter-range { display: flex; align-items: center; gap: 8px; }
.filter-range input {
  width: 100%; min-width: 0; padding: 8px 9px; border: 1px solid var(--border); font-size: 13px; font-family: inherit;
}
.filter-range span { color: var(--text-muted); font-size: 13px; }
.filter-clear {
  align-self: flex-start; font-size: 12.5px; font-weight: 700; color: var(--navy);
  background: none; border: none; padding: 0; cursor: pointer; text-decoration: underline;
}
.filter-clear:hover { color: var(--navy-2); }

/* --- mobile filter sheet ------------------------------------------------
   Desktop keeps the sidebar exactly as it was, so everything specific to
   the sheet is off by default and switched on in the query below. The
   sheet's own chrome (open button, title bar, apply bar) has no desktop
   equivalent and stays hidden. */
.filters-open, .filters-head, .filters-apply { display: none; }

@media (max-width: 900px) {
  .filters-open {
    display: flex; align-items: center; justify-content: center; gap: 8px; width: 100%;
    padding: 11px 22px; margin-bottom: 20px; cursor: pointer; border-radius: 3px; line-height: 1.55;
    font-family: inherit; font-size: 14px; font-weight: 700;
    color: #fff; background: var(--navy); border: 1px solid var(--navy);
  }
  .filters-count {
    display: inline-flex; align-items: center; justify-content: center; min-width: 21px; height: 21px;
    padding: 0 6px; border-radius: 999px; background: #fff; color: var(--navy);
    font-size: 12px; font-weight: 700;
  }
  .filters-count[hidden] { display: none; }

  /* A sheet rather than an accordion in the page: with twelve width bands
     and twenty-odd colours, an inline panel is still a screen and a half,
     and Apply ends up somewhere below the fold. Fixed to the viewport, it
     scrolls on its own and Apply stays on screen the whole time. */
  .filters {
    position: fixed; inset: 0; z-index: 70; display: none;
    width: auto; flex-direction: column; gap: 0;
    background: var(--bg); overflow-y: auto; -webkit-overflow-scrolling: touch;
    padding: 0 20px calc(76px + env(safe-area-inset-bottom));
  }
  .filters.is-open { display: flex; }
  body.filters-locked { overflow: hidden; }

  .filters-head {
    position: sticky; top: 0; z-index: 2; display: flex; align-items: center; justify-content: space-between;
    margin: 0 -20px 6px; padding: 14px 20px;
    background: var(--bg); border-bottom: 1px solid var(--border);
    font-family: var(--font-head); font-size: 17px; font-weight: 600; color: var(--navy);
  }
  .filters-close {
    width: 38px; height: 38px; margin-right: -8px; padding: 0; cursor: pointer;
    font-size: 26px; line-height: 1; color: var(--navy); background: none; border: none;
  }

  .filter-group { padding: 14px 0; border-bottom: 1px solid var(--border); gap: 12px; }
  .filter-group:last-of-type { border-bottom: none; }
  /* The label is a real button on mobile, so the whole row is the tap
     target rather than just the word. */
  .filter-group > .filter-label {
    display: flex; align-items: center; justify-content: space-between; width: 100%;
    padding: 0; cursor: pointer; text-align: left;
    background: none; border: none; font-family: inherit;
    font-size: 13px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: var(--navy);
  }
  .filter-group > .filter-label::after {
    content: ''; width: 9px; height: 9px; margin-right: 3px;
    border-right: 2px solid var(--grey); border-bottom: 2px solid var(--grey);
    transform: rotate(45deg) translate(-2px, -2px); transition: transform .18s ease;
  }
  .filter-group.is-collapsed > .filter-label::after { transform: rotate(-45deg) translate(-1px, 1px); }
  /* Matched by exclusion rather than by class: populateCheckFilter()
     overwrites the container's className with filter-pills or
     filter-swatches, so the .filter-checks it starts with is gone by the
     time anything is rendered. */
  .filter-group.is-collapsed > *:not(.filter-label) { display: none; }

  .filters-actions {
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 3;
    display: flex; align-items: center; gap: 14px;
    padding: 12px 20px calc(12px + env(safe-area-inset-bottom));
    background: var(--surface); border-top: 1px solid var(--border);
  }
  .filters-apply {
    display: block; flex: 1; padding: 13px 18px; cursor: pointer;
    font-family: inherit; font-size: 14.5px; font-weight: 700;
    color: #fff; background: var(--navy); border: 1px solid var(--navy);
  }
  .filter-clear { align-self: center; flex: 0 0 auto; }
}

/* The results column owns the flex sizing now that the grid sits inside
   a wrapper alongside the "show more" control — without this the grid
   keeps flex:1 against the wrong parent and the layout collapses. */
.frames-results { flex: 1; min-width: 0; }
.moulding-grid {
  min-width: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 20px; align-content: start;
}
.load-more-wrap {
  display: flex; flex-direction: column; align-items: center; gap: 10px;
  padding: 28px 0 4px;
}
.load-more-count { font-size: 13px; color: var(--text-muted); }
.moulding-card {
  display: block; border: 1px solid var(--border); background: var(--surface); overflow: hidden;
  text-decoration: none; color: inherit; box-shadow: var(--e1);
  transition: border-color .2s ease, box-shadow .2s ease, transform .2s ease;
}
/* A small lift on hover. Kept to 2px: these sit in a dense grid, and
   anything more makes the whole page twitch as the pointer crosses it. */
.moulding-card:hover { border-color: var(--navy); box-shadow: var(--e3); transform: translateY(-2px); }
@media (prefers-reduced-motion: reduce) { .moulding-card:hover { transform: none; } }
/* 4/3 matches the supplier photos' own proportions (they're 1200x900),
   so nothing gets cropped — a square box was cutting ~25% off each side
   of the moulding. `contain` rather than `cover` as a safety net for any
   photo that isn't exactly 4:3: these are shot on white, so the
   letterboxing it produces is invisible against the card. */
.moulding-photo { width: 100%; aspect-ratio: 4/3; object-fit: contain; background: var(--surface); }

/* Cover shot (chevron) by default, crossfades to the corner shot on
   hover — a glimpse of the joint detail without leaving the grid. */
.moulding-photo-hover { position: relative; width: 100%; aspect-ratio: 4/3; overflow: hidden; background: var(--surface); }
.moulding-photo-hover img {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain;
  transition: opacity .25s ease;
}
.moulding-photo-hover img.example { opacity: 0; }
.moulding-card:hover .moulding-photo-hover img.cover { opacity: 0; }
.moulding-card:hover .moulding-photo-hover img.example { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .moulding-photo-hover img { transition: none; }
}
.moulding-body { padding: 16px 18px; }
.moulding-code { font-size: 12px; font-weight: 700; color: var(--grey); letter-spacing: .04em; }
.moulding-desc { font-size: 15px; font-weight: 600; margin: 4px 0 8px; }
.moulding-meta { display: flex; justify-content: space-between; font-size: 13px; color: var(--text-muted); border-top: 1px solid var(--border); padding-top: 10px; }
.moulding-meta b { color: var(--navy); }

@media (max-width: 900px) {
  .frames-layout { flex-direction: column; }
  /* The filters used to reflow into a wrapping row here, which is what
     put two screens of pills above the first frame. They're a sheet now
     (see the filter-sheet block above); this block must not re-style
     .filters or .filter-group, since it would win on source order. */

  /* Stacked, the parent's align-items:flex-start would otherwise shrink
     this to its content width instead of filling the screen. */
  .frames-results { width: 100%; }
}
@media (max-width: 700px) { .moulding-grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); } }

/* --- single moulding product page (frame.html) ------------------------- */
.back-link { display: inline-block; font-size: 13px; color: var(--text-muted); text-decoration: none; margin: 24px 0 18px; }
.back-link:hover { color: var(--navy); }

.product-layout { display: grid; grid-template-columns: 1fr 1fr; gap: 36px; align-items: start; padding-bottom: 60px; }
@media (max-width: 760px) { .product-layout { grid-template-columns: 1fr; gap: 20px; } }

/* No fixed shape: the photo is shown at its own proportions and the
   border hugs it. This was a fixed 4:3 box with object-fit:cover, which
   suits the chevron shots but cut the top and bottom off the example
   photos — those are pictures of finished work and are as often
   portrait. Letterboxing them inside 4:3 instead would have avoided the
   crop but left dead bars inside the border, which is the "it shrinks
   it" version of the same problem.
   min-height reserves roughly a chevron's worth of space so the page
   doesn't jump as each photo lands; max-height stops a tall portrait
   running past the details column beside it. */
.main-photo {
  display: block; width: auto; max-width: 100%; height: auto;
  min-height: 300px; max-height: 620px; margin: 0 auto;
  background: var(--bg-alt); border: 1px solid var(--border);
}
.main-photo-empty {
  width: 100%; aspect-ratio: 4/3; background: var(--bg-alt); border: 1px solid var(--border);
  display: flex; align-items: center; justify-content: center; color: var(--text-muted); font-size: 13px;
}
.thumb-strip { display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap; }
.thumb-strip img {
  width: 64px; height: 64px; object-fit: cover; border: 2px solid transparent; cursor: pointer; opacity: .75;
}
.thumb-strip img:hover { opacity: 1; }
.thumb-strip img.active { border-color: var(--navy); opacity: 1; }

.product-code { font-size: 13px; font-weight: 700; color: var(--grey); letter-spacing: .04em; margin-bottom: 6px; }
.product-info h1 { font-size: 24px; margin-bottom: 16px; }
.product-meta-grid { display: flex; gap: 28px; padding: 16px 0; border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); margin-bottom: 20px; }
.product-meta-grid div span { display: block; font-size: 12px; color: var(--text-muted); margin-bottom: 3px; }
.product-meta-grid div b { font-size: 18px; color: var(--navy); }

/* --- contact ----------------------------------------------------------
   Laid out on rules and a grid rather than in bordered boxes. Four
   identical cards gave four identical weights, so nothing led the page
   and it read as a pile of containers. Here the hierarchy is typographic:
   a small label, the thing you actually act on set large, and a quiet
   line of reasoning underneath. Hairlines separate; boxes are reserved
   for the two things that genuinely are objects, the map and the
   buttons. */
.contact-open { padding-bottom: 64px; }
.contact-open-grid { display: grid; grid-template-columns: 1.15fr 1fr; gap: 56px; align-items: start; }
.contact-lead { font-size: 17px; line-height: 1.6; color: var(--text); max-width: 44ch; margin: 0 0 30px; }

.way-list { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--border); }
.way { display: grid; gap: 3px; padding: 18px 0; border-bottom: 1px solid var(--border); }
.way-label {
  font-size: 12px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; color: var(--grey);
}
.way-value {
  font-family: var(--font-head); font-size: 26px; font-weight: 600; line-height: 1.15;
  color: var(--navy); text-decoration: none; justify-self: start;
  border-bottom: 2px solid transparent; transition: border-color .2s ease;
}
.way-value:hover, .way-value:focus-visible { border-bottom-color: var(--navy); }
/* An email address is a long string, not a headline. Dropping it a step
   keeps it on one line at every width instead of wrapping mid-domain. */
.way-value-sm { font-size: 18px; word-break: break-word; }
.way-note { font-size: 13.5px; color: var(--text-muted); max-width: 42ch; }

.contact-visit { position: sticky; top: 88px; }
.map-frame { position: relative; aspect-ratio: 4 / 3; border: 1px solid var(--border); background: var(--surface); }
.map-frame iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; display: block; }
.visit-detail { margin-top: 20px; }
.visit-detail address {
  font-style: normal; font-family: var(--font-head); font-size: 19px; font-weight: 600;
  line-height: 1.5; color: var(--navy); margin: 4px 0 12px;
}
.visit-parking { font-size: 14px; color: var(--text-muted); margin-bottom: 18px; }
.visit-parking b { color: var(--text); }

.social-block { margin-top: 38px; padding-top: 26px; border-top: 1px solid var(--border); }
.social-block p { font-size: 14px; color: var(--text-muted); margin-bottom: 14px; }
.social-links { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 10px; }
.social-link {
  display: inline-flex; align-items: center; gap: 9px;
  padding: 11px 18px 11px 14px; border: 1px solid var(--border); border-radius: 3px; background: var(--surface);
  color: var(--navy); text-decoration: none; font-size: 14px; font-weight: 700; line-height: 1.55;
  transition: border-color .2s ease, background-color .2s ease;
}
.social-link:hover { border-color: var(--navy); background: var(--bg); }
/* Real brand marks drawn inline rather than an icon font: the site has
   already lost a page to a CDN going down once, and that is not a risk
   worth taking for three logos. */
.social-link svg { width: 19px; height: 19px; flex: none; display: block; }

.contact-band { background: var(--surface); border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); }

/* Hours are a small amount of information given a full band, so they run
   as one line across it rather than being stacked into a narrow column. */
.hours-grid { display: grid; grid-template-columns: 1fr 1.1fr 1fr; gap: 48px; align-items: start; }
.hours-grid h2 { margin: 0; }
.hours-list { margin: 0; font-size: 15px; border-top: 1px solid var(--border); }
.hours-list > div {
  display: flex; justify-content: space-between; gap: 20px;
  padding: 11px 0; border-bottom: 1px solid var(--border);
}
.hours-list dt { color: var(--text); }
.hours-list dd { margin: 0; font-weight: 700; color: var(--navy); font-variant-numeric: tabular-nums; }
.hours-list .is-closed dt, .hours-list .is-closed dd { color: var(--text-muted); font-weight: 400; }
.hours-note { font-size: 14px; color: var(--text-muted); margin: 0; }

.section-head { max-width: 58ch; margin-bottom: 34px; }
.section-head h2 { margin-bottom: 8px; }
.section-head .section-sub { margin-bottom: 0; }

/* Four across with a rule over each, so the sequence reads as a run of
   stages rather than a stack of paragraphs. */
.visit-steps { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(4, 1fr); gap: 30px; }
/* The home page runs the same idea as a shorter three-stage version.
   Only the column count differs, so it stays one component rather than a
   near-copy that drifts out of step with this one. */
.visit-steps-3 { grid-template-columns: repeat(3, 1fr); }
.visit-steps li { padding-top: 16px; border-top: 2px solid var(--navy); }
/* --sky is a tint meant for text on the navy; on the cream it measures
   about 2.2:1 and fails as body-sized text. --grey clears 4.5:1 and the
   numeral still reads as a marker rather than a heading. */
.step-num {
  display: block; font-family: var(--font-head); font-size: 13px; font-weight: 700;
  letter-spacing: .08em; color: var(--grey); margin-bottom: 10px;
}
/* Links inside body copy. The base rule is `a { color: inherit }`, which
   inside muted paragraph text leaves a link indistinguishable from the
   sentence around it. */
.inline-link { color: var(--navy); text-decoration: underline; text-underline-offset: 2px; }
.inline-link:hover { text-decoration-thickness: 2px; }

.visit-steps h3 { font-size: 16.5px; margin-bottom: 6px; }
.visit-steps p { font-size: 14px; line-height: 1.6; color: var(--text-muted); margin: 0; }

.contact-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 56px; }
.contact-cols h3 { font-size: 13px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--grey); margin-bottom: 4px; }
.rule-list { list-style: none; margin: 0; padding: 0; font-size: 15px; }
.rule-list li { padding: 12px 0; border-bottom: 1px solid var(--border); color: var(--text); }
.cols-cta { margin: 22px 0 0; }

@media (max-width: 900px) {
  .contact-open-grid { grid-template-columns: 1fr; gap: 40px; }
  .contact-visit { position: static; }
  .hours-grid { grid-template-columns: 1fr; gap: 22px; }
  .visit-steps { grid-template-columns: 1fr 1fr; gap: 26px; }
  /* Four halve neatly; three would leave one stranded on its own row,
     which reads as a mistake rather than a stage. Straight to one. */
  .visit-steps-3 { grid-template-columns: 1fr; gap: 24px; }
}
@media (max-width: 560px) {
  .contact-open { padding-bottom: 40px; }
  .contact-lead { font-size: 15.5px; }
  .way-value { font-size: 22px; }
  .way-value-sm { font-size: clamp(15px, 4.4vw, 18px); }
  .visit-steps { grid-template-columns: 1fr; gap: 22px; }
  .contact-cols { grid-template-columns: 1fr; gap: 32px; }
}

/* --- legal pages -------------------------------------------------------
   One readable column. A privacy notice nobody can get through is not a
   privacy notice, so this is set at body size on a normal measure rather
   than shrunk to look like small print. */
.legal { max-width: 68ch; }
.legal-lead { font-size: 16px; line-height: 1.65; margin-bottom: 32px; }
.legal h2 { font-size: 19px; margin: 34px 0 10px; }
.legal p { font-size: 15px; line-height: 1.7; color: var(--text); }
.legal a { color: var(--navy); font-weight: 600; }
.legal dl { margin: 0 0 8px; }
.legal dt { font-size: 15px; font-weight: 700; color: var(--navy); margin-top: 16px; }
.legal dd { margin: 4px 0 0; font-size: 15px; line-height: 1.7; color: var(--text-muted); }
.legal-updated { margin-top: 36px; padding-top: 16px; border-top: 1px solid var(--border); font-size: 13px; color: var(--text-muted); }

/* --- footer ------------------------------------------------------------ */
/* The footer carries the address, phone and hours on every page rather
   than only on Contact. For a shop those are the details people are
   actually after, and they arrive on whichever page a search result or a
   shared link drops them on, not necessarily the one built for it. */
.site-footer { border-top: 1px solid var(--border); background: var(--surface); padding: 42px 0 22px; margin-top: 40px; }
/* The contact column is wider than the rest because the email address is
   the longest single string in the footer, and in equal columns it broke
   mid-domain ("...creative.c / o.uk"). */
.footer-cols { display: grid; grid-template-columns: 1.4fr 1fr 1.3fr .9fr; gap: 34px; align-items: start; }
.footer-brand img { height: 56px; width: auto; opacity: .9; margin-bottom: 14px; }
.footer-brand p { font-size: 13.5px; color: var(--text-muted); max-width: 30ch; margin-bottom: 14px; }

.footer-col h2 {
  font-size: 12px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
  color: var(--grey); margin-bottom: 12px;
}
.footer-col address { font-style: normal; font-size: 13.5px; color: var(--text-muted); line-height: 1.7; }
.footer-action { display: inline-block; margin-top: 10px; font-size: 13px; font-weight: 700; color: var(--navy); text-decoration: none; }
.footer-action:hover { text-decoration: underline; }
.footer-note { font-size: 12.5px; color: var(--text-muted); margin: 10px 0 0; }

.footer-contact { list-style: none; margin: 0; padding: 0; font-size: 13.5px; }
.footer-contact li { margin-bottom: 8px; }
/* overflow-wrap rather than word-break: it only breaks a string that has
   no other option, instead of breaking every long word by default. */
.footer-contact a { color: var(--navy); font-weight: 700; text-decoration: none; overflow-wrap: anywhere; }
.footer-contact .is-email { font-size: 12.5px; }
.footer-contact a:hover { text-decoration: underline; }

.footer-hours { list-style: none; margin: 0; padding: 0; font-size: 13.5px; color: var(--text-muted); }
.footer-hours li { display: flex; justify-content: space-between; gap: 14px; margin-bottom: 8px; }
.footer-hours li span:last-child { color: var(--navy); font-weight: 700; font-variant-numeric: tabular-nums; }
.footer-hours .is-closed span:last-child { color: var(--text-muted); font-weight: 400; }

.footer-socials { list-style: none; display: flex; gap: 9px; margin: 0; padding: 0; }
.footer-socials a {
  display: flex; align-items: center; justify-content: center; width: 34px; height: 34px;
  border: 1px solid var(--border); color: var(--grey); background: var(--bg);
}
.footer-socials svg { width: 17px; height: 17px; display: block; }
/* Colour on hover only. Three saturated brand marks sitting in the
   footer of every page pull harder than anything else down there. */
.footer-socials a:hover { border-color: var(--navy); color: var(--navy); }

.footer-bottom {
  display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap;
  margin-top: 34px; padding-top: 18px; border-top: 1px solid var(--border);
}
.footer-bottom p { margin: 0; font-size: 12.5px; color: var(--text-muted); }
.footer-links { display: flex; gap: 18px; flex-wrap: wrap; }
.footer-links a { font-size: 12.5px; color: var(--text-muted); text-decoration: none; }
.footer-links a:hover { color: var(--navy); }

/* Touch sizing keyed to the breakpoint rather than to phones only: a
   tablet at 768px is a finger too. Footer links are a single line of text,
   about a 16px target; padding them out to finger height costs nothing
   once the columns have stacked. */
@media (max-width: 900px) {
  .footer-cols { grid-template-columns: 1fr 1fr; gap: 30px; }
  .footer-contact a, .footer-action, .footer-links a { display: inline-block; padding: 10px 0; }
  .footer-contact li { margin-bottom: 0; }
  .footer-links { gap: 8px 20px; }
}
@media (max-width: 560px) {
  .footer-cols { grid-template-columns: 1fr; gap: 26px; }
  .footer-bottom { flex-direction: column; align-items: flex-start; gap: 12px; }
}

.build-tag {
  position: fixed; bottom: 12px; right: 12px; z-index: 100;
  font-size: 10px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
  color: #fff; background: var(--navy); padding: 4px 10px; border-radius: 3px; opacity: .85;
}

/* --- frame builder (builder.html) --------------------------------------
   Real customer feature now, reading moulding data live from Supabase
   (the "builder" kind photo specifically — the overhead length shot,
   never the corner/example ones). Ported from the internal test tool;
   same rendering logic (mitred corners, mount + bevel, aspect lock),
   just wearing the site's real chrome and design tokens instead of the
   plain black-and-white tool look. ------------------------------------- */
.builder-layout { display: grid; grid-template-columns: 340px 1fr; gap: 24px; align-items: start; padding: 30px 0 70px; }
/* On a phone the two columns stack, and in source order that buried the
   preview under all five steps: you were changing a frame you could not
   see, and had to scroll to the bottom after every tweak to check it.
   The preview goes to the top and sticks there, so it stays on screen and
   updates live while you work down the controls underneath it.
   `display: contents` on the wrapper is what makes that possible: it lets
   the preview and the disclaimer become grid items in their own right so
   they can be ordered independently, rather than moving as one block and
   dragging the small print up with them. */
@media (max-width: 900px) {
  .builder-layout { grid-template-columns: 1fr; }
  /* Scoped to the parent to outrank the base `.builder-right` rule, which
     is defined further down the file and would otherwise win on source
     order despite this being inside a media query. */
  .builder-layout > .builder-right { display: contents; }
  #preview-panel { order: -1; }
  #builder-steps { order: 0; }
  .builder-disclaimer { order: 1; }

  #preview-panel {
    position: sticky; z-index: 20;
    /* clears the sticky site header: 77px here, 69px on a phone (below,
       where the logo shrinks). At 69px throughout, the top 8px of the
       preview hid under the header on an upright iPad. */
    top: 77px;
    min-height: 0; padding: 12px;
    max-height: 46vh;
  }
  /* The canvas has to fit the shorter box rather than overflow it, and
     the frame is usually taller than it is wide. */
  #builder-canvas { max-height: calc(46vh - 24px); width: auto; max-width: 100%; }
  /* #preview-panel in front so these beat the base rules further down,
     which would otherwise win on source order (they did, until 29 Sep). */
  #preview-panel .builder-preview-empty { font-size: 12.5px; padding: 12px 14px; }
  #preview-panel .builder-preview-empty b { font-size: 15px; }
  .builder-preview-empty i { font-size: 24px !important; margin-bottom: 6px !important; }
  #preview-panel .preview-empty-btn { margin: 8px auto; padding: 8px 16px; font-size: 13px; }

  /* The glass comparison's phone layout is further down, after its base
     rule, so it isn't overridden by it. */
}
@media (max-width: 760px) { #preview-panel { top: 69px; } }

/* --- builder: Standard / Advanced tabs ---------------------------------
   The same pair heads builder.html and advanced.html. They are links to
   two pages rather than a tab widget, but they are drawn as tabs so the
   advanced builder reads as a choice you can see, not a line of small
   print. Square corners and the navy fill, like the print-size chips. */
.builder-modes { display: grid; grid-template-columns: 1fr 1fr; max-width: 560px; margin: 16px 0 12px; }
.builder-mode {
  display: flex; align-items: center; gap: 12px; padding: 11px 16px;
  border: 1px solid var(--border); background: var(--surface);
  color: var(--text); text-decoration: none;
  transition: border-color .2s ease, background-color .2s ease;
}
/* One shared border between the two, not a doubled one. The lit or
   hovered tab sits on top so its whole outline shows. */
.builder-mode + .builder-mode { margin-left: -1px; }
.builder-mode:hover { border-color: var(--navy); position: relative; z-index: 1; }
.builder-mode i { font-size: 22px; color: var(--navy); flex: none; }
.builder-mode b { display: block; font-family: var(--font-head); font-size: 15px; font-weight: 600; color: var(--navy); }
.builder-mode small { display: block; font-size: 12.5px; line-height: 1.35; color: var(--text-muted); }
.builder-mode[aria-current] { background: var(--navy); border-color: var(--navy); position: relative; z-index: 1; }
.builder-mode[aria-current] i,
.builder-mode[aria-current] b { color: #fff; }
.builder-mode[aria-current] small { color: var(--sky); }
.builder-intro { color: var(--text-muted); font-size: 15px; max-width: 600px; margin: 0; }
@media (max-width: 480px) {
  /* Half a phone is too narrow for the icon and the words; the words win. */
  .builder-mode { padding: 10px 12px; }
  .builder-mode i { display: none; }
}

.builder-panel {
  background: var(--surface); border: 1px solid var(--border); border-top: 3px solid var(--navy);
  padding: 22px; box-shadow: 0 16px 40px -24px rgba(20,15,8,.35);
}
.b-field { margin-bottom: 16px; }
.b-field label {
  display: flex; align-items: center; font-family: var(--font-head); font-size: 14px; font-weight: 600;
  color: var(--text); margin-bottom: 7px;
}
.b-field .hint { font-size: 12px; color: var(--text-muted); font-weight: 400; margin-top: 5px; }
.b-field .hint a { color: var(--navy); text-decoration: underline; }

/* --- builder: numbered steps -------------------------------------------
   The panel showed every control at once, which is five decisions
   presented as equals with nothing saying where to start. Numbering them,
   and dimming the ones that cannot mean anything yet, makes it one
   instruction at a time without hiding anything or turning it into a
   wizard you have to click through. */
.b-step {
  display: inline-flex; align-items: center; justify-content: center;
  width: 21px; height: 21px; margin-right: 9px; flex: none;
  background: var(--navy); color: #fff;
  font-family: var(--font-head); font-size: 12px; font-weight: 700;
}
.needs-photo { opacity: .4; transition: opacity .25s ease; }
/* Not merely dimmed but unusable, so nobody types a size for a photo that
   isn't there and wonders why nothing happened. */
.needs-photo, .needs-photo * { pointer-events: none; }
.has-photo .needs-photo { opacity: 1; }
.has-photo .needs-photo, .has-photo .needs-photo * { pointer-events: auto; }
@media (prefers-reduced-motion: reduce) { .needs-photo { transition: none; } }

.b-dropzone {
  border: 1px dashed var(--border); padding: 10px; border-radius: 2px;
  display: flex; align-items: center; gap: 10px; cursor: pointer; background: var(--bg);
}
.b-dropzone:hover, .b-dropzone.drag { border-color: var(--navy); }
.b-dropzone input { display: none; }
.b-dropzone-thumb {
  width: 44px; height: 44px; background: #fff; flex-shrink: 0;
  background-size: cover; background-position: center; display: flex; align-items: center; justify-content: center;
  font-size: 16px; color: var(--text-muted); border: 1px solid var(--border);
}
.b-dropzone-text { font-size: 12.5px; color: var(--text-muted); line-height: 1.35; }
.b-dropzone-text b { display: block; color: var(--text); font-size: 13px; margin-bottom: 1px; font-weight: 600; }

/* Toggle switch for Mount / Double mount — .b-field's generic
   `label { display: block }` would otherwise override the flex layout
   here, hence the extra specificity (.b-field label.b-toggle-row). */
.b-field label.b-toggle-row {
  display: flex; align-items: center; gap: 10px;
  cursor: pointer; margin-bottom: 0; font-weight: 400;
}
.b-toggle { position: relative; display: inline-block; width: 40px; height: 23px; flex-shrink: 0; }
.b-toggle input {
  position: absolute; inset: 0; width: 100%; height: 100%; margin: 0;
  opacity: 0; cursor: pointer; z-index: 1;
}
.b-toggle-track {
  position: absolute; inset: 0; border-radius: 999px;
  background: #d7d7d7; transition: background 0.15s ease;
}
.b-toggle-track::before {
  content: ''; position: absolute; top: 2px; left: 2px;
  width: 19px; height: 19px; border-radius: 50%;
  background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.3);
  transition: transform 0.15s ease;
}
.b-toggle input:checked ~ .b-toggle-track { background: var(--navy); }
.b-toggle input:checked ~ .b-toggle-track::before { transform: translateX(17px); }
.b-toggle input:focus-visible ~ .b-toggle-track { box-shadow: 0 0 0 3px rgba(28,44,88,0.28); }
.b-toggle-label { font-size: 13.5px; font-weight: 600; color: var(--text); }

.b-swatch-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 6px; }
.b-swatch-chip {
  aspect-ratio: 1; border: 1px solid var(--border); background-size: cover; background-position: center;
  cursor: pointer; position: relative;
}
.b-swatch-chip.is-picked { border: 2px solid var(--navy); }
.b-swatch-chip span {
  position: absolute; left: 2px; bottom: 1px; font-size: 8px; font-weight: 700;
  color: rgba(0,0,0,.6); text-shadow: 0 1px 1px rgba(255,255,255,.5);
}

.b-num-row { display: flex; gap: 8px; }
/* A named number box with its unit inside it, so it's never a guess
   whether 400 means millimetres. A box on its own takes half the row, the
   same width as one of a pair. */
.b-field label.b-num {
  display: block; flex: 1 1 0; min-width: 0; margin: 0;
  font-family: var(--font-body); font-size: 12px; font-weight: 600; color: var(--text-muted);
}
.b-num-row .b-num:only-child { flex: 0 1 calc(50% - 4px); }
.b-num-box { display: flex; align-items: center; margin-top: 4px; border: 1px solid var(--border); background: #fff; }
.b-num-box:focus-within { outline: 1px solid var(--navy); }
.b-field .b-num-box input[type="number"],
.b-field .b-num-box input[type="number"]:focus { border: 0; outline: 0; background: transparent; flex: 1; min-width: 0; }
.b-unit { padding: 0 10px 0 2px; font-size: 12.5px; font-weight: 600; color: var(--text-muted); }

/* A step's heading with a small control on its right: the size step's
   mm | in switch. */
.b-field-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 7px; }
.b-field .b-field-head > label { margin-bottom: 0; }

/* mm | in: two buttons, the one in use filled like a picked size chip. */
.unit-switch { display: inline-flex; flex: none; }
.unit-switch button {
  font-family: inherit; font-size: 12.5px; font-weight: 600; line-height: 1.2; padding: 7px 12px;
  border: 1px solid var(--border); background: #fff; color: var(--text); cursor: pointer;
}
.unit-switch button + button { margin-left: -1px; }
.unit-switch button:hover { color: var(--navy); }
.unit-switch button[aria-pressed="true"] { position: relative; background: var(--navy); border-color: var(--navy); color: #fff; }

/* "Use our crop tool": a row you can't miss under the size, dashed like
   the photo box it belongs with, rather than another full-width button. */
.b-crop-link {
  display: flex; align-items: center; gap: 9px; width: 100%; margin-top: 10px; padding: 9px 11px;
  border: 1px dashed var(--border); background: var(--bg); color: var(--text-muted);
  font-size: 12.5px; line-height: 1.35; text-align: left; cursor: pointer;
}
.b-crop-link:hover { border-color: var(--navy); }
.b-crop-link i { font-size: 18px; color: var(--navy); flex: none; }
.b-crop-link b { color: var(--navy); font-weight: 600; text-decoration: underline; text-underline-offset: 2px; white-space: nowrap; }

.b-field input[type="number"] {
  width: 100%; padding: 8px 10px; border: 1px solid var(--border); background: #fff; color: var(--text);
  font-size: 13.5px; font-family: inherit;
}
.b-field input[type="number"]:focus { outline: 1px solid var(--navy); }
/* The print page's name, contact and notes boxes are .b-field text inputs
   and a textarea, which had no rule at all and fell back to the browser's
   own: narrow boxes, and a typewriter font in the notes. Same look as the
   number inputs above. */
.b-field input[type="text"], .b-field input[type="email"], .b-field input[type="tel"], .b-field textarea {
  width: 100%; padding: 8px 10px; border: 1px solid var(--border); background: #fff; color: var(--text);
  font-size: 13.5px; font-family: inherit;
}
.b-field textarea { resize: vertical; min-height: 64px; line-height: 1.45; }
.b-field input[type="text"]:focus, .b-field input[type="email"]:focus, .b-field input[type="tel"]:focus,
.b-field textarea:focus { outline: 1px solid var(--navy); }

.builder-preview {
  position: relative;   /* the glass comparison overlays this */
  min-height: 400px; display: flex; align-items: center; justify-content: center; padding: 36px;
  background: radial-gradient(ellipse at center, var(--bg) 0%, var(--surface) 72%);
}
.builder-preview-empty {
  text-align: center; color: var(--text-muted); font-size: 13.5px; max-width: 300px; line-height: 1.5;
  /* it's a <button>: undo the browser's button look */
  font-family: inherit; background: none; border: 1px dashed transparent; padding: 20px 22px;
  transition: border-color .2s ease, background-color .2s ease;
}
.builder-preview-empty:not(:disabled) { cursor: pointer; }
.builder-preview-empty:not(:disabled):hover,
.builder-preview-empty:focus-visible,
#preview-panel.drag .builder-preview-empty,
#crop-panel.drag .builder-preview-empty { border-color: var(--navy); background: rgba(255,255,255,.55); outline: none; }
.builder-preview-empty:disabled { color: var(--text-muted); }
.preview-empty-btn {
  display: table; margin: 10px auto 12px; padding: 10px 20px;
  font-size: 14px; font-weight: 700; color: #fff; background: var(--navy); border-radius: 3px;
}
.builder-preview-empty:not(:disabled):hover .preview-empty-btn { background: var(--navy-2); }
#preview-panel.drag, #crop-panel.drag { outline: 2px dashed var(--navy); outline-offset: -8px; }
.builder-preview-empty b { display: block; font-family: var(--font-head); font-size: 17px; color: var(--navy); margin-bottom: 5px; }
#builder-canvas { max-width: 100%; height: auto; display: none; box-shadow: 0 24px 50px -20px rgba(20,15,8,.4); }
/* Drawn at the column's full width, a portrait frame came out up to 720px
   tall, more than a laptop shows under the 77px header, so its bottom edge
   was always off the screen. Capped to the window, less the header and
   the panel's padding; the width follows the frame's shape. (svh is the
   window with the browser's bars showing; the vh line is the fallback.) */
@media (min-width: 901px) {
  #builder-canvas {
    max-height: max(320px, calc(100vh - 180px));
    max-height: max(320px, calc(100svh - 180px));
  }
}

/* --- gallery mosaic -----------------------------------------------------
   Deliberately not the moulding grid's uniform tiles — these are examples
   of work, not products, and cropping them all to the same square made
   them read like a catalogue. CSS columns lets each photo keep its own
   proportions, so a tall shot sits alongside two square ones and the gaps
   close up on their own.

   The trade-off is that reading order runs down each column rather than
   across rows, which is fine for a gallery where nothing is sequential. */
/* The flex sizing moves to the wrapper now that a "show more" control
   sits under the mosaic. A column-count element cannot own both. */
.gallery-results { flex: 1; min-width: 0; }
@media (max-width: 900px) { .gallery-results { width: 100%; } }
.gallery-mosaic {
  column-count: 3; column-gap: 0;
}
@media (max-width: 1100px) { .gallery-mosaic { column-count: 2; } }
/* Two a row on a phone too: one photo per row made 12 photos a 5,600px
   scroll. */
@media (max-width: 560px)  { .gallery-mosaic { column-count: 2; } }

.mosaic-item {
  /* Without this a photo can be split across a column break. */
  break-inside: avoid; -webkit-column-break-inside: avoid;
  /* No gap at all — photos butt straight up against each other.
     line-height:0 matters here: without it the inline image would sit on
     a text baseline and leave a few px of white under every photo, which
     would look like a gap the CSS hadn't removed. */
  display: block; margin: 0; line-height: 0;
  position: relative; overflow: hidden;
}
.mosaic-item img {
  width: 100%; height: auto; display: block;
  transition: transform .35s ease, filter .35s ease;
}
/* No caption or code on screen — the only cue that a photo leads
   somewhere is that it lifts slightly under the cursor. */
a.mosaic-item:hover img { transform: scale(1.03); filter: brightness(1.04); }
a.mosaic-item:focus-visible { outline: 3px solid var(--navy); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
  .mosaic-item img { transition: none; }
  a.mosaic-item:hover img { transform: none; }
}

/* --- homepage gallery wall ---------------------------------------------
   A lit wall panning past a fixed spotlight. Everything except the frame
   photos themselves is drawn in CSS, so there's no wall image to tile and
   therefore no seam when the loop wraps — the gradients vary vertically
   only, which is horizontally seamless by definition. */
/* Overrides the global `section { padding: 56px 0 }`, which was leaving
   a band of page background above and below — the wall is meant to butt
   straight up against the hero and run edge to edge. */
.gallery-wall { position: relative; padding: 0; }
.gw-stage {
  position: relative; height: 360px; overflow: hidden;
  /* Ends on the wall colour, not the page colour — the base is meant to
     be a clean hard line where the wall stops. */
  background: linear-gradient(180deg, #f6f1e7 0%, #f2ece0 44%, #e9e2d3 100%);
  /* Navy bar capping the wall, tying it back to the hero above. */
  border-top: 14px solid var(--navy);
}
/* The pool of light on the wall, behind the frames. Both edges are
   clean lines — the wall is a distinct band, not something blended into
   the page. */
.gw-stage::before {
  content: ''; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  /* Softer than before — the centre was blowing out to near-pure white,
     which read as a glare rather than a lit wall. Warmer and lower
     opacity keeps the falloff without bleaching the middle. */
  background: radial-gradient(40% 106% at 50% -6%,
    rgba(255,251,240,.72) 0%, rgba(253,247,234,.52) 18%, rgba(248,240,222,.28) 42%,
    rgba(132,114,86,.16) 68%, rgba(84,70,50,.34) 100%);
}
/* And the same pool over the top, blended, so frames passing beneath are
   tinted by the light rather than the light looking painted on. */
.gw-stage::after {
  content: ''; position: absolute; inset: 0; z-index: 2; pointer-events: none;
  mix-blend-mode: soft-light;
  background: radial-gradient(36% 100% at 50% -6%,
    rgba(255,255,255,.62) 0%, rgba(255,255,255,.28) 34%, rgba(255,255,255,0) 60%);
}

.gw-track {
  position: absolute; top: 0; bottom: 0; left: 0; z-index: 1;
  display: flex; align-items: center; gap: 60px; padding: 0 44px; width: max-content;
  animation: gwPan 68s linear infinite;
}
@keyframes gwPan { from { transform: translateX(0); } to { transform: translateX(-50%); } }
.gw-stage:hover .gw-track,
.gw-stage.is-paused .gw-track { animation-play-state: paused; }
@media (prefers-reduced-motion: reduce) { .gw-track { animation: none; } }

.gw-frame {
  flex: 0 0 auto; display: block; line-height: 0;
  --sx: 11px; --sy: 15px; --b: 7px; --sp: 1px; --sa: .60; --lift: 0;
}
.gw-frame img {
  display: block; height: var(--h, 190px); width: auto;
  /* box-shadow rather than filter: drop-shadow — chained drop-shadows
     shadow each other's output, which reads as a vague doubled edge.
     From the element's own box it stays crisp, and spread works. */
  box-shadow: var(--sx) var(--sy) var(--b) var(--sp) rgba(26,20,13,var(--sa));
  filter: brightness(calc(1 + var(--lift)));
}
a.gw-frame { cursor: pointer; }
a.gw-frame:focus-visible img { outline: 3px solid var(--navy); outline-offset: 4px; }

@media (max-width: 700px) {
  .gw-stage { height: 260px; }
  .gw-track { gap: 38px; padding: 0 26px; }
}

/* The right-hand column holds the preview and the caveats that qualify
   it. The caveats used to be one line at the very bottom of the page,
   under everything, which is not where anyone reads a disclaimer. */
.builder-right { display: flex; flex-direction: column; gap: 20px; min-width: 0; }

.builder-disclaimer { padding: 22px 24px; background: var(--surface); border: 1px solid var(--border); }
.builder-disclaimer h2 {
  font-size: 12px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
  color: var(--grey); margin-bottom: 14px;
}
.builder-disclaimer dl { margin: 0; display: grid; gap: 14px; }
.builder-disclaimer dt { font-size: 14px; font-weight: 700; color: var(--navy); margin-bottom: 3px; }
.builder-disclaimer dd { margin: 0; font-size: 13px; line-height: 1.55; color: var(--text-muted); }
/* Three across on a wide screen so it reads as a footnote rather than a
   wall of terms; stacked once there isn't room for that. */
@media (min-width: 1000px) { .builder-disclaimer dl { grid-template-columns: repeat(3, 1fr); gap: 26px; } }

.b-finished-size { font-size: 13.5px; font-weight: 600; color: var(--text); background: var(--bg); border: 1px solid var(--border); border-radius: 3px; padding: 10px 12px; margin-bottom: 10px; text-align: center; }
.b-finished-size span { font-weight: 400; color: var(--text-muted); }

/* Separates "you configured this" (moulding/photo/size/mount/glazing)
   from "here's the result" (size, price, actions) — same panel, but the
   eye should land on the price, not treat it as one more field among
   many. */
.b-summary { margin-top: 20px; padding-top: 18px; border-top: 1px solid var(--border); }

.b-price-estimate {
  font-family: var(--font-head); font-size: 28px; font-weight: 600; color: #fff;
  background: var(--navy); border-radius: 3px; padding: 16px; margin-bottom: 10px; text-align: center;
}
.b-price-estimate span { display: block; font-family: var(--font-body); font-size: 11.5px; font-weight: 400; color: var(--sky); margin-top: 3px; }

/* Regular-vs-Art glazing compare slider — two identically-sized canvases
   stacked via position:absolute, the top one (Regular) clipped to reveal
   only its left portion so the bottom one (Art) shows through past the
   handle. Sits right next to the glazing choice itself, not off in the
   main preview, since that's where the decision is actually being made. */
/* Opened from step 5 and drawn over the preview rather than inline under
   the step, where it was a second, smaller copy of the frame competing
   with the real one a few inches away. */
.glazing-compare-open {
  margin-top: 10px; padding: 0; border: none; background: none; cursor: pointer;
  font: inherit; font-size: 12.5px; font-weight: 700; color: var(--navy); text-decoration: underline;
}
.glazing-compare-open:hover { color: var(--navy-2); }

.glazing-compare {
  position: absolute; inset: 0; z-index: 5;
  display: flex; align-items: center; justify-content: center; padding: 22px;
  background: rgba(251,250,247,.94); backdrop-filter: blur(3px);
}
.glazing-compare[hidden] { display: none; }
/* --ar is the frame's width over its height (set by builder.js), so a tall
   frame narrows until the comparison fits the window instead of running
   off the bottom of it. */
.glazing-compare-inner {
  width: 100%;
  max-width: min(460px, calc((100vh - 200px) * var(--ar, 1)));
  max-width: min(460px, calc((100svh - 200px) * var(--ar, 1)));
}
/* On a phone the preview is only 46vh tall, so the comparison takes the
   screen below the header instead of being squeezed into it: it's a detail
   view and needs the room more than the panel does. It stops at the header
   rather than covering it because it sits in the preview's stacking layer,
   which is under the header's. This must come after the base
   .glazing-compare rule: when it came before, the base rule won on source
   order and a tall frame spilled out of the little preview. */
@media (max-width: 900px) {
  /* The extra room at the bottom keeps the basket button off the note. */
  .glazing-compare { position: fixed; inset: 77px 0 0 0; z-index: 80; padding: 16px 16px 76px; }
  .glazing-compare-inner {
    max-width: min(460px, calc((100vh - 220px) * var(--ar, 1)));
    max-width: min(460px, calc((100svh - 220px) * var(--ar, 1)));
  }
}
@media (max-width: 760px) {
  .glazing-compare { top: 69px; }   /* the header is shorter here */
}
.glazing-compare-note { font-size: 12.5px; color: var(--text-muted); text-align: center; margin: 10px 0 0; }
.glazing-compare-close {
  position: absolute; top: 10px; right: 12px; width: 34px; height: 34px; padding: 0;
  font-size: 24px; line-height: 1; color: var(--navy); cursor: pointer;
  background: var(--surface); border: 1px solid var(--border);
}
.glazing-compare-close:hover { border-color: var(--navy); }

.glazing-compare-stage {
  position: relative; width: 100%; overflow: hidden; cursor: ew-resize;
  -webkit-user-select: none; user-select: none; border: 1px solid var(--border); background: var(--bg);
}
.glazing-compare-stage canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.glazing-compare-label {
  position: absolute; top: 8px; z-index: 3; font-family: var(--font-head);
  font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: .05em;
  background: rgba(28,44,88,.85); color: #fff; padding: 3px 8px; border-radius: 2px; pointer-events: none;
}
.glazing-compare-label.left { left: 8px; }
.glazing-compare-label.right { right: 8px; }
.glazing-compare-handle {
  position: absolute; top: 0; bottom: 0; width: 2px; background: #fff;
  box-shadow: 0 0 0 1px rgba(0,0,0,.25); transform: translateX(-50%);
  z-index: 2; display: flex; align-items: center; justify-content: center; pointer-events: none;
}
.glazing-compare-handle::before {
  content: ''; position: absolute; width: 32px; height: 32px; border-radius: 50%;
  background: #fff; box-shadow: 0 3px 10px rgba(0,0,0,.3);
}
.glazing-compare-handle i { position: relative; color: var(--navy); font-size: 15px; }

/* --- builder: compact moulding picker + popup dialog -------------------
   With 5 mouldings an inline grid is fine; with hundreds it'd dominate
   the page and, worse, force a re-scroll/re-pick feeling every time you
   just want to compare a couple of options. So the panel shows only the
   current pick, and "Change" opens a dialog with the full grid — the
   photo, size and mount all stay exactly as they were underneath it. */
.moulding-current {
  width: 100%; display: flex; align-items: center; gap: 12px; text-align: left;
  border: 1px solid var(--border); background: #fff; padding: 8px 12px; cursor: pointer; font-family: inherit;
}
.moulding-current:hover { border-color: var(--navy); }
.moulding-current-thumb {
  width: 40px; height: 40px; flex-shrink: 0; background: var(--bg-alt); background-size: cover; background-position: center;
  border: 1px solid var(--border);
}
.moulding-current-text { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.moulding-current-text b { font-size: 13.5px; font-weight: 700; color: var(--text); }
.moulding-current-text span { font-size: 12px; color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.moulding-current i { color: var(--text-muted); }

#moulding-dialog {
  border: 1px solid var(--border); padding: 20px; max-width: 640px; width: 90vw;
  max-height: 80vh; box-shadow: 0 24px 60px -20px rgba(0,0,0,.35);
}
#moulding-dialog::backdrop { background: rgba(20,20,20,.5); }
.moulding-dialog-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }

/* --- quote cart --------------------------------------------------------
   Injected by cart.js on both builder.html and print.html — a floating
   button + dialog, not baked into either page's own markup, so both
   pages share one implementation. */
.cart-fab {
  position: fixed; right: 20px; bottom: 20px; z-index: 40;
  display: none; align-items: center; gap: 8px;
  background: var(--navy); color: #fff; border: none; border-radius: 3px;
  padding: 12px 18px; font-size: 14px; font-weight: 700; cursor: pointer;
  box-shadow: 0 10px 30px -8px rgba(0,0,0,.4);
}
.cart-fab:hover { background: var(--navy-2); }
/* Something just went into the basket (cart.js flyToCart). */
.cart-fab.bump { animation: cart-bump .5s cubic-bezier(.3, 1.6, .6, 1); }
@keyframes cart-bump { 0% { transform: scale(1); } 45% { transform: scale(1.22); } 100% { transform: scale(1); } }
.cart-fly {
  position: fixed; z-index: 90; pointer-events: none; object-fit: contain;
  box-shadow: 0 18px 40px -14px rgba(0,0,0,.45);
}
.cart-fab i { font-size: 17px; }
#cart-fab-count {
  background: #fff; color: var(--navy); border-radius: 999px;
  min-width: 18px; height: 18px; padding: 0 5px; font-size: 11px;
  display: inline-flex; align-items: center; justify-content: center;
}

#cart-dialog {
  border: 1px solid var(--border); padding: 20px; max-width: 480px; width: 90vw;
  max-height: 80vh; overflow-y: auto; box-shadow: 0 24px 60px -20px rgba(0,0,0,.35);
}
#cart-dialog::backdrop { background: rgba(20,20,20,.5); }

.cart-item {
  display: flex; align-items: center; gap: 12px; padding: 10px 0;
  border-bottom: 1px solid var(--border);
}
.cart-item:last-child { border-bottom: none; }
.cart-item-thumb {
  width: 52px; height: 52px; object-fit: contain; flex-shrink: 0;
}
.cart-item-text { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1; }
.cart-item-text b { font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: var(--text-muted); }
.cart-item-text span { font-size: 13.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cart-item-price { font-weight: 700; color: var(--navy); }
.cart-item-price small { font-weight: 400; color: var(--text-muted); }
.cart-item-ref { color: var(--text-muted); font-size: 12px; }
/* Only the summary line is cut short with "…". The price and the print
   reference wrap, or a phone showed "£117.21 rough estim…". */
.cart-item-text .cart-item-price, .cart-item-text .cart-item-ref { white-space: normal; }
.cart-item-qty {
  width: 44px; padding: 6px 4px; text-align: center; font-size: 13px;
  border: 1px solid var(--border); font-family: inherit; flex-shrink: 0;
}
.cart-item-remove {
  border: none; background: none; font-size: 20px; line-height: 1; cursor: pointer;
  color: var(--text-muted); padding: 4px 6px; flex-shrink: 0;
}
.cart-item-remove:hover { color: #a33; }

.cart-total-row {
  display: flex; justify-content: space-between; align-items: center;
  padding: 10px 0 0; margin-top: 4px; border-top: 1px solid var(--border);
  font-size: 14px; font-weight: 700; color: var(--navy);
}
.cart-total-row.cart-total-muted { font-weight: 400; color: var(--text-muted); font-size: 12.5px; border-top: none; padding-top: 2px; }

.cart-clear-link {
  display: block; margin: 10px auto 0; border: none; background: none;
  font-size: 12px; color: var(--text-muted); text-decoration: underline;
  cursor: pointer; padding: 4px;
}
.cart-clear-link:hover { color: #a33; }

.moulding-dialog-head h3 { margin: 0; font-size: 16px; }
.dialog-close {
  border: none; background: none; font-size: 22px; line-height: 1; cursor: pointer; color: var(--text-muted); padding: 2px 6px;
}
.dialog-close:hover { color: var(--text); }
.moulding-search {
  width: 100%; padding: 9px 12px; border: 1px solid var(--border); font-size: 13.5px; font-family: inherit;
}
.moulding-search:focus { outline: 1px solid var(--navy); }

/* --- moulding picker filters (frame builder) ---------------------------
   Deliberately quieter than the Frames sidebar. In here the grid of
   mouldings is the content and the filters are a tool for narrowing it,
   so they stay collapsed behind one control unless something is already
   filtered. */
.picker-filters { margin-top: 10px; }
.picker-bar { display: flex; align-items: center; gap: 12px; }
.picker-toggle {
  display: inline-flex; align-items: center; gap: 7px; padding: 7px 13px; cursor: pointer;
  font-family: inherit; font-size: 13px; font-weight: 700; color: var(--navy);
  background: var(--surface); border: 1px solid var(--border);
  transition: border-color .2s ease;
}
.picker-toggle:hover { border-color: var(--navy); }
.picker-toggle .filters-count {
  display: inline-flex; align-items: center; justify-content: center; min-width: 19px; height: 19px;
  padding: 0 5px; border-radius: 999px; background: var(--navy); color: #fff; font-size: 11px;
}
.picker-toggle .filters-count[hidden] { display: none; }
.picker-result { font-size: 12.5px; color: var(--text-muted); font-variant-numeric: tabular-nums; }

.picker-carried {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin: 10px 0 0; padding: 9px 12px; font-size: 12.5px;
  color: var(--text); background: #eef2fb; border: 1px solid #c9d4ee;
}
.picker-carried button, .picker-empty button {
  font: inherit; font-weight: 700; color: var(--navy);
  background: none; border: none; padding: 0; cursor: pointer; text-decoration: underline;
}
.picker-empty { margin: 14px 0 0; font-size: 13px; color: var(--text-muted); }

.picker-body {
  display: grid; gap: 16px; margin-top: 12px; padding: 14px;
  background: var(--bg); border: 1px solid var(--border);
}
.picker-group { display: flex; flex-direction: column; gap: 8px; }
/* The swatch list is the long one; capped so the picker's own grid stays
   the thing you are looking at rather than being pushed off screen. */
#pf-colours { max-height: 160px; overflow-y: auto; }

/* The note, the filter panel and the "nothing matches" line are shown and
   hidden with the hidden attribute, which their own display rules above
   were overriding: the panel stood open and the note claimed a carried-
   over filter on every open, with nothing carried over at all. */
.picker-carried[hidden], .picker-body[hidden], .picker-empty[hidden] { display: none; }
/* The Type filter hides itself when there's only one type to choose. */
.filter-group[hidden], .picker-group[hidden] { display: none; }

/* --- print upload (print.html) ------------------------------------------
   Same two-panel shape as the frame builder (controls left, live preview
   right) since customers already learned that layout there. The crop
   itself reuses the builder's "cover" idea but interactively: drag to
   pan, slider to zoom, all stored as a fraction of the ORIGINAL photo so
   the shop can crop the full-resolution file the same way at print time
   — see print.js and supabase_vpc_print_uploads.sql. */
.print-layout { display: grid; grid-template-columns: 360px 1fr; gap: 24px; align-items: start; padding: 24px 0 70px; }
@media (max-width: 900px) { .print-layout { grid-template-columns: 1fr; } }

.print-size-grid { display: flex; flex-wrap: wrap; gap: 8px; }
.print-size-chip {
  font-family: inherit; font-size: 13px; font-weight: 600; padding: 8px 13px;
  border: 1px solid var(--border); background: #fff; color: var(--text); cursor: pointer;
}
.print-size-chip:hover { border-color: var(--navy); }
.print-size-chip.is-picked { border-color: var(--navy); background: var(--navy); color: #fff; }

.print-status { font-size: 12.5px; color: var(--text-muted); margin-top: 8px; min-height: 16px; }
.print-status.err { color: #a33; }
.print-status.ok { color: #1a7a3a; }

/* Stacked, so anything under the crop (the Frame Builder's way back) sits
   below it rather than beside it. */
#crop-panel { flex-direction: column; }
.crop-stage { display: flex; flex-direction: column; align-items: center; width: 100%; }
.crop-frame {
  width: 100%; max-width: 480px; overflow: hidden; background: #111; border: 1px solid var(--border);
  cursor: grab; touch-action: none; box-shadow: 0 14px 34px -18px rgba(0,0,0,.4);
}
.crop-frame.dragging { cursor: grabbing; }
.crop-frame canvas { display: block; width: 100%; height: 100%; user-select: none; -webkit-user-drag: none; }
/* Rotate and zoom share a row while there's room for a decent slider
   (200px); on a phone the zoom takes its own full-width row underneath.
   The slider's own width is 0 so it never props the row wider than the
   screen; it grows into whatever space there is. */
.crop-controls { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px 16px; width: 100%; max-width: 480px; margin-top: 14px; }
.crop-controls .btn { flex: none; white-space: nowrap; }
.crop-zoom { display: flex; align-items: center; gap: 4px; flex: 1 1 200px; min-width: 0; }

/* Zoom out / in at the slider's ends. */
.crop-zoom-step {
  display: inline-flex; align-items: center; justify-content: center; flex: none;
  width: 32px; height: 32px; padding: 0; border: 0; background: none;
  color: var(--navy); font-size: 18px; cursor: pointer;
}
.crop-zoom-step:hover:not(:disabled) { background: var(--bg); }
.crop-zoom-step:disabled { color: var(--text-muted); opacity: .45; cursor: default; }

/* The zoom slider in the site's own look rather than the browser's: a
   thin track filled navy up to the handle (--f, 0 to 1, set by print.js)
   and a square navy handle in a white ring. Bigger handle on touch. */
.crop-zoom input[type="range"] {
  --f: 0; --thumb: 18px;
  -webkit-appearance: none; appearance: none; flex: 1 1 0; width: 0; min-width: 0; height: 32px; margin: 0;
  background: transparent; cursor: pointer;
}
.crop-zoom input[type="range"]:focus { outline: none; }
.crop-zoom input[type="range"]::-webkit-slider-runnable-track {
  height: 3px; border: 0;
  background: linear-gradient(to right, var(--navy) calc(var(--thumb) / 2 + (100% - var(--thumb)) * var(--f)), var(--border) 0);
}
.crop-zoom input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none; box-sizing: border-box;
  width: var(--thumb); height: var(--thumb); margin-top: calc((3px - var(--thumb)) / 2);
  background: var(--navy); border: 3px solid #fff; border-radius: 0;
  box-shadow: 0 0 0 1px var(--navy), 0 2px 6px rgba(28,44,88,.25);
}
.crop-zoom input[type="range"]::-moz-range-track { height: 3px; border: 0; background: var(--border); }
.crop-zoom input[type="range"]::-moz-range-progress { height: 3px; background: var(--navy); }
.crop-zoom input[type="range"]::-moz-range-thumb {
  box-sizing: border-box; width: var(--thumb); height: var(--thumb);
  background: var(--navy); border: 3px solid #fff; border-radius: 0;
  box-shadow: 0 0 0 1px var(--navy), 0 2px 6px rgba(28,44,88,.25);
}
.crop-zoom input[type="range"]:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 1px var(--navy), 0 0 0 5px rgba(28,44,88,.22); }
.crop-zoom input[type="range"]:focus-visible::-moz-range-thumb { box-shadow: 0 0 0 1px var(--navy), 0 0 0 5px rgba(28,44,88,.22); }
@media (pointer: coarse) { .crop-zoom input[type="range"] { --thumb: 24px; } }

.success-panel { text-align: center; padding: 60px 20px; grid-column: 1 / -1; }
.success-panel h2 { margin: 14px 0 8px; }
.success-panel p { color: var(--text-muted); margin-bottom: 22px; }
.success-panel b { color: var(--navy); }
#print-success { grid-column: 1 / -1; }

.full-photo-check {
  display: flex; align-items: center; gap: 8px; margin-top: 12px;
  font-size: 13.5px; font-weight: 600; color: var(--text); cursor: pointer;
}
.full-photo-check input { width: auto; }

/* "Custom size": width, height and mm | in on one row under the chips,
   the switch the same height as the boxes beside it. */
.custom-size { display: flex; align-items: flex-end; gap: 8px; margin-top: 12px; }
.custom-size[hidden] { display: none; }
.custom-size .unit-switch button { padding-top: 9px; padding-bottom: 9px; }

/* The note at the top when the Frame Builder sent the photo to be cropped. */
.crop-for-frame {
  display: flex; align-items: flex-start; gap: 10px; margin: 0 0 18px; padding: 10px 12px;
  background: var(--bg); border-left: 3px solid var(--navy);
}
.crop-for-frame[hidden] { display: none; }
.crop-for-frame i { font-size: 18px; color: var(--navy); flex: none; margin-top: 1px; }
.crop-for-frame p { margin: 0; font-size: 12.5px; line-height: 1.45; color: var(--text-muted); }
.crop-for-frame b { color: var(--text); }
.crop-actions { display: flex; flex-direction: column; gap: 10px; width: 100%; max-width: 480px; margin: 18px auto 0; }
.crop-actions[hidden] { display: none; }

/* --- quote cart: contact fields ------------------------------------
   The cart dialog is built in JS (cart.js) but styled here with
   everything else, so the look stays in one place. */
.cart-field-label { display: block; font-size: 12px; font-weight: 700; margin: 10px 0 4px; }
.cart-field {
  width: 100%; padding: 9px 11px; border: 1px solid var(--border);
  font-size: 14px; font-family: inherit; background: #fff; color: var(--text);
}
.cart-field:focus { outline: 2px solid var(--navy); outline-offset: -2px; }
textarea.cart-field { resize: vertical; min-height: 46px; }
.cart-send-error { font-size: 12.5px; color: #a33; margin-top: 9px; }

/* --- cookie choice (consent.js) ---------------------------------------
   Asks once, before Microsoft Clarity loads. Bottom left on wider screens,
   clear of the quote list button in the bottom right; a full-width strip
   on phones. Both answers are the same size: saying no has to be as easy
   as saying yes. */
.consent {
  position: fixed; left: 20px; bottom: 20px; z-index: 60;
  width: min(440px, calc(100vw - 40px)); padding: 16px 18px;
  background: var(--surface); border: 1px solid var(--border); border-top: 3px solid var(--navy);
  box-shadow: var(--e4);
}
.consent[hidden] { display: none; }
.consent p { margin: 0 0 12px; font-size: 13.5px; line-height: 1.5; color: var(--text-muted); }
.consent b { display: block; margin-bottom: 2px; font-size: 14px; color: var(--text); }
.consent a { color: var(--navy); font-weight: 600; }
.consent-actions { display: flex; gap: 8px; }
.consent-actions .btn { flex: 1 1 0; justify-content: center; }
@media (max-width: 560px) {
  .consent { left: 0; bottom: 0; width: 100%; padding: 14px 16px 16px; border-width: 3px 0 0; }
}

/* --- touch screens: text fields at 16px ---------------------------------
   iPhone Safari zooms the whole page into any text field under 16px the
   moment it's tapped, and leaves it zoomed afterwards. Mouse users keep
   the compact sizes above. Last in the file so it wins on source order. */
@media (pointer: coarse) {
  .b-field input[type="number"], .b-field input[type="text"], .b-field input[type="email"],
  .b-field input[type="tel"], .b-field textarea, .moulding-search, .cart-field,
  .cart-item-qty, #filter-search { font-size: 16px; }
}
