/* Components, built from tokens.css. No utility framework — each class is a
   named part of the system, documented in .claude/skills/pacific-design-system.

   The .band--ink context class recolours components rather than duplicating
   them: add the context, don't write .btn--primary-on-dark. */

/* ══ Buttons ═══════════════════════════════════════════════════════════ */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding: var(--space-4) var(--space-6);
  border: var(--hairline) solid transparent;
  border-radius: var(--radius);
  font-family: inherit;
  font-size: var(--size-body);
  font-weight: 700;
  line-height: 1.2;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  transition: background var(--duration) var(--ease),
              color var(--duration) var(--ease),
              border-color var(--duration) var(--ease);
}

.btn:hover { text-decoration: none; }

/* ocean-600, not the raw brand blue: paper on ocean-500 is only 3.45:1.
   This pairing measures 5.38:1. */
.btn--primary {
  background: var(--brand-fill);
  color: var(--paper);
}

.btn--primary:hover { background: var(--ink); color: var(--paper); }

.btn--outline {
  border-color: var(--ink);
  color: var(--ink);
  background: transparent;
}

.btn--outline:hover { background: var(--ink); color: var(--paper); }

.band--ink .btn--outline {
  border-color: var(--rule-on-ink);
  color: var(--paper);
}

.band--ink .btn--outline:hover { background: var(--paper); color: var(--ink); }

.btn--ghost { color: var(--brand-text); background: transparent; }
.btn--ghost:hover { background: var(--ocean-100); }

.btn--lg { padding: var(--space-5) var(--space-8); font-size: 17px; }
.btn--block { width: 100%; }
.btn[disabled], .btn[aria-disabled="true"] { opacity: 0.45; pointer-events: none; }

/* ══ Tags and chips ════════════════════════════════════════════════════ */

.tag {
  display: inline-block;
  padding: var(--space-2) var(--space-3);
  font-family: var(--font-mono);
  font-size: var(--size-eyebrow);
  font-weight: 500;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  line-height: 1;
}

.tag--before { background: color-mix(in srgb, var(--ink) 84%, transparent); color: var(--paper); }
.tag--after  { background: var(--brand); color: var(--paper); }
.tag--muted  { background: var(--ink-200); color: var(--ink-800); }

/* Filter tabs. A real <button> in a group, not a styled div. */
.chip-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: var(--space-8) 0 var(--space-7);
}

.chip {
  border: var(--hairline) solid var(--ink);
  background: transparent;
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: var(--space-3) var(--space-5);
  cursor: pointer;
  border-radius: var(--radius);
  transition: background var(--duration) var(--ease), color var(--duration) var(--ease);
}

.chip:hover { background: color-mix(in srgb, var(--ink) 8%, transparent); }

.chip[aria-selected="true"],
.chip[aria-pressed="true"] {
  background: var(--ink);
  color: var(--paper);
}

/* A chip that is a link, not a button — used by the blog and project filters.
   Lives here rather than in one page's stylesheet, because both need it. */
.chip--on { background: var(--ink); color: var(--paper); }

/* The :not() pair is load-bearing, not decoration. base.css underlines
   `li a:not(.btn):not(.cell)` — (0,2,2) — and every chip is an <a> inside an
   <li>, so a plain `a.chip` at (0,1,1) LOSES and each filter renders as a
   12px uppercase mono pill with a line through it. Matching the pair takes
   this to (0,3,1), which wins on class count.

   Matched rather than weakening the base rule, which is right for the inline
   links it was written for. admin.css already carries the same fix for
   .admin-recent, and .ai/rules/css.md records the trap. */
a.chip:not(.btn):not(.cell) { text-decoration: none; }
a.chip:not(.btn):not(.cell):hover { text-decoration: none; }
.chip-row { list-style: none; padding: 0; }

/* ══ Cells — the modular grid unit ═════════════════════════════════════ */

.cell {
  padding: var(--space-8) var(--space-6) var(--space-9);
  display: flex;
  flex-direction: column;
}

.cell__index {
  font-family: var(--font-mono);
  font-size: var(--size-eyebrow);
  font-weight: 500;
  letter-spacing: var(--track-label);
  color: var(--brand-text);
}

.band--ink .cell__index { color: var(--label-on-ink); }

