/* ===== THEME TOKENS — GENERATED by tools/new-site.py from brand.js =====
   Edit the palettes in brand.js, not here. Anything hand-written between
   these two markers is overwritten on the next stamp.

   Light is the base. Dark arrives either from the system preference or
   from an explicit choice recorded on <html data-theme>. The media query
   is scoped :not([data-theme="light"]) so a visitor who has deliberately
   chosen light is not overridden by their operating system. */
:root {
  color-scheme: light;
  --c-ink: 247 249 251;   /* #F7F9FB */
  --c-paper: 14 17 22;   /* #0E1116 */
  --c-line: 192 201 211;   /* #C0C9D3 */
  --c-mute: 99 115 131;   /* #637383 */
  --c-accent: 23 120 184;   /* #1778B8 */
  --c-accent-dk: 19 100 153;   /* #136499 */
  --c-accent-lt: 23 128 118;   /* #178076 */
  --c-heat: 203 69 18;   /* #CB4512 */
  --c-surface: 255 255 255;   /* #FFFFFF */
  --c-on-accent: 247 249 251;   /* #F7F9FB */
  --c-on-heat: 247 249 251;   /* #F7F9FB */
}

/* An inverted island: a section deliberately grounded in the opposite
   pole from the page around it, so the page keeps its alternating rhythm
   in BOTH themes. It takes the whole opposite palette rather than a few
   borrowed tokens — `bg-accent text-ink` only stays legible while accent
   and ink come from the SAME palette, so swapping the accent family on
   its own turns every button inside the island into 2.4:1. */
.invert {
  --c-ink: 10 14 18;   /* #0A0E12 */
  --c-paper: 237 243 246;   /* #EDF3F6 */
  --c-line: 24 34 44;   /* #18222C */
  --c-mute: 135 149 163;   /* #8795A3 */
  --c-accent: 77 171 233;   /* #4DABE9 */
  --c-accent-dk: 46 157 229;   /* #2E9DE5 */
  --c-accent-lt: 117 251 238;   /* #75FBEE */
  --c-heat: 236 96 42;   /* #EC602A */
  --c-surface: 19 19 22;   /* #131316 */
  --c-on-accent: 10 14 18;   /* #0A0E12 */
  --c-on-heat: 10 14 18;   /* #0A0E12 */
  color: rgb(var(--c-paper));
}
/* An island inside an island returns to the page's own palette, so the
   alternation keeps working at any depth instead of stacking. */
