/* The skeleton every generated site shares.

   One file, not one per site: the structure is the same and a change should
   reach every site already built. It also has to be a file rather than an
   inline block — generated sites are served from the same origin as the
   funnel, under default-src 'self' with no 'unsafe-inline', so a <style> block
   would be dropped and every site would render unstyled.

   ## What belongs here, and what belongs in a pack

   This file owns *structure*: what is a grid, what is sticky, what stacks on a
   phone, what the claim gate does. It states almost no design opinion of its
   own — every colour, radius, weight and rhythm below is read from a custom
   property, and a pack (pack-*.css, loaded after this) sets those.

   The values in :root are therefore two things at once: the Slate look's
   defaults, and the contract a pack is filling in. A pack that sets nothing
   still renders a complete, legible site; a pack that sets all of it is a
   different site.

   Colour is the one part of that contract a pack must state in full — an
   inherited colour is how a palette ends up internally inconsistent in a way
   nobody sees until it is on a customer's site. Spacing and type may fall
   through; a pack with no opinion about the gutter should not have to have
   one. `tests/test_sitegen.py` holds both halves of that rule, along with the
   contrast floor every pack's colours have to clear.

   Deliberately not yellow, in any pack. This is the customer's website, not
   ours; it should look like their business rather than like the tool that made
   it. */

/* Self-hosted fonts (sitegen/fonts.py). Served from this same origin via
   /s/_assets/, so default-src 'self' allows them where a Google Fonts URL
   would be dropped. Each pack's --font / --font-display below still lists the
   old system stack as the fallback, so a site whose font fails to load looks
   exactly as it did before these existed. font-display: swap — show the
   fallback immediately, swap the real face in when it arrives. */
@font-face {
  font-family: "Inter"; font-style: normal; font-weight: 400; font-display: swap;
  src: url("/s/_assets/inter-400.woff2") format("woff2");
}
@font-face {
  font-family: "Inter"; font-style: normal; font-weight: 600; font-display: swap;
  src: url("/s/_assets/inter-600.woff2") format("woff2");
}
@font-face {
  font-family: "Inter"; font-style: normal; font-weight: 700; font-display: swap;
  src: url("/s/_assets/inter-700.woff2") format("woff2");
}
@font-face {
  font-family: "Fraunces"; font-style: normal; font-weight: 400; font-display: swap;
  src: url("/s/_assets/fraunces-400.woff2") format("woff2");
}
@font-face {
  font-family: "Fraunces"; font-style: normal; font-weight: 600; font-display: swap;
  src: url("/s/_assets/fraunces-600.woff2") format("woff2");
}
@font-face {
  font-family: "Space Grotesk"; font-style: normal; font-weight: 500; font-display: swap;
  src: url("/s/_assets/space-grotesk-500.woff2") format("woff2");
}
@font-face {
  font-family: "Space Grotesk"; font-style: normal; font-weight: 700; font-display: swap;
  src: url("/s/_assets/space-grotesk-700.woff2") format("woff2");
}
@font-face {
  font-family: "Quicksand"; font-style: normal; font-weight: 500; font-display: swap;
  src: url("/s/_assets/quicksand-500.woff2") format("woff2");
}
@font-face {
  font-family: "Quicksand"; font-style: normal; font-weight: 700; font-display: swap;
  src: url("/s/_assets/quicksand-700.woff2") format("woff2");
}

:root {
  /* palette */
  --ink: #16181d;
  --muted: #5b6472;
  --line: #e3e6ea;
  --paper: #ffffff;
  --wash: #f6f8fa;
  --accent: #14539a;
  --accent-deep: #0e3d73;
  --on-accent: #ffffff;
  /* The same accent, lightened enough to be legible on --band. An accent
     picked to pass on white is usually 2-3:1 on near-black, so a pack that
     inverts anything needs both. */
  --accent-light: #7db3ea;
  /* A dark full-width band — the footer, and whatever else a pack wants to
     invert. Held apart from --ink so a pack can have near-black text on a band
     that is not black. */
  --band: #16181d;
  --on-band: #f2f4f7;

  /* type. The faces are self-hosted (see the @font-face block above and
     sitegen/fonts.py) rather than pulled from a CDN — default-src 'self'
     drops a Google Fonts URL, so a pack whose identity rested on one would
     fall back to something it never chose. Every value keeps the old system
     stack after the web face as the fallback: sans, serif and rounded still
     read as three different kinds of business even if a font never loads. */
  --font: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-display: var(--font);
  --body-size: 17px;
  --h1-size: 2.6rem;
  --h1-size-wide: 3.2rem;
  --h2-size: 1.75rem;
  --lead-size: 1.15rem;
  --display-weight: 700;
  /* The hero catchline's weight. A pack whose display face has no 600 sets
     the weight it does have (Space Grotesk: 500). */
  --catchline-weight: 600;
  --display-tracking: -.02em;
  --kicker-transform: uppercase;
  --kicker-tracking: .1em;

  /* shape */
  --radius-card: 12px;
  --radius-cta: 10px;
  --radius-shot: 12px;
  --card-bg: var(--paper);
  --card-border: 1px solid var(--line);
  --card-shadow: none;
  --card-pad: 1.25rem;
  --shot-bg: var(--line);

  /* rhythm */
  --section-y: 3rem;
  --hero-y: 4rem;
  --hero-bg: transparent;
  --gutter: 1.25rem;
  /* The width every section, grid and full-bleed hero's inner content is
     held to. Prose still gets its own, tighter measure regardless of this
     one — .lead is capped at 36rem and .about p at 40rem — so this governs
     structure rather than line length.

     It has been 56rem (too narrow: a card grid is not a paragraph, and the
     page read as a column with unused page either side) and then 75rem, and
     75rem was the worse mistake. This product's whole premise is that a
     tradie fills in nine short fields, so the content is always short — and
     1200px of measure with three services and a two-line About produced a
     page that was mostly gaps. Blank space on a small business's site does
     not read as confident restraint, it reads as a template with holes in
     it, which is the exact thing a customer is being asked to trust.

     64rem holds the same content close enough together to look composed.
     The rule of thumb when changing it: set it by the SHORTEST plausible
     site, not the longest. Any site here can be the shortest one. */
  --measure: 64rem;

  /* chrome */
  --header-bg: var(--paper);
  --header-ink: var(--ink);
  --header-border: 1px solid var(--line);
}

* { box-sizing: border-box; }
body {
  /* Business names and service text may contain long unbroken words. */
  overflow-wrap: anywhere;
  margin: 0; background: var(--paper); color: var(--ink);
  font: var(--body-size)/1.6 var(--font);
}
h1, h2, h3 {
  font-family: var(--font-display);
  font-weight: var(--display-weight);
  line-height: 1.15; letter-spacing: var(--display-tracking);
}
h2 { font-size: var(--h2-size); }
section {
  padding: var(--section-y) var(--gutter); max-width: var(--measure); margin: 0 auto;
  position: relative;   /* anchors an .edit-pencil, when there is one — see below */
}

/* The one band above the header — see site.html. Owner-only: it renders only
   when `editable` is true, never on the public rendering. It carries the trial
   countdown, the way back to the public rendering, and the way into settings:

     [7 Days left in Trial | View Live Site | Settings]

   Fixed blue rather than the pack's accent, because this band is our chrome
   and not the business's: an accent-coloured band on an orange or a green pack
   would read as part of the page rather than as a note about it. The same
   precedent as .claim-gate's own backing below.

   nowrap + overflow-x: auto rather than flex-wrap: wrap, which is what the old
   .trial-banner did and what made it two lines on a phone. A band that scrolls
   sideways stays one line at every width. */