.cell__title { margin-top: var(--space-6); }

.cell__body {
  margin-top: var(--space-3);
  font-size: 15px;
  line-height: 1.55;
  color: var(--muted);
}

.band--ink .cell__body { color: var(--muted-on-ink); }

/* A cell that is a link should feel like one without becoming a blue block. */
a.cell { color: inherit; text-decoration: none; }
a.cell:hover { background: var(--paper); text-decoration: none; }
.band--ink a.cell:hover { background: var(--ink-900); }
a.cell:hover .cell__title { color: var(--brand-text); }
.band--ink a.cell:hover .cell__title { color: var(--label-on-ink); }

/* ══ Job sheet — project metadata as a contractor's job ticket ═════════ */

.job-sheet {
  display: grid;
  gap: var(--hairline);
  background: var(--rule);
}

.band--ink .job-sheet { background: var(--rule-on-ink); }

.job-sheet__row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-4);
  padding: var(--space-4) 0;
  background: var(--concrete);
  font-size: var(--size-data);
}

.band--ink .job-sheet__row { background: var(--ink); }

.job-sheet__key {
  flex: none;
  font-family: var(--font-mono);
  font-size: var(--size-eyebrow);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--muted);
}

.band--ink .job-sheet__key { color: var(--muted-on-ink); }

.job-sheet__val { text-align: right; font-weight: 500; }

/* ══ Forms ═════════════════════════════════════════════════════════════ */

.field { display: block; }

.field + .field { margin-top: var(--space-5); }

.field__label {
  display: block;
  margin-bottom: var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--size-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--muted);
}

.band--ink .field__label { color: var(--muted-on-ink); }

.input {
  width: 100%;
  background: transparent;
  border: 0;
  border-bottom: var(--hairline) solid var(--rule);
  border-radius: var(--radius);
  color: inherit;
  font-family: inherit;
  font-size: var(--size-body);
  padding: var(--space-3) 0;
}

.band--ink .input { border-bottom-color: var(--rule-on-ink); }

.input:focus-visible {
  outline: none;
  border-bottom-color: var(--brand);
  border-bottom-width: 2px;
}

.input::placeholder { color: var(--muted); opacity: 1; }
.band--ink .input::placeholder { color: var(--muted-on-ink); }

textarea.input { resize: vertical; min-height: 96px; }

/* .field__error lives with the rest of the validation styles further down.
   The rule that was here used --danger, which measures about 2:1 on the ink
   band both enquiry forms sit on. */

.field-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-5);
}

.field-row .field + .field { margin-top: 0; }

/* Radio group rendered as selectable pills, on native inputs. */
.pick { display: flex; flex-wrap: wrap; gap: var(--space-2); }

.pick__opt { position: relative; }

.pick__opt input {
  position: absolute;
  inset: 0;
  opacity: 0;
  cursor: pointer;
}

.pick__opt span {
  display: block;
  border: var(--hairline) solid var(--rule-on-ink);
  padding: var(--space-3) var(--space-4);
  font-size: 13.5px;
  cursor: pointer;
  transition: background var(--duration) var(--ease);
}

.pick__opt input:checked + span { background: var(--brand-fill); border-color: var(--brand-fill); }
.pick__opt input:focus-visible + span { outline: 2px solid var(--ocean-300); outline-offset: 2px; }

/* ══ Comparison slider (behaviour in js/before-after.js) ═══════════════ */

.compare {
  position: relative;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  background: var(--ink);
  /* pan-y keeps vertical page scrolling working over the slider on touch. */
  touch-action: pan-y;
  user-select: none;
}

.compare__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.compare__before-wrap {
  position: absolute;
  inset: 0;
  clip-path: inset(0 var(--compare-inset, 54%) 0 0);
}

.compare__handle {
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--compare-pos, 46%);
  width: 2px;
  padding: 0;
  border: 0;
  background: var(--paper);
  cursor: ew-resize;
  transform: translateX(-1px);
}

.compare__grip {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 54px;
  height: 54px;
  border-radius: 50%;
  background: var(--paper);
  display: grid;
  place-items: center;
  box-shadow: 0 6px 24px color-mix(in srgb, var(--ink) 40%, transparent);
}