.invert .invert {
  --c-ink: 247 249 251;   /* #F7F9FB */
  --c-paper: 14 17 22;   /* #0E1116 */
  --c-line: 192 201 211;   /* #C0C9D3 */
  --c-mute: 99 115 131;   /* #637383 */
  --c-accent: 23 120 184;   /* #1778B8 */
  --c-accent-dk: 19 100 153;   /* #136499 */
  --c-accent-lt: 23 128 118;   /* #178076 */
  --c-heat: 203 69 18;   /* #CB4512 */
  --c-surface: 255 255 255;   /* #FFFFFF */
  --c-on-accent: 247 249 251;   /* #F7F9FB */
  --c-on-heat: 247 249 251;   /* #F7F9FB */
  color: rgb(var(--c-paper));
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;
    --c-ink: 10 14 18;   /* #0A0E12 */
    --c-paper: 237 243 246;   /* #EDF3F6 */
    --c-line: 24 34 44;   /* #18222C */
    --c-mute: 135 149 163;   /* #8795A3 */
    --c-accent: 77 171 233;   /* #4DABE9 */
    --c-accent-dk: 46 157 229;   /* #2E9DE5 */
    --c-accent-lt: 117 251 238;   /* #75FBEE */
    --c-heat: 236 96 42;   /* #EC602A */
    --c-surface: 19 19 22;   /* #131316 */
    --c-on-accent: 10 14 18;   /* #0A0E12 */
    --c-on-heat: 10 14 18;   /* #0A0E12 */
  }
  :root:not([data-theme="light"]) .invert {
    --c-ink: 247 249 251;   /* #F7F9FB */
    --c-paper: 14 17 22;   /* #0E1116 */
    --c-line: 192 201 211;   /* #C0C9D3 */
    --c-mute: 99 115 131;   /* #637383 */
    --c-accent: 23 120 184;   /* #1778B8 */
    --c-accent-dk: 19 100 153;   /* #136499 */
    --c-accent-lt: 23 128 118;   /* #178076 */
    --c-heat: 203 69 18;   /* #CB4512 */
    --c-surface: 255 255 255;   /* #FFFFFF */
    --c-on-accent: 247 249 251;   /* #F7F9FB */
    --c-on-heat: 247 249 251;   /* #F7F9FB */
    color: rgb(var(--c-paper));
  }
  :root:not([data-theme="light"]) .invert .invert {
    --c-ink: 10 14 18;   /* #0A0E12 */
    --c-paper: 237 243 246;   /* #EDF3F6 */
    --c-line: 24 34 44;   /* #18222C */
    --c-mute: 135 149 163;   /* #8795A3 */
    --c-accent: 77 171 233;   /* #4DABE9 */
    --c-accent-dk: 46 157 229;   /* #2E9DE5 */
    --c-accent-lt: 117 251 238;   /* #75FBEE */
    --c-heat: 236 96 42;   /* #EC602A */
    --c-surface: 19 19 22;   /* #131316 */
    --c-on-accent: 10 14 18;   /* #0A0E12 */
    --c-on-heat: 10 14 18;   /* #0A0E12 */
    color: rgb(var(--c-paper));
  }
}

:root[data-theme="dark"] {
  color-scheme: dark;
  --c-ink: 10 14 18;   /* #0A0E12 */
  --c-paper: 237 243 246;   /* #EDF3F6 */
  --c-line: 24 34 44;   /* #18222C */
  --c-mute: 135 149 163;   /* #8795A3 */
  --c-accent: 77 171 233;   /* #4DABE9 */
  --c-accent-dk: 46 157 229;   /* #2E9DE5 */
  --c-accent-lt: 117 251 238;   /* #75FBEE */
  --c-heat: 236 96 42;   /* #EC602A */
  --c-surface: 19 19 22;   /* #131316 */
  --c-on-accent: 10 14 18;   /* #0A0E12 */
  --c-on-heat: 10 14 18;   /* #0A0E12 */
}
:root[data-theme="dark"] .invert {
  --c-ink: 247 249 251;   /* #F7F9FB */
  --c-paper: 14 17 22;   /* #0E1116 */
  --c-line: 192 201 211;   /* #C0C9D3 */
  --c-mute: 99 115 131;   /* #637383 */
  --c-accent: 23 120 184;   /* #1778B8 */
  --c-accent-dk: 19 100 153;   /* #136499 */
  --c-accent-lt: 23 128 118;   /* #178076 */
  --c-heat: 203 69 18;   /* #CB4512 */
  --c-surface: 255 255 255;   /* #FFFFFF */
  --c-on-accent: 247 249 251;   /* #F7F9FB */
  --c-on-heat: 247 249 251;   /* #F7F9FB */
  color: rgb(var(--c-paper));
}
:root[data-theme="dark"] .invert .invert {
  --c-ink: 10 14 18;   /* #0A0E12 */
  --c-paper: 237 243 246;   /* #EDF3F6 */
  --c-line: 24 34 44;   /* #18222C */
  --c-mute: 135 149 163;   /* #8795A3 */
  --c-accent: 77 171 233;   /* #4DABE9 */
  --c-accent-dk: 46 157 229;   /* #2E9DE5 */
  --c-accent-lt: 117 251 238;   /* #75FBEE */
  --c-heat: 236 96 42;   /* #EC602A */
  --c-surface: 19 19 22;   /* #131316 */
  --c-on-accent: 10 14 18;   /* #0A0E12 */
  --c-on-heat: 10 14 18;   /* #0A0E12 */
  color: rgb(var(--c-paper));
}