.site-chrome { position: sticky; top: 0; z-index: 6; }
.portal-band {
  display: flex; align-items: center; justify-content: center; gap: .5rem;
  flex-wrap: nowrap; overflow-x: auto;
  padding: .5rem var(--gutter);
  font-size: .8rem; white-space: nowrap;
  background: #14539a; color: #fff;
}
.portal-band .band-link {
  color: inherit; font-weight: 700; text-decoration: none; white-space: nowrap;
}
.portal-band .band-link:hover, .portal-band .band-link:focus-visible {
  text-decoration: underline;
}
.band-days { font-weight: 600; }
/* The owner's nudge band (#262) under the trial band: a sentence rather than
   a row of links, so it wraps on a phone instead of scrolling, and lighter
   than the band above so the two read as separate things. Fixed colours for
   the same reason as .portal-band: this is our chrome, not the business's. */
.portal-band.portal-band-nudge {
  flex-wrap: wrap; white-space: normal; text-align: center; gap: .25rem .6rem;
  background: #e8f0fb; color: #0e3d73;
}
.portal-band-nudge .band-link { text-decoration: underline; }
.band-sep { opacity: .6; }

.site-header {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  flex-wrap: nowrap;   /* all three children stay on the one row — see below */
  padding: 1rem var(--gutter); border-bottom: var(--header-border);
  background: var(--header-bg); color: var(--header-ink);
  z-index: 5;
}
/* No longer sticky of its own accord: it is one half of .site-chrome above,
   which sticks the bands and the masthead as a single block so the header
   always sits directly under whichever bands rendered. Two independently
   sticky siblings would need the lower one offset by the upper one's height,
   which changes the moment a band wraps to two lines on a narrow phone. */
.logo {
  font-family: var(--font-display); font-weight: 800; font-size: 1.05rem;
  letter-spacing: var(--display-tracking);
  /* Never truncated: a business name cut off with an ellipsis reads as
     broken — it is the customer's own name being mangled. A name too long for
     the header wraps in full instead of being cut. overflow-wrap: anywhere
     breaks an unbroken name, and min-width: 0 lets this flex item shrink
     below its content so the wrap happens inside the header rather than
     pushing the page wider. Below the phone breakpoint the masthead still
     shows the brand mark alone and hides .logo — see the narrow block below —
     which is omission, not truncation. */
  white-space: normal;
  overflow-wrap: anywhere;
  min-width: 0;
}
/* The default brand mark, shown when a business has not uploaded a logo —
   see site.html. Coloured from --accent/--on-accent rather than a fixed
   value, so it matches whichever pack, palette or custom colours the page
   is already using, and it needs no image file to do it. Sized against
   .logo's own 1.05rem/1.6 above rather than picked arbitrarily: 1.8rem is
   a little taller than that line box, which is what makes it read as a
   mark beside the name rather than as another word. .logo-link's own
   inline-flex/align-items already lays the two out side by side. */
.logo-mark {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.8rem; height: 1.8rem; margin-right: .5rem;
  border-radius: 8px;
  background: var(--accent); color: var(--on-accent);
  font-weight: 800; font-size: .8rem; letter-spacing: .02em;
  flex: 0 0 auto;
}
/* The header's logo is a link back to the top of the page whether it holds
   an uploaded image or the plain text name — see site.html. No underline
   and no colour of its own: it inherits the header's own ink, so a pack
   that inverts the header gets a legible logo without saying anything. */
.logo-link {
  display: inline-flex; align-items: center; text-decoration: none; color: inherit;
  /* The one child allowed to give up width when the header is narrow — the
     nav toggle keeps all of its own. min-width: 0 is what lets a flex item
     shrink below its content's own width at all, and the flex-grow here is
     also what puts the nav on the right: it is the only child that expands,
     so everything after it is pushed to the far edge. */
  flex: 1 1 auto; min-width: 0;
}
/* A fixed height with auto width, so a wide wordmark and a square mark both
   sit on the same baseline as the header's own text. */
.logo-image { height: 2rem; width: auto; display: block; }

/* --- the header nav ---------------------------------------------------------
   Scroll links to whichever sections this brief actually rendered — see
   render.py's _nav_items, which decides the list. A business with no
   testimonials gets no "Testimonials" link, so nothing here is ever a dead
   anchor.

   The mobile collapse is a checkbox and a label, not a script: a generated
   page is served under default-src 'self' with no 'unsafe-inline', so an
   inline <script> is dropped and a control that only works with scripting is
   a control that does not work. Same technique as .contact-tabs below — the
   input comes first in the markup so the label and the links are its
   siblings, and :checked plus the sibling combinator do the whole job. The
   input is .sr-only rather than display:none, which is what keeps it
   focusable.

   The nav is at the right-hand end of the header at every width, and no
   `order` declaration is involved: .logo-link is the only child of the
   header that grows, so it takes the free space and the nav is pushed against
   the far edge. Below the breakpoint that puts the hamburger on the right;
   above it, the links. Nothing else is in the header — see site.html for why
   the CTA button that used to sit here is gone, and why the phone number that
   briefly replaced it now lives in the hero instead. */
.site-nav { display: flex; align-items: center; }
.nav-toggle-input { position: absolute; }   /* .sr-only already hides it */
.nav-toggle-btn {
  display: none; cursor: pointer; font-size: 1.4rem; line-height: 1;
  padding: .25rem .5rem; color: inherit;
}
.nav-links { display: flex; flex-wrap: wrap; gap: 1rem; }
.nav-links a {
  color: inherit; text-decoration: none; font-weight: 600; font-size: .92rem;
}
.nav-links a:hover { text-decoration: underline; }
/* The section the reader is in, marked by nav.js with aria-current, and the
   link under a finger. A soft tint of the header's own ink rather than a
   colour of its own, so it reads on a light header and a dark one alike and
   no pack has to say anything; on desktop the underline does the same job.
   The tap tint is what the phone menu was missing: a tapped link closed the
   menu with no change at all, which read as the tap not registering. */
.nav-links a[aria-current] { text-decoration: underline; text-underline-offset: 4px; }
.nav-links a:active { background: color-mix(in srgb, currentColor 18%, transparent); }
/* The same ring .edit-pencil and the contact tabs use. */
.nav-toggle-input:focus-visible ~ .nav-toggle-btn {
  outline: 3px solid var(--ink); outline-offset: 2px;
}

.cta {
  display: inline-block; background: var(--accent); color: var(--on-accent);
  text-decoration: none; font-weight: 700;
  padding: .7rem 1.1rem; border-radius: var(--radius-cta);
}
.cta:hover { background: var(--accent-deep); }
.cta.big { padding: 1rem 1.75rem; font-size: 1.05rem; margin-top: 1.5rem; }

.hero { padding-top: var(--hero-y); padding-bottom: var(--hero-y); }
.kicker {
  margin: 0 0 .6rem; font-size: .78rem; font-weight: 800;
  letter-spacing: var(--kicker-tracking); text-transform: var(--kicker-transform);
  color: var(--accent);
}
.hero h1 { margin: 0 0 1.25rem; font-size: var(--h1-size); overflow-wrap: anywhere; text-wrap: balance; }
.lead { margin: 0; font-size: var(--lead-size); color: var(--muted); max-width: 36rem; }
/* A hero with no catchline and nothing to say beyond its kicker prints an
   empty lead (render.py omits a line that would only repeat "Glazier in
   Ringwood"); it takes no space rather than leaving a gap. */
.lead:empty { display: none; }
/* The optional line under the lead — the catchline's or generated sentence's
   own quieter second line. Smaller and quieter than .lead, and only ever in the
   markup when the business actually wrote one. */
.lead-detail { margin: .6rem 0 0; font-size: 1rem; color: var(--muted); max-width: 36rem; }
/* The hero's phone number, on its own line under the CTA. Contact information,
   not a second button: it inherits the hero's ink (white over a photo or band)
   and only underlines on hover, so the single filled thing in the hero stays
   the CTA. display: block so it sits below the inline CTA rather than beside
   it, and the whole line is the tap target on a phone. */