.compare__grip svg { width: 22px; height: 22px; color: var(--ink); }

.compare__tag { position: absolute; top: var(--space-4); }
.compare__tag--before { left: var(--space-4); }
.compare__tag--after { right: var(--space-4); }

/* Only animate when the handle is driven by the keyboard — dragging must
   track the pointer exactly. */
@media (prefers-reduced-motion: no-preference) {
  .compare--animating .compare__handle,
  .compare--animating .compare__before-wrap {
    transition: left var(--duration) var(--ease), clip-path var(--duration) var(--ease);
  }
}

/* ══ Header ════════════════════════════════════════════════════════════ */

.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  border-bottom: var(--hairline) solid var(--rule-on-ink);
}

.site-header__inner {
  min-height: var(--header-height);
  display: flex;
  align-items: center;
  gap: var(--space-9);
}

.site-header__brand {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  color: var(--paper);
  text-decoration: none;
  margin-right: auto;
}

.site-header__brand:hover { text-decoration: none; }
/* The client's mark is detailed (twin gables, windows, a breaking wave), so it
   needs more room than a simplified glyph would. Below 48px it turns to mud. */
.site-header__mark { width: 54px; height: auto; flex: none; }

.site-header__name {
  display: block;
  font-size: 19px;
  font-weight: 800;
  letter-spacing: -0.02em;
  line-height: 1.05;
}

.site-header__sub {
  display: block;
  margin-top: 3px;
  font-family: var(--font-mono);
  font-size: var(--size-label);
  letter-spacing: var(--track-label);
  color: var(--muted-on-ink);
}

.site-nav {
  display: flex;
  align-items: center;
  gap: var(--space-8);
  font-size: 14px;
  font-weight: 500;
}

/* `.site-nav a` alone is (0,1,1) and layout.css's
   `.band--ink a:not(.btn):not(.contact-row)` is (0,3,1), so this rule never
   applied: every header link rendered --ocean-300 and the hover below was a
   no-op. Found by reading computed styles in a browser, not the stylesheet.

   Scoped to .site-header to reach (0,3,1) and win on load order, which is
   fixed — layout.css before components.css, see the site layout. */
.site-header .site-nav a:not(.btn) { color: var(--paper); text-decoration: none; }
.site-header .site-nav a:not(.btn):hover { color: var(--ocean-300); text-decoration: none; }

/* The section a visitor is in.

   The markup has carried aria-current since the header was built, so a screen
   reader announced the current section correctly the whole time — but with no
   rule for it, a sighted visitor on /projects saw five identical links.

   Colour AND a rule, never colour alone. base.css makes this argument for
   inline links and it holds here: --ocean-300 against --paper is nowhere near
   the 3:1 that would let colour carry the distinction by itself. The rule is
   --ocean-500, which is display-only as text but is exactly what that token
   is for as a rule. */
.site-header .site-nav a[aria-current]:not(.btn) {
  color: var(--ocean-300);
  border-bottom: 2px solid var(--ocean-500);
  padding-bottom: 2px;
}

.site-nav .btn { font-size: 14px; padding: var(--space-3) var(--space-5); }

.site-nav__phone {
  font-family: var(--font-mono);
  letter-spacing: 0.02em;
  font-weight: 500;
}

.site-header__toggle {
  display: none;
  width: 44px;
  height: 44px;
  background: transparent;
  border: var(--hairline) solid var(--rule-on-ink);
  cursor: pointer;
  padding: 0;
  position: relative;
}

.site-header__bars,
.site-header__bars::before,
.site-header__bars::after {
  position: absolute;
  left: 50%;
  width: 20px;
  height: 2px;
  background: var(--paper);
  transform: translateX(-50%);
  transition: transform var(--duration) var(--ease), opacity var(--duration) var(--ease);
}

.site-header__bars { top: calc(50% - 1px); }
.site-header__bars::before { content: ""; top: -6px; }
.site-header__bars::after { content: ""; top: 6px; }

[aria-expanded="true"] .site-header__bars { background: transparent; }
[aria-expanded="true"] .site-header__bars::before { transform: translateX(-50%) translateY(6px) rotate(45deg); }
[aria-expanded="true"] .site-header__bars::after { transform: translateX(-50%) translateY(-6px) rotate(-45deg); }

