/* Five-step booking flow. Lives inside the design's .quote-card, so it borrows
   that palette (--night-2, --line-night, --accent, --on-night-2) rather than
   inventing colours. Anything tappable is at least 44px tall. */

.bk { display: block; }

/* The design's .btn sets its own `display`, which BEATS the [hidden] attribute's
   UA `display:none`. Without this the step-5 submit button is visible on step 1.
   Attribute selector + !important is the only thing that reliably wins here. */
.bk [hidden], .bk__nav [hidden] { display: none !important; }

/* ---------- progress ---------- */
.bk__bar {
  display: flex; gap: .3rem; margin: 0 0 .5rem; padding: 0; list-style: none;
}
.bk__bar li {
  height: 4px; flex: 1; border-radius: 2px;
  background: var(--line-night); transition: background .18s ease;
}
.bk__bar li[data-done] { background: var(--accent); }
.bk__count {
  margin: 0 0 1.1rem; font: 600 .78rem/1 var(--mono, monospace);
  letter-spacing: .06em; text-transform: uppercase; color: var(--on-night-2);
}

/* ---------- steps ---------- */
.bk__step { border: 0; padding: 0; margin: 0; min-inline-size: 0; }
.bk__step[hidden] { display: none; }
.bk__step legend {
  padding: 0; margin: 0 0 .2rem;
  font-size: clamp(1.15rem, 2.4vw, 1.35rem); font-weight: 700; line-height: 1.25;
}
.bk__hint { margin: 0 0 1rem; font-size: .88rem; color: var(--on-night-2); }

/* ---------- choice tiles ---------- */
.bk__choices { display: grid; gap: .45rem; margin: 0 0 .2rem; }
.bk__choices--2 { grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); }

.bk__choice { position: relative; display: block; }
.bk__choice input {
  position: absolute; inset: 0; width: 100%; height: 100%;
  margin: 0; opacity: 0; cursor: pointer;
}
.bk__choice span {
  display: flex; align-items: center; min-height: 46px;
  padding: .6rem .8rem; border: 1px solid var(--line-night); border-radius: 6px;
  background: transparent; font-size: .93rem; line-height: 1.3;
  transition: border-color .15s ease, background .15s ease;
}
.bk__choice input:checked + span {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  font-weight: 600;
}
.bk__choice input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: 2px; }
.bk__choice input:hover + span { border-color: var(--accent); }

/* ---------- consent ---------- */
.bk__consent {
  display: flex; gap: .6rem; align-items: flex-start;
  margin: 1rem 0 0; padding: .8rem; border: 1px solid var(--line-night); border-radius: 6px;
}
.bk__consent input { width: 20px; height: 20px; margin: .1rem 0 0; flex: 0 0 auto; accent-color: var(--accent); }
.bk__consent span { font-size: .8rem; line-height: 1.5; color: var(--on-night-2); }

/* ---------- review ---------- */
.bk__review { margin: 0 0 1rem; display: grid; gap: .1rem; }
.bk__review div {
  display: flex; gap: .8rem; justify-content: space-between;
  padding: .5rem 0; border-bottom: 1px solid var(--line-night); font-size: .9rem;
}
.bk__review dt, .bk__review b { color: var(--on-night-2); font-weight: 600; flex: 0 0 auto; }
.bk__review span { text-align: right; }

/* ---------- nav ---------- */
.bk__nav { display: flex; gap: .6rem; margin: 1.2rem 0 0; }
.bk__nav .btn { flex: 1 1 auto; min-height: 48px; }
.bk__back {
  flex: 0 0 auto; min-height: 48px; padding: 0 1rem;
  background: transparent; color: inherit; cursor: pointer;
  border: 1px solid var(--line-night); border-radius: 6px;
  font: inherit; font-size: .9rem;
}
.bk__back:hover { border-color: var(--accent); }
.bk__back[hidden] { display: none; }

/* ---------- emergency escape hatch ---------- */
.bk__urgent {
  margin: 1rem 0 0; padding: 1rem; border-radius: 6px;
  border: 1px solid var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}
.bk__urgent p { margin: 0 0 .8rem; font-size: .92rem; line-height: 1.5; }
.bk__urgent .btn { width: 100%; min-height: 48px; }
.bk__urgent[hidden] { display: none; }