/* Tap-to-call is the highest-value action on a trade site, and the text alone
   was a 27px target on a phone — over the WCAG 2.2 minimum of 24px but well
   under the 44px a thumb actually needs. The padding buys the height without
   changing the type size.

   inline-block, not block: a block-level link would make the full width of the
   hero tappable, so a thumb landing anywhere on that line would start dialling.
   Shrink-wrapped, it stays centred in a centred hero and left in a left-aligned
   one, because either way it is an inline box its parent is already aligning. */
/* A ghost button: the CTA's size and radius, outlined in the hero's own ink
   instead of filled, so it reads as the deliberate second thing to press
   rather than a number that was left beside the button. Reviewed by Gemini
   on 23 September 2026 after the masthead copy was removed; the outline
   subordinates it to the filled CTA rather than competing with it. The
   border is taken out of the padding so the two stand the same height. */
.hero-phone {
  display: inline-block;
  margin-top: .75rem;
  padding: calc(1rem - 2px) 1.5rem;
  border: 2px solid currentColor; border-radius: var(--radius-cta);
  color: inherit;
  text-decoration: none;
  font-weight: 700; font-size: 1.05rem;
  /* A phone number is allowed to wrap between its digit groups when a narrow
     screen and enlarged text leave no room for the whole thing on one line;
     keeping it on one line was how it overflowed the page. It stays a single
     <a>, so the whole block is still one tap target, and overflow-wrap:
     anywhere keeps even an unspaced number inside the hero. */
  overflow-wrap: anywhere;
}
.hero-phone:hover { background: color-mix(in srgb, currentColor 14%, transparent); }
/* The gap sits on the button, not the phone: when a narrow screen wraps the
   phone onto its own line it then starts flush with the button above it
   instead of indented by the gap. Two 44px+ targets used to be 5px apart. */
.hero .cta.big { margin-right: .75rem; }

.about p { margin: 0 0 1rem; color: var(--muted); max-width: 40rem; }
.about p:last-child { margin-bottom: 0; }

/* "Where we work" used to be a heading and a list of suburb names, which on a
   phone was one word marooned in a full-height band. The map is the section's
   content now and the names read as a caption beside it. */
.area-body { display: grid; gap: 1.5rem; align-items: start; }
.area-list {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: .5rem;
}
/* Pills rather than bullets: a served-suburbs list is a set, not a sequence,
   and the set reads at a glance where a stacked column of single words does
   not. --surface, never --accent — see the accent-as-text guard in the app's
   own build tests for why. */
.area-list li {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 999px; padding: .3rem .75rem;
  font-size: .9rem; font-weight: 600;
}
.area-address { margin: 1rem 0 0; color: var(--muted); }
.area-map { display: inline-block; margin-top: .5rem; }

/* The iframe has no intrinsic aspect ratio and defaults to 300x150, so the
   wrapper is what decides the shape. 4:3 on a phone shows enough of the
   surrounding streets to orient; the desktop rule below goes squarer because
   the frame is only half the width there. */
.area-map-frame {
  aspect-ratio: 5 / 4; border-radius: var(--radius);
  overflow: hidden; border: 1px solid var(--line); background: var(--surface);
}
.area-map-frame { position: relative; }
.area-map-frame iframe { display: block; width: 100%; height: 100%; border: 0; }
/* Covers the frame so a tap anywhere on the map opens Google Maps. Sits above
   the iframe rather than around it — an iframe consumes its own clicks, so a
   wrapping anchor would never fire. */
.area-map-hit { position: absolute; inset: 0; cursor: pointer; }
/* Styled explicitly, because this is the only link on the page that sits
   outside a nav, a card or a button, and left alone it renders in the
   browser's default blue-and-underlined — which on a site whose whole point
   is looking chosen rather than defaulted is the most visible tell there is. */
.area-credit { margin: .75rem 0 0; font-size: .85rem; }
.area-credit a { color: var(--muted); text-decoration-color: var(--line); }
.area-credit a:hover { color: var(--ink); }

@media (min-width: 48rem) {
  /* The map takes the measure at 3:1 with the suburb pills above it as its
     caption. It used to sit capped at 30rem on the right with the pills
     centred beside it, which on a desktop left one or two pills floating in
     white space next to a square map: finding 3.2b, "so much empty space in
     its multi-column approach". The owner chose this layout for Ironbark
     first (db479e1) and #256 extends it to every pack. Across the measure the
     map is the section's subject rather than its illustration, and a wider
     frame shows the surrounding suburbs, which is what a local visitor is
     actually checking. */
  .area-body { grid-template-columns: minmax(0, 1fr); gap: 1.5rem; max-width: none; }
  .area-detail { align-self: start; }
  .area-map-frame { aspect-ratio: 3 / 1; }

  /* Two lines of opening hours cannot fill the measure as a paragraph, and
     spreading them apart to try would read as a mistake. A band does it
     instead: the wash runs the width, the hours sit at a size worth reading,
     and the section reads as a deliberate block rather than text that ran
     out. Colours are the pack's own tokens, so every pack gets its own band. */
  .hours-text {
    background: var(--wash); border-left: 3px solid var(--accent);
    padding: 1.25rem 1.5rem; font-size: 1.1rem; line-height: 1.7;
  }
}
@media (max-width: 47.99rem) {
  /* Source order puts the names first so the section still makes sense with
     no stylesheet and so the pencil's section lands on text, but on a phone
     the map is the thing worth leading with. */
  .area-map-col { order: -1; }
}

/* A one-line promo, full width, above the hero — the same "strip" idea as
   .portal-bands (above) but with the opposite weight: the bands are a quiet
   status line about the site, this is the business's own message and
   is allowed to be the loudest thing on the page. */
.offer-banner {
  background: var(--accent); color: var(--on-accent);
  text-align: center; padding: .7rem var(--gutter);
  /* Clears the pencil chip (2rem wide, .6rem from the right edge) so the
     banner's own text never runs underneath it. */
  padding-right: 2.75rem;
  position: relative;   /* not a <section>, so it needs its own anchor for the pencil */
}
.offer-banner p { margin: 0; font-weight: 700; font-size: .95rem; }
/* The "add an offer" prompt, not the business's own text — see offer.html.
   Distinguished by weight and style only, never colour: .placeholder's muted
   grey is validated against --paper/--wash, not against this banner's own
   accent background, so a colour change here would need its own contrast
   check. Weight and style never affect contrast ratio. */
.offer-banner p.is-placeholder { font-style: italic; font-weight: 600; }

/* Full-bleed washed bands, with their contents held to the same measure as
   every other section. Which sections get the wash is a structural decision,
   so it lives here rather than in a pack.

   The cap is the measure *minus the gutter*, not the measure. A plain
   section's own horizontal padding sits inside its max-width (`* {
   box-sizing: border-box }` above), so its content is --measure minus
   2 × --gutter wide and its left edge is one gutter in from that box. A
   band's background has to reach the page edges, so its padding stays on the
   section — which means capping the children at the full --measure left every
   heading in here one gutter further out than every heading between them, and
   its card grid that much wider on both sides. Each block was right on its
   own, which is why it read as a page with two widths rather than as a bug.
   .hero-inner below holds its banner to the measure the same way this does. */
.services, .gallery { background: var(--wash); max-width: none; }
.services > *, .gallery > * {
  max-width: calc(var(--measure) - 2 * var(--gutter));
  margin-left: auto; margin-right: auto;
}
.cards { display: grid; gap: 1rem; grid-template-columns: 1fr; }
.card {
  background: var(--card-bg); border: var(--card-border);
  border-radius: var(--radius-card); padding: var(--card-pad);
  box-shadow: var(--card-shadow);
}
.card h3 { margin: 0; font-size: 1.15rem; }
/* A service's description, when it has one — see sections/services.html.
   Muted like .about p, and with the UA default's top margin removed: the
   card's own padding is the gap between the name and this. */
