@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=Source+Sans+3:wght@400;500;600&display=swap');

/* ==========================================================================
   BASE.CSS — structural layout only, shared by every base × every theme.
   Visual identity comes entirely from CSS custom properties (theme.css).
   Do not hardcode color/font/radius values in this file.
   ========================================================================== */

:root {
  --font-display: 'Inter', system-ui, sans-serif;
  --font-body: 'Inter', system-ui, sans-serif;
  --font-utility: 'Inter', system-ui, sans-serif;

  --color-bg: #FFFFFF;
  --color-surface: #F7F7F7;
  --color-text: #1A1A1A;
  --color-text-muted: #5B5B5B;
  --color-primary: #1A1A1A;
  --color-primary-text: #FFFFFF;
  --color-accent: #1A1A1A;
  --color-accent-2: #1A1A1A;
  --color-border: #E2E2E2;
  --color-warn-bg: #1A1A1A;
  --color-warn-text: #FFFFFF;

  --radius-sm: 6px;
  --radius-md: 10px;
  --radius-lg: 16px;

  --shadow-sm: 0 1px 3px rgba(0,0,0,0.08);
  --shadow-md: 0 8px 24px rgba(0,0,0,0.08);

  --border-width: 1px;
  --container-width: 1180px;
  --transition: 200ms ease;
  --letter-spacing-eyebrow: 0.08em;

  /* ---- Identity hooks -------------------------------------------------
     Everything below exists so a theme can change the SHAPE of the page,
     not just its colors. A gallery where all 24 styles are one skeleton
     under 24 palettes reads as generated, however good the palettes are;
     these are the joints each theme is expected to set for itself. ---- */

  /* Section rhythm. --section-align is the single highest-leverage one:
     center-everything is the default look of a generated page, so base
     now starts left and a theme opts into symmetry when its world is
     actually symmetrical (Deco, Athletic, Patisserie). */
  --section-align: left;
  --section-heading-margin: 0 0 40px;
  --section-heading-width: 46ch;
  --space-section: 72px;

  /* Photo slots. Never a gradient by default -- see .hero-media-placeholder.
     A theme sets --media-fill to whatever its own world would put behind a
     photo (paper, hatch, halftone, blueprint grid) and --media-ratio to
     reframe the crop. */
  --media-fill: var(--color-surface);
  --media-ratio: 4/3;
  --media-radius: var(--radius-lg);

  /* The one authored motion moment per theme, rather than the same
     translateY(-4px) on every card in every style. */
  --card-lift: none;
  --card-hover-border: var(--color-accent);
  --ease: cubic-bezier(0.22, 1, 0.36, 1);

  /* Rule weight for the drawn marks (checks, dashes, dividers) that
     replaced the Unicode glyphs base.css used to print as icons. */
  --rule-width: 1px;
}

*, *::before, *::after { box-sizing: border-box; }
html { scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; }
}

body { margin: 0; font-family: var(--font-body); color: var(--color-text); background: var(--color-bg); line-height: 1.6; -webkit-font-smoothing: antialiased; }
h1, h2, h3, h4 { font-family: var(--font-display); line-height: 1.15; margin: 0 0 0.5em; color: var(--color-text); text-wrap: balance; }
h1 { font-size: clamp(2.1rem, 4.5vw, 3.4rem); }
h2 { font-size: clamp(1.6rem, 3vw, 2.3rem); }
h3 { font-size: 1.15rem; }
/* More space above a heading than below it -- a heading belongs to what
   follows it, and the default equal margins are what make long pages read
   as an undifferentiated stack of sections. */
h2:not(:first-child), h3:not(:first-child) { margin-top: 1.6em; }
.section-heading h2, .page-header h1, .hero-content h1 { margin-top: 0; }
p { margin: 0 0 1em; text-wrap: pretty; }
a { color: var(--color-accent); }
ul { margin: 0; padding: 0; list-style: none; }
img { max-width: 100%; display: block; }
:focus-visible { outline: 3px solid var(--color-accent); outline-offset: 2px; }

.container { max-width: var(--container-width); margin: 0 auto; padding: 0 24px; }
/* The floating accent-colored uppercase kicker is the single most
   recognizable tell of a generated page. It stays (the bases' copy needs
   it) but it now reads as an editorial section marker: muted, tied to a
   drawn rule, sitting ON the baseline of the block instead of hovering in
   accent above the headline. Themes that want it loud say so themselves. */
.eyebrow {
  display: flex; align-items: center; gap: 0.7em;
  font-family: var(--font-utility); text-transform: uppercase;
  letter-spacing: var(--letter-spacing-eyebrow); font-size: 0.76rem; font-weight: 600;
  color: var(--color-text-muted); margin: 0 0 1em;
}
.eyebrow::before {
  content: ''; flex: none; width: 1.6em; height: var(--rule-width);
  background: var(--color-accent);
}
[style*="center"] .eyebrow, .hero-centered .eyebrow, .is-centered .eyebrow { justify-content: center; }

/* ---- Emergency / top banner ---- */
.emergency-banner { background: var(--color-warn-bg); color: var(--color-warn-text); font-family: var(--font-utility); }
.emergency-banner .container { display: flex; align-items: center; justify-content: center; gap: 10px; padding: 10px 24px; font-size: 0.88rem; text-align: center; flex-wrap: wrap; }
.emergency-banner a { color: var(--color-warn-text); font-weight: 700; text-decoration: underline; }

/* ---- Buttons ---- */
/* appearance:none + -webkit-appearance:none is the actual fix for the
   long-standing "shaded box with an outline" look: without it, Safari
   (desktop and especially iOS) renders its own native button chrome
   underneath/around any custom button styling -- a filled, bordered box
   that has nothing to do with focus state, loading state, or anything
   else -- it's just what a <button> looks like by default on that
   platform until you explicitly opt out. -webkit-tap-highlight-color
   removes the separate grey/blue flash box mobile browsers draw on tap. */