/* Permanently dark, in either theme. The hero, the photo bands and the
   lightbox carry a dark scrim burned into the artwork, so their text has
   to stay light whatever the rest of the page is doing. Re-declaring the
   tokens inside the scope means that markup needs no per-theme variant. */
.on-dark {
  color-scheme: dark;
  --c-ink: 10 14 18;   /* #0A0E12 */
  --c-paper: 237 243 246;   /* #EDF3F6 */
  --c-line: 24 34 44;   /* #18222C */
  --c-mute: 135 149 163;   /* #8795A3 */
  --c-accent: 77 171 233;   /* #4DABE9 */
  --c-accent-dk: 46 157 229;   /* #2E9DE5 */
  --c-accent-lt: 117 251 238;   /* #75FBEE */
  --c-heat: 236 96 42;   /* #EC602A */
  --c-surface: 19 19 22;   /* #131316 */
  --c-on-accent: 10 14 18;   /* #0A0E12 */
  --c-on-heat: 10 14 18;   /* #0A0E12 */
  /* `color` inherits its RESOLVED value, so redefining the variables
     here is not enough on its own: text with no explicit colour class
     would keep the value it inherited from <body>. */
  color: rgb(var(--c-paper));
}
/* ===== end theme tokens ===== */

/* ===== Invoice state colours =====
   Hand-written, NOT part of the generated block above: an invoice being overdue
   is a state, not branding, so these are not a client's to choose. They still
   have to theme — the dark set is ~2.7:1 on a white page — so each one is the
   lightest shade of its hue that clears 4.5:1 against its own tinted badge. */
:root { --c-due: 201 29 35; --c-paid: 35 121 55; --c-overdue: 167 71 19; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) { --c-due: 255 106 111; --c-paid: 95 207 126; --c-overdue: 255 154 92; }
}
:root[data-theme="dark"] { --c-due: 255 106 111; --c-paid: 95 207 126; --c-overdue: 255 154 92; }
.on-dark { --c-due: 255 106 111; --c-paid: 95 207 126; --c-overdue: 255 154 92; }


/* Badwound Climate Solutions LLC — shared styles for all pages */