@media (max-width: 900px) {
  .site-header__toggle { display: block; }



  .site-nav {
    position: fixed;
    inset: var(--header-height) 0 0;
    background: var(--ink);
    flex-direction: column;
    align-items: stretch;
    justify-content: flex-start;
    gap: 0;
    padding: var(--space-6) var(--gutter) var(--space-10);
    font-size: 22px;
    font-weight: 700;
    /* display:none rather than opacity: a hidden panel must not be a focus trap */
    display: none;
    overflow-y: auto;
  }

  .site-nav[data-open] { display: flex; }

  .site-nav > a:not(.btn) {
    padding: var(--space-5) 0;
    border-bottom: var(--hairline) solid var(--rule-on-ink);
  }

  /* The active item, in the drawer.

     The rule above is (0,2,2) and the desktop [aria-current] rule is (0,2,1),
     so the row divider was winning and the accent vanished below 901px — the
     width most of this client's visitors are on. Caught by driving the page,
     not by the stylesheet test, which only ever saw the desktop rule.

     A left marker rather than a heavier bottom border: every row already has
     a bottom rule, so thickening one is ambiguous, while a marker down the
     side reads immediately in a stacked list. */
  .site-nav > a[aria-current]:not(.btn) {
    border-left: 3px solid var(--ocean-500);
    padding-left: var(--space-4);
  }

  .site-nav .btn { margin-top: var(--space-5); font-size: 16px; padding: var(--space-4) var(--space-6); }
}

/* The strapline plus the full nav needs ~1000px of header. Below that it wraps
   to two or three lines and inflates the sticky header, so it is dropped —
   the mark and wordmark carry the identity, and the licence is on Contact and
   in the footer. Never let it wrap. */
.site-header__sub { white-space: nowrap; }

@media (max-width: 1100px) {
  .site-header__sub { display: none; }
}

@media (max-width: 560px) {
  .site-header__mark { width: 46px; }
  .site-header__name { font-size: 17px; }
}

/* ══ Footer ════════════════════════════════════════════════════════════ */

.site-footer { border-top: var(--hairline) solid var(--rule-on-ink); }

/* Top-aligned since the link groups gained headings. Baseline alignment read
   well when all three blocks were short and unlabelled; with headings, the
   brand sank to the bottom of a column-height row and left a hole above it. */
.site-footer__inner {
  padding-block: var(--space-11) var(--space-9);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-10);
  justify-content: space-between;
  align-items: flex-start;
}

.site-footer__name {
  font-size: 26px;
  font-weight: 900;
  letter-spacing: -0.03em;
  line-height: 1.1;
}

.site-footer__licence {
  margin-top: var(--space-3);
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.14em;
  color: var(--muted-on-ink);
}

/* Labelled columns. The links used to be two unheaded runs in a flex row,
   which on a phone wrapped into a hedge of words with nothing to say what
   they were. */
.site-footer__col {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.site-footer__heading {
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--label-on-ink);
}

/* A column, not a row: five links reading downward are scannable, and each
   one gets a full-width hit area instead of a word-sized one. */
.site-footer__nav {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  font-size: 15px;
}

.site-footer__contact {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  font-size: 15px;
}

.site-footer__note {
  margin-top: var(--space-2);
  font-size: 13px;
  color: var(--muted-on-ink);
}

.site-footer__legal {
  width: 100%;
  border-top: var(--hairline) solid var(--rule-on-ink);
  padding-top: var(--space-6);
  margin-top: var(--space-4);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-6);
  justify-content: space-between;
  font-size: 13px;
  color: var(--muted-on-ink);
}

.site-footer__legal a { color: var(--muted-on-ink); }
.site-footer__legal a:hover { color: var(--ocean-300); }

/* ══ Contact rows — call / text / email ════════════════════════════════ */

.contact-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  border: var(--hairline) solid var(--ink);
  padding: var(--space-5) var(--space-6);
  color: var(--ink);
  text-decoration: none;
  transition: background var(--duration) var(--ease), color var(--duration) var(--ease);
}

.contact-row + .contact-row { margin-top: var(--space-3); }
.contact-row:hover { background: var(--ink); color: var(--paper); text-decoration: none; }