.btn {
  appearance: none;
  -webkit-appearance: none;
  -webkit-tap-highlight-color: transparent;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 0.75em 1.4em;
  border-radius: var(--radius-sm);
  font-family: var(--font-utility);
  font-weight: 600;
  font-size: 0.95rem;
  text-decoration: none;
  border: var(--border-width) solid transparent;
  cursor: pointer;
  transition: transform var(--transition), box-shadow var(--transition), background var(--transition), color var(--transition), opacity var(--transition);
}
.btn svg { width: 18px; height: 18px; flex-shrink: 0; }
.btn-primary { background: var(--color-primary); color: var(--color-primary-text); box-shadow: var(--shadow-sm); }
.btn-primary:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }
.btn-outline { background: transparent; color: var(--color-text); border-color: var(--color-border); }
.btn-outline:hover { border-color: var(--color-accent); color: var(--color-accent); }
.btn-lg { padding: 0.95em 1.8em; font-size: 1.02rem; }
.btn:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
.btn:disabled { cursor: not-allowed; opacity: 0.6; box-shadow: none; transform: none; }

/* Loading spinner: a real child element (<span class="btn-spinner">,
   see contact_form() in build_bases.py), shown/hidden via the `hidden`
   attribute from JS -- not a ::before pseudo-element. A real element is
   simpler to get right under an appearance:none button and easier to
   debug (it shows up in devtools like anything else). */
.btn-spinner {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
  border: 2px solid color-mix(in srgb, currentColor 35%, transparent);
  border-top-color: currentColor;
  border-radius: 50%;
  animation: lwl-btn-spin 0.6s linear infinite;
}
.btn-spinner[hidden] { display: none; }
@keyframes lwl-btn-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .btn-spinner { animation-duration: 1.5s; }
}

/* ---- Header ---- */
.site-header { position: sticky; top: 0; z-index: 50; background: var(--color-bg); border-bottom: var(--border-width) solid var(--color-border); }
.header-inner { display: flex; align-items: center; justify-content: space-between; gap: 24px; padding: 16px 24px; }
.logo { display: flex; align-items: center; gap: 10px; text-decoration: none; color: var(--color-text); font-family: var(--font-display); font-weight: 700; font-size: 1.05rem; }
.logo-icon { width: 30px; height: 30px; color: var(--color-accent); flex-shrink: 0; }
.main-nav { display: flex; align-items: center; }
.nav-list { display: flex; gap: 28px; }
.nav-list a { text-decoration: none; color: var(--color-text); font-family: var(--font-utility); font-weight: 500; font-size: 0.95rem; padding: 6px 0; border-bottom: 2px solid transparent; transition: border-color var(--transition), color var(--transition); }
.nav-list a:hover, .nav-list a[aria-current="page"] { border-color: var(--color-accent); color: var(--color-accent); }
.nav-toggle { display: none; background: none; border: none; padding: 6px; cursor: pointer; color: var(--color-text); }
.nav-toggle svg { width: 26px; height: 26px; }
.header-cta { flex-shrink: 0; }
.header-cta span { white-space: nowrap; }

@media (max-width: 860px) {
  .nav-toggle { display: inline-flex; order: 3; margin-left: 12px; }
  .logo { order: 1; }
  .header-cta { order: 2; }
  .main-nav { order: 3; }
  .nav-list { position: absolute; top: 100%; left: 0; right: 0; flex-direction: column; gap: 0; background: var(--color-bg); border-bottom: var(--border-width) solid var(--color-border); max-height: 0; overflow: hidden; transition: max-height var(--transition); }
  .main-nav.open .nav-list { max-height: 400px; }
  .nav-list a { display: block; padding: 14px 24px; border-bottom: var(--border-width) solid var(--color-border); }
  .header-cta span { display: none; }
}

/* ---- Hero (split) ---- */
.hero { padding: 64px 0; background: var(--color-surface); }
.hero-inner { display: grid; grid-template-columns: 1.1fr 0.9fr; gap: 48px; align-items: center; }
.hero-content h1 { margin-bottom: 0.4em; }
.hero-lede { font-size: 1.1rem; color: var(--color-text-muted); max-width: 46ch; }
.hero-actions { display: flex; flex-wrap: wrap; gap: 14px; margin: 1.5em 0; }
.hero-trust-list { display: flex; flex-wrap: wrap; gap: 20px; font-family: var(--font-utility); font-size: 0.9rem; color: var(--color-text-muted); }
.hero-trust-list li { display: flex; align-items: baseline; gap: 9px; }
/* A drawn tick, not a 6px accent dot and not a "✓" character. Two borders
   on a rotated box: it inherits the theme's own --rule-width, so it gets
   heavier in Neubrutalism and hairline-fine in Swiss without a per-theme
   override, which a glyph could never do. */
.hero-trust-list li::before {
  content: ''; flex: none; width: 0.42em; height: 0.72em; margin-top: 0.1em;
  border: solid var(--color-accent); border-width: 0 var(--rule-width) var(--rule-width) 0;
  transform: rotate(42deg);
}
/* Photo slots.
   These used to be `linear-gradient(135deg, accent, surface)` at 0.85
   opacity, repeated on hero, split and team -- the same two-stop diagonal
   mood-blur that marks a page as machine-made, and it was the one visual
   every single theme had in common. They are now what they actually are:
   an empty frame waiting for the owner's photo. Flat field, hairline,
   drawn corner crop marks, quiet label. Themes fill --media-fill with
   something from their own world instead of a rainbow ramp. */
