/* =============================================================================
   Kodu Meister — components added on top of the v6 design
   Loaded after base.css, which is kodu-help-redesign-v6/app/globals.css with
   the Tailwind entry point removed (nothing in it used a utility class).

   Everything here is either a component the v6 build did not have, or the dark
   theme, which it had no tokens for.

   ── Accessibility rules held throughout ────────────────────────────────────
   * every interactive target is at least 44×44 (pointer: coarse gets more)
   * :focus-visible is always drawn — never `outline: none` without a
     replacement, or a keyboard user loses their place entirely
   * nothing conveys meaning by colour alone: the status pills carry text, the
     badges carry text, the FAQ carries a rotating chevron AND aria-expanded
   * every animation is inside a prefers-reduced-motion guard
   ============================================================================= */

/* ─────────────────────────────────────────────────────────────────────────────
   1 · Theme tokens
   base.css defines the light palette on :root. Dark mode redefines ONLY the
   tokens, so every component below is written once and themes itself.

   Three states, as required: an explicit choice stamps data-theme on <html>,
   and the default "system" setting stamps nothing, leaving prefers-color-scheme
   to decide. The :not([data-theme="light"]) guard is what lets the toggle win
   in both directions.
   ───────────────────────────────────────────────────────────────────────────── */
:root {
  /*
   * base.css says `font-family: var(--font-sans), Arial, sans-serif`. In v6
   * next/font defined those two variables at build time; there is no build step
   * here, so without these declarations every page silently falls back to Arial
   * and the whole design loses its typography.
   *
   * A system stack, not a web font: no third-party request, no CSP host, no
   * consent question about the visitor's IP reaching Google, and no swap. See
   * the note in templates/layout/head.php for how to reinstate Manrope if the
   * brand requires it.
   */
  --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
               "Helvetica Neue", "Noto Sans", "Liberation Sans", sans-serif;
  --font-serif: ui-serif, Georgia, Cambria, "Times New Roman", serif;

  --focus:        #0c6545;
  --focus-ring:   0 0 0 3px rgba(12, 101, 69, .35);
  --card:         #ffffff;
  --card-2:       #f8faf7;
  --shadow-sm:    0 1px 2px rgba(16, 27, 23, .06);
  --shadow-md:    0 8px 24px rgba(16, 27, 23, .08);
  --shadow-lg:    0 24px 64px rgba(16, 27, 23, .12);
  --radius:       14px;
  --radius-sm:    10px;
  /* The spotlight gradient (requirement 8), as tokens so the light and dark
     variants are defined in one place rather than per component. */
  --spot-a:       rgba(12, 101, 69, .16);
  --spot-b:       rgba(87, 201, 142, .10);
  --spot-edge:    rgba(12, 101, 69, .30);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --ink:      #e8efea;
    --muted:    #94a29b;
    --line:     #22322b;
    --paper:    #0f1714;
    --cream:    #141d19;
    --green:    #4fbb8c;
    --dark:     #0a120f;
    --red:      #f0715c;
    --blue:     #7fa0f2;
    --card:     #141d19;
    --card-2:   #18231e;
    --focus:    #4fbb8c;
    --focus-ring: 0 0 0 3px rgba(79, 187, 140, .38);
    --shadow-sm: 0 1px 2px rgba(0, 0, 0, .5);
    --shadow-md: 0 8px 24px rgba(0, 0, 0, .55);
    --shadow-lg: 0 24px 64px rgba(0, 0, 0, .6);
    --spot-a:   rgba(79, 187, 140, .20);
    --spot-b:   rgba(12, 101, 69, .14);
    --spot-edge: rgba(79, 187, 140, .38);
  }
}

:root[data-theme="dark"] {
  --ink:      #e8efea;
  --muted:    #94a29b;
  --line:     #22322b;
  --paper:    #0f1714;
  --cream:    #141d19;
  --green:    #4fbb8c;
  --dark:     #0a120f;
  --red:      #f0715c;
  --blue:     #7fa0f2;
  --card:     #141d19;
  --card-2:   #18231e;
  --focus:    #4fbb8c;
  --focus-ring: 0 0 0 3px rgba(79, 187, 140, .38);
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, .5);
  --shadow-md: 0 8px 24px rgba(0, 0, 0, .55);
  --shadow-lg: 0 24px 64px rgba(0, 0, 0, .6);
  --spot-a:   rgba(79, 187, 140, .20);
  --spot-b:   rgba(12, 101, 69, .14);
  --spot-edge: rgba(79, 187, 140, .38);
}

/* base.css sets body{background:#fff} literally. Override with the token so the
   page actually turns dark rather than staying white under dark text. */
body { background: var(--paper); color: var(--ink); }
.site-header, .card, .service-card { background: var(--card); }

/* ─────────────────────────────────────────────────────────────────────────────
   2 · Focus, targets, motion — the three global accessibility guarantees
   ───────────────────────────────────────────────────────────────────────────── */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  box-shadow: var(--focus-ring);
  border-radius: 4px;
}

/* Touch targets. The visual box may be smaller; the HIT AREA may not. */
@media (pointer: coarse) {
  .site-header nav a,
  .head-actions button,
  .lang-switch button,
  .faq-q,
  .gallery-nav button { min-height: 48px; }
}

.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: 999;
  background: var(--ink); color: #fff; padding: 12px 18px; font-weight: 700;
}
.skip-link:focus { left: 8px; top: 8px; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ─────────────────────────────────────────────────────────────────────────────
   3 · Spotlight hover  (requirement 8)

   A gradient that follows the pointer across a card. --mx/--my are written by
   assets/js/spotlight.js; the fallback 50% keeps it centred when JS has not run
   or the device has no pointer at all.

   The gradient sits in a ::before at opacity 0 rather than being painted on the
   element, so the transition animates OPACITY only. Animating a background-image
   forces a repaint of the whole card on every mouse move.
   ───────────────────────────────────────────────────────────────────────────── */
.spotlight { position: relative; isolation: isolate; overflow: hidden; }
.spotlight::before {
  content: "";
  position: absolute; inset: 0; z-index: -1;
  background: radial-gradient(
    420px circle at var(--mx, 50%) var(--my, 50%),
    var(--spot-a) 0%, var(--spot-b) 42%, transparent 72%
  );
  opacity: 0;
  transition: opacity .28s ease-out;
}
@media (hover: hover) and (pointer: fine) {
  .spotlight:hover::before { opacity: 1; }
  .spotlight:hover { border-color: var(--spot-edge); }
}
/* Keyboard users get the same affordance; without this the effect is
   mouse-only and the card looks inert on tab. */
.spotlight:focus-within::before { opacity: 1; }
.spotlight:focus-within { border-color: var(--spot-edge); }

/* ─────────────────────────────────────────────────────────────────────────────
   4 · Service cards, price line, badges  (requirement 2)
   ───────────────────────────────────────────────────────────────────────────── */
.service-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
  gap: 22px;
  margin-block: 34px;
}
.service-card {
  display: flex; flex-direction: column; gap: 14px;
  padding: 26px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--card);
  text-decoration: none; color: inherit;
  transition: transform .2s ease-out, box-shadow .2s ease-out, border-color .2s ease-out;
}
@media (hover: hover) {
  .service-card:hover { transform: translateY(-3px); box-shadow: var(--shadow-md); }
}
.service-card h3 { margin: 0; font-size: 21px; letter-spacing: -.4px; line-height: 1.25; }
.service-card p  { margin: 0; color: var(--muted); font-size: 14px; line-height: 1.6; }

.price-line {
  display: inline-flex; align-items: baseline; gap: 4px;
  font-size:14px; font-weight: 800; letter-spacing: .01em;
  color: var(--green);
  /* Prices sit in a column across cards; proportional digits make them jitter. */
  font-variant-numeric: tabular-nums;
}
.price-line .amount { font-size: 17px; }
.vat-note { color: var(--muted); font-size:13px; margin-top: 6px; }

/* The badges. Text, not colour, carries the meaning — `note` is a real
   sub-line, not a tooltip, so it survives on touch where hover does not. */
.badge-row { display: flex; flex-wrap: wrap; gap: 8px; }
.badge {
  display: inline-flex; flex-direction: column; gap: 2px;
  padding: 8px 12px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--card-2);
  font-size:13px; font-weight: 700; line-height: 1.3;
}
.badge.has-note { border-radius: 12px; }
.badge .badge-note { font-weight: 500; color: var(--muted); font-size:13px; }
.badge::before {
  content: ""; width: 6px; height: 6px; border-radius: 50%;
  background: var(--green); display: inline-block; margin-right: 6px;
  vertical-align: middle;
}

/* The urgency radio-toggle on the contact form. A distinct `.badge-toggle`
   class — not `.badge` — on purpose: `.badge`'s ::before dot exists to mark a
   list item elsewhere on the site, and stacking it on a radio input doubles
   up with the browser's own circle. The input stays in the DOM and
   focusable (never display:none — that drops it from the tab order and from
   screen readers); it is only visually hidden, clipped to 1px instead of
   removed. The label's <span> carries the checked/unchecked look and is what
   the sibling selector and :has() both target, so the visual state tracks
   the real radio state instead of duplicating it in JS. */
.badge-toggle { position: relative; display: inline-flex; cursor: pointer; }
.badge-toggle-input {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap;
}
.badge-toggle span {
  display: inline-flex; align-items: center;
  padding: 8px 14px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--card-2);
  color: var(--ink);
  font-size: 13px; font-weight: 700; line-height: 1.3;
  transition: background-color .18s ease, border-color .18s ease,
              color .18s ease, transform .18s ease;
}
.badge-toggle-input:checked + span {
  background: var(--green);
  border-color: var(--green);
  color: #fff;
  transform: translateY(-1px);
}
/* The input itself is 1px and clipped, so its own focus-visible outline
   would render as a stray fragment; move the indicator to the visible span
   instead, using the same tokens as the site-wide focus rule (§2 above). */
.badge-toggle-input:focus-visible { outline: none; }
.badge-toggle-input:focus-visible + span {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  box-shadow: var(--focus-ring);
}
@media (prefers-reduced-motion: reduce) {
  .badge-toggle span { transition: none; }
}
/* :has() progressive enhancement — same result as the sibling selector
   above, kept as a second, independent source of truth so the checked look
   still applies even if the span/input order in the markup ever changes. */
@supports selector(:has(*)) {
  .badge-toggle:has(.badge-toggle-input:checked) span {
    background: var(--green);
    border-color: var(--green);
    color: #fff;
    transform: translateY(-1px);
  }
}

.feature-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; }
.feature-list li {
  display: grid; grid-template-columns: 22px 1fr; gap: 10px;
  align-items: start; font-size: 15px; line-height: 1.55;
}
.feature-list li::before {
  content: ""; margin-top: 7px; width: 14px; height: 8px;
  border-left: 2px solid var(--green); border-bottom: 2px solid var(--green);
  transform: rotate(-45deg);
}
.step-list { list-style: none; margin: 0; padding: 0; counter-reset: step; display: grid; gap: 18px; }
.step-list li { counter-increment: step; display: grid; grid-template-columns: 38px 1fr; gap: 14px; }
.step-list li::before {
  content: counter(step, decimal-leading-zero);
  font-size:13px; font-weight: 800; color: var(--green);
  padding-top: 3px; font-variant-numeric: tabular-nums;
}