.contact-row__key {
  font-family: var(--font-mono);
  font-size: var(--size-eyebrow);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
}

.contact-row__val { font-size: 19px; font-weight: 700; text-align: right; }

@media (max-width: 480px) {
  .contact-row__val { font-size: 16px; }
  .field-row { grid-template-columns: 1fr; }
  .field-row .field + .field { margin-top: var(--space-5); }
}

/* ══ Testimonial card ══════════════════════════════════════════════════
   The whole component lives here, not split with pages/home.css. That split
   is how the rating stars ended up unreachable on the homepage: a shared
   partial's styles belong in the shared stylesheet, all of them.

   The anatomy comes from the client's reference designs — a quote mark
   opening the card, the words, a divider, then the person who said them, with
   the portrait given room to be a face. The styling does not: all three
   references round their corners, drop a shadow beneath them and, in one
   case, add a second accent colour. --radius is 0 across this system on
   purpose and the design rules allow exactly one accent voice. Structure
   travels between designs; decoration does not. */

.testimonials__grid { margin-top: var(--space-9); }

.testimonial {
  display: flex;
  flex-direction: column;
  padding: var(--space-8) var(--space-7);
  background: var(--paper);
  border: var(--hairline) solid var(--rule);
  border-radius: var(--radius);
}

/* The card announces itself as a quotation before a word of it is read.
   --ocean-500 is display-only as text and this is a 56px glyph, which is
   exactly what that token is for. */
.testimonial__mark {
  font-size: 56px;
  line-height: 0.6;
  font-weight: 900;
  color: var(--brand);
  /* The glyph carries a generous side bearing; pull it back so it optically
     aligns with the text below instead of looking indented. */
  margin-left: -0.06em;
}

.testimonial__rating {
  margin-top: var(--space-5);
  font-size: 18px;
  color: var(--brand);
  letter-spacing: 2px;
}

.testimonial__rating-off { color: var(--ink-300); }

.testimonial__quote {
  margin: var(--space-5) 0 0;
  font-size: var(--size-body-l);
  line-height: 1.6;
  /* Pushes the attribution down, so a row of cards lines its footers up
     rather than ending raggedly. */
  flex: 1;
}

/* "Show more" on a trimmed review, and the dialog it opens.

   The card shows the first hundred characters or so; the rest lives in a
   native <dialog>, which handles focus trapping, Escape and the backdrop
   itself. Square corners and hairlines here as everywhere — a modal is not a
   licence to round something. */

.testimonial__more {
  /* A link in a sentence, not a button under one. It inherits the quote's
     size and colour so it reads as the continuation it is — the boxed
     version looked like a call to action competing with the review. */
  display: inline;
  padding: 0;
  margin: 0;
  background: none;
  border: 0;
  color: var(--brand-text);
  font: inherit;
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-thickness: var(--hairline);
  cursor: pointer;
}

.testimonial__more:hover { color: var(--ink); }

.testimonial__more[hidden] { display: none; }

.quote-dialog {
  width: min(56ch, calc(100vw - var(--space-8)));
  padding: 0;
  border: var(--hairline) solid var(--ink);
  border-radius: var(--radius);
  background: var(--paper);
  color: var(--ink);
}

.quote-dialog::backdrop { background: color-mix(in srgb, var(--ink) 72%, transparent); }

.quote-dialog__body {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-5);
  padding: var(--space-8) var(--space-7);
}

.quote-dialog__quote {
  margin: 0;
  font-size: var(--size-body-l);
  line-height: 1.6;
}

/* No coarse-pointer floor here on purpose. The design system exempts links
   inside a sentence from the 44px target — forcing a height on an inline
   element breaks the line box it sits in, and the text around it is the
   generous hit area. */

/* The divider from the references: what stops the card reading as one
   undifferentiated block of grey. */
.testimonial__by {
  margin-top: auto;
  padding-top: var(--space-6);
  border-top: var(--hairline) solid var(--rule);
  display: flex;
  align-items: center;
  gap: var(--space-5);
}

/* Square, not a circle — every reference rounds it and this system does not.
   56px rendered from a larger file, so it stays sharp on the phone screens
   most of these visitors arrive on. */
.testimonial__photo {
  flex: none;
  width: 56px;
  height: 56px;
  object-fit: cover;
  background: var(--ink-200);
}