.hero-media-placeholder,
.split-media-placeholder,
.team-photo-placeholder {
  position: relative;
  aspect-ratio: var(--media-ratio);
  border-radius: var(--media-radius);
  background: var(--media-fill);
  border: var(--border-width) solid var(--color-border);
  display: flex; align-items: center; justify-content: center;
  overflow: hidden;
}
/* Crop marks -- the corner registration ticks off a real photo comp. Two
   pseudo-elements, four corners, drawn in the border color so they read as
   part of the frame rather than as content. */
.hero-media-placeholder::before, .hero-media-placeholder::after,
.split-media-placeholder::before, .split-media-placeholder::after {
  content: ''; position: absolute; width: 18px; height: 18px;
  border: var(--rule-width) solid var(--color-text-muted); opacity: 0.45;
}
.hero-media-placeholder::before, .split-media-placeholder::before {
  top: 14px; left: 14px; border-width: var(--rule-width) 0 0 var(--rule-width);
}
.hero-media-placeholder::after, .split-media-placeholder::after {
  bottom: 14px; right: 14px; border-width: 0 var(--rule-width) var(--rule-width) 0;
}
@media (max-width: 860px) { .hero-inner { grid-template-columns: 1fr; } .hero-media { order: -1; } }

/* ---- Hero (centered, for structural variety across bases) ---- */
.hero-centered { padding: 88px 0 72px; background: var(--color-surface); text-align: center; }
.hero-centered .container { max-width: 760px; }
.hero-centered .hero-lede { margin: 0 auto; }
.hero-centered .hero-actions { justify-content: center; }
.hero-centered .hero-trust-list { justify-content: center; }

/* ---- Sections / headings ----
   Was: max-width 640px, margin auto, text-align center -- on every section
   of every page of every theme. Five identical centered heading blocks
   stacked down a page is the rhythm of a generated site. Base now sets
   left, and a theme that is genuinely symmetrical re-centers itself by
   setting --section-align: center. */
.section { padding: var(--space-section) 0; }
.section-heading {
  max-width: var(--section-heading-width);
  margin: var(--section-heading-margin);
  text-align: var(--section-align);
}
.section-lede { color: var(--color-text-muted); }
.section-cta { text-align: var(--section-align); margin-top: 32px; }
.section-alt { background: var(--color-surface); }

/* ---- Grids & cards ---- */
.grid { display: grid; gap: 28px; }
/* Grid items default to min-width:auto and so refuse to shrink below their
   widest word; every narrow-screen overflow on these templates traced back
   to that. Opting every direct child out of it once, here, is what makes
   the single-column breakpoints below actually hold. */
.grid > *, .pricing-table > *, .team-grid > *, .process-steps > * { min-width: 0; }
.grid-2 { grid-template-columns: repeat(2, 1fr); }
.grid-3 { grid-template-columns: repeat(3, 1fr); }
.grid-4 { grid-template-columns: repeat(4, 1fr); }
@media (max-width: 860px) { .grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; } }

.card {
  background: var(--color-bg); border: var(--border-width) solid var(--color-border);
  border-radius: var(--radius-lg); padding: 28px; box-shadow: var(--shadow-sm);
  transition: transform var(--transition), box-shadow var(--transition), border-color var(--transition), background var(--transition);
}
/* Hover was translateY(-4px) + shadow-md, identically, on all 24 themes.
   The lift is now opt-in per theme via --card-lift; what every theme gets
   for free is the quieter, more honest signal: the border takes the accent. */
.card:hover { transform: var(--card-lift); border-color: var(--card-hover-border); }

/* The 46px tinted rounded square with an accent glyph in it is the
   canonical generated feature-card. Default is now the drawn icon alone at
   its real size, sitting on the text's own left edge; themes that want the
   plate back add it deliberately. */
.card-icon {
  display: flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; color: var(--color-accent); margin-bottom: 18px;
}
.card-icon svg { width: 28px; height: 28px; stroke-width: 1.5; }
.card-link { display: inline-block; margin-top: 8px; font-family: var(--font-utility); font-weight: 600; text-decoration: none; color: var(--color-accent); text-underline-offset: 4px; }
.card-link:hover { text-decoration: underline; }

/* ---- Badges ---- */
.badge-row { display: flex; flex-wrap: wrap; justify-content: var(--section-align, left); gap: 10px; }
.badge { display: inline-flex; align-items: center; gap: 6px; padding: 8px 14px; border-radius: 999px; background: var(--color-surface); border: var(--border-width) solid var(--color-border); font-family: var(--font-utility); font-size: 0.85rem; font-weight: 600; color: var(--color-text); }
.badge svg { width: 16px; height: 16px; color: var(--color-accent); }

/* ---- Process steps ---- */
.process-steps { counter-reset: step; display: grid; grid-template-columns: repeat(4, 1fr); gap: 24px; }
/* The filled accent disc with a number in it is the stock "how it works"
   badge. These steps are genuinely sequential so the number stays, but it
   now sits above a rule as a set numeral -- the step reads as a column in a
   sequence rather than as another decorated chip. */
.process-step { position: relative; padding-top: 18px; border-top: var(--rule-width) solid var(--color-border); }
.process-step::before {
  counter-increment: step; content: counter(step, decimal-leading-zero);
  display: block; font-family: var(--font-display); font-size: 0.9rem; font-weight: 700;
  letter-spacing: 0.08em; color: var(--color-accent); margin-bottom: 12px;
}
@media (max-width: 860px) { .process-steps { grid-template-columns: 1fr 1fr; } }
@media (max-width: 560px) { .process-steps { grid-template-columns: 1fr; } }

/* ---- Before / after ---- */
.before-after { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; }
.before-after-panel { border-radius: var(--radius-lg); padding: 28px; border: var(--border-width) solid var(--color-border); }
.before-after-panel.is-before { background: var(--color-surface); }
.before-after-panel.is-after { background: var(--color-bg); border-color: var(--color-accent); }
.before-after-label { font-family: var(--font-utility); font-weight: 700; text-transform: uppercase; font-size: 0.78rem; letter-spacing: 0.08em; color: var(--color-text-muted); margin-bottom: 10px; }
@media (max-width: 720px) { .before-after { grid-template-columns: 1fr; } }