html { scroll-behavior: smooth; }
body {
  font-family: 'Public Sans', system-ui, sans-serif;
  background: rgb(var(--c-ink));
  color: rgb(var(--c-paper));
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
/* Safety net: nothing may make the page scroll sideways. On phones a single
   over-wide element makes Safari zoom the whole page out to fit it, which looks
   like the site "shrank". No position:sticky is used, so this is safe. */
html, body { max-width: 100%; overflow-x: hidden; }
/* Archivo Black is a wide black grotesque where the previous face was a
   condensed one. It ships weight 400 only, so hierarchy comes from size, case
   and spacing — never a heavier weight. Tighter tracking stops the wide
   letterforms sprawling at display sizes. */
.display { font-family: 'Archivo Black', Impact, sans-serif; letter-spacing: -0.02em; }

/* Hero headline sizing. Uses clamp(), never raw vw. Rotating landscape→portrait
   on iOS can leave vw computed against the OLD (wider) viewport, so unbounded
   vw text stays landscape-sized in portrait and drags the page wider than the
   screen. The rem ceiling keeps it contained even if vw goes stale. */
.hero-h1     { font-size: clamp(2.05rem, 7.6vw, 5.5rem); }
.hero-script { font-size: clamp(1.6rem, 5.4vw, 3.9rem); }
.err-code    { font-size: clamp(4.5rem, 19vw, 9rem); }
/* Retired: the serif italic was the source brand's signature voice. The class
   stays only until the pages stop using it, and is no longer a serif — so
   nothing falls back to a face that is no longer loaded. */
.serif-it { font-family: 'Public Sans', system-ui, sans-serif; font-style: italic; font-weight: 400; }

.grid-bg {
  background-image:
    linear-gradient(rgb(var(--c-paper) / .04) 1px, transparent 1px),
    linear-gradient(90deg, rgb(var(--c-paper) / .04) 1px, transparent 1px);
  background-size: 56px 56px;
}
.noise::after {
  content: '';
  position: absolute; inset: 0;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0.95 0 0 0 0 0.93 0 0 0 0 0.88 0 0 0 0.06 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
  pointer-events: none;
  mix-blend-mode: overlay;
  opacity: .5;
}

.reveal { opacity: 0; transform: translateY(24px); transition: opacity .9s cubic-bezier(.2,.6,.2,1), transform .9s cubic-bezier(.2,.6,.2,1); }
.reveal.in { opacity: 1; transform: none; }

.stat-num { font-feature-settings: "tnum"; }

/* min-width:0 is load-bearing. Grid/flex items default to min-width:auto, so they
   refuse to shrink below their content's intrinsic width — and iOS Safari gives
   form controls a big intrinsic width (a <select> sizes to its longest option).
   Without this the quote form pushed the whole page wider than a phone screen,
   and mobile Safari zoomed out to fit. Don't remove. */
.field { background: transparent; border: 0; border-bottom: 1px solid rgb(var(--c-paper) / .16); padding: .9rem 0; color: rgb(var(--c-paper)); width: 100%; min-width: 0; max-width: 100%; box-sizing: border-box; font-size: 1.05rem; transition: border-color .3s; }
.field:focus { outline: none; border-color: rgb(var(--c-accent)); }
.field::placeholder { color: rgb(var(--c-mute) / .85); }
/* Let the form's grid/flex tracks shrink too — the item wrapping each .field
   inherits the same min-width:auto problem. */
#quote .grid > *, #quote > * { min-width: 0; }

/* Footer nav links were ~19px tall — well under the 44px minimum for a reliable
   thumb tap. Padding gives them a proper hit area without changing how the
   footer reads (it replaces the old space-y-2 gap rather than adding to it). */
.footer-link { display: block; padding: .8rem 0; line-height: 1.35; }

/* ===== Portrait frame (about page) =====
   Same rule as the service covers: until a headshot exists the frame shows
   the slats on the dark ground rather than printing the alt text across the
   card. A real photo covers it. */
.portrait-frame {
  background-color: rgb(var(--c-surface));
  background-image: repeating-linear-gradient(180deg, rgb(var(--c-line)) 0 2px, transparent 2px 10px);
}
.portrait-frame img { color: transparent; }

/* ===== The office (about page) ===== */

.team-photo {
  position: relative; aspect-ratio: 4/5; overflow: hidden;
  border: 1px solid rgb(var(--c-line)); background: rgb(var(--c-surface));
  display: grid; place-items: center;
}
.team-photo img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: center top; }
/* Shown until a real headshot exists. Deliberate-looking initials beat a broken
   image or an empty frame, and swapping in a photo needs no other changes. */
.team-initials {
  font-family: 'Archivo Black', Impact, sans-serif; font-size: clamp(2rem, 7vw, 3.25rem);
  letter-spacing: .04em; color: rgb(var(--c-paper) / .22); user-select: none;
}
.team-member:hover .team-photo { border-color: rgb(var(--c-accent)); }
.team-photo, .team-member:hover .team-photo { transition: border-color .3s; }

.nav-solid { background: rgb(var(--c-ink) / .85); backdrop-filter: blur(12px); border-bottom: 1px solid rgb(var(--c-line)); }
.nav-link { transition: color .25s; }
.nav-link:hover { color: rgb(var(--c-accent)); }
.nav-link.active { color: rgb(var(--c-accent)); }

/* ===== Louver band =====
   Horizontal slats, the way a return-air register or a condenser fin stack
   reads. Replaces the diagonal hazard tape the source brand used: that stripe
   belongs to a construction site, this belongs to the equipment.

   Every colour here is a plain hex so new-site.py can stamp it. Do not reach
   for rgba() in this file for a brand colour — the stamper only rewrites
   #RRGGBB, so an rgba would silently keep the previous client's colour. */