/* ---------- validation + status ---------- */
.bk [aria-invalid="true"] { outline: 2px solid #ff6b6b; outline-offset: 1px; }
.bk__err { margin: .35rem 0 0; font-size: .82rem; color: #ff8f8f; }
.bk__err[hidden] { display: none; }

@media (prefers-reduced-motion: reduce) {
  .bk__bar li, .bk__choice span { transition: none; }
}

/* ---------- fit to screen ----------
   The card sits in the hero, so on a laptop it has to fit the viewport or the
   whole above-the-fold moment is a scroll. Two moves:
     1. tighter vertical rhythm inside the card,
     2. a hard cap on the STEP area with its own scroll, so no combination of
        options can ever push the Continue button off screen.
   The cap only applies where the hero is genuinely side-by-side. On a phone the
   hero stacks and normal page scrolling is the right behaviour. */
@media (min-width: 900px) {
  .quote-card { padding-block: clamp(1rem, 2vw, 1.35rem); }
  .quote-card h2 { font-size: clamp(1.35rem, 2.4vw, 1.7rem); line-height: 1.15; }
  .quote-card__lead { margin-block: .35rem .8rem; font-size: .88rem; }

  .bk__count { margin-bottom: .8rem; }
  .bk__step legend { font-size: 1.12rem; }
  .bk__hint { margin-bottom: .7rem; font-size: .84rem; }

  /* Header, progress and nav stay put; only the answers scroll, and only when
     they genuinely do not fit. */
  /* The scroll lives on this plain div, never on the <fieldset>. A fieldset
     with overflow-y:auto still paints its children outside its own box, which
     dropped the last service tile on top of the Continue button. */
  .bk__steps {
    flex: 1 1 auto;
    min-height: 0;              /* without this a flex child refuses to shrink */
    overflow-y: auto;
    overscroll-behavior: contain;
    padding-right: .3rem;
  }
  .bk__steps::-webkit-scrollbar { width: 6px; }
  .bk__steps::-webkit-scrollbar-thumb { background: var(--line-night); border-radius: 3px; }

  .bk__choice span { min-height: 44px; padding-block: .5rem; }
  .bk__nav { margin-top: .9rem; }
}

/* Two-across tiles need a tighter min so nine services land in five rows
   rather than wrapping back to one column on a narrow card. */
.bk__choices--2 { grid-template-columns: repeat(auto-fit, minmax(7.5rem, 1fr)); }

/* Reclaiming the last of the fold. The hero's own padding and the card chrome
   (lead, progress label, fineprint) were the remaining overspill once the
   tiles went two-across. Short-viewport laptops are the case that matters:
   1280x720 is still the most common desktop size we see. */
@media (min-width: 900px) {
  .hero { padding-block: clamp(1.5rem, 3vw, 2.5rem); }
  .quote-card__lead { margin-block: .3rem .6rem; }
  .bk__count { margin-bottom: .6rem; }
  .fineprint { margin-top: .6rem; }
}

@media (min-width: 900px) {
  .quote-card { padding-block: clamp(.85rem, 1.6vw, 1.15rem); }
  .quote-card h2 { font-size: clamp(1.25rem, 2.2vw, 1.5rem); }
  .bk__nav { margin-top: .75rem; }
  .fineprint { margin-top: .5rem; font-size: .68rem; }
}

/* ---------- the actual fit ----------
   Guessing pixel budgets does not survive a different viewport or a longer
   step. Cap the CARD to the viewport, make it a flex column, and let the step
   area absorb whatever is left over. The heading, progress and the Continue
   button are then always on screen, at any height, whatever the step contains. */
@media (min-width: 900px) {
  .quote-card {
    display: flex; flex-direction: column;
    max-height: calc(100svh - 11rem);
  }
  .bk { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
}

/* ================= STICKY MOBILE CALL BAR =================
   Most of these visits are on a phone, and the header CTA scrolls away. This
   keeps calling and texting one thumb-reach away for the whole session.
   Desktop already has a persistent header CTA, so it is mobile only. */
.callbar {
  position: fixed; inset: auto 0 0 0; z-index: 60;
  background: var(--night-2, #14170f);
  border-top: 1px solid var(--line-night, #2a2f24);
  padding: .5rem .75rem calc(.5rem + env(safe-area-inset-bottom));
  box-shadow: 0 -8px 24px rgb(0 0 0 / .35);
}
.callbar[hidden] { display: none; }
.callbar__status {
  margin: 0 0 .4rem; text-align: center;
  font: 600 .72rem/1.3 var(--mono, monospace);
  letter-spacing: .05em; text-transform: uppercase; color: var(--on-night-2, #9aa295);
}
.callbar__status b { color: var(--accent, #9ede4d); font-weight: 700; }
.callbar__acts { display: grid; grid-template-columns: 1fr 1fr; gap: .5rem; }
.callbar__btn {
  display: flex; align-items: center; justify-content: center;
  min-height: 48px; padding: 0 .75rem; border-radius: 6px;
  font: 700 .95rem/1 var(--body, system-ui); letter-spacing: .02em;
  text-decoration: none; border: 1px solid var(--accent, #9ede4d);
  color: var(--accent, #9ede4d);
}
.callbar__btn--call { background: var(--accent, #9ede4d); color: #10130c; }
.callbar__btn:focus-visible { outline: 3px solid #fff; outline-offset: 2px; }

/* Keep the bar from covering the end of the page. */
@media (max-width: 899.98px) {
  body { padding-bottom: calc(6.5rem + env(safe-area-inset-bottom)); }
}
@media (min-width: 900px) {
  .callbar { display: none !important; }
}

/* ================= SITE-WIDE OVERRIDES =================
   Client-level tuning of the vault design. Kept here rather than in the vault
   so other clients built from `proof` are unaffected. */

/* The design ships --wrap: 1180px, which left roughly 130px of dead gutter on
   each side of a 1440px screen. Widen it and pull the side padding in. */
:root { --wrap: 1400px; }
.wrap { padding-inline: clamp(1rem, 2.5vw, 1.75rem); }

/* Long prose must not stretch with the container. A 1400px measure is
   unreadable, so the paragraphs keep their own line length. */
.hero .lead, .lead--center, .quote-card__lead { max-width: 62ch; }

/* ---------- the phone number is the product ----------
   For a trade site the call IS the conversion. Make the number the loudest
   thing on the page after the H1, on a button that dials on tap. */
.hero .btn--lg {
  font-size: clamp(1.15rem, 1.6vw, 1.5rem);
  font-weight: 800;
  letter-spacing: .01em;
  padding: 1.05rem 1.9rem;
  min-height: 64px;
  box-shadow: 0 6px 0 color-mix(in srgb, var(--accent) 45%, #000);
}
.hero .btn--lg:hover { transform: translateY(2px); box-shadow: 0 4px 0 color-mix(in srgb, var(--accent) 45%, #000); }
.hero .btn--lg:active { transform: translateY(6px); box-shadow: none; }

/* The always-visible number in the utility bar. */
.meta__phone {
  font-size: clamp(.95rem, 1.3vw, 1.15rem);
  font-weight: 800;
  letter-spacing: .02em;
}

/* Header CTA carries the number too, so it is never more than one glance away. */
.bar .btn--fill { font-weight: 800; }

@media (prefers-reduced-motion: reduce) {
  .hero .btn--lg:hover, .hero .btn--lg:active { transform: none; }
}

/* The header scrolled away on desktop, so past the hero there was no phone
   number on screen at all. Mobile has the call bar; desktop gets a sticky
   header carrying the number. */
@media (min-width: 900px) {
  .bar { position: sticky; top: 0; z-index: 50; }
}

/* ================= BRAND PALETTE =================
   Deep navy / electric yellow / white / cool gray / charcoal.

   🚨 The rule that governs this palette: **electric yellow is FILL ONLY on a
   light ground.** #FFC928 as text on white measures 1.54:1 and on cool gray
   1.42:1, both far below the 4.5:1 floor. So the accent has two forms:
   the yellow itself for fills and for text on navy (10.84:1), and a dark amber
   for accent-coloured TEXT on a light ground (6.65:1 on white). Never paint
   yellow words on white.

   Overridden on body[data-theme] rather than :root, because custom properties
   inherit and the design sets its palette on <body>. A :root override would
   lose to it for every element inside body. */
body[data-theme] {
  /* grounds */
  --night:      #0B1F33;   /* deep navy: header, hero, footer, dark bands */
  --night-2:    #12293F;   /* raised surfaces on navy, e.g. the quote card */
  --bone:       #FFFFFF;   /* bright white: the main light ground */
  --bone-2:     #FFFFFF;   /* cards stay white, separated by border not fill */
  --plate:      #12293F;

  /* type */
  --ink:        #202B33;   /* charcoal body text, 14.4:1 on white */
  --ink-2:      #4A5761;   /* muted on light, 7.4:1 on white, 6.8:1 on gray */
  --on-night:   #FFFFFF;   /* 16.7:1 on navy */
  --on-night-2: #A9BACB;   /* muted on navy, 8.4:1 */

  /* hairlines */
  --line:       #D9E1E8;
  --line-night: #1E3A55;

  /* accent */
  --accent:          #FFC928;  /* fills, icons, highlights */
  --accent-on-night: #FFC928;  /* 10.8:1 on navy, safe as text */
  --accent-on-bone:  #7A5600;  /* accent TEXT on a light ground, 6.7:1 */
}

/* Cool gray for the alternating light bands, so the page steps
   white -> gray -> navy instead of running as one flat white sheet.
   Cards inside stay white and keep their separation.

   This was `:nth-of-type(even)` until the gallery was added. Positional
   banding silently re-bands every section BELOW any newly inserted one, which
   is a layout regression no test would catch, so the grey bands are now named
   in build.py instead of counted here. */
main > .section--band { background: #F3F6F8; }

/* The call bar borrows the same palette instead of its own hardcoded values. */
.callbar__btn--call { color: var(--ink); }

/* ================= SERVICE AREA MAP ================= */
.svcmap { margin: 1.6rem 0 1.4rem; }
@media (prefers-reduced-motion: reduce) { .svcmap__pulse { animation: none; opacity: .35; } }

/* Badge logo in the wordmark. Circular, so it needs no border or plate. */
.brand { display: inline-flex; align-items: center; gap: .6rem; }
.brand__mark { width: 40px; height: 40px; border-radius: 50%; flex: 0 0 auto; }
@media (max-width: 480px) { .brand__mark { width: 34px; height: 34px; } }

/* ================= WHITE QUOTE CARD =================
   On navy the card read as a slightly different navy. White makes it the one
   object on the screen that is clearly a thing you DO something with.
   Flipping the ground means flipping every token inside it, and the accent
   changes meaning: yellow is a FILL on white, never text. */
.quote-card {
  background: var(--bone-2);
  border: 1px solid var(--line);
  border-top: 5px solid var(--accent);
  color: var(--ink);
  box-shadow: 0 18px 44px rgb(0 0 0 / .28);
}
.quote-card h2 { color: var(--ink); }
.quote-card__lead { color: var(--ink-2); }
.quote-card .fineprint { color: var(--ink-2); }

.bk__count { color: var(--ink-2); }
.bk__bar li { background: #E3E9EE; }
.bk__bar li[data-done] { background: var(--accent); }
.bk__step legend { color: var(--ink); }
.bk__hint { color: var(--ink-2); }

.quote-card .field label { color: var(--ink); }
.quote-card .field__opt { color: var(--ink-2); }
.quote-card .field input,
.quote-card .field textarea {
  background: #FFFFFF; color: var(--ink); border: 1px solid #C7D2DB;
}
.quote-card .field input::placeholder,
.quote-card .field textarea::placeholder { color: #6B7884; }
.quote-card .field input:focus, .quote-card .field textarea:focus {
  outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--ink);
}

.bk__choice span { border-color: #C7D2DB; color: var(--ink); }
.bk__choice input:checked + span {
  border-color: var(--ink); background: color-mix(in srgb, var(--accent) 30%, #fff); font-weight: 700;
}
.bk__choice input:hover + span { border-color: var(--ink); }

.bk__consent { border-color: #C7D2DB; }
.bk__consent span { color: var(--ink-2); }

.bk__review div { border-bottom-color: var(--line); }
.bk__review b { color: var(--ink-2); }

.bk__back { border-color: #C7D2DB; color: var(--ink); }
.bk__back:hover { border-color: var(--ink); }

/* Error red has to clear AA on white, so it is darkened from the on-navy tone. */
.bk__err { color: #B3261E; }
.bk [aria-invalid="true"] { outline-color: #B3261E; }

.bk__urgent {
  border-color: var(--ink);
  background: color-mix(in srgb, var(--accent) 22%, #fff);
}
.bk__urgent p { color: var(--ink); }

.bk__steps::-webkit-scrollbar-thumb { background: #C7D2DB; }

/* Bigger badge. It is a real illustration, not a wordmark, so at 40px the wave
   and the skeleton were mud. 56px is where the artwork starts to read. */
.brand__mark { width: 56px; height: 56px; }
.bar .wrap { padding-block: .7rem; }
@media (max-width: 899.98px) { .brand__mark { width: 48px; height: 48px; } }
@media (max-width: 480px)    { .brand__mark { width: 44px; height: 44px; } }

/* ================= THE BADGE AS A SEAL =================
   The About section shipped a grey placeholder rectangle. The badge belongs
   there instead: it kills a known placeholder and gives the logo a full-size
   showing on the ground it was drawn for. */
.seal { display: flex; align-items: center; justify-content: center; padding: clamp(1rem, 3vw, 2rem); }
.seal img {
  width: min(360px, 78%); height: auto; border-radius: 50%;
  filter: drop-shadow(0 18px 40px rgb(0 0 0 / .45));
}
@media (prefers-reduced-motion: no-preference) {
  .seal img { transition: transform .5s cubic-bezier(.2,.7,.3,1); }
  .seal:hover img { transform: rotate(-4deg) scale(1.03); }
}

/* Two thresholds the white ground moved us onto the wrong side of.
   Placeholder text was 4.52:1 against a 4.5 floor, which is a pass with no
   margin at all. Input borders were 1.54:1, and WCAG 1.4.11 wants 3:1 for the
   boundary of a control you are meant to find and click. */
.quote-card .field input::placeholder,
.quote-card .field textarea::placeholder { color: #5E6B77; }      /* 5.46:1 */
.quote-card .field input,
.quote-card .field textarea { border-color: #7F8C99; }            /* 3.24:1 */
.bk__choice span { border-color: #7F8C99; }
.bk__consent { border-color: #7F8C99; }
.bk__back { border-color: #7F8C99; }

/* ================= EDGE ARTWORK =================
   Wave-and-lightning strips down both margins, running the full length of every
   page. Three things this had to solve:

   1. The source shipped OPAQUE with a cream middle, not transparent. The
      background was flood-filled out from the centre rather than colour-keyed,
      because the wave foam is also cream and a global key would have eaten it.
   2. The source did not tile: its top and bottom edges differed by ~32/255, so
      stacking copies would show a seam every screenful. The strips are built
      from the art plus a vertically mirrored copy, so every join is an edge
      meeting itself. Measured wrap difference is now exactly 0.
   3. Text must never sit on it. The strips are fixed to the viewport BEHIND the
      content column, and they only appear once the viewport is wide enough to
      have margin to spare.

   Fixed rather than scrolling: the art stays put while the page moves over it,
   which reads as one continuous flow instead of a repeating pattern. */
.edge {
  position: fixed; top: 0; bottom: 0; width: 150px; z-index: 1;
  pointer-events: none; background-repeat: repeat-y;
  background-size: 100% auto; opacity: .9;
  display: none;
}
.edge--l { left: 0;  background-image: url("/assets/edge-left-240.webp");  background-position: left top; }
.edge--r { right: 0; background-image: url("/assets/edge-right-240.webp"); background-position: right top; }

/* Content sits above the art. The wrap has no background of its own, so the
   art is only ever visible in the margin beside it, never under the words. */
.wrap { position: relative; z-index: 2; }
.callbar { z-index: 60; }

/* Only where there is genuinely room. Below this the content needs the full
   width and the art would crowd the text. */
@media (min-width: 1180px) {
  .edge { display: block; }
  :root { --wrap: 1080px; }
}
@media (min-width: 1500px) {
  .edge { width: 200px; }
  :root { --wrap: 1240px; }
}
@media (min-width: 1800px) {
  .edge { width: 260px; }
  :root { --wrap: 1400px; }
}

/* ================= HEADER =================
   The thin top strip is gone. Its hours and phone number were duplicated in the
   header CTA, the hero button, the trust strip and the footer, so nothing was
   lost, and the space it used now goes to the badge. */
.bar .wrap { padding-block: 1.15rem; }
.brand__mark { width: 84px; height: 84px; }
.brand { gap: .85rem; font-size: 1.7rem; }

@media (max-width: 899.98px) {
  .bar .wrap { padding-block: .9rem; }
  .brand__mark { width: 64px; height: 64px; }
  .brand { font-size: 1.35rem; }
}
@media (max-width: 480px) {
  .brand__mark { width: 54px; height: 54px; }
  .brand { font-size: 1.15rem; gap: .6rem; }
}

/* ================= OVERSCROLL =================
   Dragging past the top rubber-banded the page away from the viewport while the
   fixed edge art stayed pinned, so you saw artwork sitting against a bare strip
   above the header. Two fixes together, because neither covers every browser:
   suppress the bounce where the browser supports it, and make the ground behind
   the page the same navy as the header so any bounce that still happens reveals
   nothing but more navy. */
html {
  /* Literal, NOT var(--night). The palette is defined on body[data-theme], and
     <html> sits above that, so the variable resolves to the vault's original
     dark grey here and the overscroll strip comes out the wrong colour. */
  background: #0B1F33;
  overscroll-behavior-y: none;
}
body { overscroll-behavior-y: none; background: #0B1F33; }

/* The art starts below the header rather than behind it, so nothing of the
   design can appear above the bar at any scroll position. */
.edge { top: var(--edge-top, 0px); }

/* ================= TRUST BAR ================= */
/* One featured fact with the rest peeking either side. See the carousel block
   at the foot of this file for the geometry; these are just the text styles. */
.tbar { background: var(--night-2); border-block: 1px solid var(--line-night); padding-block: 1.3rem; }
.tbar__item { display: flex; flex-direction: column; gap: .12rem; }
.tbar__item b { color: var(--accent-on-night); font-size: .95rem; font-weight: 800; letter-spacing: .01em; }
.tbar__item span { color: var(--on-night-2); font-size: .82rem; }

/* ================= WORK QUALITY ================= */
.qgrid { display: grid; gap: 1rem; margin-top: 2rem;
         grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); }
.qcard { background: var(--bone-2); border: 1px solid var(--line); border-left: 4px solid var(--accent);
         border-radius: 6px; padding: 1.1rem 1.2rem; }
.qcard h3 { margin: 0 0 .35rem; font-size: 1.02rem; font-weight: 800; color: var(--ink); }
.qcard p { margin: 0; font-size: .92rem; line-height: 1.55; color: var(--ink-2); }

/* ================= SECONDARY SERVICES ================= */
.more-h { margin: 2.6rem 0 1rem; text-align: center; font-size: 1rem; font-weight: 800;
          letter-spacing: .06em; text-transform: uppercase; color: var(--ink-2); }
.scards { display: grid; gap: .6rem; grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr)); }
.scard { display: flex; align-items: center; justify-content: space-between; gap: .75rem;
         min-height: 56px; padding: .75rem 1rem; text-decoration: none;
         background: var(--bone-2); border: 1px solid var(--line);
         border-left: 4px solid var(--accent); border-radius: 6px;
         color: var(--ink); font-weight: 700; font-size: .95rem;
         transition: border-color .15s ease, transform .15s ease; }
.scard::after { content: "\2192"; color: var(--accent-on-bone); font-weight: 800; }
.scard:hover { border-color: var(--ink); transform: translateX(2px); }
.scard:focus-visible { outline: 2px solid var(--accent-on-bone); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .scard:hover { transform: none; } }

/* Google rating sits first in the trust bar and links to the profile, so the
   claim is checkable in one click rather than asserted. */
.tbar__item--rating { text-decoration: none; }
.tbar__item--rating:hover b { text-decoration: underline; }
.tbar__stars { color: var(--accent-on-night); letter-spacing: .06em; font-size: .9em; }

/* Interactive map, revealed on request. */
.svcmap__frame { width: 100%; height: clamp(320px, 46vh, 460px); border: 0;
                 border-radius: 8px; display: block; }

/* ================= EDGE ART: OPAQUE =================
   The strips were 90% opacity AND the artwork itself is transparent between the
   wave shapes, so with the layer fixed and the page scrolling underneath you
   could watch section backgrounds slide past behind the waves. Two fixes, and
   both are needed: full opacity, and a solid navy ground under the art so the
   gaps between the shapes have something still behind them rather than a moving
   page. The result reads as a fixed frame around the site instead of a veil
   laid over it. */
/* Opacity only. The strips were at .9, which made the wave SHAPES themselves
   see-through, so the page slid past inside the artwork. Full opacity fixes
   that. A solid ground behind the art was overcorrecting: it turned a frame
   into a navy band down each side. The gaps between the shapes are meant to be
   open. */
.edge { opacity: 1; }

/* ================= EDGE ART: FULL HEIGHT, IN FRONT =================
   The art now starts at y=0 and paints ABOVE the header bar, so the frame runs
   unbroken to the top of the screen instead of stopping at a horizontal seam.

   The catch: the strips were wider than the gutter beside the content column
   and only got away with it because they sat behind everything. In front, a
   150px strip at a 1180px viewport would cover the logo and the call button.
   So each strip is now capped to the space that actually exists next to the
   content: min(design width, half the leftover). It can never reach the text.

   z-index 51 clears the sticky header at 50; the mobile call bar stays at 60. */
.edge {
  top: 0;
  z-index: 51;
}
@media (min-width: 1180px) { .edge { width: min(150px, max(0px, (100vw - 1080px) / 2)); } }
@media (min-width: 1500px) { .edge { width: min(200px, max(0px, (100vw - 1240px) / 2)); } }
@media (min-width: 1800px) { .edge { width: min(260px, max(0px, (100vw - 1400px) / 2)); } }

/* ================= CTA RED =================
   The call to action is red; the brand accent stays electric yellow for
   eyebrows, rules and the progress bar. Red is reserved for the one thing we
   want pressed, which is what makes it read as an action rather than decoration.

   #CE2029 measures 5.44:1 against a white label, and 3.07:1 against the navy
   ground, so it clears AA for the text and the 3:1 non-text floor for the
   button shape itself. Labels are white, never the design's dark ink. */
:root { --cta: #CE2029; --cta-ink: #FFFFFF; --cta-dark: #A8121C; }

.btn--fill,
.hero .btn--lg,
.bar .btn--fill,
#submitBtn,
#bkNext,
.callbar__btn--call {
  background: var(--cta) !important;
  border-color: var(--cta) !important;
  color: var(--cta-ink) !important;
}
.hero .btn--lg { box-shadow: 0 6px 0 var(--cta-dark); }
.hero .btn--lg:hover { box-shadow: 0 4px 0 var(--cta-dark); }
.hero .btn--lg:active { box-shadow: none; }
.btn--fill:focus-visible, #submitBtn:focus-visible, #bkNext:focus-visible,
.callbar__btn--call:focus-visible { outline: 3px solid #fff; outline-offset: 2px; }

/* The outlined text-me button on the call bar stays on the yellow accent so the
   two are told apart by colour as well as by fill. */
.callbar__btn:not(.callbar__btn--call) { border-color: var(--accent); color: var(--accent); }

/* Two actions per service card: learn more, or ask now. */
.card__more--alt { margin-left: 1rem; opacity: .85; }

/* ================= WORK GALLERY =================
   Nine square tiles of real job photos. Square is the compromise that suits a
   mixed set: the source photos run from 1:2 portrait to landscape, and any
   other single ratio crops at least one of them past the point where you can
   tell what you are looking at. The crops are baked in at build time
   (build.py -> GALLERY), so the browser is never asked to art-direct.

   The tiles are plain <img> inside <figure>. No lightbox: a full screen viewer
   is one more thing to get wrong on a phone, and the caption is what actually
   sells the photo. */
.gal {
  list-style: none; margin: 2.4rem 0 0; padding: 0;
  display: grid; gap: 1.15rem;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 17rem), 1fr));
}
.gal__item { margin: 0; }

.gal__fig {
  margin: 0; height: 100%;
  display: flex; flex-direction: column;
  background: var(--bone-2);
  border: 1px solid var(--line);
  border-radius: 10px; overflow: hidden;
}

.gal__img {
  display: block; width: 100%; height: auto;
  aspect-ratio: 1 / 1; object-fit: cover;
  /* Navy behind the image so a photo that has not decoded yet is a deliberate
     block of brand colour rather than a white hole in the grid. */
  background: var(--plate);
}

.gal__cap {
  padding: .85rem .95rem 1rem;
  font-size: .9rem; line-height: 1.45;
  color: var(--ink-2);          /* 7.4:1 on white, 6.8:1 on the grey band */
}

.gal__tag {
  display: block; width: fit-content; margin: 0 0 .4rem;
  padding: .2rem .55rem;
  font: 700 .68rem/1.3 var(--body);
  letter-spacing: .07em; text-transform: uppercase;
  color: #6B4B00;               /* accent ink, 7.0:1 on the pale wash below */
  background: #FFF3D1;
  border-radius: 999px;
}

/* A before/after pair only reads as a pair if the two tiles are side by side.
   Ordering alone does not survive the column count changing, so the FIRST tile
   of each pair is pinned to column 1: the pair then always opens a row, at
   three columns, at two, and trivially at one. */
.gal__item--rowstart { grid-column-start: 1; }

/* Visually hidden but still announced. There was no such utility in this build
   and the reviews link needs one to say "opens in a new tab" out loud. */
.vh {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

/* ---- Google reviews link ----
   This sits in a row of five plain facts styled identically to it. Nothing but
   a hover underline said it was clickable, and hover does not exist on a phone,
   so on mobile it was not discoverable at all. Boxed, underlined and arrowed. */
.tbar__item--rating {
  text-decoration: none;
  padding: .5rem .75rem;
  margin: -.5rem 0;
  border: 1px solid var(--accent);
  border-radius: 9px;
  background: rgb(255 201 40 / .10);
  transition: background .16s ease;
}
.tbar__item--rating span {
  color: var(--on-night);              /* white on navy, not the muted grey */
  text-decoration: underline;
  text-underline-offset: 2px;
}
.tbar__ext { display: inline-block; margin-left: .25rem; text-decoration: none; }
.tbar__item--rating:hover { background: rgb(255 201 40 / .20); }
.tbar__item--rating:focus-visible {
  outline: 3px solid var(--accent); outline-offset: 2px;
}

/* ================= ABOUT PORTRAIT =================
   The About column held the badge logo for the whole build, standing in for a
   headshot that never arrived. Jayson's photo replaced it 2026-08-22.

   It inherits `.portrait` from the vault, which is `border-radius` plus
   `overflow: hidden` for a bare image. A caption now sits under the image, so
   the clipping has to be undone here and the radius moved onto the img itself,
   or the caption gets cropped by its own container. */
.portrait--photo {
  margin: 0;
  overflow: visible;
  display: flex; flex-direction: column; align-items: center; gap: .85rem;
}
.portrait--photo img {
  display: block; width: min(24rem, 100%); height: auto;
  border-radius: 10px;
  border: 3px solid var(--accent);
  box-shadow: 0 18px 40px rgb(0 0 0 / .45);
}
.portrait__cap {
  display: flex; flex-direction: column; gap: .1rem;
  text-align: center; line-height: 1.35;
}
/* --accent-on-night, not --accent: the same gold, but the variable that is
   documented as safe AS TEXT on the navy ground (10.8:1). */
.portrait__cap b { color: var(--accent-on-night); font-size: 1.05rem; font-weight: 800; }
.portrait__cap span { color: var(--on-night-2); font-size: .86rem; }

/* ================= TRUST CAROUSEL =================
   Six facts in a five-column grid meant the sixth always dropped to a row of
   its own. Rather than cutting a fact or shrinking the type, the strip is now a
   scroll-snap carousel: one card featured, the neighbours peeking either side,
   and a Next button that stays put while the cards move past it.

   Scroll-snap rather than a transform track, for three reasons: swipe works on
   a phone with no JS at all, the browser owns the physics, and every card stays
   in the DOM and in the tab order so nothing is hidden from a crawler or a
   keyboard. With JS off you lose the Next button and the dimming, and keep a
   plain scrollable strip holding all six facts. */
/* Capped, not full width. A carousel has no neighbour to the left of its first
   card or the right of its last, and across a 1024px track that shows up as a
   350px hole beside the featured card, which reads as a broken layout rather
   than as the start of a list. At 44rem the module is a deliberate object and
   the end states look like ends. */
.tbar__stage {
  display: flex; flex-direction: column; gap: .95rem;
  container-type: inline-size;
  max-width: 44rem; margin-inline: auto; width: 100%;
}

.tbar__row {
  /* The feature width drives the peek: whatever the card does not use is what
     shows of its neighbours. Sizing it in vw was a guess that measured 18px of
     peek on a 390px phone -- a sliver, because vw counts the page margins the
     strip never gets. 100cqi is the track's OWN width, so this reserves exactly
     2.5rem of neighbour on each side at any width, and the 20rem cap stops the
     card stretching into a banner on a desktop.
     The vw line above is the fallback for browsers without container queries;
     they simply keep the first declaration. */
  --tbar-w: min(22rem, 58vw);
  --tbar-w: min(22rem, calc(100cqi - 5rem));
  display: flex;
  gap: .8rem;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  /* Lets the first and last card reach the centre of the track. Without this
     they snap to the edges and the carousel looks broken at both ends. */
  padding-inline: calc(50% - (var(--tbar-w) / 2));
  /* overflow-x:auto computes overflow-y to auto too, so the active card's ring
     needs room or it triggers a stray vertical scrollbar. */
  padding-block: .4rem;
  scrollbar-width: none;
}
.tbar__row::-webkit-scrollbar { display: none; }
.tbar__row:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; border-radius: 12px; }

.tbar__item {
  flex: 0 0 var(--tbar-w);
  scroll-snap-align: center;
  align-items: center;
  justify-content: center;
  /* Centred, and this is load-bearing rather than taste. Left-aligned text put
     every card's words against its left edge, so the card peeking on the LEFT
     showed only its blank tail while the one on the right showed its heading.
     The strip looked like it had a neighbour on one side only, which is the
     opposite of the point. Centring means both peeks show words. */
  text-align: center;
  min-height: 5rem;
  padding: .85rem 1rem;
  /* The same reason the peeks need a surface of their own: an off-centre card
     with its text out of the slice is invisible without one, and the carousel
     reads as if it has run out of cards. */
  background: rgb(255 255 255 / .035);
  border: 1px solid var(--line-night);
  border-radius: 10px;
  /* Held at .8, not lower. These cards carry real text, and at .7 the muted
     sub-line drops under 4.5:1 against the strip's own background. */
  opacity: .8;
  transition: opacity .2s ease, background .2s ease, box-shadow .2s ease;
}
.tbar__item[data-active] {
  opacity: 1;
  background: rgb(255 255 255 / .07);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 60%, transparent);
}
@media (prefers-reduced-motion: reduce) {
  .tbar__row { scroll-behavior: auto; }
  .tbar__item { transition: none; }
}

/* The rating link is a card in its own right now, so its stand-alone boxing
   would double up. It keeps the accent border and tint, which are the things
   that tell the client it is a link, and the card supplies the geometry.
   Restated here, after .tbar__item[data-active], so the gold survives being
   the featured card instead of being repainted neutral. */
.tbar__item--rating { padding: .85rem 1rem; margin: 0; border-radius: 10px; }
.tbar__item--rating,
.tbar__item--rating[data-active] { border-color: var(--accent); }
.tbar__item--rating[data-active] { background: rgb(255 201 40 / .18); }
.tbar__item--rating:hover,
.tbar__item--rating[data-active]:hover { background: rgb(255 201 40 / .26); }

.tbar__ctrl { display: flex; align-items: center; justify-content: center; gap: .9rem; }
.tbar__count {
  margin: 0; font-family: var(--mono); font-size: .72rem;
  letter-spacing: .12em; text-transform: uppercase; color: var(--on-night-2);
}
.tbar__next {
  display: inline-flex; align-items: center; gap: .45rem;
  min-height: 44px; padding: .5rem 1.15rem;
  background: transparent; border: 2px solid var(--accent); border-radius: 999px;
  color: var(--accent-on-night);
  font-family: var(--display); font-weight: 800; font-size: .85rem;
  letter-spacing: .06em; text-transform: uppercase; cursor: pointer;
  transition: background .16s ease;
}
.tbar__next:hover { background: rgb(255 201 40 / .16); }
.tbar__next:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; }

/* ================= ABOUT BIO =================
   Five paragraphs where the vault template expects one lead plus a bullet
   list. `.section--night` sets `color: var(--on-night)`, so a bare <p> here
   inherits bright white and is perfectly readable -- the trap that bit the
   reviews button does not apply. It is toned to --on-night-2 anyway, because
   that is what every other run of body copy on a navy ground uses, and the
   first paragraph stays bright since it carries the licences. */
.bio p {
  margin: .9rem 0 0;
  max-width: var(--measure-body);
  color: var(--on-night-2);
  font-size: 1rem;
  line-height: 1.65;
}
.bio p:first-child {
  margin-top: 1.1rem;
  color: var(--on-night);
  font-size: 1.08rem;
}