/* ---- Pricing table ---- */
.pricing-table { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; align-items: stretch; }
.pricing-tier { border: var(--border-width) solid var(--color-border); border-radius: var(--radius-lg); padding: 28px; display: flex; flex-direction: column; gap: 14px; }
.pricing-tier.is-featured { border-color: var(--color-accent); box-shadow: var(--shadow-md); }
.pricing-tier-name { font-family: var(--font-utility); font-weight: 700; text-transform: uppercase; font-size: 0.8rem; letter-spacing: 0.06em; color: var(--color-text-muted); }
.pricing-tier-price { font-family: var(--font-display); font-size: 2rem; font-weight: 700; }
.pricing-tier-price span { font-size: 0.95rem; font-weight: 400; color: var(--color-text-muted); }
.pricing-tier-features { display: flex; flex-direction: column; gap: 8px; font-size: 0.92rem; flex-grow: 1; }
.pricing-tier-features li { display: flex; gap: 10px; align-items: baseline; }
/* Same drawn tick as .hero-trust-list, for the same reason: "✓" is a text
   character borrowed as an icon, rendered by whatever font happens to
   contain it, and it looked pasted-in on every serif theme. */
.pricing-tier-features li::before {
  content: ''; flex: none; width: 0.4em; height: 0.68em; margin-top: 0.12em;
  border: solid var(--color-accent); border-width: 0 var(--rule-width) var(--rule-width) 0;
  transform: rotate(42deg);
}
@media (max-width: 860px) { .pricing-table { grid-template-columns: 1fr; } }

/* ---- Team grid ---- */
.team-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 24px; }
.team-card { text-align: center; }
/* Framing (fill, radius, border, crop marks) is shared with the hero and
   split slots up in the Hero section -- only what is specific to a portrait
   in a grid belongs here. This rule used to restate the old 135deg gradient
   and, sitting later in the file, silently overrode that shared treatment. */
.team-photo-placeholder { aspect-ratio: 1; margin-bottom: 14px; }
.placeholder-label { font-family: var(--font-utility); font-weight: 700; font-size: 0.8rem; letter-spacing: 0.03em; text-align: center; color: var(--color-primary-text); background: rgba(0,0,0,0.35); padding: 6px 12px; border-radius: var(--radius-sm); }
.team-card h3 { margin-bottom: 2px; }
.team-role { font-family: var(--font-utility); font-size: 0.85rem; color: var(--color-text-muted); }
@media (max-width: 860px) { .team-grid { grid-template-columns: 1fr 1fr; } }

/* ---- FAQ ---- */
.faq-list { display: flex; flex-direction: column; gap: 12px; max-width: 760px; margin: 0 auto; }
.faq-item { border: var(--border-width) solid var(--color-border); border-radius: var(--radius-md); padding: 6px 20px; background: var(--color-bg); }
.faq-item summary { cursor: pointer; font-family: var(--font-utility); font-weight: 600; padding: 14px 0; list-style: none; display: flex; justify-content: space-between; gap: 12px; align-items: center; }
.faq-item summary::-webkit-details-marker { display: none; }
/* Drawn +/− rather than the "+" and "–" characters: two bars painted with
   gradients so the vertical one can rotate away on open. The old glyph pair
   also never optically aligned -- "+" and "–" have different side bearings
   in most faces, so the marker visibly shifted when a question expanded. */
.faq-item summary::after {
  content: ''; flex: none; width: 13px; height: 13px; position: relative;
  background:
    linear-gradient(var(--color-accent), var(--color-accent)) center/100% var(--rule-width) no-repeat,
    linear-gradient(var(--color-accent), var(--color-accent)) center/var(--rule-width) 100% no-repeat;
  transition: transform var(--transition);
}
.faq-item[open] summary::after {
  background: linear-gradient(var(--color-accent), var(--color-accent)) center/100% var(--rule-width) no-repeat;
  transform: rotate(180deg);
}
.faq-item p { color: var(--color-text-muted); padding-bottom: 16px; margin: 0; }

/* ---- Split feature row (about-story style, reused generically) ---- */
.split-row { display: grid; grid-template-columns: 1fr 1fr; gap: 48px; align-items: center; }
.split-row.is-reverse { direction: rtl; }
.split-row.is-reverse > * { direction: ltr; }
/* Framed with the hero and team slots up in the Hero section; this rule
   restated the old gradient and, being later in the file, won. */
.split-text p { color: var(--color-text-muted); }
@media (max-width: 860px) { .split-row, .split-row.is-reverse { grid-template-columns: 1fr; direction: ltr; } }

/* ---- Stats band ---- */
.stats-band { background: var(--color-primary); color: var(--color-primary-text); padding: 48px 0; }
.stats-grid { text-align: center; }
.stat { display: flex; flex-direction: column; gap: 6px; }
.stat-number { font-family: var(--font-display); font-size: clamp(1.9rem, 3.4vw, 2.6rem); font-weight: 700; line-height: 1; font-variant-numeric: tabular-nums; letter-spacing: -0.02em; }
/* opacity:0.85 on a label over a colored band is the lazy way to make
   secondary text -- it fades toward the band and loses contrast. Full
   opacity, and the hierarchy comes from size and letterspacing instead. */
.stat-label { font-family: var(--font-utility); font-size: 0.82rem; letter-spacing: 0.04em; text-wrap: balance; }

/* ---- Reviews ---- */
/* The 4px accent border-left was doing the work a testimonial should do
   with typography. It is now a real pull-quote: the quote is set larger and
   looser than body copy, and the attribution hangs below a hairline instead
   of being flagged by a colored bar. */