.louver {
  background-image: repeating-linear-gradient(
    180deg,
    rgb(var(--c-accent)) 0 2px,
    rgb(var(--c-accent-dk)) 2px 3px,
    transparent 3px 9px
  );
}
.louver-heat {
  background-image: repeating-linear-gradient(
    180deg,
    rgb(var(--c-heat)) 0 2px,
    transparent 2px 9px
  );
}
/* One hairline crossing cool to warm — the two things this trade sells. */
.rule-duotone { height: 2px; background: linear-gradient(90deg, rgb(var(--c-accent)) 0%, rgb(var(--c-heat)) 100%); }


/* Mobile menu — fully removed when closed (display:none), so its backdrop-blur
   panel can't paint a phantom blurred strip over the page on scroll. */
#mobile-menu.closed { display: none; }

/* ===== Review ticker (runs through the site) ===== */
.ticker-wrap { overflow: hidden; position: relative; }
.ticker { display: flex; align-items: center; gap: 4rem; width: max-content; animation: ticker-scroll 55s linear infinite; padding: .85rem 0; will-change: transform; }
.ticker-wrap:hover .ticker { animation-play-state: paused; }
@keyframes ticker-scroll { to { transform: translateX(-50%); } }
.ticker .t-item { display: inline-flex; align-items: baseline; gap: .9rem; white-space: nowrap; font-size: .95rem; }
.ticker .t-stars { color: rgb(var(--c-paper)); letter-spacing: .15em; font-size: .8rem; }
.ticker .t-name { text-transform: uppercase; letter-spacing: .15em; font-size: .7rem; font-weight: 700; opacity: .8; }

/* Load-bearing: without the .reveal fallback here, every revealed section stays
   at opacity 0 for anyone browsing with reduced motion — i.e. a blank page. */
@media (prefers-reduced-motion: reduce) {
  .ticker { animation: none; }
  .reveal { opacity: 1; transform: none; }
}

/* ===== Review cards ===== */
.review-card { background: rgb(var(--c-surface)); border: 1px solid rgb(var(--c-line)); padding: 2rem; display: flex; flex-direction: column; gap: 1rem; transition: border-color .3s, transform .3s; }
.review-card:hover { border-color: rgb(var(--c-accent)); transform: translateY(-4px); }
.review-card .stars { color: rgb(var(--c-accent)); letter-spacing: .2em; font-size: .95rem; }

/* ===== Full gallery (masonry via columns) ===== */
.masonry { columns: 2; column-gap: 1rem; }
@media (min-width: 768px) { .masonry { columns: 3; } }
@media (min-width: 1200px) { .masonry { columns: 4; } }
.masonry .g-item { break-inside: avoid; margin-bottom: 1rem; display: block; width: 100%; cursor: zoom-in; overflow: hidden; position: relative; background: rgb(var(--c-surface)); }
/* No saturate() filter here on purpose. The source build's photos were bright
   daylight exteriors that could take a 10% desaturation as "styling"; a client
   shooting dim mechanical rooms and night call-outs just gets grey, and reads
   it as the site having wrecked their colours. Show the photograph. */
.masonry .g-item img { width: 100%; height: auto; display: block; transition: transform .6s cubic-bezier(.2,.6,.2,1); }
.masonry .g-item:hover img { transform: scale(1.03); }

/* ===== Hero photo background ===== */
.hero-bg {
  background-image: url('../photos/site/hero.jpg');
  background-size: cover;
  background-position: center 62%;
}
/* Two jobs, and they pull against each other: the headline needs a dark ground
   on the LEFT, and the photograph needs to be visible on the RIGHT. The first
   gradient is steep across the horizontal so the left stays legible while the
   right opens up; the second only seats the hero into the page below. The
   earlier values (.96 -> .3 across, .3/.38 down) buried the photo entirely. */
.hero-scrim {
  background:
    linear-gradient(100deg, rgba(11,11,12,.94) 0%, rgba(11,11,12,.66) 45%, rgba(11,11,12,.12) 100%),
    linear-gradient(180deg, rgba(11,11,12,.22) 0%, rgba(11,11,12,.26) 60%, rgb(var(--c-ink) / .93) 94%, rgb(var(--c-ink)) 100%);
}