/* ─────────────────────────────────────────────────────────────────────────────
   5 · Project gallery slider  (requirement 3)

   CSS scroll-snap rather than a carousel library: it is keyboard-scrollable,
   screen-reader-linear and works with zero JavaScript. spotlight.js only wires
   the two arrow buttons and the dots, and the slider is fully usable if that
   file never loads.
   ───────────────────────────────────────────────────────────────────────────── */
.gallery { position: relative; margin-block: 34px; }
.gallery-track {
  display: grid; grid-auto-flow: column;
  grid-auto-columns: minmax(290px, 33%);
  gap: 20px;
  overflow-x: auto; scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  padding-bottom: 14px;
  /* The one place a horizontal scroll is legitimate: it is INSIDE its own
     container, so the page body never scrolls sideways. */
  scrollbar-width: thin;
}
.gallery-track::-webkit-scrollbar { height: 6px; }
.gallery-track::-webkit-scrollbar-thumb { background: var(--line); border-radius: 3px; }

.gallery-item {
  scroll-snap-align: start;
  display: flex; flex-direction: column;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--card); overflow: hidden;
}
.gallery-item figure { margin: 0; aspect-ratio: 4 / 3; background: var(--cream); }
.gallery-item img { width: 100%; height: 100%; object-fit: cover; display: block; }
.gallery-body { padding: 20px; display: flex; flex-direction: column; gap: 10px; flex: 1; }
.gallery-body h3 { margin: 0; font-size: 18px; letter-spacing: -.3px; }
.gallery-body p  { margin: 0; color: var(--muted); font-size: 14px; line-height: 1.6; flex: 1; }
.gallery-meta { display: flex; gap: 10px; font-size:13px; font-weight: 700; color: var(--muted); }

/* The clickable button under each slide, which requirement 3 asks for. */
.gallery-cta {
  display: inline-flex; align-items: center; gap: 8px;
  align-self: flex-start;
  min-height: 44px; padding: 0 18px;
  border: 0; border-radius: 999px;
  background: var(--ink); color: #fff;
  font-size:13px; font-weight: 800; text-decoration: none;
  transition: background .2s ease-out;
}
.gallery-cta:hover { background: var(--green); }

.gallery-nav { display: flex; gap: 8px; justify-content: flex-end; margin-top: 8px; }
.gallery-nav button {
  width: 44px; height: 44px; border-radius: 50%;
  border: 1px solid var(--line); background: var(--card); color: inherit;
  display: grid; place-items: center;
}
.gallery-nav button[disabled] { opacity: .38; cursor: not-allowed; }

/* ─────────────────────────────────────────────────────────────────────────────
   6 · FAQ scroll accordion  (requirement 9)

   Built on <details>/<summary>, so it opens with no JavaScript, is announced
   correctly by screen readers, and is findable by the browser's own in-page
   search — all three of which a div-and-click accordion loses.
   ───────────────────────────────────────────────────────────────────────────── */
.faq { display: grid; gap: 12px; margin-block: 30px; }
.faq-item {
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--card); overflow: hidden;
}
.faq-item[open] { border-color: var(--spot-edge); }
.faq-q {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  padding: 20px 22px; cursor: pointer;
  font-size: 16px; font-weight: 700; line-height: 1.4;
  list-style: none;
}
.faq-q::-webkit-details-marker { display: none; }
.faq-q::after {
  content: ""; flex: 0 0 auto; width: 10px; height: 10px;
  border-right: 2px solid var(--green); border-bottom: 2px solid var(--green);
  transform: rotate(45deg); transition: transform .22s ease-out;
}
.faq-item[open] .faq-q::after { transform: rotate(-135deg); }
.faq-a { padding: 0 22px 22px; color: var(--muted); font-size: 15px; line-height: 1.7; }
.faq-a p { margin: 0; }

/* ─────────────────────────────────────────────────────────────────────────────
   7 · QR contact badge  (requirement 1)
   ───────────────────────────────────────────────────────────────────────────── */
.qr-badge {
  display: grid; grid-template-columns: auto 1fr; gap: 22px; align-items: center;
  padding: 24px;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--card);
}
.qr-badge figure {
  margin: 0; width: 168px; height: 168px; padding: 12px;
  /* Always a light plate, in both themes. A QR code inverted for dark mode is
     unreadable to a lot of scanners, which expect dark-on-light. */
  background: #fff; border-radius: var(--radius-sm);
  display: grid; place-items: center;
}
.qr-badge svg, .qr-badge img { width: 100%; height: 100%; display: block; }
.qr-badge h3 { margin: 0 0 8px; font-size: 19px; letter-spacing: -.3px; }
.qr-badge p  { margin: 0 0 14px; color: var(--muted); font-size: 14px; line-height: 1.6; }
.qr-actions { display: flex; flex-wrap: wrap; gap: 10px; }
.qr-actions a {
  display: inline-flex; align-items: center; gap: 8px;
  min-height: 44px; padding: 0 16px;
  border: 1px solid var(--line); border-radius: 999px;
  font-size:14px; font-weight: 700; text-decoration: none; color: inherit;
}
.qr-actions a.primary { background: var(--green); border-color: var(--green); color: #fff; }
@media (max-width: 620px) {
  .qr-badge { grid-template-columns: 1fr; justify-items: center; text-align: center; }
  .qr-actions { justify-content: center; }
}

/* ─────────────────────────────────────────────────────────────────────────────
   8 · Language switcher
   ───────────────────────────────────────────────────────────────────────────── */
.lang-switch { position: relative; }
.lang-switch > button { display: flex; align-items: center; gap: 6px; }
.lang-menu {
  position: absolute; right: 0; top: calc(100% + 6px); z-index: 60;
  min-width: 168px; padding: 6px;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--card); box-shadow: var(--shadow-md);
  list-style: none; margin: 0;
}
.lang-menu[hidden] { display: none; }
.lang-menu a {
  display: flex; align-items: center; justify-content: space-between;
  min-height: 44px; padding: 0 12px; border-radius: 8px;
  text-decoration: none; color: inherit; font-size: 14px; font-weight: 600;
}
.lang-menu a:hover { background: var(--card-2); }
.lang-menu a[aria-current="true"] { color: var(--green); font-weight: 800; }

.services-switch { position: relative; }
.services-toggle { display: flex; align-items: center; gap: 6px; background: none; border: 0; font: inherit; color: inherit; cursor: pointer; padding: 0; }
.services-menu {
  position: absolute; left: 0; top: calc(100% + 6px); z-index: 60;
  min-width: 260px; padding: 6px;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--card); box-shadow: var(--shadow-md);
  list-style: none; margin: 0;
}
.services-menu[hidden] { display: none; }
.services-menu a {
  display: flex; flex-direction: column; gap: 1px;
  padding: 8px 12px; border-radius: 8px;
  text-decoration: none; color: inherit;
}
.services-menu a:hover { background: var(--card-2); }
.services-menu a span { font-size: 14px; font-weight: 700; }
.services-menu a small { font-size: 12px; color: var(--muted); }

/* ─────────────────────────────────────────────────────────────────────────────
   9 · Chat  (requirements 6 and 7)

   Requirement 7 asks for a chat button beside the social links in the footer
   and for the "call" button to be removed there. .footer-chat is that button.
   Both this and the per-service button call openJivoChat() in jivo.js, which
   passes the service name through to the operator.
   ───────────────────────────────────────────────────────────────────────────── */