.review-card {
  background: var(--color-surface); border-radius: var(--radius-lg);
  padding: 30px 28px; margin: 0;
  display: flex; flex-direction: column;
}
.review-card p {
  font-family: var(--font-display);
  font-size: clamp(1.05rem, 1.6vw, 1.22rem);
  line-height: 1.45; margin: 0 0 auto; padding-bottom: 22px;
  color: var(--color-text);
}
.review-card footer {
  font-family: var(--font-utility); font-size: 0.85rem; color: var(--color-text-muted);
  display: flex; flex-direction: column; gap: 2px;
  padding-top: 16px; border-top: var(--rule-width) solid var(--color-border);
}
.review-card cite { font-style: normal; font-weight: 600; color: var(--color-text); }
.review-meta { opacity: 0.8; }

/* ---- CTA band ---- */
.cta-band { background: var(--color-surface); }
.cta-band-inner { text-align: center; max-width: 620px; margin: 0 auto; }
.cta-band-inner p { color: var(--color-text-muted); margin-bottom: 1.5em; }

/* ---- Footer ---- */
.site-footer { background: var(--color-primary); color: var(--color-primary-text); padding-top: 56px; }
.footer-inner { display: grid; grid-template-columns: 1.2fr 2fr; gap: 40px; padding-bottom: 40px; }
/* Footer text used to be faded with opacity (0.75 / 0.85 / 0.7), which is
   what pushed several themes under 4.5:1 against their own footer color.
   Hierarchy here comes from size and weight instead; the color pair is
   whatever the theme already committed to for primary/primary-text. */
.footer-brand p { color: var(--color-primary-text); max-width: 34ch; font-size: 0.95rem; }
.footer-brand .logo { color: var(--color-primary-text); }
.footer-brand .logo-icon { color: var(--color-primary-text); }
/* min-width:0 is load-bearing on every one of these columns. A grid item's
   default min-width is auto, meaning it refuses to shrink below the widest
   unbreakable word it contains -- so two columns of long link labels
   ("Emergency Plumbing", "Water Heater Repair") pushed the footer, and with
   it the whole page, past the viewport on a 375px screen. It was worst in
   the monospace theme, where the same strings are widest. */
.footer-nav { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; min-width: 0; }
.footer-col { min-width: 0; }
.footer-col a { overflow-wrap: break-word; }
.footer-col h4 { font-family: var(--font-utility); font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.1em; margin-bottom: 14px; color: var(--color-primary-text); }
.footer-col li { margin-bottom: 8px; font-size: 0.92rem; }
.footer-col a { color: var(--color-primary-text); text-decoration: none; }
.footer-col a:hover { text-decoration: underline; text-underline-offset: 3px; }
.footer-bottom { border-top: var(--border-width) solid rgba(255,255,255,0.15); padding: 20px 24px; text-align: center; font-size: 0.82rem; }
@media (max-width: 720px) { .footer-inner { grid-template-columns: 1fr; } .footer-nav { grid-template-columns: 1fr 1fr; } }
@media (max-width: 560px) { .footer-nav { grid-template-columns: 1fr; gap: 28px; } }

/* ---- Page header ---- */
.page-header { padding: 56px 0 24px; }
.page-header.is-centered { text-align: center; }
.page-header.is-centered .page-header-lede { margin: 0 auto; }
.page-header-lede { max-width: 60ch; color: var(--color-text-muted); font-size: 1.05rem; }

/* ---- Services page (row layout) ---- */
.service-row { display: grid; grid-template-columns: 64px 1fr 220px; gap: 28px; align-items: start; padding: 32px 0; border-bottom: var(--border-width) solid var(--color-border); }
.service-row:first-child { padding-top: 0; }
.service-row-icon { width: 56px; height: 56px; border-radius: var(--radius-md); background: var(--color-surface); color: var(--color-accent); display: flex; align-items: center; justify-content: center; }
.service-row-icon svg { width: 28px; height: 28px; }
.service-row-body p { color: var(--color-text-muted); }
.service-features { display: flex; flex-direction: column; gap: 6px; margin-top: 12px; font-size: 0.92rem; }
.service-features li { display: flex; gap: 12px; align-items: baseline; }
/* Drawn rule instead of an em-dash character, so its weight tracks the
   theme's --rule-width and it stays optically level across faces. */
.service-features li::before {
  content: ''; flex: none; width: 14px; height: var(--rule-width);
  background: var(--color-accent); transform: translateY(-0.35em);
}
.service-row-price { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; text-align: left; }
.price-from { font-size: 0.78rem; color: var(--color-text-muted); font-family: var(--font-utility); }
.price-amount { font-family: var(--font-display); font-size: 1.5rem; font-weight: 700; margin-bottom: 10px; }
.price-period { font-size: 0.9rem; font-weight: 400; color: var(--color-text-muted); }
@media (max-width: 720px) { .service-row { grid-template-columns: 1fr; } .service-row-price { flex-direction: row; align-items: center; justify-content: space-between; width: 100%; } }

/* ---- Contact page (form-free: map + info) ---- */
.contact-grid { align-items: start; }
.contact-info { display: flex; flex-direction: column; gap: 24px; }
.contact-info-block h3 { margin-bottom: 4px; font-size: 0.95rem; }
/* A long address or email is one unbreakable run at display size; without
   this it set the column's min-content width and pushed the contact page
   sideways on every theme. */
.contact-info-link { text-decoration: none; font-family: var(--font-display); font-size: 1.15rem; font-weight: 700; color: var(--color-text); overflow-wrap: anywhere; }
.contact-info, .contact-info-block { min-width: 0; }
/* Same reason: the day and the hours are two flex items that would rather
   overflow than wrap. gap + wrap lets the row break on a narrow screen. */