/* ===== Full-bleed photo bands ===== */
.band { position: relative; background-size: cover; background-position: center; overflow: hidden; }
@media (min-width: 1024px) { .band-fixed { background-attachment: fixed; } }
.band .band-scrim { position: absolute; inset: 0; background: linear-gradient(180deg, rgba(11,11,12,.82) 0%, rgba(11,11,12,.45) 50%, rgba(11,11,12,.88) 100%); }
.band > .band-inner { position: relative; z-index: 1; }

/* ===== Service card photo headers =====
   The box carries its own empty state. Until a client sends photos — or if one
   is ever missing — the slats show through on the dark ground and the card
   still reads as finished, the same way the team grid falls back to initials
   rather than a broken image. The alt text is hidden in that state because a
   broken <img> otherwise prints its alt across the card in plain text. */
.svc-img {
  position: relative; aspect-ratio: 4/3; overflow: hidden;
  background-color: rgb(var(--c-surface));
  background-image: repeating-linear-gradient(180deg, rgb(var(--c-line)) 0 2px, transparent 2px 10px);
}
/* A missing photo still leaves the browser's own broken-image glyph in the
   corner. Scaling the <img> to nothing hides it while the slatted box below
   carries the card; a real photo overrides both rules. */
.svc-img img { color: transparent; }
.svc-img img:not([src]), .svc-img img[src=""] { display: none; }
.svc-img img::before { content: ''; display: block; width: 100%; height: 100%; background: transparent; }

/* Phone in landscape: the screen is only ~390px tall, but a 2-column service
   card put a 285px image on top of its text — one card ended up taller than the
   whole screen. Go to a single row of four short cards so they actually fit. */
/* Class is doubled on purpose: the Tailwind Play CDN injects its styles after
   this file, so an equal-specificity rule would lose to md:grid-cols-2. */
@media (orientation: landscape) and (max-height: 560px) {
  .svc-grid.svc-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: .65rem; }
  .svc-grid .svc-img { aspect-ratio: 5 / 4; }
  .svc-grid h3 { font-size: 1.35rem; }
  .svc-grid ul { font-size: .74rem; line-height: 1.35; }
  .svc-grid .p-6 { padding: .8rem; }
}
.svc-img img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; transition: transform .8s cubic-bezier(.2,.6,.2,1); }
.group:hover .svc-img img { transform: scale(1.05); }
/* Starts lower and lands lighter than it used to. It exists to seat the photo
   against the card below, not to darken the work in it. */
.svc-img::after { content: ''; position: absolute; inset: 0; background: linear-gradient(180deg, transparent 65%, rgb(var(--c-ink) / .38) 100%); }

/* ===== Process steps =====
   Used for "how a job goes". Numbering is honest here because the content is
   an actual sequence — unlike service lines, which are a set. The slat rule
   above each step ties the sequence to the louver motif. */
.step { position: relative; padding-top: 1.35rem; }
.step::before {
  content: ''; position: absolute; top: 0; left: 0; right: 0; height: 6px;
  background-image: repeating-linear-gradient(180deg, rgb(var(--c-accent)) 0 2px, transparent 2px 6px);
}
.step .step-num {
  font-family: 'Archivo Black', Impact, sans-serif; font-size: 1.05rem;
  letter-spacing: .12em; line-height: 1; color: rgb(var(--c-accent));
  font-feature-settings: "tnum";
}

/* ===== FAQ accordion ===== */
.faq details { border-bottom: 1px solid rgb(var(--c-line)); }
.faq summary { cursor: pointer; list-style: none; display: flex; justify-content: space-between; align-items: center; gap: 1.5rem; padding: 1.4rem 0; }
.faq summary::-webkit-details-marker { display: none; }
.faq .plus { flex: none; color: rgb(var(--c-accent)); font-size: 1.6rem; line-height: 1; transition: transform .3s; }
.faq details[open] .plus { transform: rotate(45deg); }
.faq .faq-a { padding: 0 0 1.6rem; color: rgb(var(--c-paper) / .72); max-width: 46rem; line-height: 1.7; }