/* Name, village and project as one unit beside the portrait, rather than
   three loose spans the flex row would space apart as if unrelated. */
.testimonial__who {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--space-1);
  min-width: 0;
  /* The portrait's own height, held whether or not there is a portrait. Most
     testimonials will never carry one, and without this the divider sits at a
     different height on every card in a row — which reads as a wonky grid
     rather than as a deliberate difference between the cards.

     On __who and not on __by: box-sizing is border-box, so a min-height on
     __by smaller than its padding plus content does nothing at all. It was
     56px there and measured no change. */
  min-height: 56px;
}

.testimonial__name { font-weight: 700; }

.testimonial__village {
  font-family: var(--font-mono);
  font-size: var(--size-caption);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--muted);
}

.testimonial__project {
  font-size: var(--size-caption);
  color: var(--brand-text);
}

/* The media slot: either a video of the client saying it, or a photograph of
   the job they are talking about. One slot, because a card showing a video
   AND a still of the same kitchen shows the same thing twice.

   16:9 because that is what a phone records held sideways, and object-fit
   covers the portrait ones without letterboxing the card. The ratio is fixed
   here so the space is reserved before either arrives — a card that grows
   when its photograph loads shoves the whole grid down.

   For the video, the still is what a visitor sees until they press play, and
   the player fetches nothing before then: six of these on a page must not
   pull six videos over Guam mobile data. */
.testimonial__video,
.testimonial__work {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  margin-bottom: var(--space-6);
  background: var(--ink-200);
  object-fit: cover;
}

/* A player with nothing behind it is a black rectangle; a photograph is not. */
.testimonial__video { background: var(--ink); }

@media (max-width: 560px) {
  .testimonial { padding: var(--space-7) var(--space-6); }
  .testimonial__mark { font-size: 44px; }
}

/* ══ Service area ══════════════════════════════════════════════════════
   Nineteen village names, as a directory rather than a list.

   In components.css, NOT pages/interior.css. The band renders on the home
   page too, and home.blade.php does not load interior.css — parked there,
   every rule below was unreachable and nineteen villages rendered as a
   default bulleted list. That is the third time this stylesheet split has
   bitten; the rule is simply that anything a shared partial renders belongs
   here.

   A modular grid is the system's own idiom: equal cells divided by hairlines,
   not gaps. Each name in its own cell reads as a place served rather than as
   a run of words. */

.village-list {
  list-style: none;
  margin-top: var(--space-9);
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));

  /* The grid draws itself from the cells' own two edges; these two close the
     open sides so the block is a complete rectangle at any column count. */
  border-top: var(--hairline) solid var(--rule);
  border-left: var(--hairline) solid var(--rule);
}

.village-list__item {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-5) var(--space-6);
  border-right: var(--hairline) solid var(--rule);
  border-bottom: var(--hairline) solid var(--rule);
  font-family: var(--font-mono);
  font-size: var(--size-data);
  letter-spacing: 0.01em;
  background: var(--paper);
}

/* A short brand rule before each name. The system has no icon set, and a
   drawn rule is sharper at every zoom than a glyph would be — the same
   reasoning as .tick-list. --ocean-500 is display-only as text and exactly
   right as a rule. */
.village-list__item::before {
  content: "";
  flex: none;
  width: 10px;
  height: 2px;
  background: var(--brand);
}

.village-list__note {
  margin-top: var(--space-5);
  font-family: var(--font-mono);
  font-size: var(--size-caption);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
}

/* Two across on a phone. 190px cells would collapse to one column and turn
   nineteen villages into a very long scroll on the screen most of these
   visitors are using. */
@media (max-width: 560px) {
  .village-list { grid-template-columns: repeat(2, 1fr); }

  .village-list__item {
    padding: var(--space-4);
    gap: var(--space-2);
  }
}