.hours-list li { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 4px 16px; padding: 4px 0; border-bottom: var(--border-width) dashed var(--color-border); font-size: 0.92rem; }
.map-embed-wrap { border-radius: var(--radius-lg); overflow: hidden; border: var(--border-width) solid var(--color-border); aspect-ratio: 16/10; }
.map-embed-wrap iframe { width: 100%; height: 100%; border: 0; display: block; }
@media (max-width: 860px) { .contact-grid { grid-template-columns: 1fr; } }

/* ---- Contact form (emails the business via the gallery backend relay) ---- */
.contact-form-section { max-width: 640px; margin: 0 auto; }
.contact-form { display: flex; flex-direction: column; gap: 16px; margin-top: 8px; }
.contact-form .form-row { display: flex; flex-direction: column; gap: 6px; }
.contact-form label { font-family: var(--font-utility); font-size: 0.85rem; font-weight: 600; color: var(--color-text-muted); }
.contact-form input, .contact-form textarea {
  font-family: var(--font-body); font-size: 1rem; color: var(--color-text); background: var(--color-surface);
  border: var(--border-width) solid var(--color-border); border-radius: var(--radius-sm); padding: 0.7em 0.9em; resize: vertical;
}
.contact-form input:focus-visible, .contact-form textarea:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 1px; }
/* Neutralize the browser's own default :invalid styling (a box-shadow
   or outline that Firefox/Chrome apply automatically to any required-
   but-empty field, even with novalidate on the form -- novalidate only
   suppresses the popup bubble, not this default rendering). Without
   this override every empty required field shows a stray shaded/
   outlined box from the moment the page loads, regardless of anything
   in this file. Validity is communicated only through aria-invalid +
   .field-error below, both fully in our control. */
.contact-form input:invalid,
.contact-form textarea:invalid,
.contact-form input:-moz-ui-invalid,
.contact-form textarea:-moz-ui-invalid {
  box-shadow: none;
  outline: none;
}
.contact-form-submit { align-self: flex-start; }
.contact-form[hidden] { display: none; }
/* Field-level errors: an icon + a line of text sitting directly under
   the offending input, no background/border box. A "shaded box with an
   outline" reads as generic/wrong on half these themes (a boxed alert
   makes no sense next to e.g. neubrutalism's own thick-bordered
   elements, or a hairline-only theme like Swiss Minimalism) -- so this
   never draws its own surface at all. Color comes entirely from
   --color-text (body copy, same weight as a label) with the icon in
   --color-accent, so it always matches whatever palette the active
   theme defines -- nothing here is a fixed red/green. */
.field-error {
  display: flex;
  align-items: flex-start;
  gap: 6px;
  margin: 6px 0 0;
  font-family: var(--font-utility);
  font-size: 0.85rem;
  color: var(--color-text);
}
.field-error[hidden] { display: none; }
/* Real inline <svg> icon (see FIELD_ERROR_ICON in build_bases.py), not a
   CSS mask/background-image trick. The old approach painted the icon by
   masking a background-color through an SVG data-URI, gated behind an
   @supports(mask-image) check -- but some mobile browser engines report
   support for the property while still failing to clip it correctly,
   rendering a plain solid box instead of the icon shape (exactly the
   stray "shaded box" that check was meant to prevent). A real <svg>
   element has no such feature-detection gamble: it either renders as the
   icon it is, everywhere, or -- in a truly ancient browser with no SVG
   support at all -- collapses to nothing, never a wrong box. */
.field-error-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 16px;
  height: 16px;
  margin-top: 1px;
  color: var(--color-accent);
}
/* Percentage sizing on a nested <svg> inside a flex item is a known weak
   spot in several mobile engines (notably iOS Safari, some Android
   WebViews) -- they don't always resolve width/height:100% against the
   flex item's box reliably, and fall back to the SVG's undeclared
   intrinsic default size instead (historically ~300x150), which reads as
   a suddenly-huge icon. The <svg> tag itself now also carries explicit
   width="16" height="16" attributes (see FIELD_ERROR_ICON in
   build_bases.py) as the real fix -- markup attributes aren't subject to
   this percentage-resolution quirk at all, so this CSS rule is now just
   a belt-and-suspenders backstop, not the only thing holding the size.
   color: inherit is likewise explicit here rather than assumed: some of
   the same mobile engines don't reliably carry `color` from the wrapping
   span into stroke="currentColor" on a nested SVG without it being
   restated directly on the svg element. */
.field-error-icon svg { width: 100%; height: 100%; display: block; color: inherit; }
.contact-form-error { margin-top: 4px; }
.contact-form input[aria-invalid="true"],
.contact-form textarea[aria-invalid="true"] { border-color: var(--color-accent); }

/* Success view: the entire form is swapped out for this (see gallery.js),
   rather than stacking a banner above/below the still-visible form --
   the form has done its job, so nothing about it needs to stay on
   screen. Circle + check use --color-accent/--color-primary-text (the
   same pairing the primary button already uses for this theme), so it
   always reads as "this theme's own accent color," never a fixed green. */