/* ===== Sticky mobile call bar ===== */
.callbar { position: fixed; bottom: 0; left: 0; right: 0; z-index: 60; display: grid; grid-template-columns: 1.2fr 1fr; box-shadow: 0 -6px 24px rgba(0,0,0,.45); }
.callbar a { display: flex; align-items: center; justify-content: center; gap: .5rem; padding: 1rem .5rem; font-family: 'Archivo Black', Impact, sans-serif; font-size: 1.05rem; letter-spacing: .02em; }
@media (min-width: 768px) { .callbar { display: none; } }
@media (max-width: 767.98px) { body { padding-bottom: 64px; } }

/* ===== Shared section helpers ===== */
.eyebrow { display: inline-flex; align-items: center; gap: .75rem; }
.eyebrow .tick { width: 2.5rem; height: 1px; background: rgb(var(--c-accent)); flex: none; }
.eyebrow span:last-child { font-size: .75rem; letter-spacing: .25em; text-transform: uppercase; color: rgb(var(--c-mute)); }

.rate-row { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; border-bottom: 1px solid rgb(var(--c-line)); padding: 1.15rem 0; }
.rate-row .rr-fig { font-family: 'Archivo Black', Impact, sans-serif; font-feature-settings: "tnum"; color: rgb(var(--c-paper)); }

/* ===== Pay / invoice ===== */
.pay-card { background: rgb(var(--c-surface)); border: 1px solid rgb(var(--c-line)); }
.invoice-line { display: flex; justify-content: space-between; gap: 1rem; padding: .7rem 0; border-bottom: 1px solid rgb(var(--c-line)); }
.invoice-line:last-child { border-bottom: 0; }
.invoice-line .il-k { color: rgb(var(--c-mute)); font-size: .75rem; letter-spacing: .12em; text-transform: uppercase; }
.invoice-line .il-v { font-feature-settings: "tnum"; text-align: right; }
.status-badge { display: inline-flex; align-items: center; gap: .4rem; padding: .25rem .7rem; font-size: .68rem; letter-spacing: .14em; text-transform: uppercase; font-weight: 700; border-radius: 999px; }
/* Semantic, deliberately NOT the brand accent: an invoice being due is a
   state, not branding. Fixed hexes so no stamp is needed. */
.status-badge.due { background: rgb(var(--c-due) / .14); color: rgb(var(--c-due)); border: 1px solid rgb(var(--c-due) / .4); }
.status-badge.paid { background: rgb(var(--c-paid) / .14); color: rgb(var(--c-paid)); border: 1px solid rgb(var(--c-paid) / .4); }
.status-badge.overdue { background: rgb(var(--c-overdue) / .16); color: rgb(var(--c-overdue)); border: 1px solid rgb(var(--c-overdue) / .45); }
.status-badge .dot { width: .45rem; height: .45rem; border-radius: 999px; background: currentColor; }
.pay-method { display: inline-flex; align-items: center; gap: .45rem; border: 1px solid rgb(var(--c-paper) / .16); padding: .5rem .8rem; font-size: .78rem; color: rgb(var(--c-paper) / .8); }
.field-block label { display: block; font-size: .7rem; letter-spacing: .16em; text-transform: uppercase; color: rgb(var(--c-mute)); margin-bottom: .15rem; }
.lookup-error { color: rgb(var(--c-due)); }
/* Used in markup, so it needs a class rather than an arbitrary Tailwind hex. */
.text-paid { color: rgb(var(--c-paid)); }

/* ===== Mini gallery preview (home) ===== */
.mini-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: .75rem; }
@media (min-width: 768px) { .mini-grid { grid-template-columns: repeat(4, 1fr); } }
.mini-grid a { position: relative; overflow: hidden; aspect-ratio: 1; display: block; background: rgb(var(--c-surface)); }
.mini-grid img { width: 100%; height: 100%; object-fit: cover; transition: transform .6s cubic-bezier(.2,.6,.2,1); }
.mini-grid a:hover img { transform: scale(1.06); }