/* ══ Field validation ══════════════════════════════════════════════════
   The message sits under the input it belongs to, not only in a summary at
   the top: on a phone the summary has scrolled off before the visitor
   reaches the field, which is the argument the admin's own field component
   already makes.

   The pale --danger-on-ink is SCOPED, not the default. Both enquiry forms sit
   on .band--ink, where #B3261E measures about 2:1 and the message is
   unreadable in the one place it has to be read — so that band gets the pale
   step, exactly as .field__label already does.

   It used to be the unconditional value, which meant the admin inherited it:
   #F2B8B5 measures 1.38:1 on the admin ground and 1.58:1 on a card. Every
   validation message in the admin was in the DOM, passed any test that
   asserted the text was on the page, and could not be read off the screen.
   That is the failure mode this comment exists to stop coming back. */

.field__error {
  display: block;
  margin-top: var(--space-2);
  font-size: 14px;
  line-height: 1.4;
  color: var(--danger);
}

.band--ink .field__error { color: var(--danger-on-ink); }

/* The field itself, so the error is not carried by the message alone —
   somebody scanning back up the form should see which input to return to. */
.input--invalid {
  border-color: var(--danger-on-ink);
}

.input--invalid:focus-visible {
  outline-color: var(--danger-on-ink);
}

/* ══ Back to top ═══════════════════════════════════════════════════════
   Floating, bottom-right, on every public page.

   [hidden] needs an explicit display:none because .to-top sets display:flex,
   and a class beats the UA's [hidden] rule — the control would otherwise sit
   on screen from the moment the page loads, which is the state the script
   exists to avoid.

   Square, like everything else here: --radius is 0 on purpose. */

.to-top {
  position: fixed;
  right: var(--space-6);
  bottom: var(--space-6);
  z-index: 40;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  background: var(--ink);
  color: var(--paper);
  border: var(--hairline) solid var(--ink);
  text-decoration: none;
  opacity: 0;
  transform: translateY(8px);
  transition: opacity var(--duration) var(--ease), transform var(--duration) var(--ease),
              background var(--duration) var(--ease);
}

.to-top[hidden] { display: none; }

/* Set by site.js one frame after the control is unhidden, so the transition
   has a state to move from. */
.to-top[data-visible] { opacity: 1; transform: none; }

.to-top:hover { background: var(--ocean-600); border-color: var(--ocean-600); }

@media (prefers-reduced-motion: reduce) {
  .to-top { transition: none; transform: none; }
}

/* ══ Touch targets ═════════════════════════════════════════════════════
   Keyed to `pointer: coarse` rather than to a width, because the input device
   is the actual question — a 1024px tablet needs finger-sized targets and a
   700px desktop window does not.

   WCAG 2.5.8 sets the floor at 24×24px; 44px is the platform convention and
   what a thumb actually wants. Links sitting inside a sentence are exempt from
   2.5.8 and are deliberately left alone — padding them out would wreck the
   line rhythm of body copy. */

@media (pointer: coarse) {
  .site-nav a,
  .site-footer__nav a,
  .site-footer__contact a,
  /* Descendant, not child: these links were wrapped in a <nav> when the legal
     pages landed, and a child selector silently stopped matching — three
     footer links dropped to a 21px touch target with nothing to show for it. */
  .site-footer__legal a,
  .crumbs a,
  .project-card__more a,
  .project-card__title a,
  .work__all a,
  .service-cell__more,
  .work__service a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }

  /* Radio pills and chips are hit directly with a finger. */
  .pick__opt span,
  .chip {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
  }

  /* An underlined bottom border is a small target; give the whole field row
     the height a finger expects. */
  .input {
    min-height: 44px;
  }

  textarea.input {
    min-height: 120px;
  }
}

/* ══ Long-form prose ═══════════════════════════════════════════════════
   Shared by blog posts and the legal pages, so it lives here rather than
   in one page's stylesheet. */

/* Article body. A measure of ~68 characters is what makes long copy readable;
   the rest of the site runs wider because it is not prose. */
.prose {
  margin-top: var(--space-7);
  max-width: 68ch;
  font-size: 18px;
  line-height: 1.7;
}

.prose > * + * { margin-top: var(--space-6); }

.prose h2 {
  margin-top: var(--space-11);
  font-size: var(--size-display-m);
  letter-spacing: var(--track-display);
}

.prose h3 { margin-top: var(--space-9); font-size: var(--size-heading); }
.prose h4 { margin-top: var(--space-8); }

.prose ul, .prose ol { padding-left: var(--space-6); }
.prose li + li { margin-top: var(--space-2); }
.prose ul li { list-style: disc; }
.prose ol li { list-style: decimal; }