.contact-form-success {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
  padding: 32px 0;
  text-align: center;
}
.contact-form-success[hidden] { display: none; }
.contact-form-success .success-check {
  width: 64px;
  height: 64px;
  border-radius: 50%;
  background: var(--color-accent);
  color: var(--color-primary-text);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.contact-form-success .success-check svg { width: 28px; height: 28px; }
.contact-form-success p {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(1.2rem, 2.4vw, 1.6rem);
  font-weight: 700;
  color: var(--color-text);
}

/* ---- AI chat widget (only rendered when the business has AI enabled --
   see admin-settings.html / gallery_widgets, injected server-side by
   gallery.js) ---- */
#lwl-ai-widget { position: fixed; right: 20px; bottom: 20px; z-index: 200; font-family: var(--font-body); }
#lwl-ai-toggle {
  /* Pill, not a circle: the widget introduces itself with a visible
     "Chat with our AI receptionist" label on load and then collapses
     around the icon (the .lwl-intro class is added/removed by the widget
     core in embed.js). min-width + overflow:hidden let the same element
     be both states. */
  min-width: 56px; height: 56px; border-radius: 28px; border: none; padding: 0; gap: 0; overflow: hidden;
  background: var(--color-primary); color: var(--color-primary-text);
  /* Deliberately NOT var(--shadow-md): themes like Neubrutalism use a hard,
     zero-blur offset-duplicate shadow that reads fine on rectangular cards
     but renders as a second solid circle behind this round button (looks
     like two stacked chat icons). Floating action controls keep their own
     soft, realistic shadow regardless of theme. */
  box-shadow: 0 4px 16px rgba(0,0,0,0.25); cursor: pointer; display: flex; align-items: center; justify-content: center;
  transition: transform var(--transition), gap 0.45s ease, padding 0.45s ease;
}
#lwl-ai-toggle:hover { transform: translateY(-2px); }
#lwl-ai-toggle svg { flex: none; }
#lwl-ai-toggle .lwl-ai-toggle-label {
  max-width: 0; opacity: 0; white-space: nowrap; font-family: inherit; font-size: 0.88rem; font-weight: 600;
  transition: max-width 0.45s ease, opacity 0.3s ease;
}
#lwl-ai-widget.lwl-intro #lwl-ai-toggle { gap: 10px; padding: 0 20px 0 16px; }
#lwl-ai-widget.lwl-intro #lwl-ai-toggle .lwl-ai-toggle-label { max-width: 260px; opacity: 1; }
#lwl-ai-panel {
  position: absolute; right: 0; bottom: 68px; width: min(340px, 88vw); max-height: 60vh; display: flex; flex-direction: column;
  background: var(--color-bg); border: var(--border-width) solid var(--color-border); border-radius: var(--radius-lg); box-shadow: var(--shadow-md); overflow: hidden;
}
#lwl-ai-panel[hidden] { display: none; }
#lwl-ai-header { display: flex; justify-content: space-between; align-items: center; padding: 12px 14px; background: var(--color-primary); color: var(--color-primary-text); font-weight: 600; }
#lwl-ai-header button { background: none; border: none; color: inherit; font-size: 1.3rem; line-height: 1; cursor: pointer; }
#lwl-ai-messages { flex: 1; overflow-y: auto; padding: 12px 14px; display: flex; flex-direction: column; gap: 10px; min-height: 160px; }
.lwl-ai-msg { max-width: 85%; padding: 8px 12px; border-radius: var(--radius-md); font-size: 0.92rem; line-height: 1.4; }
.lwl-ai-msg.user { align-self: flex-end; background: var(--color-primary); color: var(--color-primary-text); }
.lwl-ai-msg.assistant { align-self: flex-start; background: var(--color-surface); color: var(--color-text); border: var(--border-width) solid var(--color-border); }
#lwl-ai-form { display: flex; gap: 8px; padding: 10px; border-top: var(--border-width) solid var(--color-border); }
/* min-width:0 on the input and flex:none on Send are both load-bearing. A
   flex item won't shrink below its min-content width by default, so without
   the first the text input refuses to give up room and the (shrinkable)
   Send button absorbs the whole overflow instead -- clipping its own label
   inside the 340px panel. It showed up "in most themes but not all" because
   the input inherits --font-body, so how much room it demands changes with
   every theme's typeface. */
#lwl-ai-input { flex: 1 1 auto; min-width: 0; font-family: var(--font-body); font-size: 0.92rem; padding: 0.6em 0.8em; border: var(--border-width) solid var(--color-border); border-radius: var(--radius-sm); background: var(--color-surface); color: var(--color-text); }
#lwl-ai-input:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 1px; }
#lwl-ai-form button[type="submit"] { flex: none; white-space: nowrap; padding: 0.6em 1em; border-radius: var(--radius-sm); border: none; background: var(--color-primary); color: var(--color-primary-text); font-weight: 600; cursor: pointer; }
/* Attach control. These rules exist in embed.js's own injected stylesheet
   too -- they have to be repeated here because gallery previews (and
   exported sites) load this stylesheet instead. Without them the button
   fell back to the browser's default button chrome, which is what made
   the paperclip look like stock browser UI. display:none is the default
   state; the widget core flips it to flex only when the site actually has
   chat uploads enabled. */
#lwl-ai-attach {
  display: none; align-items: center; justify-content: center; flex: none; width: 38px; height: 38px; padding: 0;
  border: var(--border-width) solid var(--color-border); border-radius: var(--radius-sm);
  background: var(--color-surface); color: var(--color-text); cursor: pointer;
}
#lwl-ai-attach:hover { background: var(--color-bg); }
#lwl-ai-attach[disabled] { opacity: 0.5; cursor: default; }
.lwl-ai-file {
  align-self: flex-end; display: flex; align-items: center; gap: 6px; max-width: 85%; padding: 6px 10px;
  border-radius: var(--radius-md); background: var(--color-surface); border: var(--border-width) solid var(--color-border);
  color: var(--color-text); font-size: 0.8rem;
}
.lwl-ai-file svg { flex: none; }
.lwl-ai-file span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Detail form -- the assistant asking for several short facts at once by
   emitting a [form:Name, Phone] bubble. Same reason as the attach rules
   above: embed.js injects its own copy for real client sites, and gallery
   previews and exported sites load this stylesheet instead, so both copies
   have to exist. Shaped as an assistant bubble because that is what it is. */
.lwl-ai-formcard {
  align-self: flex-start; display: flex; flex-direction: column; gap: 6px;
  width: 85%; max-width: 85%; padding: 10px;
  border-radius: var(--radius-md); background: var(--color-surface);
  border: var(--border-width) solid var(--color-border);
}
/* 16px, not --font-body's scale: anything under 16px makes iOS zoom the
   whole page when the box is focused, which on a client's site reads as the
   widget breaking. */