.card p { margin: .5rem 0 0; color: var(--muted); font-size: .95rem; }

/* Operating hours are free text and typed as lines — "Mon-Fri 7am-4pm" then
   "24/7 for emergencies" — so the line breaks the owner typed are content,
   not whitespace to collapse. */
.hours-text { white-space: pre-wrap; }

.shot {
  background: var(--shot-bg); border-radius: var(--radius-shot); aspect-ratio: 4 / 3;
  overflow: hidden;   /* clips a real photo to the tile's own corners */
}
.shot img { display: block; width: 100%; height: 100%; object-fit: cover; }
.placeholder { color: var(--muted); font-size: .9rem; font-style: italic; }

/* A tinted circle behind a small inline SVG (see templates/_icons.html) — the
   why-us and services cards' one bit of visual weight beyond a border. Reads
   from --accent and --radius-card, the same two properties every pack
   already sets for its own reasons, so the icon matches whichever pack
   renders it without a pack having to say anything extra about it. */
.card-icon {
  width: 2.75rem; height: 2.75rem; margin: 0 0 .9rem;
  display: flex; align-items: center; justify-content: center;
  border-radius: var(--radius-card);
  background: color-mix(in srgb, var(--accent) 12%, var(--paper));
  color: var(--accent);
}
.card-icon svg { width: 1.5rem; height: 1.5rem; }

/* An accented service card — see sections/services.html for why the dot tile
   that used to sit here is gone. Its own class rather than a rule on
   `.services .card`, because Ironbark, Folio and Ledger template their own
   services and must keep their own look.

   A border rather than a pseudo-element or an extra span: it costs no
   vertical space, it follows --radius-card so it curves with whatever corner
   the pack chose, and it adds nothing for a screen reader to read out. The
   accent is a surface here, never text, which is the rule the rest of the
   stylesheet follows and test_build enforces. */
.card-accent { position: relative; padding-left: max(var(--card-pad), 1.25rem); }
.card-accent::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px;
  background: var(--accent);
  border-radius: var(--radius-card) 0 0 var(--radius-card);
}

/* Short claims, not a checklist — see why_us.html. Centred, so the icon
   above needs centring too rather than inheriting the left-aligned default
   services cards use. */
/* A reason to choose this business is a short claim — often two or three
   words — not a card with a paragraph under it. Stacked (icon above text,
   the services-card rhythm) each one took a third of a phone screen and two
   of them read as two abandoned elements rather than a list. Icon beside the
   text, tight gap, so the set scans as a checklist at the size it actually
   gets read at. The grid still spreads them out once there is width for it. */
.why-us-card {
  display: flex; align-items: center; gap: .75rem; text-align: left;
}
.why-us-card .card-icon {
  margin: 0; flex: 0 0 auto;
  width: 2rem; height: 2rem;
}
.why-us-card .card-icon svg { width: 1.1rem; height: 1.1rem; }
.why-us-card p { margin: 0; font-weight: 700; line-height: 1.25; }
/* Claims are short, so they do not need a services-sized gutter between them
   on a phone — the section is a list there, not a grid. */
@media (max-width: 39.99rem) {
  .why-us .cards { gap: .85rem; }
}
/* Above phone width the claims pack left instead of sitting in .cards' fixed
   columns. Two claims in a three-column grid left the last third of a
   full-width band empty, which read as a missing third claim rather than as
   a business with two — and the number of claims is the owner's choice, so
   the layout has to look deliberate at two, three or four. Flex sizes each
   to its own text and puts the rest of the row back. */
@media (min-width: 40rem) {
  .why-us .cards { display: flex; flex-wrap: wrap; gap: .75rem 2.5rem; }
}

.testimonial .stars { margin: 0 0 .6rem; color: var(--accent); letter-spacing: .1em; }
.testimonial .quote { margin: 0 0 .8rem; color: var(--muted); }
/* A decorative opening mark, not new markup — CSS only, so it costs nothing
   in the template and can't end up read aloud twice. Sized off the display
   font so it picks up whichever face a pack chose (Slate's serif, Verge's
   rounded face) rather than looking like a stray character. */
.testimonial .quote::before {
  content: "\201C"; display: block; font-family: var(--font-display);
  font-size: 2.75rem; line-height: 1; color: var(--accent);
  opacity: .3; margin-bottom: -.25rem;
}
.testimonial .author { margin: 0; font-weight: 700; font-size: .92rem; }

/* Visually hidden, not display:none — screen readers still read it. The one
   place today that needs it is the star rating: the stars themselves are
   aria-hidden decoration, and this is the text a screen reader gets instead. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

/* --- the contact section's tabs ---------------------------------------------
   Step 2 can ask for more than one of the wants this one section answers — see
   render.py's _contact_tabs — and stacked, the second of them read as a block
   bolted on underneath the first. As tabs each keeps exactly the content it
   always had, and a business that asked for one or none still gets the single
   block above, because a strip around one thing is furniture rather than
   something to navigate.

   Not quite one tab per want: the two ways of being reached share one, since
   both produce the phone number and nothing else. See CONTACT_TABS.

   Radios and labels, not a script. A generated page is served under
   default-src 'self' with no 'unsafe-inline', so an inline <script> is
   dropped, and a control that only works with scripting is a control that does
   not work. The inputs come first in the markup so the strip and the panels
   below are their siblings: :checked and the sibling combinator then do the
   whole job, with nothing to keep in sync.

   They are .sr-only rather than display:none, which is what keeps them
   focusable — the arrow keys are how anyone not using a pointer moves between
   tabs. That does mean each rule below has to name its tab by position, since
   a stylesheet cannot read an attribute value. Two can land here at most —
   the contact details and the pricing and payments panel — so there are two of
   each. */
.contact-tabs { margin-top: 1.5rem; }
.contact-tabs .tab-strip {
  display: flex; flex-wrap: wrap; gap: .25rem;
  border-bottom: 1px solid var(--line);
}
.contact-tabs .tab-label {
  /* Sits on the strip's own rule rather than just above it. */
  margin-bottom: -1px; padding: .5rem .8rem;
  border-bottom: 3px solid transparent; cursor: pointer;
  font-weight: 700; color: var(--muted);
}
.contact-tabs .tab-panel { display: none; padding-top: 1.25rem; }

.contact-tabs > input:nth-of-type(1):checked ~ .tab-strip .tab-label:nth-of-type(1),
.contact-tabs > input:nth-of-type(2):checked ~ .tab-strip .tab-label:nth-of-type(2) {
  color: var(--accent); border-bottom-color: var(--accent);
}
.contact-tabs > input:nth-of-type(1):checked ~ .tab-panels .tab-panel:nth-of-type(1),
.contact-tabs > input:nth-of-type(2):checked ~ .tab-panels .tab-panel:nth-of-type(2) {
  display: block;
}
/* The same ring .edit-pencil uses, drawn on the label rather than on the input
   because the input is the part nobody can see. */
.contact-tabs > input:nth-of-type(1):focus-visible ~ .tab-strip .tab-label:nth-of-type(1),
.contact-tabs > input:nth-of-type(2):focus-visible ~ .tab-strip .tab-label:nth-of-type(2) {
  outline: 3px solid var(--ink); outline-offset: 2px;
}

/* --- the enquiry form -------------------------------------------------------
   The contact section's own content: a visitor's message to the business.
   Always present, whatever step 2 asked for — see sections/contact.html — so
   this is the one way to reach the business that every site has. The direct
   phone/email block below it is opt-in (show_contact_info).

   Field styling mirrors .claim-card's own form above — same border, radius
   and focus ring — so the two forms a generated site can carry read as the
   same thing rather than two different designs. */