.chat-btn {
  display: inline-flex; align-items: center; gap: 8px;
  min-height: 44px; padding: 0 18px;
  border: 1px solid var(--green); border-radius: 999px;
  background: transparent; color: var(--green);
  font-size:14px; font-weight: 800;
  transition: background .2s ease-out, color .2s ease-out;
}
.chat-btn:hover { background: var(--green); color: #fff; }
.chat-btn svg { width: 17px; height: 17px; }
.chat-btn.solid { background: var(--green); color: #fff; }
.chat-btn.solid:hover { filter: brightness(1.08); }
/* Hidden until the widget reports ready, so a dead button is never shown. */
.chat-btn[hidden] { display: none; }

.footer-social { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
.footer-social a, .footer-social button {
  display: inline-grid; place-items: center;
  width: 44px; height: 44px; border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, .22);
  background: transparent; color: inherit;
}

/* ─────────────────────────────────────────────────────────────────────────────
   10 · Consent banner
   Bottom sheet on mobile, corner card on desktop. Never a full-screen overlay:
   a consent wall that hides the content is both hostile and, on a page Google
   is rendering, a way to hide the content from indexing too.
   ───────────────────────────────────────────────────────────────────────────── */
.consent-banner {
  position: fixed; z-index: 900; left: 16px; right: 16px;
  bottom: calc(16px + env(safe-area-inset-bottom));
  max-width: 520px; max-height: 70vh; overflow-y: auto;
  padding: 22px;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--card); box-shadow: var(--shadow-lg);
  transform: translateY(16px); opacity: 0;
  transition: transform .28s ease-out, opacity .28s ease-out;
}
.consent-banner.is-visible { transform: none; opacity: 1; }
.consent-banner[hidden] { display: none; }
.consent-banner h2 { margin: 0 0 8px; font-size: 17px; }
.consent-banner p  { margin: 0 0 16px; font-size: 14px; line-height: 1.6; color: var(--muted); }
.consent-actions { display: flex; flex-wrap: wrap; gap: 10px; }
.consent-actions button {
  flex: 1 1 auto; min-height: 44px; padding: 0 18px;
  border-radius: 999px; border: 1px solid var(--line);
  background: var(--card-2); color: inherit; font-size:14px; font-weight: 800;
}
.consent-actions button[data-consent="accept"] {
  background: var(--green); border-color: var(--green); color: #fff;
}
/* Below 600px the mobile-dock (see "Stage 3" further down, --dock-height)
   occupies a fixed bar along the bottom edge and Jivo's bubble shares its
   right end — see --jivo-corner. A bottom offset of 16px put the banner
   UNDER that whole row: at z-index 900 (above the dock's 90) it rendered on
   top of the dock, hiding the primary CTA and, on a page with enough copy
   above it, blocking most of the viewport on first load. Clearing the dock's
   own height puts the banner above that row instead of over it. */
@media (max-width: 600px) {
  .consent-banner { bottom: calc(var(--dock-height) + 12px + env(safe-area-inset-bottom)); }
}
/* Desktop: a corner card, not a full-width bar (see the note at the top of
   this section on why). Jivo's bubble lives in the same bottom-right corner
   and is "not ours to place" (see --jivo-corner, Stage 4 z-index tokens) —
   24px put the banner's card directly against it. Clearing --jivo-corner
   keeps the two from crowding each other. */
@media (min-width: 720px) {
  .consent-banner { left: auto; right: 24px; bottom: calc(var(--jivo-corner) + 16px); }
}

/* ─────────────────────────────────────────────────────────────────────────────
   11 · Mobile refinements  (requirement 7)
   ───────────────────────────────────────────────────────────────────────────── */
@media (max-width: 900px) {
  .wrap { width: min(1220px, calc(100% - 32px)); }
  .hero-v2-grid { grid-template-columns: 1fr; gap: 34px; padding-block: 36px; min-height: 0; }
  .hero-main h1 { font-size: clamp(38px, 10vw, 56px); letter-spacing: -2px; line-height: 1.02; }
  .service-grid { grid-template-columns: 1fr; }
  .gallery-track { grid-auto-columns: 82%; }
  .site-header nav {
    display: none; position: absolute; left: 0; right: 0; top: 100%;
    flex-direction: column; gap: 0; padding: 8px;
    background: var(--card); border-bottom: 1px solid var(--line);
    box-shadow: var(--shadow-md);
  }
  .site-header nav.open { display: flex; }
  .site-header nav a { padding: 14px 12px; border-radius: 8px; font-size: 15px; }
  .menu-btn { display: grid; place-items: center; width: 44px; height: 44px; }
  .ghost-cta { display: none; }

  /* The sticky bottom bar. Two actions, both written contact: requirement 7
     removes the call button from the footer, and a phone-first dock would put
     it straight back. Padding accounts for the gesture bar. */
  .mobile-dock {
    position: fixed; z-index: 800; left: 0; right: 0;
    bottom: 0; display: grid; grid-template-columns: 1fr auto; gap: 10px;
    padding: 10px 14px calc(10px + env(safe-area-inset-bottom));
    background: var(--card); border-top: 1px solid var(--line);
  }
  .mobile-dock a, .mobile-dock button {
    min-height: 48px; display: inline-flex; align-items: center; justify-content: center;
    gap: 8px; padding: 0 20px; border-radius: 999px;
    font-size: 14px; font-weight: 800; text-decoration: none;
    background: var(--ink); color: #fff; border: 0;
  }
  .mobile-dock .chat-btn { background: transparent; color: var(--green); border: 1px solid var(--green); }
  /* Reserve the dock's height so the footer is not hidden behind it. */
  body.has-dock { padding-bottom: 84px; }
}

/* ─────────────────────────────────────────────────────────────────────────────
   12 · Staff sign-in
   ───────────────────────────────────────────────────────────────────────────── */
.login-page {
  min-height: 100dvh; display: grid; place-items: center;
  padding: 24px; background: var(--paper);
}
.login-card {
  width: min(430px, 100%); padding: 34px;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--card); box-shadow: var(--shadow-md);
}
.login-card h1 { margin: 0 0 6px; font-size: 25px; letter-spacing: -.6px; }
.login-card .lead { margin: 0 0 26px; color: var(--muted); font-size: 14px; }
.field { display: grid; gap: 6px; margin-bottom: 18px; }
.field label { font-size:14px; font-weight: 700; }
.field input, .field select, .field textarea {
  min-height: 48px; padding: 12px 14px;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--card-2); color: inherit;
  /* 16px minimum, or iOS zooms the whole page on focus. */
  font-size: 16px;
}
.field textarea { min-height: 120px; resize: vertical; }
.field .hint { font-size:13px; color: var(--muted); }
.field.invalid input, .field.invalid select, .field.invalid textarea { border-color: var(--red); }
.field .err { font-size:13px; color: var(--red); font-weight: 600; }
.check { display: flex; align-items: center; gap: 10px; margin-bottom: 22px; font-size: 14px; }
.check input { width: 20px; height: 20px; }
.btn-primary {
  width: 100%; min-height: 48px; border: 0; border-radius: 999px;
  background: var(--green); color: #fff; font-size: 14px; font-weight: 800;
}
.form-error {
  padding: 12px 14px; margin-bottom: 18px;
  border: 1px solid var(--red); border-radius: var(--radius-sm);
  background: rgba(215, 72, 52, .08); color: var(--red);
  font-size: 14px; font-weight: 600;
}
.form-ok {
  padding: 12px 14px; margin-bottom: 18px;
  border: 1px solid var(--green); border-radius: var(--radius-sm);
  background: rgba(12, 101, 69, .08); color: var(--green);
  font-size: 14px; font-weight: 600;
}

/* ─────────────────────────────────────────────────────────────────────────────
   13 · Panel additions
   base.css already carries .admin-shell / .admin-side / .metric-grid /
   .ops-panel / .request-row / .detail-panel from the v6 dashboard. Only what is
   genuinely new is here: the status pills, the master's job cards, the audit
   trail, and the services editor.
   ───────────────────────────────────────────────────────────────────────────── */