.lwl-ai-field {
  width: 100%; box-sizing: border-box; font-family: var(--font-body); font-size: 16px;
  padding: 0.55em 0.7em; border: var(--border-width) solid var(--color-border);
  border-radius: var(--radius-sm); background: var(--color-bg); color: var(--color-text);
}
.lwl-ai-field::placeholder { color: var(--color-text); opacity: 0.45; }
.lwl-ai-field:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 1px; }
.lwl-ai-field:disabled { opacity: 1; background: var(--color-surface); color: var(--color-text); }
.lwl-ai-formsend {
  align-self: flex-end; padding: 0.5em 1em; border: none; border-radius: var(--radius-sm);
  background: var(--color-primary); color: var(--color-primary-text);
  font-family: var(--font-body); font-weight: 600; cursor: pointer;
}
.lwl-ai-formsend[disabled] { opacity: 0.55; cursor: default; }
/* Choice buttons -- offered times, services, yes/no. Wraps: five times will
   not sit on one row inside the panel. Same two-copies note as above. */
.lwl-ai-choices { align-self: flex-start; display: flex; flex-wrap: wrap; gap: 6px; max-width: 85%; }
/* 36px min-height -- the one control a visitor taps rather than types into,
   usually one-handed on a phone. */
.lwl-ai-choice {
  min-height: 36px; padding: 0 12px; border-radius: var(--radius-sm);
  border: var(--border-width) solid var(--color-border);
  background: var(--color-bg); color: var(--color-text);
  font-family: var(--font-body); font-size: 0.86rem; cursor: pointer;
}
.lwl-ai-choice:hover:not([disabled]) { background: var(--color-surface); }
.lwl-ai-choice:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 1px; }
.lwl-ai-choice[disabled] { opacity: 0.4; cursor: default; }
.lwl-ai-choice.is-picked {
  opacity: 1; background: var(--color-primary); color: var(--color-primary-text);
  border-color: var(--color-primary);
}
/* Animated "assistant is typing" dots, shown while a reply is in flight. */
.lwl-ai-typing {
  align-self: flex-start; display: flex; align-items: center; gap: 4px; padding: 11px 13px;
  border-radius: var(--radius-md); background: var(--color-surface); color: var(--color-text);
  border: var(--border-width) solid var(--color-border);
}
.lwl-ai-typing i { width: 6px; height: 6px; border-radius: 50%; background: currentColor; opacity: 0.3; animation: lwl-ai-typing-bounce 1.2s infinite ease-in-out; }
.lwl-ai-typing i:nth-child(2) { animation-delay: 0.15s; }
.lwl-ai-typing i:nth-child(3) { animation-delay: 0.3s; }
@keyframes lwl-ai-typing-bounce {
  0%, 60%, 100% { opacity: 0.3; transform: translateY(0); }
  30% { opacity: 0.9; transform: translateY(-4px); }
}
@media (prefers-reduced-motion: reduce) {
  .lwl-ai-typing i { animation-duration: 0s; opacity: 0.6; }
  #lwl-ai-toggle, #lwl-ai-toggle .lwl-ai-toggle-label { transition: none; }
}
@media (max-width: 480px) { #lwl-ai-panel { width: calc(100vw - 24px); right: -8px; } }

/* ==== THEME: Coastal Clean ==== */

/* POV: sun-bleached, not aquarium-blue. The palette is weathered clapboard,
   sea glass and wet sand; the horizon line is the recurring motif. Wide,
   low, and light -- a coastal page should feel like it has air moving
   through it, which means long horizontals and no heavy verticals. */

:root{
  --font-display:'Outfit',sans-serif; --font-body:'Source Sans 3',sans-serif; --font-utility:'Source Sans 3',sans-serif;
  --color-bg:#FBFAF6; --color-surface:#E6EFEC; --color-text:#132B30; --color-text-muted:#4B676C;
  --color-primary:#0E6B66; --color-primary-text:#FFFFFF; --color-accent:#0E6B66; --color-accent-2:#D98E2B; --color-border:#CFE0DC;
  --radius-sm:6px; --radius-md:10px; --radius-lg:16px;
  --shadow-sm:0 1px 3px rgba(19,43,48,0.05); --shadow-md:0 14px 32px rgba(19,43,48,0.12);
  --border-width:1px; --transition:240ms ease; --letter-spacing-eyebrow:0.14em;
  --rule-width:1px;
  --section-align:left; --space-section:88px;
  --media-radius:16px; --media-ratio:16/10; --card-lift:none;
}

h1,h2{ font-weight:600; letter-spacing:-0.02em; }
h1{ font-size:clamp(2.2rem,5vw,3.6rem); line-height:1.06; }
.eyebrow{ color:#8A5A12; }
.eyebrow::before{ background:var(--color-accent-2); width:2.2em; }

.card{ border-radius:16px; }
.card:hover{ background:var(--color-surface); }
.card-icon{ color:var(--color-primary); }
.btn{ border-radius:999px; }
.btn-primary:hover{ transform:translateY(-1px); }
.badge svg{ color:var(--color-accent-2); }
.hero-trust-list li::before{ border-color:var(--color-accent-2); }
.stat-number{ color:var(--color-primary-text); }
.process-step{ border-top-color:var(--color-accent-2); }

/* The horizon: bands of sky, water and sand, drawn flat and level. No
   135-degree diagonal anywhere -- the sea is horizontal. */
.hero-media-placeholder,.split-media-placeholder,.team-photo-placeholder{
  --media-fill:
    linear-gradient(#DCEAE9 0 0) 0 0/100% 46% no-repeat,
    linear-gradient(#B7D4D0 0 0) 0 46%/100% 26% no-repeat,
    linear-gradient(#E8DAC0 0 0) 0 100%/100% 28% no-repeat,
    var(--color-surface);
}