/* The form is the page's actual goal, so it gets a card of its own rather
   than fields floating loose on the section's background — Ironbark did this
   for itself and read as a distinct unit next to the packs that didn't, which
   is the difference this generalises. A pack that wants sharper corners or a
   different surface still overrides it; --wash and --line are both colours
   every pack already sets. */
.enquiry-form {
  margin-top: 1.5rem; max-width: 32rem;
  background: var(--wash); border: 1px solid var(--line);
  border-radius: var(--radius-card); padding: 1.5rem;
}
.enquiry-form .field { margin: 0 0 1rem; }
.enquiry-form label {
  display: block; font-weight: 600; font-size: .92rem; margin: 0 0 .3rem;
}
.enquiry-form input, .enquiry-form select, .enquiry-form textarea {
  width: 100%; padding: .8rem .85rem;
  font: inherit; color: var(--ink); background: var(--paper);
  border: 1.5px solid var(--muted); border-radius: 10px;
}
.enquiry-form textarea { resize: vertical; }
.enquiry-form input:focus, .enquiry-form select:focus, .enquiry-form textarea:focus {
  outline: 3px solid var(--ink); outline-offset: 2px;
  border-color: var(--muted);
}
/* Full width, and bigger than an inline .cta: this is the one press the whole
   page is asking for, and left-aligned at its natural width it read as a
   minor control next to the full-width fields above it. Also the tap target
   on a phone, which is where most of these sites are opened. */
.enquiry-form .cta {
  border: 0; cursor: pointer; font: inherit; font-weight: 700;
  width: 100%; padding-block: 1rem; font-size: 1.05rem;
}
.enquiry-form .cta:disabled { opacity: .6; cursor: not-allowed; }
.enquiry-status { margin: .8rem 0 0; font-size: .9rem; color: var(--muted); min-height: 1.2em; }

/* The business's own phone and email, shown below the form only when it has
   opted in — see sections/contact.html. */
.contact-direct { margin-top: 2rem; border-top: 1px solid var(--line); padding-top: 1.5rem; }
.contact-direct h3 { margin: 0 0 .75rem; font-size: 1.05rem; }
.contact-direct p { margin: 0 0 .5rem; }
/* Keep the underline, but use the section's readable text colour instead of
   the browser's blue, which fails on dark custom palettes. */
.contact-direct a:not(.cta) { color: inherit; }

/* The enquiry form's honeypot. Visually hidden but still in the layout and
   still focusable — display:none and visibility:hidden are both skipped by
   the bots this is meant to catch. Same technique as .sr-only above, kept
   separate because this one is a deliberate trap rather than an
   accessibility affordance. */
.hp-field {
  position: absolute; width: 1px; height: 1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap;
}

/* The ABN / licence line: small and muted, present only when typed. */
.site-credentials { font-size: .82rem; color: var(--muted); margin-top: .25rem; }
.site-footer {
  border-top: 1px solid var(--line); padding: 2rem var(--gutter);
  color: var(--muted); font-size: .9rem; text-align: center;
}
.manage-link { color: var(--muted); }

/* Footer legal links, and the panels they open.
 *
 * The panels are hidden until one is the :target — that is the whole
 * mechanism, and it is CSS rather than JavaScript on purpose. Production
 * sends default-src 'self', so an inline handler would be dropped; a
 * stylesheet rule is honoured, works with JS off, and leaves /s/{token}#terms
 * a real link somebody can send.
 *
 * `display: none` rather than opacity or a transform, so the closed panel is
 * out of the accessibility tree and out of tab order. A reader that ignores
 * CSS entirely — a crawler, reader mode — sees both panels as ordinary
 * sections at the end of the document, which is the right fallback: the terms
 * being readable is more important than them being tidy.
 */
.site-legal { display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap; }
.site-legal a { color: var(--muted); }

.legal-panel { display: none; }
.legal-panel:target {
  display: block;
  position: fixed; inset: 0; z-index: 20;
  overflow-y: auto; overscroll-behavior: contain;
  background: var(--paper);
  padding: 2rem var(--gutter) 4rem;
}
.legal-inner { max-width: var(--measure); margin: 0 auto; position: relative; }
/* Room for the close control, which is positioned over this corner. Without
   it a two-line heading runs under the x on a phone. */
.legal-inner h2 { margin-top: 0; padding-right: 3.25rem; }
.legal-inner h3 { margin: 1.75rem 0 .35rem; font-size: 1rem; }
.legal-inner p { margin: 0; }
.legal-meta { color: var(--muted); font-size: .9rem; }
/* A link, not a button: it changes the fragment, which is what closes the
   panel. Sized to the 44px touch target a phone needs. */
.legal-close {
  position: absolute; top: -.35rem; right: 0;
  display: flex; align-items: center; justify-content: center;
  width: 2.75rem; height: 2.75rem;
  color: var(--ink); font-size: 1.75rem; line-height: 1; text-decoration: none;
}
.legal-close:hover, .legal-close:focus-visible { color: var(--accent); }

/* --- structures a pack's sections can ask for -------------------------------
   Still structure, so still here: these are the shapes a section override in
   templates/packs/ can reach for, described once rather than restated in each
   pack that wants one. A pack that uses none of them is not paying for them —
   an unused class costs a generated page nothing. */

/* A full-bleed section: escapes the shared measure, holds its contents to it. */
.hero-band, .hero-centred { max-width: none; padding-left: 0; padding-right: 0; }
.hero-inner { max-width: var(--measure); margin: 0 auto; padding: 0 var(--gutter); }

/* Inverted hero. --accent on --band is typically 2-3:1, so the kicker takes
   the lightened accent and the lead a tint of --on-band rather than --muted,
   which was picked against --paper and is unreadable here. */
.hero-band { background: var(--band); color: var(--on-band); }
/* A custom palette's band can be light: its guaranteed readable foreground
   is on-band, whereas accent-light is deliberately light for dark surfaces. */
.hero-band .kicker { color: var(--on-band); }
.hero-band .lead { color: var(--on-band); }
.hero-band .lead-detail { color: var(--on-band); opacity: .85; }

/* Centred hero over an optional wash. */
.hero-centred { background: var(--hero-bg); text-align: center; }
.hero-centred .lead { margin-left: auto; margin-right: auto; }
.hero-centred .lead-detail { margin-left: auto; margin-right: auto; }

/* --- hero background photo --------------------------------------------------
   Optional, per business_type — see render.py's hero_photo_url (an operator's
   generic banner, or the first gallery photo) and gallery.py. A flat pack
   colour is still the whole look when there's no photo to show; this only
   adds to it. The photo and scrim are plain
   siblings of the text, not a wrapper around it, so `section`'s own
   `position: relative` (and .hero-inner's, for the full-bleed packs) is all
   either needs to anchor to — no extra nesting for a section that might not
   have a photo at all. */
.hero-photo {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; z-index: 0;
}
/* Heavier toward the bottom, where the button sits — a flat scrim would need
   to be dark enough everywhere to protect the button, which crushes the top
   of the photo for no reason the top of the hero needs. */
.hero-scrim {
  position: absolute; inset: 0; z-index: 1;
  background: linear-gradient(180deg, rgba(10, 12, 16, .72) 0%, rgba(10, 12, 16, .8) 100%);
}
/* The owner's darkness choice (#255): five steps, 3 being the default above.
   The lighter two let more of the photo through at the cost of contrast, which
   the banner dialog warns about rather than refuses. */
.hero-scrim-1 .hero-scrim { background: linear-gradient(180deg, rgba(10, 12, 16, .30) 0%, rgba(10, 12, 16, .45) 100%); }
.hero-scrim-2 .hero-scrim { background: linear-gradient(180deg, rgba(10, 12, 16, .50) 0%, rgba(10, 12, 16, .62) 100%); }
.hero-scrim-4 .hero-scrim { background: linear-gradient(180deg, rgba(10, 12, 16, .80) 0%, rgba(10, 12, 16, .86) 100%); }
.hero-scrim-5 .hero-scrim { background: linear-gradient(180deg, rgba(10, 12, 16, .88) 0%, rgba(10, 12, 16, .92) 100%); }
/* Which part of the photo stays in view when it is cropped (#255), for every
   pack's hero image, whatever its own class. */