.status {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 11px; border-radius: 999px;
  font-size:13px; font-weight: 800; font-style: normal;
  border: 1px solid currentColor;
}
.status::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.status.fresh  { color: #2f6fd0; }
.status.ready  { color: #0c6545; }
.status.active { color: #b8791a; }
.status.done   { color: #4a7c5f; }
.status.dead   { color: #98a3a0; }
.status.warn   { color: #d74834; }

.job-list { display: grid; gap: 14px; }
.job-card {
  display: grid; gap: 12px; padding: 20px;
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--card);
}
.job-card.is-today { border-color: var(--green); }
.job-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 14px; }
.job-head h3 { margin: 0; font-size: 17px; }
.job-ref { font-size:13px; color: var(--muted); font-weight: 700; letter-spacing: .04em; }
.job-meta { display: grid; gap: 6px; font-size: 14px; }
.job-meta span { display: flex; gap: 8px; }
.job-meta small { color: var(--muted); min-width: 86px; }
.job-actions { display: flex; flex-wrap: wrap; gap: 10px; }
.job-actions button {
  min-height: 44px; padding: 0 18px; border-radius: 999px;
  border: 1px solid var(--green); background: transparent; color: var(--green);
  font-size:14px; font-weight: 800;
}
.job-actions button.primary { background: var(--green); color: #fff; }

.audit { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
.audit li {
  display: grid; grid-template-columns: 120px 1fr; gap: 12px;
  padding-bottom: 10px; border-bottom: 1px solid var(--line);
  font-size:14px;
}
.audit time { color: var(--muted); font-variant-numeric: tabular-nums; }

.editor-tabs { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 20px; }
.editor-tabs a {
  min-height: 40px; display: inline-flex; align-items: center; padding: 0 16px;
  border: 1px solid var(--line); border-radius: 999px;
  font-size:14px; font-weight: 700; text-decoration: none; color: inherit;
}
.editor-tabs a[aria-current="page"] { background: var(--green); border-color: var(--green); color: #fff; }
.editor-tabs a .missing { color: var(--red); font-weight: 800; margin-left: 6px; }

.data-table { width: 100%; border-collapse: collapse; font-size: 14px; }
.data-table th, .data-table td { padding: 14px 12px; text-align: left; border-bottom: 1px solid var(--line); }
.data-table th { font-size:13px; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); }
.data-table td.num { text-align: right; font-variant-numeric: tabular-nums; }
.table-scroll { overflow-x: auto; }

/* ─────────────────────────────────────────────────────────────────────────────
   14 · Print
   Somebody prints a job sheet and takes it to the property.
   ───────────────────────────────────────────────────────────────────────────── */
@media print {
  .site-header, .notice, .mobile-dock, .consent-banner,
  .chat-btn, .gallery-nav, .admin-side, footer { display: none !important; }
  body { background: #fff; color: #000; }
  .job-card, .service-card { break-inside: avoid; border-color: #999; }
  a[href^="http"]::after { content: " (" attr(href) ")"; font-size:13px; }
}

/* =============================================================================
   Ecosystem rail + author signature
   Ported from a1ex.cc. The reasoning behind each non-obvious rule is in
   projects-footer.md and footer.md; the load-bearing parts are repeated here,
   because whoever edits this file next will not have those documents to hand.
   ============================================================================= */

/* Tokens are scoped to .eco on purpose. This block came from a site with a
   different palette, and defining --accent or --surface at :root here would
   quietly re-theme the rest of kodu.help. */
.eco {
  --eco-bg:          #08120f;   /* a shade deeper than the footer --ink */
  --eco-grid:        rgba(226, 236, 231, .030);
  --eco-line:        rgba(226, 236, 231, .14);
  --eco-surface:     rgba(255, 255, 255, .045);
  --eco-surface-2:   rgba(255, 255, 255, .085);
  --eco-accent:      #57c98e;   /* the same green as the header notice dot */
  --eco-accent-soft: rgba(87, 201, 142, .18);
  --eco-text:        #e8efeb;
  --eco-muted:       #9aa9a1;
  --eco-gap:         18px;
  --eco-fade:        88px;      /* how far the edge mask reaches in */

  position: relative;
  margin-top: 44px;
  padding-block: clamp(34px, 5vw, 52px) clamp(26px, 4vw, 40px);
  border-top: 1px solid rgba(255, 255, 255, .08);
  background: var(--eco-bg);
  overflow: hidden;
}

/* Faint engineering grid. Pure CSS — no image request for decoration. */
.eco::before {
  content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background-image:
    repeating-linear-gradient(to right, var(--eco-grid) 0 1px, transparent 1px 64px),
    repeating-linear-gradient(to bottom, var(--eco-grid) 0 1px, transparent 1px 64px);
  -webkit-mask-image: radial-gradient(120% 90% at 50% 40%, #000 30%, transparent 100%);
          mask-image: radial-gradient(120% 90% at 50% 40%, #000 30%, transparent 100%);
}

/* Dust, not a particle system: six specks drifting on one composited
   transform, held at .16 opacity. The moment these read as "floating dots"
   the whole band starts to look generated. */
.eco::after {
  content: ""; position: absolute; inset: -15% -6%; z-index: 0; pointer-events: none;
  opacity: .16;
  background-image:
    radial-gradient(2px 2px at 11% 26%, var(--eco-accent) 50%, transparent 51%),
    radial-gradient(1.5px 1.5px at 27% 74%, var(--eco-accent) 50%, transparent 51%),
    radial-gradient(1.5px 1.5px at 44% 18%, var(--eco-accent) 50%, transparent 51%),
    radial-gradient(2px 2px at 63% 66%, var(--eco-accent) 50%, transparent 51%),
    radial-gradient(1.5px 1.5px at 79% 32%, var(--eco-accent) 50%, transparent 51%),
    radial-gradient(2px 2px at 92% 78%, var(--eco-accent) 50%, transparent 51%);
  animation: eco-drift 38s ease-in-out infinite alternate;
}
@keyframes eco-drift { to { transform: translate3d(-14px, -30px, 0); } }

/* Both pseudo-elements are absolutely positioned, so the content has to be
   lifted above them explicitly or it paints underneath. */
.eco__head, .eco .wrap { position: relative; z-index: 1; }

.eco__head { text-align: center; margin-bottom: clamp(20px, 3vw, 30px); }
.eco__eyebrow {
  display: block; font-size:12px; font-weight: 800; letter-spacing: .18em;
  text-transform: uppercase; color: var(--eco-accent);
}
.eco__head h2 {
  margin: 10px 0 0; font-size: clamp(19px, 2.2vw, 26px);
  letter-spacing: -.02em; color: var(--eco-text);
}
.eco__sub { margin: 8px 0 0; font-size:13px; color: var(--eco-muted); }

/* The rail. Its hairlines glow toward the middle, which gives the band edges
   without drawing two flat 1px lines across the page. */
.eco__rail {
  position: relative;
  padding-block: 20px;
  border-top: 1px solid transparent; border-bottom: 1px solid transparent;
  border-image: linear-gradient(90deg, transparent, var(--eco-accent) 18%, var(--eco-accent) 82%, transparent) 1;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
.eco__rail::-webkit-scrollbar { display: none; }
.eco__rail::before {
  content: ""; position: absolute; left: 0; right: 0; top: -1px; height: 90px;
  pointer-events: none;
  background: radial-gradient(60% 100% at 50% 0%, var(--eco-accent-soft), transparent 70%);
}

/* site.js sets this once it owns the loop: the strip stops being a scroller,
   and the ends fade instead of being cut. */
.eco__rail.is-live {
  overflow: hidden;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 var(--eco-fade), #000 calc(100% - var(--eco-fade)), transparent);
          mask-image: linear-gradient(90deg, transparent, #000 var(--eco-fade), #000 calc(100% - var(--eco-fade)), transparent);
}

.eco__marquee { display: flex; width: max-content; will-change: transform; }

/* THE HALF-GAP PADDING IS WHAT HIDES THE SEAM. site.js appends a second
   identical track and one loop is exactly one track wide, so the trailing half
   of one track plus the leading half of the next add up to the same gap that
   sits between any two cards. Removed as "extra padding", a pulsing hole rides
   past once per cycle and reads as a stutter. */
.eco__track {
  display: flex; align-items: stretch; gap: var(--eco-gap);
  padding-inline: calc(var(--eco-gap) / 2);
  margin: 0; list-style: none;
}
/* Only the scroller needs room at the ends; the live rail has the mask. */
.eco__rail:not(.is-live) .eco__track { padding-inline: 4px; }

.eco__item { display: flex; }
.eco__item a {
  display: flex; align-items: center; gap: 12px;
  min-height: 60px; padding: 10px 20px 10px 10px;
  border-radius: 999px;
  border: 1px solid var(--eco-line);
  background: var(--eco-surface);
  -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
  text-decoration: none; white-space: nowrap;
  transition: transform .22s ease, opacity .22s ease, border-color .22s ease, background .22s ease;
}
.eco__tile {
  width: 38px; height: 38px; flex: 0 0 auto;
  display: grid; place-items: center; padding: 6px;
  border-radius: 50%;
  border: 1px solid var(--eco-line);
  background: var(--eco-surface-2);
  transition: box-shadow .22s ease, border-color .22s ease;
}
.eco__tile img { width: 100%; height: 100%; object-fit: contain; }
/* logo-alt.svg is solid #000 and would be invisible here. This band is always
   dark, so the inversion is unconditional rather than theme-dependent. */
.eco__tile--mono img { filter: invert(1); }

.eco__text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.eco__name {
  font-size:14px; font-weight: 700; letter-spacing: -.01em; color: var(--eco-text);
  transition: color .22s ease;
}
/* --eco-muted and not something fainter: the card sits on --eco-surface rather
   than the band background, and this pair measures 6.8:1 there. 11px text needs
   4.5:1, and the obvious darker grey does not clear it on this surface. */
.eco__note { font-size:13px; color: var(--eco-muted); transition: color .22s ease; }

/* Hover: the card under the pointer lifts, the rest step back. Scoped to fine
   pointers so a tap on a phone never leaves a card stuck "hovered". The dimming
   lives on the RAIL in :hover, not on each card — one rule instead of eight. */
@media (hover: hover) and (pointer: fine) {
  .eco__rail:hover .eco__item a { opacity: .5; transform: scale(.97); }
  .eco__rail .eco__item a:hover {
    opacity: 1; transform: scale(1.06);
    border-color: var(--eco-accent); background: var(--eco-surface-2);
  }
  .eco__item a:hover .eco__tile {
    border-color: var(--eco-accent);
    box-shadow: 0 0 0 4px var(--eco-accent-soft), 0 0 22px -2px var(--eco-accent);
  }
  .eco__item a:hover .eco__note { color: var(--eco-text); }
}
/* Outside the media query: focus has to work where there is no mouse. */
.eco__item a:focus-visible { opacity: 1; transform: scale(1.06); border-color: var(--eco-accent); }

@media (max-width: 720px) {
  .eco { margin-top: 32px; }
  .eco__item a { min-height: 52px; padding: 8px 16px 8px 8px; }
  .eco__tile { width: 32px; height: 32px; }
  .eco__name { font-size:13px; }
}

/* Honour the setting in CSS too, not only in site.js: the drift above is a CSS
   animation and would keep running even though the rail itself does not. */
@media (prefers-reduced-motion: reduce) {
  .eco::after { animation: none; }
  .eco__item a, .eco__tile, .eco__name, .eco__note { transition: none; }
}

/* ── Author signature ───────────────────────────────────────────────────────
   Finish only. Every structural property — the flex row, the 36px mark — is
   inline in templates/components/a1ex-signature.php, because an inline SVG has
   no intrinsic size and fills its container without a stylesheet. That reached
   production once on a1ex.cc as a mark several thousand pixels tall.

   Opacity belongs on the CHILDREN. On .a1ex-sig it would fade the band showing
   through behind it as well. */
.a1ex-sig { max-width: 1220px; margin-inline: auto; }
.a1ex-sig__made, .a1ex-sig__name, .a1ex-sig__mark { transition: opacity .22s ease; }
.a1ex-sig:hover .a1ex-sig__made,
.a1ex-sig:hover .a1ex-sig__name { opacity: .9; }
.a1ex-sig:focus-visible { outline: 2px solid #57c98e; outline-offset: 4px; }

@media (max-width: 720px) {
  .a1ex-sig { gap: 12px !important; padding: 20px 12px !important; }
  .a1ex-sig__made { display: none; }
}

@media print { .eco, .a1ex-sig { display: none !important; } }

/* =============================================================================
   Accessibility and responsive corrections
   Applied against the ui-ux-pro-max checklist. Each block says which rule it
   answers, because "someone tidied the CSS" is how these come back.
   ============================================================================= */

/* ── The brand mark ──────────────────────────────────────────────────────────
   base.css sizes .brand-logo img as a 38x38 square with object-fit:cover and a
   9px radius, which suited the flat SVG tile it was written for. The real mark
   is 1.15:1 on transparency: `cover` crops it and the radius rounds off corners
   that are not there. */
.brand-logo img { width: auto; height: 34px; object-fit: contain; border-radius: 0; }
.brand-logo picture { display: flex; flex: 0 0 auto; }
@media (max-width: 620px) { .brand-logo img { height: 30px; } }

/* base.css inverts the footer mark — `.brand-logo.inverse img { filter:
   invert(1); mix-blend-mode: screen }` — which was right when it was a solid
   dark SVG that had to come out white on the dark band.

   The real mark is green. invert(1) maps #00a050 to #ff5faf, so the footer logo
   rendered bright pink. Its own green measures 5.2:1 against the footer, so it
   needs no help: turn the inversion off rather than fight it with a second
   filter. */
.brand-logo.inverse img { filter: none; mix-blend-mode: normal; }

/* ── The footer chat button is a labelled pill, not an icon ──────────────────
   `.footer-social a, .footer-social button` forces a 44x44 circle, which is
   right for the Facebook and Instagram glyphs sitting next to it. The chat
   button carries a word as well, and the word does not fit inside a 44px
   circle — it spilled out under the ring. Give it back its own width. */
.footer-social .chat-btn {
  width: auto;
  height: auto;
  min-height: 44px;
  padding: 0 18px;
  border-radius: 999px;
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

/* ── Breadcrumbs must be allowed to wrap ─────────────────────────────────────
   A flex row with no wrap and a long service name pushes its last item off the
   line; the site name and the separator end up stacked on top of each other.
   Service titles here run to sixty characters. */
.breadcrumbs { flex-wrap: wrap; row-gap: 4px; }

/* ── §5 readable-font-size · §6 font-scale ───────────────────────────────────
   Stated rather than inherited. The browser default is already 16px, so this
   changes nothing today — it stops the next person setting a smaller base and
   dragging every unstyled element down with it. */
body { font-size: 16px; line-height: 1.6; }

/* ── §5 readable-font-size — iOS zooms the page on focus ─────────────────────
   Any form control under 16px makes mobile Safari zoom in when it gets focus,
   and it does not zoom back out. The booking form had its inputs at 12px, so
   every phone user who tapped a field ended up on a page they then had to pinch
   out of. 16px is the threshold, exactly. */
input, select, textarea { font-size: 16px; }

/* ── §2 touch-target-size / touch-spacing ────────────────────────────────────
   44x44 is the floor, and it applies to the real hit area rather than the
   painted one. Only on coarse pointers: forcing 44px onto a dense desktop
   toolbar would wreck the layout to solve a problem the mouse does not have. */
@media (pointer: coarse) {
  .site-header nav a,
  .footer-v2 a,
  .lang, .ghost-cta, .menu-btn,
  .chat-btn, .filter-pills button, .finder-tabs button {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
  }
  .footer-v2 > div { gap: 4px; }
  .footer-social a, .footer-social button { min-width: 44px; min-height: 44px; }
}

/* ── §1 focus-states ─────────────────────────────────────────────────────────
   :focus-visible, not :focus — a mouse click should not leave a ring behind,
   but every keyboard stop must show one. Two pixels and an offset so it reads
   against both the cream and the dark footer. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible {
  outline: 2px solid var(--green, #0c6545);
  outline-offset: 3px;
  border-radius: 3px;
}
footer a:focus-visible,
footer button:focus-visible { outline-color: #57c98e; }

/* ── §5 horizontal-scroll ────────────────────────────────────────────────────
   One overflowing element makes the whole document scroll sideways on a phone,
   and the usual culprit is a long unbroken string — a URL, a reference code, an
   e-mail address in the footer. */
/* `clip`, NOT `hidden`. overflow-x:hidden turns body into a scroll container,
   and a scroll container breaks position:sticky for everything inside it —
   .browser-index, .directory-aside and .category-intro all rely on it and would
   silently stop sticking. `clip` cuts the overflow without creating that
   container. Safari below 16 ignores it and simply keeps the old behaviour,
   which is the pre-existing state, not a regression. */
body { overflow-x: clip; }
.footer-v2 a, .foot-bottom span, .job-ref, code { overflow-wrap: anywhere; }

/* ── §6 line-length-control ──────────────────────────────────────────────────
   Prose past about 75 characters a line is measurably harder to track back
   from the end of one line to the start of the next. Applied to running text
   only — not to headings, which are short by nature and centre badly when
   width-capped. */
.page-hero p, .service-detail-grid > div > p, .about-body p, .privacy-body p {
  max-width: 68ch;
}

/* ── §3 image-dimension / content-jumping ────────────────────────────────────
   Any image that arrives after first paint and has no reserved box pushes the
   text under it downward as it loads. The width/height ATTRIBUTES in the markup
   are what reserve that box; this rule only keeps an image from overflowing a
   narrow column.

   There used to be an `img[width][height] { height: auto }` line here as well.
   Delete-on-sight if it returns: its specificity (0,1,1) ties with
   `.brand-logo img` and it sat further down the file, so it won that tie and
   the logo rendered at its intrinsic 138x120 instead of 34px tall — a mark
   four times its size overflowing the header on every page. A blanket
   attribute selector on `img` will out-rank component rules by accident, which
   is exactly the failure mode. */
img { max-width: 100%; height: auto; }

/* ── §7 reduced-motion, site-wide ────────────────────────────────────────────
   base.css already honours it in two places. This is the backstop for
   everything added since, including anything a future component brings. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}

/* =============================================================================
   Fixes for defects visible on the live service page
   ============================================================================= */

/* ── The header call-to-action was an underlined link ────────────────────────
   base.css gives .ghost-cta a background and a colour and nothing else. It is
   an <a>, so with no `display` it stays inline: the background paints only to
   the height of the text, the default underline is never removed, and it sits
   at a different height from the .lang button next to it, which is 40px.

   §4 primary-action: one obvious primary CTA per screen. This is it, so it gets
   to look like a button. */
.ghost-cta {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 40px;
  padding: 0 18px;
  border-radius: 999px;
  text-decoration: none;
  white-space: nowrap;
  transition: background .2s ease, transform .2s ease;
}
.ghost-cta:hover { background: var(--green); }
.ghost-cta:active { transform: translateY(1px); }

/* ── The related-services band bleached its own cards ────────────────────────
   `.related-band a { color: white; font-weight: 800 }` was written for the
   "see all" link in the band header. But a service card IS an <a>, so every
   card inside the band inherited both: the <h3> went white on a white card and
   disappeared, leaving a gap where the service name should be, and the summary
   turned bold.

   Scoped back rather than made more specific upstream, so the band's own link
   keeps its styling. */
.related-band .service-card { color: var(--ink); font-weight: 400; }
.related-band .service-card h3 { color: var(--ink); font-weight: 700; }
.related-band .service-card p { color: var(--muted); font-weight: 400; }
.related-band .service-card .price-line { color: var(--green); font-weight: 700; }
.related-band .service-card .badge,
.related-band .service-card .badge-note { color: var(--ink); font-weight: 400; }

/* ── The unused hero image hotlinked a stock photo ───────────────────────────
   .hero-image is referenced by no template, but it carried a background URL
   pointing at images.unsplash.com. Dead CSS does not fetch anything, so this
   costs nothing today — it is removed so that using the class tomorrow does not
   quietly put a third party in the render path of the hero. */
.hero-image { background-image: linear-gradient(180deg, transparent 70%, #0c2c1e66); }

/* =============================================================================
   Design tokens, and the three families that consume them
   ============================================================================= */

/* ── The scales ──────────────────────────────────────────────────────────────
   The stylesheets grew 33 distinct font sizes, four card paddings and five
   different field treatments, none of them related to each other by anything
   but the eye of whoever wrote that component. These are the values the site
   actually settles on, named once.

   This block does NOT restyle the site. It names what is already there and
   then makes the three families that visitors touch most — fields, cards and
   description text — agree with each other. Rewriting every component to use
   tokens would be a much larger change with no visible benefit and real
   regression risk on a live site. */
:root {
  /* Spacing: 4px base. Ten steps, and nothing invents a value between two of
     them. Named by multiple of 4 so --s-12 is unambiguously 48px. */
  --s-1:   4px;
  --s-2:   8px;
  --s-3:  12px;
  --s-4:  16px;
  --s-6:  24px;
  --s-8:  32px;
  --s-12: 48px;
  --s-16: 64px;
  --s-24: 96px;
  --s-30: 120px;

  /* Type. The floor is 12px; below that is unreadable and the checklist calls
     it an anti-pattern outright. --text-body is 16px because that is also the
     threshold under which mobile Safari zooms a focused input.

     The two headline steps are fluid rather than fixed. A fixed 64px H1 with a
     41px mobile override is two numbers that have to be kept in agreement by
     hand, and they drifted apart on four different heroes here. One clamp is
     one number, and it produces every size in between instead of snapping at
     an arbitrary breakpoint. */
  --text-micro: 12px;   /* eyebrows, tags, table meta — with letter-spacing */
  --text-small: 13px;   /* captions, footer links, badge notes */
  --text-meta:  14px;   /* secondary body, card descriptions */
  --text-body:  16px;
  --text-lead:  18px;
  --text-h4:    20px;                     /* card and tile titles */
  --text-h3:    24px;                     /* subsection, drawer, panel titles */
  --text-h2:    clamp(28px, 3.2vw, 40px); /* section headings */
  --text-h1:    clamp(36px, 5.4vw, 64px); /* the one page title */
  --text-data:  32px;                     /* metric figures, not headings */

  /* Tracking belongs in em, not px. Every heading here carried a px value
     tuned to its desktop size — -5px on a 72px H1 — and the mobile override
     changed the size without changing the tracking, so the same heading was
     set at -0.069em on desktop and -0.109em on a phone. In em it holds. */
  --track-h1: -.055em;
  --track-h2: -.04em;
  --track-h3: -.02em;

  /* Line height by role, not by element. Long prose needs air; headings
     tighten as they grow or they look like separate lines of text. */
  --lh-tight: 1.2;
  --lh-snug:  1.4;
  --lh-body:  1.6;
  --lh-prose: 1.7;

  --radius-lg:   18px;
  --radius-pill: 999px;

  --field-height: 48px;   /* >= the 44px touch minimum, with room for a border */

  /* The one palette change in this pass, and it is a legibility fix rather
     than a brand one: same hue, four steps darker. #69746f on --paper measured
     4.46:1, which is under the 4.5:1 AA floor — and --muted is the colour of
     every card description and lead paragraph on the site. #5f6a65 measures
     5.17 on --paper, 4.95 on --cream and 5.62 on white. */
  --muted: #5f6a65;
}

/* ── Fields ──────────────────────────────────────────────────────────────────
   Five patterns existed: .contact-form and .drawer-step at 12px padding,
   .detail-panel at 9px, .field at 12/14, .finder-input and .admin-search with
   no border at all. A visitor moving from the booking form to the panel met a
   different input each time.

   One shape, listed by container rather than applied to every input on the
   site, so the deliberately borderless search fields stay borderless. */
.field input, .field select, .field textarea,
.contact-form input, .contact-form select, .contact-form textarea,
.drawer-step input, .drawer-step select, .drawer-step textarea,
.detail-panel input, .detail-panel select, .detail-panel textarea {
  width: 100%;
  min-height: var(--field-height);
  padding: var(--s-3) var(--s-4);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm, 10px);
  background: var(--card, #fff);
  color: var(--ink);
  font-size: var(--text-body);
  line-height: var(--lh-snug);
  transition: border-color .18s ease, box-shadow .18s ease;
}
.field textarea,
.contact-form textarea,
.drawer-step textarea,
.detail-panel textarea {
  min-height: 128px;
  resize: vertical;
  padding-block: var(--s-3);
}
/* The hover state tells a pointer user the box is editable before they commit
   to clicking it. Kept distinct from focus, which is a stronger signal. */
.field input:hover, .field select:hover, .field textarea:hover,
.contact-form input:hover, .contact-form select:hover, .contact-form textarea:hover {
  border-color: var(--muted);
}
/* A select must not look like a text input it is not: a caret says "this
   opens". Drawn in CSS so it inherits the theme instead of shipping an icon. */
.field select, .contact-form select, .drawer-step select, .detail-panel select {
  appearance: none;
  padding-right: var(--s-8);
  background-image:
    linear-gradient(45deg, transparent 50%, currentColor 50%),
    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: right 20px center, right 14px center;
  background-size: 6px 6px, 6px 6px;
  background-repeat: no-repeat;
}
/* Placeholders are a hint, never the label — §8 input-labels. Held above the
   3:1 a large-text contrast needs so the hint is legible rather than a ghost. */
::placeholder { color: var(--muted); opacity: 1; }

/* Labels sit above their field, visible, always. */
.field > label, .contact-form label, .drawer-step label {
  display: block;
  margin-bottom: var(--s-2);
  font-size: var(--text-meta);
  font-weight: 600;
  color: var(--ink);
}
.field { margin-bottom: var(--s-6); }

/* Error text belongs BELOW the field it describes, not in a summary at the top
   of the form where the reader has to map it back. */
.field .field-error, .contact-form .field-error {
  display: block;
  margin-top: var(--s-2);
  font-size: var(--text-small);
  color: var(--red);
}
.field.invalid input, .field.invalid select, .field.invalid textarea {
  border-color: var(--red);
  box-shadow: 0 0 0 3px rgba(215, 72, 52, .14);
}

/* The consent checkbox is a control, not a paragraph: 20px is too small a
   target on its own, so the hit area comes from the label wrapping it. */
.check, .consent { display: flex; align-items: flex-start; gap: var(--s-3); }
.check input, .consent input {
  width: 20px; height: 20px; flex: 0 0 auto; margin-top: 2px;
  accent-color: var(--green);
}
.check label, .consent label {
  font-size: var(--text-meta); line-height: var(--lh-body); cursor: pointer;
}

/* ── Cards ───────────────────────────────────────────────────────────────────
   Padding was 26px on a service card, 20px on a job card, 12px elsewhere, and
   the radius varied with it. One padding and one radius; the card that needs
   to be denser says so, once. */
.service-card, .job-card, .gallery-item, .card {
  border-radius: var(--radius-lg);
  border: 1px solid var(--line);
  background: var(--card, #fff);
}
.service-card, .card { padding: var(--s-6); gap: var(--s-4); }
.job-card { padding: var(--s-6); gap: var(--s-3); }
.gallery-item { padding: 0; overflow: hidden; }
.gallery-item .gallery-body { padding: var(--s-6); display: grid; gap: var(--s-3); }

/* ── Descriptions ────────────────────────────────────────────────────────────
   The body copy inside a card. One size, one colour, one measure, wherever it
   appears — a card on the home page and the same card in the related band
   should not read differently. */
.service-card p, .job-card p, .gallery-item p, .card p {
  margin: 0;
  font-size: var(--text-meta);
  line-height: var(--lh-body);
  color: var(--muted);
}
.service-card h3, .gallery-item h2, .card h3 {
  margin: 0;
  font-size: var(--text-h4);
  line-height: var(--lh-snug);
  letter-spacing: -.3px;
  color: var(--ink);
}

/* =============================================================================
   Stage 1 · The type scale, applied

   The audit counted fourteen distinct heading sizes at or above 24px:
   72 64 58 54 49 46 43 42 41 39 38 37 36 34. That is not a scale, it is a
   history of individual decisions. Worse, ONE ROLE had four sizes — the page
   title was 72px on the home hero, 64px on .page-hero, 58px inside the service
   grid and 54px on contact. A visitor moving between those pages sees four
   different H1 and reads it as four different templates.

   Three bands replace all fourteen:

     page title      --text-h1   clamp(36 … 64)
     section heading --text-h2   clamp(28 … 40)
     subsection      --text-h3   24
     card / tile     --text-h4   20

   Everything is stated once, fluidly, so there is no desktop value and mobile
   value to keep in agreement. base.css loads first, so these rules win at every
   width — including inside the max-width:600px block that used to hold the
   mobile half of each pair. Those overrides are now dead weight rather than a
   second source of truth, which is the point.
   ============================================================================= */

/* ── Page title ──────────────────────────────────────────────────────────────
   One H1 per page and one size for it. Tracking moves from px to em (see the
   token note) so it stays proportional as the clamp resolves. */
.hero-main h1,
.page-hero h1, .category-hero h1, .service-detail-hero h1, .areas-page-hero h1,
.property-page-hero h1, .emergency-page-hero h1, .contact-page h1,
.service-detail-grid h1 {
  font-size: var(--text-h1);
  line-height: 1.04;
  letter-spacing: var(--track-h1);
}
/* The home hero keeps its own vertical rhythm; only the size is normalised. */
.hero-main h1 { margin: var(--s-4) 0 var(--s-6); }
.page-hero h1, .category-hero h1, .service-detail-hero h1, .areas-page-hero h1,
.property-page-hero h1, .emergency-page-hero h1, .contact-page h1 {
  margin: var(--s-4) 0 var(--s-6);
}

/* ── Section heading ─────────────────────────────────────────────────────────
   Eleven sizes between 29 and 49 collapse here. These are the headings that
   open a full-width band: they should all read as the same level, because
   structurally they are. */
.editorial-head h2, .emergency-v2 h2, .property-v2 h2, .areas h2, .closing h2,
.property-solutions h2, .emergency-issues h2, .property-console h2,
.related-band h2, .area-directory > div:last-child h2, .category-intro h2,
.related-services h2, .service-final h2, .directory-group h2 {
  font-size: var(--text-h2);
  line-height: 1.08;
  letter-spacing: var(--track-h2);
}

/* ── Subsection ──────────────────────────────────────────────────────────────
   Headings that open a block INSIDE a section, or sit in a drawer or panel.
   They were 31 / 28 / 25 / 26 / 24 and are one level, not five. */
.drawer-step h2, .drawer-success h2, .calendar-form h2, .contact-success h2,
.service-body h2, .category-block h3, .login-card h1, .admin-top h1 {
  font-size: var(--text-h3);
  line-height: var(--lh-snug);
  letter-spacing: var(--track-h3);
}

/* ── Card and tile titles ────────────────────────────────────────────────────
   The smallest heading that is still a heading. 21 / 20 / 19 become one. */
.flow-grid h3, .category-service-grid h3, .property-solutions h3 {
  font-size: var(--text-h4);
  line-height: var(--lh-snug);
  letter-spacing: var(--track-h3);
}

/* ── Figures are not headings ────────────────────────────────────────────────
   A metric is data. It gets its own step, off the heading scale on purpose,
   and tabular figures so a column of numbers does not shift as it updates. */
.metric-grid b {
  font-size: var(--text-data);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--track-h3);
}
/* Prices sit in a row with other prices; same reasoning. */
.price, .service-card .price, .price-line { font-variant-numeric: tabular-nums; }

/* ── Body copy ───────────────────────────────────────────────────────────────
   .site-main was 17px, which belongs to no scale and disagreed with the 16px
   that every component I touched yesterday was already using. One base size;
   the deliberately larger intro paragraphs say so themselves below. */
.site-main { font-size: var(--text-body); line-height: var(--lh-body); }

/* Lead paragraphs — the sentence under a page title — read as a distinct step
   rather than as body text that happens to be first. */
.page-hero > p, .category-hero > p, .service-detail-hero > p,
.hero-main > p, .editorial-head > p {
  font-size: var(--text-lead);
  line-height: var(--lh-prose);
  color: var(--muted);
}

/* ── Measure ─────────────────────────────────────────────────────────────────
   65–75 characters. Long prose set the full width of a 1440px viewport is
   roughly 160 characters a line, and the eye loses the return sweep. */
.page-hero > p, .category-hero > p, .service-detail-hero > p,
.hero-main > p, .editorial-head > p, .service-body p, .prose p {
  max-width: 68ch;
}

/* =============================================================================
   Stage 2 · One button system

   Eight classes were doing the work of three roles:

     white-btn ×8   closing-link ×8   primary-link ×2   gallery-cta ×2
     chat-btn ×2    ghost-cta ×1      btn-primary ×1    menu-btn ×1

   They disagreed on height (40 / 44 / 48 / auto), on radius (999px / 0 / none),
   on padding (0 18px / 14px 18px / 14px 20px) and on whether they were pills or
   rectangles. On the services page you could see three of them at once: a pill
   in the header, a square full-width slab under each card, and a third shape in
   the closing band. Three answers to "this is a thing you click".

   The existing class names are kept and aliased onto one base rather than being
   swapped out across eight templates. Renaming classes on a live site buys a
   tidier stylesheet and risks a page that ships with an unstyled link; the
   visitor cannot tell the difference, so the visitor's version wins.

   Roles, and only these three:
     primary    — the one action this screen wants. Filled.
     secondary  — a real alternative. Outlined.
     text       — a way out, or a minor jump. Underlined, no box.
   ============================================================================= */

/* ── The shared base ─────────────────────────────────────────────────────────
   min-height rather than height: a two-line label on a narrow phone must grow
   the button, not overflow it. 48px clears the 44px touch minimum with the
   border included. */
/* Scoped to the button classes themselves, never to a descendant selector like
   `.related-band a`. base.css groups its font-size that way and it is a trap:
   .related-band holds a .service-grid, so every card's link would become a
   pill. That exact rule bleached the card headings white on white last night. */
.btn,
.primary-link, .ghost-cta, .gallery-cta, .btn-primary, .chat-btn,
.closing-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  min-height: 48px;
  padding: var(--s-3) var(--s-6);
  border: 1px solid transparent;
  border-radius: var(--radius-pill);
  font-size: var(--text-meta);
  font-weight: 800;
  line-height: var(--lh-snug);
  text-align: center;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color .18s ease, border-color .18s ease,
              color .18s ease, transform .12s ease;
}
/* .closing-link carries display:flex!important and its own padding from
   base.css. Matching !important is the only thing that reaches it, and the
   alternative — editing the minified base.css by hand — is worse. */
.closing-link {
  display: inline-flex !important;
  padding: var(--s-3) var(--s-6) !important;
}

/* Press feedback, 120ms. Every clickable thing on the site now answers. */
.btn:active,
.primary-link:active, .ghost-cta:active, .gallery-cta:active,
.btn-primary:active, .chat-btn:active, .closing-link:active {
  transform: translateY(1px);
}

/* ── Primary ─────────────────────────────────────────────────────────────────
   Filled green on light ground. One per screen. */
.btn--primary,
.primary-link, .btn-primary, .gallery-cta, .ghost-cta {
  background: var(--green);
  color: #fff;
}
.btn--primary:hover,
.primary-link:hover, .btn-primary:hover, .gallery-cta:hover, .ghost-cta:hover {
  background: var(--dark);
  color: #fff;
}

/* Primary ON a dark band inverts: white fill, green label. This is what
   .white-btn already meant, so it keeps its name and its !important — it is
   layered onto .closing-link and has to beat the rule above. */
.white-btn {
  background: #fff;
  color: var(--green);
}
.white-btn:hover {
  background: var(--cream);
  color: var(--dark);
}

/* ── Secondary ───────────────────────────────────────────────────────────────
   Outlined. Reads as an alternative, not as a second primary. */
.btn--secondary, .chat-btn {
  background: transparent;
  border-color: var(--line);
  color: var(--ink);
}
.btn--secondary:hover, .chat-btn:hover {
  border-color: var(--green);
  color: var(--green);
  background: transparent;
}
/* On a dark footer the border has to come from the text colour, not --line. */
.footer-v2 .chat-btn, .site-footer .chat-btn {
  border-color: rgba(255, 255, 255, .32);
  color: #fff;
}
.footer-v2 .chat-btn:hover, .site-footer .chat-btn:hover {
  border-color: #fff;
  background: rgba(255, 255, 255, .08);
  color: #fff;
}

/* ── Text ────────────────────────────────────────────────────────────────────
   No box at all. For "back", "skip", and links that live inside a paragraph of
   controls. Underlined, because a link that is only a colour fails the
   colour-not-only rule for anyone who cannot see the colour. */
.btn--text {
  min-height: 44px;
  padding: var(--s-2) 0;
  background: none;
  border: 0;
  border-radius: 0;
  color: var(--green);
  text-decoration: underline;
  text-underline-offset: 4px;
}
.btn--text:hover { color: var(--dark); }

/* ── Width ───────────────────────────────────────────────────────────────────
   A button is as wide as its label. The two exceptions are deliberate: a form
   submit owns its column, and on a phone a primary action spanning the width
   is easier to hit than one floated left. */
.btn-primary { width: 100%; }
@media (max-width: 600px) {
  .service-card .primary-link,
  .closing-link,
  .gallery-cta { width: 100%; }
}

/* ── The card call-to-action ─────────────────────────────────────────────────
   This was the single heaviest element on the services page: display:inline-
   block inside a column flex container stretches, so "Loe lähemalt" rendered as
   a square dark-green slab the full width of every card — six of them stacked
   down the page, each shouting louder than the service name above it.

   A pill, left-aligned, sized to its label. The card is the thing being read;
   the button is the way out of it. */
.service-card .primary-link,
.category-service-grid .primary-link {
  align-self: flex-start;
  width: auto;
  margin-top: auto;   /* pins CTAs to a common baseline across a row of cards */
}

/* =============================================================================
   Stage 3 · Less on the page

   Two things were making the site read as assembled rather than designed: a
   full-bleed watermark behind the content, and boxes drawn inside boxes.
   ============================================================================= */

/* ── The backdrop watermark, off ─────────────────────────────────────────────
   .site-main:before painted /kodu-brand-backdrop.webp at 1400px wide and
   opacity .085 behind every page, and 180vw wide on a phone.

   Being honest about where this came from: the file was returning 404 until
   last night, so the watermark had never actually been visible. I found the
   asset in the v6 source and restored it, which "fixed" the 404 and put a
   metre-wide grey wordmark behind the areas page — the strongest thing on that
   screen after the map. A decoration that competes with content is not a
   decoration. The rule stays in base.css, disabled here, so the intent is
   recoverable; the image is not requested at all once the url() is gone. */
.site-main:before { content: none; }

/* ── The service card is one box, not four ───────────────────────────────────
   Each card drew: its own border, then a bordered-and-filled pill per badge —
   three of them — then a filled slab for the CTA. Five framed regions inside a
   frame, six times down the services page. Nothing is emphasised when
   everything is.

   The badges become what they always were semantically: the list of what the
   service includes. A checkmark, a line of text, no container. Same three
   items, same words, a quarter of the ink. */
.service-card .badge-row {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  margin: 0;
}
.service-card .badge {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: start;
  column-gap: var(--s-2);
  row-gap: 2px;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  font-size: var(--text-meta);
  font-weight: 500;
  line-height: var(--lh-snug);
  color: var(--ink);
}
/* The dot becomes a check: a bullet says "another item", a check says
   "included", and the second is the thing a customer wants to know. */
.service-card .badge::before {
  content: "\2713";
  width: auto;
  height: auto;
  margin: 0;
  border-radius: 0;
  background: none;
  color: var(--green);
  font-size: var(--text-meta);
  font-weight: 800;
  line-height: var(--lh-snug);
}
.service-card .badge .badge-note {
  grid-column: 2;
  font-size: var(--text-small);
  font-weight: 400;
  color: var(--muted);
}

/* The price is the first thing read on a card, so it is allowed to be the one
   piece of emphasis — but as type, not as a chip. */
.price-line { font-size: var(--text-meta); }
.price-line .amount { font-size: var(--text-h4); font-variant-numeric: tabular-nums; }

/* One shadow scale. The card lifted 3px on hover and dropped a --shadow-md,
   which on a grid of six reads as the whole page twitching. */
.service-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-sm); }
@media (prefers-reduced-motion: reduce) {
  .service-card:hover { transform: none; }
}

/* =============================================================================
   Stage 5 · Fixed elements on a phone

   The brief asks for chat 100 / floating CTA 90 / content 1. The first number
   is not reachable: Jivo injects its own container at z-index 2147483000 —
   INT_MAX minus a rounding — from its own bundle. No value we can write in a
   stylesheet outranks it, and trying is how you end up with 2147483001 in a
   codebase.

   So the layering below is ours, and the collision with Jivo is solved by
   geometry instead: give the widget its corner and keep our controls out of it.
   ============================================================================= */
:root {
  --z-content:  1;
  --z-header:   20;
  --z-langmenu: 60;
  --z-dock:     90;
  --z-backdrop: 100;
  --z-drawer:   110;
  --z-consent:  900;
  --z-skip:     999;
  --z-preroll:  9999;
  /* Jivo lives above all of it and is not ours to place. */

  --dock-height: 76px;
  --jivo-corner: 76px;   /* the widget bubble plus its own margin */
}

/* One declaration per layer, from the tokens. .mobile-dock had z-index twice —
   50 in base.css and 800 in components.css — which is not a value, it is two
   people guessing. */
.site-header    { z-index: var(--z-header); }
.lang-menu      { z-index: var(--z-langmenu); }
.drawer-backdrop{ z-index: var(--z-backdrop); }
.consent-banner { z-index: var(--z-consent); }
.skip-link      { z-index: var(--z-skip); }
.brand-preroll  { z-index: var(--z-preroll); }

@media (max-width: 600px) {
  .mobile-dock { z-index: var(--z-dock); }

  /* Reserve the dock's real height plus the gesture bar. 84px was measured
     against the old 66px dock; the buttons are 48px now. */
  body.has-dock {
    padding-bottom: calc(var(--dock-height) + env(safe-area-inset-bottom));
  }

  /* ── Jivo's corner ─────────────────────────────────────────────────────────
     includes/jivo.php sets data-jivo="ready" on <html> from the widget's own
     onLoad callback, so this only applies when the widget genuinely rendered —
     no reserved gap on a page where chat is switched off or failed to load.

     Two changes when it is there:
       · our dock chat button goes away. The site had two chat entry points
         eight pixels apart, one of them covered by the other. Jivo's bubble is
         the one that works, so it is the one that stays.
       · the dock keeps a clear lane on the right so the bubble sits over the
         bar's empty end rather than over the primary action. */
  html[data-jivo="ready"] .mobile-dock .chat-btn { display: none; }
  html[data-jivo="ready"] .mobile-dock {
    grid-template-columns: 1fr;
    padding-right: calc(var(--jivo-corner) + var(--s-3));
  }
}

/* The dock is a phone affordance. On a tablet and up the header CTA is visible
   and the dock is a second, redundant one. */
@media (min-width: 601px) {
  .mobile-dock { display: none; }
  body.has-dock { padding-bottom: 0; }
}

/* =============================================================================
   Stage 4 · Desktop body copy, and the holes in two grids

   ── The font complaint, found ───────────────────────────────────────────────
   "шрифтов маловиден" was not a matter of taste. Desktop prose on this site is
   set between 12px and 14px across twenty-seven selectors:

     12px  .category-service-grid p · .property-solutions p · .ops-panel p
           .service-detail-grid aside p
     13px  .category-intro p · .contact-grid > div:first-child > p
           .service-body li · .emergency-v2 p · .flow-grid p · .footer-v2 p
           .directory-group header p · .drawer-step > p · .property-v2 li  …
     14px  .editorial-head p · .page-hero p · .service-detail-grid > div > p

   The max-width:600px block already raises all of these to 16–18px, so the
   site has been legible on a phone and not on a laptop — the paragraph under
   the service title is 14px on a 1440px screen and 18px on a 390px one. That
   is backwards, and it is the whole of the reported problem.

   One size for reading text, one for leads, at every width. */

.editorial-head p,
.page-hero p, .category-hero .wrap > p, .areas-page-hero .wrap > p,
.property-page-hero .wrap > p, .emergency-page-hero p,
.service-detail-grid > div > p,
.contact-grid > div:first-child > p,
.category-intro p, .category-block header p, .category-service-grid p,
.directory-group header p, .directory-aside > p,
.area-directory > div:last-child > p,
.property-v2-grid > div:last-child > p, .property-solutions p,
.property-console p, .emergency-v2 p, .flow-grid p, .ops-panel p,
.contact-success p, .detail-panel section > p,
.drawer-step > p, .drawer-success p, .calendar-form .contact-step > p,
.service-body li, .property-v2 li {
  font-size: var(--text-body);
  line-height: var(--lh-prose);
}

/* The sentence directly under a page title carries more weight than the rest. */
.page-hero p, .category-hero .wrap > p, .areas-page-hero .wrap > p,
.property-page-hero .wrap > p, .service-detail-grid > div > p,
.editorial-head p {
  font-size: var(--text-lead);
}

/* The aside beside a service title is genuinely secondary — one step down, not
   four. It was 12px next to a 58px heading. */
.service-detail-grid aside p { font-size: var(--text-meta); line-height: var(--lh-body); }
.service-detail-grid aside b { font-size: var(--text-body); }

/* The footer was 13px grey on near-black. It is the last thing on every page
   and it held the address and the licence number. */
.footer-v2 p, .footer-v2 a, .footer-v2 span, .footer-v2 b {
  font-size: var(--text-meta);
  line-height: var(--lh-body);
}

/* ── The 200px hole on the service page ─────────────────────────────────────
   .service-detail-grid was `align-items: end`. When the right-hand aside is
   taller than the left column — which it is on every service that has a
   pricing note — the row grows to the aside's height and the left column is
   pushed to the BOTTOM of it. The empty band above the heading is not spacing;
   it is the difference between two column heights.

   Top-aligned, both columns start where the section starts. The 100px gutter
   comes down to 64px at the same time: at 1440px it was reading as two
   unrelated pages side by side. */
.service-detail-grid {
  align-items: start;
  gap: var(--s-16);
}

/* ── The empty right half on contact ────────────────────────────────────────
   Same gutter, and the columns rebalanced. .85fr/1.15fr put the wider half
   under the form, which is the shorter of the two. */
.contact-grid {
  gap: var(--s-16);
  align-items: start;
}

/* Sections were padded 80px / 70px / 65px / 55px. Two steps, both on scale. */
.service-body { padding-block: var(--s-16); }
.contact-page { padding-block: var(--s-16); }
@media (max-width: 900px) {
  .service-body, .contact-page { padding-block: var(--s-12); }
  .service-detail-grid, .contact-grid { gap: var(--s-8); }
}

/* The three-column service body needs its dividers to breathe on the scale
   rather than at 38px. */
.service-body article { padding-inline: var(--s-8); }
.service-body article:first-child { padding-left: 0; }
.service-body article:last-child { padding-right: 0; }

/* ── Re-assert what the button base would have undone ────────────────────────
   The .btn base above sets display:inline-flex on .ghost-cta, and it is the
   last rule in the cascade — which would have brought the header CTA back on
   phones, where base.css and section 11 both deliberately hide it because the
   bottom dock already carries that action. Two primary CTAs on a 390px screen,
   one of them duplicating the other.

   Stated after the base, so the base cannot win. Anything added to the button
   list later that is conditionally hidden needs the same treatment. */
@media (max-width: 600px) {
  .ghost-cta { display: none; }
}

/* The three components that render with the `hidden` attribute each carry their
   own .x[hidden] guard at (0,2,0), so they survive the button base at (0,1,0).
   This is the general form, so the next component to use `hidden` does not have
   to discover the rule the hard way: any author `display` beats the UA
   stylesheet's [hidden]{display:none}, and the button base sets display. */
[hidden] { display: none !important; }

/* ── Error red, to AA ────────────────────────────────────────────────────────
   #d74834 measures 4.32:1 on white and 3.97:1 on --paper. It is the colour of
   every field error and of the emergency card's action — the two places on the
   site where a visitor most needs to read the words, and the one place where
   failing contrast is not a cosmetic complaint.

   #c83d2a: 5.05 on white, 4.65 on --paper. Same red, one step down. The dark
   theme keeps its own #f0715c, which measures 6.4:1 on the dark card. */
:root { --red: #c83d2a; }

/* =============================================================================
   Round two — four things visible in the screenshots
   ============================================================================= */

/* ── 1 · "Teised teenused" was a dark band ───────────────────────────────────
   .related-band was background:var(--dark) — near-black green — and it sat
   directly under .closing, which is --green. Two heavy green blocks stacked,
   about 900px of saturated colour before the footer, which is dark again. The
   white cards floating on it were the only light in a third of the page.

   The band is now the page's own ground. That also retires the four override
   rules further up this file that were re-colouring the card contents back to
   readable — they set --ink and --muted, which is simply correct here, so they
   become no-ops rather than patches. */
.related-band {
  background: var(--paper);
  color: var(--ink);
  padding-block: var(--s-16);
  border-top: 1px solid var(--line);
}
.related-band h2 { color: var(--ink); }
/* base.css sets `.related-band a { color: white }`, and every card IS an <a>.
   On a light ground that is invisible text. */
.related-band a { color: inherit; }

/* ── 2+3 · Give each block on the service page its own surface ───────────────
   "Mis teenuse sisse kuulub", "Kuidas töö käib" and "Populaarsed lisatööd"
   were three columns of a 1fr/310px grid separated by nothing, floating on the
   page background — so the process column was squeezed into 310px and every
   step wrapped onto two lines, while the includes column had room to spare.

   Three equal cards instead. auto-fit means two blocks become two cards rather
   than one card and a gap, which matters because most services have no extras
   row yet. */
.service-detail-grid {
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: var(--s-6);
  align-items: stretch;
}
.service-detail-grid > div {
  padding: var(--s-8);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--card);
}
.service-detail-grid > div > h2 {
  margin-top: 0;
  padding-bottom: var(--s-4);
  border-bottom: 1px solid var(--line);
}
/* No `.service-detail-grid aside` rule here on purpose: base.css has five of
   them and no template renders that element, so they have never matched
   anything. Styling a selector that cannot occur is how a stylesheet grows a
   section nobody dares delete. */

/* The extras badges are inside a card now, so they can lose their own boxes
   for the same reason the service-card badges did. */
.service-detail-grid .badge-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
}
@media (max-width: 600px) {
  .service-detail-grid > div { padding: var(--s-6); }
}

/* ── 4 · The hero counters ───────────────────────────────────────────────────
   See the note in home.php: this panel borrowed .confidence-grid, whose
   block-level b/small live under a `.confidence` ancestor it does not have, so
   "500+" and its label rendered on one line with a stray column rule beside
   them. Its own three rules, and a <dl> underneath. */
.hero-stats {
  display: grid;
  gap: var(--s-3);
  margin: 0;
}
.hero-stat {
  padding: var(--s-4) var(--s-6);
  border-left: 2px solid var(--green);
  background: var(--card);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}
.hero-stat dt {
  font-size: var(--text-data);
  font-weight: 800;
  line-height: var(--lh-tight);
  letter-spacing: var(--track-h3);
  font-variant-numeric: tabular-nums;   /* 500+ over 40min over 4.9/5 */
  color: var(--ink);
}
.hero-stat dd {
  margin: var(--s-1) 0 0;
  font-size: var(--text-meta);
  line-height: var(--lh-snug);
  color: var(--muted);
}

/* ── 5 · Back to top ─────────────────────────────────────────────────────────
   Bottom LEFT, because Jivo takes the bottom right on every page and outranks
   any z-index we can write. 48px so it clears the touch minimum, and it sits
   below the drawer backdrop so it cannot float over an open dialogue. */
.to-top {
  position: fixed;
  left: var(--s-4);
  bottom: calc(var(--s-4) + env(safe-area-inset-bottom));
  z-index: var(--z-dock);
  display: grid;
  place-items: center;
  width: 48px;
  height: 48px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: var(--card);
  color: var(--ink);
  box-shadow: var(--shadow-md);
  cursor: pointer;
  opacity: 0;
  transform: translateY(8px);
  transition: opacity .2s ease, transform .2s ease,
              border-color .2s ease, color .2s ease;
}
.to-top.is-in { opacity: 1; transform: none; }
.to-top:hover { border-color: var(--green); color: var(--green); }
.to-top svg { width: 20px; height: 20px; }

/* On a phone the dock owns the bottom edge, so the button rides above it. */
@media (max-width: 600px) {
  .to-top {
    bottom: calc(var(--dock-height) + var(--s-3) + env(safe-area-inset-bottom));
  }
}

/* The fade is decoration; the button appearing is not. Under reduced-motion it
   simply is there, with no transform to animate. */
@media (prefers-reduced-motion: reduce) {
  .to-top { transform: none; opacity: 1; }
}

/* =============================================================================
   Header menu, compact (2026-09)

   Seven top-level links plus a "sub-services" button became four: Services
   (one panel: our services by category, partner projects by kind of job),
   Projects, About (about / reviews / service area) and Contact. At 1100px the
   old row wrapped onto two lines and ran into the logo.
   ============================================================================= */
.site-header nav { align-items: center; }
.nav-drop { position: relative; }
.site-header .nav-drop--mega { position: static; }
.nav-drop-toggle {
  display: inline-flex; align-items: center; gap: 4px;
  min-height: 44px; padding: 0; border: 0; background: none;
  font: inherit; font-size: 14px; font-weight: 700; color: inherit; cursor: pointer;
}
.nav-chev { width: 16px; height: 16px; transition: transform .2s ease; }
.nav-drop-toggle[aria-expanded="true"] .nav-chev { transform: rotate(180deg); }
.nav-drop-toggle[aria-expanded="true"], .nav-drop-toggle[data-current] { color: var(--green); }
.site-header nav > a[aria-current="page"] { color: var(--green); }

.nav-panel[hidden] { display: none !important; }
.nav-panel {
  position: absolute; z-index: var(--z-langmenu); top: calc(100% + 6px);
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--card); box-shadow: var(--shadow-lg);
}
.nav-list { left: -12px; min-width: 220px; padding: 6px; display: grid; }
.site-header nav .nav-list a {
  display: flex; align-items: center; min-height: 44px; padding: 0 12px;
  border-radius: 8px; font-size: 14px;
}
.site-header nav .nav-list a:hover { background: var(--card-2); }

.mega-panel {
  top: 100%; left: 50%; transform: translateX(-50%);
  width: min(1180px, calc(100vw - 32px));
  max-height: calc(100vh - 130px); overflow: auto;
  display: grid; grid-template-columns: minmax(220px, 1fr) 3fr;
  border-radius: 0 0 var(--radius) var(--radius);
}
.mega-own { padding: var(--s-6); background: var(--card-2); display: flex; flex-direction: column; gap: var(--s-4); }
.mega-partners { padding: var(--s-6); }
.mega-title { display: block; font-size: var(--text-micro); letter-spacing: .16em; text-transform: uppercase; color: var(--muted); margin-bottom: var(--s-3); }
.mega-partners > .mega-title { margin-bottom: var(--s-4); }
.mega-partner-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--s-6); }
.mega-group { display: flex; flex-direction: column; gap: 2px; }
.mega-cat { font-size: var(--text-small); font-weight: 800; color: var(--green); padding: 0 8px 6px; }
.site-header nav .mega-group a {
  display: flex; flex-direction: column; align-items: flex-start; gap: 1px; padding: 7px 8px; border-radius: 8px;
  font-size: 14px; font-weight: 700; color: var(--ink); line-height: 1.3; min-height: 0;
}
.site-header nav .mega-group a:hover { background: var(--card-2); }
.site-header nav .mega-own .mega-group a:hover { background: var(--card); }
.mega-group a small { font-size: 12px; font-weight: 500; color: var(--muted); }
.site-header nav .mega-all { margin-top: auto; padding: 8px; font-size: 14px; font-weight: 800; color: var(--green); }

@media (max-width: 1180px) {
  .mega-partner-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* On a phone the panels open inline, inside the menu sheet. */
@media (max-width: 900px) {
  .site-header nav.open { max-height: calc(100dvh - 120px); overflow: auto; align-items: stretch; }
  .site-header nav > a, .nav-drop-toggle { width: 100%; justify-content: space-between; padding: 14px 12px; font-size: 15px; min-height: 48px; border-radius: 8px; }
  .nav-panel, .mega-panel {
    position: static; transform: none; width: auto; max-height: none; overflow: visible;
    box-shadow: none; border: 0; border-radius: var(--radius-sm); margin: 0 0 8px;
  }
  .mega-panel { display: block; background: var(--card-2); }
  .mega-own, .mega-partners { padding: var(--s-3); background: transparent; }
  .mega-partner-grid { grid-template-columns: 1fr; gap: var(--s-4); }
  .nav-list { background: var(--card-2); padding: 4px; }
  .site-header nav .mega-group a { padding: 10px 8px; }
}

/* =============================================================================
   Homepage search hero (2026-09)

   The finder styles from the v6 design (.finder, .finder-input, .suggestions,
   .smart-results in base.css) are the base; this only centres them, rounds
   them to match the site's pill buttons and themes them through the tokens.
   ============================================================================= */
.hero-search {
  position: relative; z-index: 2;
  background: var(--paper);
  padding-block: clamp(36px, 6vw, 80px) clamp(36px, 6vw, 72px);
}
.hero-search-inner { display: flex; flex-direction: column; align-items: center; text-align: center; }
.hero-search-kicker {
  display: flex; flex-wrap: wrap; justify-content: center; gap: 4px 10px; margin: 0;
  font-size: var(--text-micro); font-weight: 800; letter-spacing: .18em; color: var(--green);
}
.hero-search h1 {
  max-width: 22ch; margin: var(--s-4) 0; text-wrap: balance;
  font-size: clamp(34px, 4.8vw, 58px); line-height: 1.04; letter-spacing: var(--track-h1);
}
.hero-search h1 em { font-family: var(--font-serif), Georgia, serif; font-weight: 500; color: var(--green); }
.hero-search-lead { max-width: 620px; margin: 0; font-size: var(--text-lead); line-height: 1.65; color: var(--muted); text-wrap: pretty; }

.home-finder { width: min(760px, 100%); max-width: none; margin-top: var(--s-8); text-align: left; }
.home-finder .finder-input {
  height: 64px; grid-template-columns: 52px 1fr auto;
  border: 1px solid var(--muted); border-radius: 999px;
  background: var(--card); box-shadow: var(--shadow-md);
  transition: border-color .15s ease, box-shadow .15s ease;
}
.home-finder .finder-input:focus-within { border-color: var(--green); box-shadow: var(--shadow-md), var(--focus-ring); }
.home-finder .finder-input input { background: transparent; color: var(--ink); font-size: 17px; }
.home-finder .finder-input input:focus-visible { outline: 0; box-shadow: none; }
.home-finder .finder-input input::-webkit-search-cancel-button { filter: grayscale(1); opacity: .55; cursor: pointer; }
.home-finder .finder-input button { height: 50px; margin-right: 6px; gap: 10px; padding: 0 22px; border-radius: 999px; font-size: 15px; }
.home-finder .finder-input button:hover { filter: brightness(1.08); }

.home-results {
  top: 74px; z-index: 30; overflow: auto; max-height: min(470px, 62vh);
  border-radius: var(--radius); background: var(--card); box-shadow: var(--shadow-lg); padding: 6px 0;
}
.home-results-group + .home-results-group { border-top: 1px solid var(--line); margin-top: 4px; padding-top: 4px; }
.home-results-label { display: block; padding: 8px 18px 4px; font-size: var(--text-micro); font-weight: 800; letter-spacing: .14em; text-transform: uppercase; color: var(--muted); }
.home-results-empty { margin: 0; padding: 12px 18px 6px; font-size: var(--text-meta); color: var(--muted); }
.home-result {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  padding: 10px 18px; text-decoration: none; color: var(--ink);
}
.home-result:hover, .home-result[aria-selected="true"] { background: var(--card-2); }
.home-result[aria-selected="true"] { box-shadow: inset 3px 0 0 var(--green); }
.home-result-text { min-width: 0; }
.home-result b { display: block; font-size: 15px; font-weight: 700; }
.home-result small { display: block; margin-top: 2px; font-size: var(--text-small); color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.home-result strong { flex: none; color: var(--green); display: flex; }
.home-result strong svg { width: 16px; height: 16px; }
.home-result--request b { color: var(--green); }

.home-finder .suggestions { justify-content: center; gap: 8px; margin-top: var(--s-4); }
.home-finder .suggestions span { font-size: var(--text-small); }
.home-finder .suggestions button {
  min-height: 36px; padding: 0 14px; border: 1px solid var(--line); border-radius: 999px;
  background: var(--card); color: var(--ink); text-decoration: none; font-size: var(--text-small); font-weight: 600;
}
.home-finder .suggestions button:hover { border-color: var(--green); color: var(--green); }

.hero-stats--inline { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--s-4) var(--s-12); margin-top: var(--s-12); }
.hero-stats--inline .hero-stat { padding: 0; border: 0; background: none; border-radius: 0; text-align: center; }
.hero-stats--inline .hero-stat dt { font-size: var(--text-h3); }

@media (max-width: 600px) {
  .hero-search-lead { font-size: 16px; }
  .home-finder .finder-input { height: 56px; grid-template-columns: 42px 1fr auto; padding-top: 0; }
  .home-finder .finder-input input { height: 100%; font-size: 16px; }
  .home-finder .finder-input button { grid-column: auto; width: 44px; height: 44px; margin: 0 5px 0 0; padding: 0; justify-content: center; }
  .home-finder .finder-input button span { position: absolute; left: -9999px; }
  .home-results { top: 64px; max-height: min(420px, calc(100dvh - 250px)); }
  .home-result small { white-space: normal; }
  .hero-stats--inline { gap: var(--s-4) var(--s-8); margin-top: var(--s-8); }
}
@media (prefers-reduced-motion: reduce) {
  .nav-chev, .home-finder .finder-input { transition: none; }
}