/* ===== Lightbox ===== */
.lightbox { position: fixed; inset: 0; z-index: 100; background: rgba(11,11,12,.96); display: none; align-items: center; justify-content: center; }
.lightbox.open { display: flex; }
.lightbox img { max-width: 92vw; max-height: 86vh; object-fit: contain; box-shadow: 0 20px 60px rgba(0,0,0,.6); }
.lightbox .lb-btn { position: absolute; background: none; border: 1px solid rgb(var(--c-paper) / .16); color: rgb(var(--c-paper)); width: 52px; height: 52px; display: grid; place-items: center; cursor: pointer; font-size: 1.4rem; transition: border-color .25s, color .25s; }
.lightbox .lb-btn:hover { border-color: rgb(var(--c-accent)); color: rgb(var(--c-accent)); }
.lightbox .lb-close { top: 1.25rem; right: 1.25rem; }
.lightbox .lb-prev { left: 1.25rem; top: 50%; transform: translateY(-50%); }
.lightbox .lb-next { right: 1.25rem; top: 50%; transform: translateY(-50%); }
.lightbox .lb-count { position: absolute; bottom: 1.5rem; left: 50%; transform: translateX(-50%); font-size: .75rem; letter-spacing: .25em; text-transform: uppercase; color: rgb(var(--c-mute)); }

/* ===== Brand lockup =====
   Shown in the nav and footer when the client has no logo file yet. It is a
   real mark, not a placeholder: an accent tile carrying the initial, set
   against the short name in the display face — the same language the editor
   chrome uses. Set business.logo in site-content.js and the image takes over
   with no other change. */
.lockup { display: inline-flex; align-items: center; gap: .6rem; min-width: 0; }
/* An image emblem used in place of the generated initial tile. Same footprint,
   so the nav lockup keeps its proportions whether a client has a logo or not. */
.lockup-badge { flex: none; width: 2.25rem; height: 2.25rem; object-fit: contain; display: block; }
.lockup-lg .lockup-badge { width: 3.25rem; height: 3.25rem; }
@media (min-width: 768px) { .lockup-nav .lockup-badge { width: 2.9rem; height: 2.9rem; } }

.lockup-mark {
  flex: none; display: grid; place-items: center;
  width: 2.5rem; height: 2.5rem;
  background: rgb(var(--c-accent)); color: rgb(var(--c-ink));
  font-family: 'Archivo Black', Impact, sans-serif; font-size: 1.5rem; line-height: 1;
  padding-top: .15rem;
  box-shadow: 0 3px 0 rgb(var(--c-accent-dk));
}
.lockup-name {
  font-family: 'Archivo Black', Impact, sans-serif;
  /* Scales down on a phone so the wordmark never runs under the call button.
     clamp(), never a raw vw font size — see the rotation note in the README.
     The ellipsis is a backstop for a business name longer than this one. */
  font-size: clamp(.95rem, 3.2vw, 1.25rem);
  letter-spacing: .04em; line-height: 1;
  text-transform: uppercase; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; min-width: 0;
}
/* On a phone the header is the mark, the call button and the menu — the
   wordmark returns from tablet up. Sizing it to fit beside the buttons only
   works for names as short as this one; hiding it works for any name. */
.lockup-nav .lockup-name { display: none; }
.lockup-nav .lockup-mark { width: 2.25rem; height: 2.25rem; font-size: 1.35rem; }
.lockup-lg .lockup-mark { width: 3.25rem; height: 3.25rem; font-size: 2rem; }
.lockup-lg .lockup-name { font-size: 1.6rem; }
@media (min-width: 768px) {
  .lockup-nav .lockup-mark { width: 3rem; height: 3rem; font-size: 1.85rem; }
  .lockup-nav .lockup-name { display: inline; font-size: 1.5rem; }
}