.hero-focus-top-left img { object-position: 0% 0%; }
.hero-focus-top img { object-position: 50% 0%; }
.hero-focus-top-right img { object-position: 100% 0%; }
.hero-focus-left img { object-position: 0% 50%; }
.hero-focus-center img { object-position: 50% 50%; }
.hero-focus-right img { object-position: 100% 50%; }
.hero-focus-bottom-left img { object-position: 0% 100%; }
.hero-focus-bottom img { object-position: 50% 100%; }
.hero-focus-bottom-right img { object-position: 100% 100%; }
/* Text sits on the scrim regardless of pack, so it has to stop reading --ink
   or --muted — those were picked against --paper or --band, not a photo.
   White reads reliably on the scrim above at every darkness it uses. */
.hero-has-photo .hero-inner,
.hero-has-photo > .kicker, .hero-has-photo > h1,
.hero-has-photo > .lead, .hero-has-photo > .cta {
  position: relative; z-index: 2;
}
.hero-has-photo, .hero-has-photo h1, .hero-has-photo .lead { color: #fff; }
.hero-has-photo .kicker { color: #fff; opacity: .85; }
.hero-has-photo .lead { color: rgba(255, 255, 255, .88); }
.hero-has-photo .lead-detail { color: rgba(255, 255, 255, .78); }

/* A numbered service card: the count is decoration, so it is hidden from
   assistive technology rather than read out as part of the service name. */
.card-ruled { border-top: 3px solid var(--accent); }
.card-index {
  display: block; font-family: var(--font-display); font-weight: 800;
  font-size: .8rem; letter-spacing: .08em; color: var(--accent);
  margin-bottom: .4rem;
}

@media (min-width: 40rem) {
  .cards { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .hero h1 { font-size: var(--h1-size-wide); }
}

/* Below the breakpoint the nav collapses behind the toggle: links hidden
   until the checkbox is checked, toggle button shown. Above it the toggle is
   hidden and the links are always in a row — see .site-nav above. */
@media (max-width: 39.99rem) {
  /* Everything in this file reads its horizontal padding from --gutter, so
     this one rule lifts the breathing room on a phone everywhere at once. */
  :root { --gutter: 1.5rem; }
  .nav-toggle-btn { display: block; }
  /* Below the breakpoint the masthead shows the brand mark alone: the name is
     hidden rather than truncated (see .logo above). */
  .logo { display: none; }
  .nav-links { display: none; }
  .nav-toggle-input:checked ~ .nav-links {
    display: flex; flex-direction: column; gap: .5rem;
    position: absolute; top: 100%; left: 0; right: 0;
    padding: .75rem var(--gutter);
    background: var(--header-bg); color: var(--header-ink);
    border-bottom: var(--header-border);
  }
  /* In the dropdown each link is a full-width row, so the current one and
     a tapped one can carry a shaded row rather than an underline. */
  .nav-toggle-input:checked ~ .nav-links a {
    padding: .45rem .6rem; margin: 0 -.6rem; border-radius: 8px;
  }
  .nav-toggle-input:checked ~ .nav-links a[aria-current] {
    text-decoration: none;
    background: color-mix(in srgb, currentColor 14%, transparent);
  }
}

/* --- the expiry gate -------------------------------------------------------
   Shown once the free window has run out. A plain fixed overlay rather than a
   <dialog>: showModal() puts an element in the top layer, where the page
   behind it is inert but perfectly readable — and the page behind this one is
   the thing being withheld.

   Held outside the pack system on purpose. This is our billing screen wearing
   the site's palette, not part of the design the business is being sold, and
   it must look and behave identically whichever pack is underneath it. */
.claim-gate {
  position: fixed; inset: 0; z-index: 9999;
  display: flex; align-items: center; justify-content: center;
  padding: 1.25rem;
  background: rgba(16, 24, 32, .92);
  /* Blurred as well as darkened, so the page underneath reads as present but
     not usable. */
  backdrop-filter: blur(6px);
}
.claim-card {
  width: min(28rem, 100%); max-height: 90vh; overflow-y: auto;
  background: var(--paper); border-radius: 14px; padding: 1.75rem;
  box-shadow: 0 20px 60px rgba(0, 0, 0, .45);
}
.claim-card h2 { margin: 0 0 .5rem; font-size: 1.5rem; }
.claim-card p { margin: 0 0 .75rem; color: var(--muted); }
.claim-card a:not(.cta) { color: var(--accent); font-weight: 700; }
/* The owner's half of the card: set apart from the visitor-facing message by a
   rule, so a customer of the business reads it as someone else's business. */
.claim-owner { margin-top: 1.25rem; padding-top: 1.25rem; border-top: 1px solid var(--line); }
.claim-owner .claim-owner-title { margin: 0 0 .25rem; color: var(--ink); font-weight: 700; }
.claim-card .cta { display: block; width: 100%; text-align: center; margin-top: 1rem; }

/* The page behind the gate must not scroll away from it. */
body:has(.claim-gate) { overflow: hidden; }

/* --- inline editing ---------------------------------------------------------
   Only ever present when the visitor's portal cookie has already been
   checked against this exact submission server-side — see main.py's
   generated_site(). A pencil per editable section, one shared <dialog>
   edit.js builds a form into on click.

   The panel itself uses the pack's own custom properties, the same way
   .claim-card above does — it's our chrome, but it sits on the business's
   page, and jarring against whichever pack they chose would be its own kind
   of wrong. The pencil chip and the backdrop are the two exceptions, same
   reasoning .claim-gate's own backdrop is a fixed rgba rather than a pack
   variable: a pencil has to stay legible sitting on top of an arbitrary
   section background, including a full-bleed dark hero band, so it can't
   take its colour from the section underneath it. */
.edit-pencil {
  position: absolute; top: .6rem; right: .6rem; z-index: 3;
  width: 2rem; height: 2rem; display: flex; align-items: center; justify-content: center;
  background: rgba(255, 255, 255, .92); color: #16181d;
  border: 1px solid rgba(0, 0, 0, .12); border-radius: 999px;
  font-size: 1rem; line-height: 1; cursor: pointer;
  box-shadow: 0 2px 6px rgba(0, 0, 0, .15);
}
.edit-pencil:hover { background: #fff; }
.edit-pencil:focus-visible { outline: 3px solid var(--ink); outline-offset: 2px; }
/* A quiet corner control rather than a loud floating badge — but never
   hidden, since there is no hover on touch and this is the only way in to
   the editor on a phone. */
.edit-pencil { opacity: .85; }
.edit-pencil:hover, .edit-pencil:focus-visible { opacity: 1; }

/* The hero's banner control has no appearance of its own any more. It used to
   be a wider pill carrying a photo icon, pinned to the top-LEFT, which gave
   the page two different-looking edit affordances in two different corners.
   It is now an .edit-pencil like every other one — see edit.js, which builds
   it — so everything above applies to it, and the hero picks up the same
   dashed outline every other editable section has.

   .edit-photo-host is whichever element actually holds the hero photograph,
   tagged by edit.js when the photo sits in its own wrapper rather than
   bleeding across the section. It needs to become a containing block so the
   pencil lands in the PHOTO's top-right rather than the hero's; without this
   the absolute positioning would resolve against the section and, on a pack
   like Verge that stacks its photo under the copy, put the control nowhere
   near the thing it edits.

   A slightly deeper inset than the .6rem used elsewhere: these wrappers are
   rounded (Verge 2rem, Clarity 16px) and clip their overflow, and .6rem puts
   the chip's own corner into the curve. At .9rem the whole chip clears a 2rem
   radius. */
.edit-photo-host { position: relative; }
.edit-photo-host > .edit-pencil { top: .9rem; right: .9rem; }

/* A faint dashed outline on whichever element the pencil belongs to, so the
   pencil is not the only affordance that a section is editable. Descendant
   rather than child: the pencil is a direct child of the section in most
   templates, but not in every one, and a rule that silently matches nothing
   is worse than a slightly broader one. */
section:has(.edit-pencil), .offer-banner:has(.edit-pencil) {
  outline: 1px dashed var(--line); outline-offset: -1px;
}
/* The same reasoning .hero-band/.offer-banner's own pencil treatment already
   follows: those two render on a dark or accent-coloured background, where
   --line is invisible. */
.hero-band:has(.edit-pencil), .offer-banner:has(.edit-pencil) {
  outline-color: rgba(255, 255, 255, .35);
}
/* The hero band and the offer banner are dark or coloured everywhere a pack
   might render them; a white-on-dark pencil chip stays legible on both
   without needing a per-pack variant. */
.hero-band .edit-pencil, .offer-banner .edit-pencil { background: rgba(0, 0, 0, .55); color: #fff; }
.hero-band .edit-pencil:hover, .offer-banner .edit-pencil:hover { background: rgba(0, 0, 0, .7); }

/* Hover-reveal, gated on pointer capability rather than viewport width: a
   desktop-sized window can still be touch-primary (a touchscreen laptop, a
   tablet with a keyboard), and there is no hover there to reveal anything
   with. `hover: hover` and `pointer: fine` together mean a real mouse or
   trackpad, which is the only case where hiding the pencil until hover is
   safe. Touch keeps the always-on rules above, unchanged. */
@media (hover: hover) and (pointer: fine) {
  .edit-pencil { opacity: 0; }
  section:has(.edit-pencil), .offer-banner:has(.edit-pencil) {
    outline: 1px dashed transparent;
  }
  section:hover .edit-pencil, .offer-banner:hover .edit-pencil,
  .edit-pencil:focus-visible {
    opacity: 1;
  }
  section:hover:has(.edit-pencil), .offer-banner:hover:has(.edit-pencil),
  section:has(.edit-pencil:focus-visible), .offer-banner:has(.edit-pencil:focus-visible) {
    outline-color: var(--line);
  }
  .hero-band:hover:has(.edit-pencil), .offer-banner:hover:has(.edit-pencil),
  .hero-band:has(.edit-pencil:focus-visible), .offer-banner:has(.edit-pencil:focus-visible) {
    outline-color: rgba(255, 255, 255, .35);
  }
}

.edit-panel {
  width: min(26rem, calc(100% - 2.5rem));
  max-height: min(90vh, 32rem); overflow-y: auto;
  border: 0; border-radius: 14px; padding: 1.5rem;
  background: var(--paper); color: var(--ink);
  box-shadow: 0 20px 60px rgba(0, 0, 0, .45);
}
.edit-panel::backdrop { background: rgba(16, 24, 32, .55); }
.edit-panel h2 { margin: 0 0 1rem; font-size: 1.3rem; }
.edit-panel-close {
  position: absolute; top: .9rem; right: .9rem;
  background: none; border: 0; cursor: pointer;
  font-size: 1.6rem; line-height: 1; color: var(--muted);
}
.edit-panel-close:hover { color: var(--ink); }

.edit-field { margin: 0 0 1.1rem; }
.edit-field label { display: block; font-weight: 600; font-size: .92rem; margin: 0 0 .3rem; }
.edit-hint { margin: -.15rem 0 .5rem; font-size: .82rem; color: var(--muted); }
.edit-field input, .edit-field textarea {
  width: 100%; padding: .7rem .8rem;
  font: inherit; color: var(--ink); background: var(--paper);
  border: 1.5px solid var(--muted); border-radius: 8px;
}
.edit-field textarea { min-height: 6rem; resize: vertical; }
.edit-field input:focus, .edit-field textarea:focus {
  outline: 3px solid var(--ink); outline-offset: 2px;
  border-color: var(--muted);
}
.edit-panel-status { margin: 0 0 .8rem; font-size: .88rem; color: var(--muted); min-height: 1.2em; }
.edit-panel .cta { width: 100%; border: 0; cursor: pointer; font: inherit; font-weight: 700; }
.edit-panel .cta:disabled { opacity: .6; cursor: not-allowed; }

/* Shared alignment and usable spacing, including long customer-supplied text.
   Pack files still own colours, type choices and decorative treatments. */
.site-header {
  padding-inline: max(var(--gutter), calc((100% - var(--measure)) / 2 + var(--gutter)));
  min-height: 5rem;
}
section { scroll-margin-top: 7rem; }
section > h2 { margin-top: 0; margin-bottom: 1.75rem; text-wrap: balance; }
.card { min-width: 0; overflow-wrap: anywhere; }
.cards { gap: 1.5rem; }
.hero-inner { min-width: 0; }
.hero .lead { line-height: 1.75; }
/* The why-choose-us reasons as ticks under the hero's buttons (#260): short
   claims read as a trust bar here rather than as a section of their own. The
   colour is the hero's own text colour, so it follows every hero context the
   contrast checks already cover: ink on a plain or washed hero, white over a
   photo, --on-band on a band. */
.trust-strip {
  list-style: none; margin: 1.5rem 0 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: .5rem 1.4rem;
  font-size: .95rem; font-weight: 600; line-height: 1.4;
}
.hero-centred .trust-strip { justify-content: center; }
.trust-strip li { display: inline-flex; align-items: center; gap: .45rem; }
.trust-strip-tick { display: inline-flex; flex: none; }
.trust-strip-tick svg { width: 1.1rem; height: 1.1rem; }
.trust-strip-edit .edit-pencil { position: static; }
/* "Read our reviews on Google" (#263): a distinct outlined link under the
   reasons, in the text colour of wherever it sits, so it clears the same
   contrast as the words around it in every pack and over a hero photo. */
.reviews-link { margin: 1.25rem 0 0; }
.reviews-link a {
  display: inline-flex; align-items: center; min-height: 44px; padding: .5rem 1rem;
  border: 2px solid currentColor; border-radius: var(--radius-cta);
  color: inherit; font-weight: 600; text-decoration: none;
}
.reviews-link a:hover { text-decoration: underline; }
.hero-centred .reviews-link { text-align: center; }
.site-social { margin: .5rem 0; }  /* follows the footer's own text alignment */
.site-social a { display: inline-flex; margin: 0 .2rem; min-width: 44px; min-height: 44px; align-items: center; justify-content: center; color: inherit; }


/* The catchline, when the hero has one, is the business speaking, not the
   generated sentence it replaces, so it is set as a line of display type:
   the pack's display face at a heavier weight and a step larger, lines
   balanced so a two-line catchline breaks into two even halves rather than a
   long line and an orphan. Colour is deliberately left to the context rules
   above (muted on plain heroes, --on-band on bands, white over a photo),
   because every one of those pairings is already contrast-tested and a
   catchline is text, not a surface. The generated sentence keeps the plain
   .lead look: it is the fallback, and should read like one. */
.hero .lead.catchline {
  font-family: var(--font-display);
  /* 600 where the face has it (Fraunces, Inter). A pack whose display face
     ships only 500 and 700 sets --catchline-weight: 500, or the browser picks
     700 for a 600 request and the line competes with the business name. */
  font-weight: var(--catchline-weight);
  font-size: calc(var(--lead-size) * 1.22);
  line-height: 1.35;
  letter-spacing: var(--display-tracking, 0);
  text-wrap: balance;
  max-width: 32rem;
}
/* A step of air between the catchline and its quieter detail line: at display
   size the old .6rem read as the two lines crowding each other. */
.hero .lead.catchline + .lead-detail { margin-top: .9rem; }
.cta { text-align: center; }
.cta:focus-visible, .logo-link:focus-visible, .nav-links a:focus-visible,
.hero-phone:focus-visible {
  outline: 3px solid currentColor; outline-offset: 4px;
}
/* Button text can match the surrounding surface (white on a light page,
   black on Nocturne). Two contrasting rings keep keyboard focus visible
   on both the page and an inverse-colour band. */
.cta:focus-visible {
  outline-color: var(--ink);
  box-shadow: 0 0 0 4px var(--paper);
}
.nav-toggle-btn { min-width: 44px; min-height: 44px; align-items: center; justify-content: center; }
@media (max-width: 39.99rem) {
  .nav-toggle-btn { display: flex; }
  .site-header { gap: .5rem; }
  .hero { padding-block: min(var(--hero-y), 4rem); }
  .hero .cta { max-width: 100%; }
}
@media (min-width: 40rem) and (max-width: 63.99rem) {
  .cards { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* The enquiry form is capped at 32rem, because a text input the full width of
   a desktop page is unpleasant to fill in. That left the section heading
   alone above it and the right half of the page empty.

   Putting the heading in a column beside the form was the first answer, and
   it traded one hole for another: the heading is three words, so the column
   under it stayed empty for the form's whole height — several hundred pixels
   of nothing at the one section that turns a visitor into a job. Every pack
   showed it, because this rule is shared.

   So the heading goes back on top and the form takes the full measure, with
   its own fields in two columns. No input is any wider than it was — the
   column is about the same 32rem the cap gave it — and the form is half as
   tall, which is what fills the space rather than stretching anything to
   reach the edges. Fields keep their document order, so the two columns read
   and tab in the order they are written. The message box and the button span
   both columns because they want the width; the honeypot is positioned out
   of flow and takes no cell (see .hp-field). */
@media (min-width: 48rem) {
  .contact .enquiry-form {
    display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0 1.5rem; max-width: none; align-items: start;
  }
  .contact .enquiry-form > .field:has(textarea),
  .contact .enquiry-form > button { grid-column: 1 / -1; }
}

/* Contact us and Where we work as one section (#261, finding 11.8). The
   area is the Contact section's second column: stacked under the form on a
   phone, beside it from 64rem. Inside the column the map is a 4:3 panel with
   the suburb pills above it, and the form, now half the measure, goes back to
   one column of inputs. The section takes the full measure while it has the
   area, over the narrower caps some packs give a lone form (Atrium, Verge,
   Nocturne, Clarity), hence the extra element in those selectors. */
.contact-area { position: relative; margin-top: 2.5rem; }
.contact-area > h3 { margin: 0 0 1rem; font-size: 1.25rem; }
.contact .contact-area .area-body { grid-template-columns: minmax(0, 1fr); gap: 1rem; max-width: none; }
.contact .contact-area .area-detail { align-self: start; }
.contact .contact-area .area-map-frame { aspect-ratio: 4 / 3; }
@media (min-width: 64rem) {
  section.contact.contact-with-area { max-width: var(--measure); }
  .contact-with-area .contact-cols {
    display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
    gap: 3rem; align-items: start;
  }
  .contact-with-area .contact-area { margin-top: 1.5rem; }
  section.contact-with-area .enquiry-form { display: block; max-width: none; }
}

/* On wide screens a centred pack's photo sits beside its copy. The photo is
   decorative, so reflowing it does not change the reading or editing order. */
@media (min-width: 64rem) {
  .hero-centred.hero-has-photo {
    min-height: 35rem; display: flex; align-items: center;
    background: var(--wash); text-align: left;
    color: var(--ink);
  }
  .hero-centred.hero-has-photo .hero-photo {
    left: 52%; width: 48%; height: 100%;
  }
  .hero-centred.hero-has-photo .hero-scrim { display: none; }
  .hero-centred.hero-has-photo .hero-inner {
    width: 100%; padding-right: calc(var(--measure) * .5 + var(--gutter));
  }
  .hero-centred.hero-has-photo h1 { color: var(--ink); }
  .hero-centred.hero-has-photo .kicker { color: var(--accent); opacity: 1; }
  .hero-centred.hero-has-photo .lead { color: var(--muted); margin-inline: 0; }
  .hero-centred.hero-has-photo .trust-strip { justify-content: flex-start; }


  /* The detail under the catchline follows the lead onto the wash. Without
     this it kept .hero-has-photo's white-over-photo colour and sat almost
     invisible, near-white on near-white, beside the photo on every desktop. */
  .hero-centred.hero-has-photo .lead-detail { color: var(--muted); margin-inline: 0; }
}

/* The menu checkbox must not be a hidden keyboard stop on desktop. */
@media (min-width: 40rem) { .nav-toggle-input { display: none; } }
.nav-toggle-input:focus-visible ~ .nav-toggle-btn { outline-color: var(--header-ink); }
.edit-pencil:focus-visible {
  outline: 3px solid #16181d; outline-offset: 3px;
  box-shadow: 0 0 0 3px #ffffff;
}
.edit-panel-close { min-width: 32px; min-height: 32px; }
.edit-panel-close:focus-visible { outline: 3px solid var(--ink); outline-offset: 2px; }
.skip-link {
  position: fixed; top: .5rem; left: .5rem; z-index: 100;
  padding: .75rem 1rem; background: var(--paper); color: var(--ink);
  transform: translateY(-200%);
}
.skip-link:focus { transform: none; outline: 3px solid var(--ink); }
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    scroll-behavior: auto !important;
    animation-duration: 0s !important; transition-duration: 0s !important;
  }
}

/* Social icons in the header (#303), when the owner puts them at the top.
   A phone: between the brand mark and the menu button, in the order the DOM
   gives them. A wide screen: after the nav links. Same 44px targets as the
   footer's; a little tighter on a phone, where six icons, the mark and the
   menu button have to share one row. */
.header-social { display: inline-flex; align-items: center; }
.header-social a {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 44px; min-height: 44px; color: inherit;
}
.header-social a:focus-visible { outline: 3px solid currentColor; outline-offset: 2px; }
@media (min-width: 40rem) {
  .header-social { order: 2; margin-left: .75rem; }
}
@media (max-width: 39.99rem) {
  .header-social a { min-width: 36px; }
  /* Six icons, the menu button and the brand mark are wider than a 320px
     phone. The mark never gives way (it is the brand, and the name is already
     hidden here); the icons shrink their row and wrap onto a second one
     instead of sliding under it (#326). */
  .site-header > .logo-link { flex-shrink: 0; }
  .site-nav { min-width: 0; }
  .header-social { flex-wrap: wrap; justify-content: flex-end; min-width: 0; }
}
/* The narrowest phones (320px, an iPhone SE): a tighter tap box keeps six
   icons on one row beside the mark and the menu, rather than wrapping one
   stray icon onto a line of its own. 28px is still above WCAG 2.2's 24px
   target minimum; the wrap above remains the fallback. */
@media (max-width: 22.49rem) {
  .header-social a { min-width: 28px; }
  .site-header { gap: .35rem; }
}

/* The phone above the contact form (#326): one compact line, so the form
   still starts on the first screen of a small phone. The number is a real
   tap target (44px tall) without being styled as a second button. */
.contact-call { margin: 0 0 1rem; }
.contact-call a {
  display: inline-flex; align-items: center; min-height: 44px; font-weight: 700; white-space: nowrap;
  /* The page's own ink, underlined in the pack's accent: every pack's ink is
     contrast-tested against its background, the browser's default blue is not
     and clashes with every palette. */
  color: inherit; text-decoration: underline; text-decoration-color: var(--accent);
  text-decoration-thickness: 2px; text-underline-offset: 3px;
}