.prose a { text-decoration: underline; text-underline-offset: 3px; }

.prose blockquote {
  margin-inline: 0;
  padding-left: var(--space-6);
  border-left: 3px solid var(--brand);
  font-size: 20px;
  line-height: 1.55;
}

.prose img { width: 100%; height: auto; }

.prose table { width: 100%; border-collapse: collapse; font-size: 16px; }
.prose th, .prose td {
  text-align: left;
  padding: var(--space-3) var(--space-4);
  border-bottom: var(--hairline) solid var(--rule);
}

.prose hr { margin-block: var(--space-9); }

@media (max-width: 768px) {
  .prose { font-size: 17px; }
}

/* Legal links sit alongside the copyright, wrapping under it on narrow
   screens rather than squeezing the notice. */
.site-footer__legal-links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-5);
}

.quote__privacy { margin-top: var(--space-3); }
.quote__privacy a { text-decoration: underline; text-underline-offset: 2px; }

/* Pagination. Laravel's default view is Tailwind-classed, which this project
   forbids and does not load — paginated screens were rendering unstyled. */
.pagination {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-top: var(--space-9);
}

.pagination__pages {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  list-style: none;
  margin: 0;
  padding: 0;
}

.pagination__link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 40px;
  min-height: 40px;
  padding-inline: var(--space-3);
  font-family: var(--font-mono);
  font-size: var(--size-caption);
  letter-spacing: var(--track-label);
  color: var(--ink-800);
  border: var(--hairline) solid var(--rule);
}

a.pagination__link:hover { border-color: var(--brand); color: var(--brand); }

.pagination__link--current {
  background: var(--brand);
  border-color: var(--brand);
  color: var(--brand-text);
}

.pagination__link--off { color: var(--muted); border-color: var(--rule); }

.pagination__gap { display: inline-flex; align-items: center; min-height: 40px; color: var(--muted); }

@media (pointer: coarse) {
  .pagination__link { min-width: 44px; min-height: 44px; }

  /* Already 44px, but a floating control is the one that gets shrunk in a
     later tidy-up because it "looks big" on a desktop. Pinned here, and a
     test asserts .to-top appears in this block. Lifted off the bottom edge
     as well, clear of a browser's own toolbar. */
  .to-top {
    width: 48px;
    height: 48px;
    right: var(--space-4);
    bottom: var(--space-9);
  }
}

/* ══ Empty state ═══════════════════════════════════════════════════════
   Shown where a grid would otherwise render nothing under a heading. The
   client creates a category before the first job in it and adds a project
   the evening after the walkthrough, so these are ordinary states, not
   errors — quiet type, not a warning. */

.empty-state {
  margin-top: var(--space-8);
  padding: var(--space-8) 0;
  border-top: var(--hairline) solid var(--rule);
  color: var(--muted);
  font-size: var(--size-body-l);
}

/* ══ Folded testimonials ═══════════════════════════════════════════════
   A native <details>, no script. It opens with JavaScript off, so the folded
   quotes stay in the document for a crawler and are one click away for
   everyone else.

   Safe to use here: the display:contents trap recorded in .ai/rules/css.md
   applies to a <details> that must vanish into a flex context at a
   breakpoint. This one is a block that stays a block. */

.testimonials__more { margin-top: var(--space-9); }

.testimonials__more-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-6);
  border: var(--hairline) solid var(--ink);
  cursor: pointer;
  font-family: var(--font-mono);
  font-size: var(--size-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  list-style: none;
  transition: background var(--duration) var(--ease), color var(--duration) var(--ease);
}

/* Safari draws its own triangle through ::-webkit-details-marker, which the
   list-style above does not reach. */
.testimonials__more-toggle::-webkit-details-marker { display: none; }

.testimonials__more-toggle::after {
  content: "+";
  font-size: 16px;
  line-height: 1;
}

.testimonials__more[open] .testimonials__more-toggle::after { content: "–"; }

.testimonials__more-toggle:hover {
  background: var(--ink);
  color: var(--paper);
}

.testimonials__more .testimonials__grid { margin-top: var(--space-8); }

@media (pointer: coarse) {
  .testimonials__more-toggle { min-height: 44px; }
}
