/* ==========================================================================
   site.css — D & I Repair, Inc. (Manhattan, KS). editorial-modern family,
   manifest-stack archetype (brief.md, approved C3 round 2, 2026-10-03).

   HARD RULE (house-tech-spec §3): no raw hex, no px/rem size literals, no
   font-name literals in this file. Tokens only, except the house breakpoints
   (48em/64em) and an explicit @allow-literal comment.
   ========================================================================== */

/* base.css's skip-link defaults to --color-accent/--color-accent-ink
   (3.48:1 here, below the 4.5:1 AA-body floor) — this brief reserves accent
   strictly for the one-shot tick-mark stroke, never a text/fill pairing
   (brief §2, C2 fix #2). Swapped for the reversed ink/bg pair the primary
   CTA already uses, 15.38:1. */
.skip-link {
  background-color: var(--color-ink);
  color: var(--color-accent-ink);
}

/* --- Layout primitives ---------------------------------------------------- */

.container {
  width: 100%;
  max-width: var(--layout-container);
  margin-inline: auto;
  padding-inline: var(--layout-gutter);
}

/* Vertical rhythm hook. --section-gap is the only inter-section spacing value
   on a site (design-rules §7.1); the brief picks which scale step it is. */
.section {
  padding-block: var(--section-gap);
}

.measure {
  max-width: var(--layout-measure);
}

.eyebrow {
  font-size: var(--text-small);
  font-weight: var(--font-weight-body-strong);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--color-ink-muted);
}

/* C2 fix #2: the eyebrow's "— " prefix renders in --color-ink-muted
   (8.09:1), never --color-accent (3.48:1, below the 4.5:1 AA-body floor at
   this size) — brief §2. A CSS-generated glyph, not authored copy. */
.eyebrow::before {
  content: '\2014\0020';
  color: var(--color-ink-muted);
}

.section h2 {
  font-size: var(--text-h2);
  margin-block-end: var(--space-md);
}

.section .measure + h2,
.section h2 + .measure,
.section h2 + p {
  margin-block-start: var(--space-sm);
}

/* --- Header & navigation ----------------------------------------------------
   Sticky, house chrome (brief §4.2 row 0, §4.6). --bar-height (<=56px, no
   distinct desktop height) as a MIN, not a hard block-size: JS-on collapsed
   and every desktop width fit it exactly (the brief's chrome arithmetic),
   but a JS-off visitor at <48em gets the full nav inline via the
   <noscript> override, which a hard-clipped height would overlap rather
   than grow for. Preview-only stacking offset lives in css/preview.css,
   never here, so permanent behaviour survives purchase. */

.site-header {
  position: sticky;
  inset-block-start: 0;
  z-index: var(--z-header);
  min-block-size: var(--bar-height);
  display: flex;
  align-items: center;
  border-bottom: var(--border-hairline) var(--border-style) var(--color-border);
  background-color: var(--color-bg);
}

.site-header__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  width: 100%;
}

.site-header__brand {
  font-family: var(--font-display);
  font-size: var(--text-lead);
  font-weight: var(--font-weight-display);
  text-decoration: none;
  letter-spacing: var(--tracking-tight);
  color: var(--color-ink);
}

.nav-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  padding: var(--space-2xs) var(--space-xs);
  /* The nav-toggle's only shape is this hairline, so it is a CONTROL boundary
     (WCAG 1.4.11, >= 3:1), not a decorative one — use --color-border-strong. */
  border: var(--border-hairline) var(--border-style) var(--color-border-strong);
  border-radius: var(--radius-sm);
  font-size: var(--text-small);
  font-weight: var(--font-weight-body-strong);
}

.site-nav__list {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm);
  margin: 0;
  padding: 0;
  list-style: none;
}

.site-nav__link {
  display: inline-flex;
  align-items: center;
  text-decoration: none;
  font-size: var(--text-small);
  font-weight: var(--font-weight-body-strong);
}

.site-nav__link:hover,
.site-nav__link:focus-visible {
  text-decoration: underline;
  text-underline-offset: var(--space-3xs);
}

/* QA r3 FIX perf/cls (this skeleton-level defect was confirmed present,
   verbatim, on every site built from it -- see
   runs/3-elements-landscaping-asheville-nc/qa/critic-round-2.md): the
   collapsed state used to apply only once a deferred `type="module"`
   script called `initNav()` and set `[data-nav-ready]`, so the full nav
   list rendered open at first paint and then collapsed into the hamburger
   AFTER first contentful paint -- a timing race with no fixed outcome that
   shifted the whole header (and everything below it) and scored CLS
   0.066-0.094 in the majority of Lighthouse runs. Inverting the default
   removes the race entirely: closed is now the plain CSS default below
   48em, with no JS gate, so there is nothing left to collapse post-paint.
   The one visitor this would otherwise strand -- JS off, so
   `[data-nav-ready]` never lands -- gets the nav back via the <noscript>
   stylesheet override in `@shared:head-boilerplate` (index.html/buy.html/
   thanks.html), which ships last in source order and wins the
   display:block/display:none tie on cascade order alone, no
   `!important`. The nav-toggle button already ships `hidden` in the HTML
   and only JS ever clears that, so JS-off visitors never see a dead
   control -- nothing to change there. */
.site-nav {
  display: none;
}

[data-nav-ready] .site-nav[data-nav-open='true'] {
  display: block;
  flex-basis: 100%;
}

@media (min-width: 48em) {
  .site-nav {
    display: block;
  }

  [data-nav-ready] .nav-toggle {
    display: none;
  }
}

/* --- Buttons --------------------------------------------------------------
   Two roles, no more. Radius, weight and colour all come from the brief via
   tokens — including --radius-none, which is a legitimate answer.          */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2xs);
  padding: var(--space-xs) var(--space-md);
  border: var(--border-hairline) var(--border-style) transparent;
  border-radius: var(--radius-sm);
  font-weight: var(--font-weight-body-strong);
  line-height: var(--leading-snug);
  text-align: center;
  text-decoration: none;
  transition: background-color var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out);
}

/* BINDING (brief §2, C2 fix #2): accent is reserved strictly for the
   one-shot tick-mark stroke. The primary CTA fill is --color-ink with
   --color-accent-ink (paper-white) as its text — the reversed ink/bg pair,
   15.38:1 — never --color-accent as a fill. */
.btn--primary {
  background-color: var(--color-ink);
  color: var(--color-accent-ink);
}

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

.btn--quiet {
  /* Outline button: the border is the whole control, so it takes the 3:1
     control-boundary token, not the decorative hairline (WCAG 1.4.11). */
  border-color: var(--color-border-strong);
  color: var(--color-ink);
}

.btn--quiet:hover {
  background-color: var(--color-surface);
}

/* --- Form -----------------------------------------------------------------
   One form per site (spec §6). POSTs to the endpoint constant in main.js;
   works as a plain HTML POST with JS off; auto-hides when no endpoint is
   configured so a spec site never shows a dead form.                       */

.form {
  display: grid;
  gap: var(--space-md);
  max-width: var(--layout-measure);
}

.field {
  display: grid;
  gap: var(--space-3xs);
}

.field__label {
  font-size: var(--text-small);
  font-weight: var(--font-weight-body-strong);
}

.field__control {
  width: 100%;
  min-height: var(--layout-tap-min);
  padding: var(--space-2xs) var(--space-xs);
  background-color: var(--color-surface);
  color: var(--color-ink);
  border: var(--border-hairline) var(--border-style) var(--color-border-strong);
  border-radius: var(--radius-sm);
}

textarea.field__control {
  min-height: var(--space-3xl);
  resize: vertical;
}

.field__hint {
  font-size: var(--text-small);
  color: var(--color-ink-muted);
}

/* Honeypot: present in the DOM for bots, unreachable for humans and assistive
   tech. Not display:none — some bots skip those. */
.form__trap {
  position: absolute;
  /* @allow-literal: off-canvas parking position, not a design value */
  left: -9999px;
  opacity: 0;
  pointer-events: none;
}

.form__status {
  font-size: var(--text-small);
  color: var(--color-ink-muted);
}

.form__status[data-state='error'] {
  color: var(--color-ink);
  font-weight: var(--font-weight-body-strong);
}

/* --- Hero — the living-hero moment -----------------------------------------
   manifest-stack archetype (brief §4.3/§4.4/§4.5). DOM order unmodified:
   eyebrow -> h1 -> primary CTA -> field -> lead -> facts. Single column
   below 64em; 46%/54% split at and above it (brief §4.6), placed with grid
   positioning on the flat children rather than a wrapper div, so the DOM
   order above is never disturbed by the visual split. */

.hero {
  padding-block-end: var(--section-gap);
  background-color: var(--color-bg);
}

.hero__inner {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  padding-block-start: var(--space-lg);
  padding-inline: var(--space-md);
}

/* max-width in ch, not px: self-scales with whichever face is currently
   painted, so the fallback-to-webfont swap breaks the line at the same word
   in both states (perf/cls — see tokens.css). */
.hero__eyebrow {
  max-width: var(--measure-eyebrow);
}

.hero__title {
  font-family: var(--font-display-italic);
  font-style: italic;
  font-size: var(--text-hero);
  line-height: var(--leading-tight);
}

.hero__cta-row {
  margin: 0;
}

.hero__lead {
  font-size: var(--text-lead);
  line-height: var(--leading-body);
}

/* C2 fix #2: the facts strip's "✓ " marker renders in --color-ink-muted
   (8.09:1), never --color-accent — the one fact in the strip, no star
   rating (brief §4.2/§7 item 6). */
.hero__facts {
  margin: 0;
  font-size: var(--text-small);
  font-weight: var(--font-weight-body-strong);
  color: var(--color-ink-muted);
}

.hero__facts::before {
  content: '\2713\0020';
  color: var(--color-ink-muted);
}

/* --- The field: the full 5-item manifest + tick + satellites + atmosphere -
   .hero-field is the EXACT selector brief §2.1/C2 binding instruction #1
   requires wired to a real element — the tile's own failure was this exact
   class, defined in CSS, applied to nothing. */

.hero-field {
  position: relative;
  overflow: clip;
  padding: var(--space-sm);
  background-color: var(--color-surface);
}

/* Two-layer radial atmosphere, reproduced verbatim from brief §2.1 — both
   layers anchored at the field's bottom edge, structurally never reaching
   manifest row 1 (the marginal-accent row). rgba stops are the brief's own
   computed, re-measured geometry, not a reusable colour role. */
.hero-field::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background:
    radial-gradient(85% 70% at 90% 100%, rgba(176, 122, 30, .09), transparent 58%),
    radial-gradient(70% 60% at 15% 100%, rgba(33, 31, 28, .05), transparent 55%);
  animation: atmosDrift3 var(--atmos-duration) ease-in-out infinite;
}

@keyframes atmosDrift3 {
  0%, 100% { opacity: .78; }
  50% { opacity: 1; }
}

/* IMG-1: the hero-field background plate (brief §5), decorative (alt="").
   Full-box coverage, not a 40%-height clip — a hard clip edge is a
   desktop-audit FIX (desktop/partial-bg). The brief's lower-~40%
   confinement is carried instead by a mask fading the plate to nothing
   above that line: gentler than a box edge, same zero-reach result at
   row 1 (brief §2.1/§5). */
.hero-field__texture {
  position: absolute;
  inset: 0;
  z-index: 1;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: bottom;
  opacity: .08;
  -webkit-mask-image: linear-gradient(to top, black 0%, black 35%, transparent 62%);
  mask-image: linear-gradient(to top, black 0%, black 35%, transparent 62%);
}

.manifest {
  position: relative;
  z-index: 2;
  margin: 0;
  padding: 0;
  list-style: none;
  border-block-start: var(--border-hairline) var(--border-style) var(--color-border);
}

.manifest__row {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding-block: var(--space-xs);
  /* Deterministic row height, driven by the icon's own fixed box rather
     than by text line-height — the webfont swap (Newsreader/Public Sans,
     none of which are preloaded here except the H1's italic) must not be
     able to move this row's height by even a fractional px (perf/cls). */
  min-block-size: calc(var(--space-md) + 2 * var(--space-xs));
  border-block-end: var(--border-hairline) var(--border-style) var(--color-border);
}

.manifest__label {
  font-family: var(--font-display);
  font-weight: var(--font-weight-display);
  font-size: var(--text-body);
  line-height: var(--leading-snug);
  color: var(--color-ink);
}

.manifest__mark {
  flex: none;
  width: var(--space-md);
  height: var(--space-md);
  color: var(--color-ink);
}

.manifest__mark path,
.manifest__mark circle,
.manifest__mark rect,
.manifest__mark line {
  fill: none;
  stroke: currentColor;
}

/* The one-shot moment: a hand-drawn tick settles once beside the
   Onyx-collection line (brief §4.3). Accent stroke — the tick mark is the
   ONE place --color-accent appears, and it is a non-text graphical object
   (WCAG 1.4.11, 3:1 floor, 3.48:1 measured), never text (brief §2). */
.manifest__mark--tick {
  color: var(--color-accent);
}

.manifest__mark--tick path {
  stroke-dasharray: 26;
  stroke-dashoffset: 26;
  animation: manifest-tick-draw var(--tick-draw-duration) var(--ease-out) 1 forwards;
}

@keyframes manifest-tick-draw {
  from { stroke-dashoffset: 26; }
  to { stroke-dashoffset: 0; }
}

/* Ambient satellites — two tool-outline marks on slow, independent,
   non-synchronised loops (design-rules §0; brief §4.3/§6), plus two plain
   static marks. Together the four concretely deliver the vertical's §6.1
   anchor (hand tools, fasteners, raw building material) as literal tool
   shapes inside the hero frame itself. */
.manifest__mark--screw {
  animation: manifest-sway-screw var(--satellite-screw) ease-in-out infinite;
}

.manifest__mark--hinge {
  animation: manifest-sway-hinge var(--satellite-hinge) ease-in-out infinite;
}

@keyframes manifest-sway-screw {
  0%, 100% { transform: rotate(calc(-1 * var(--drift-sm) * 4)); }
  50% { transform: rotate(calc(var(--drift-sm) * 4)); }
}

@keyframes manifest-sway-hinge {
  0%, 100% { transform: rotate(calc(var(--drift-sm) * 3)); }
  50% { transform: rotate(calc(-1 * var(--drift-sm) * 3)); }
}

/* Desktop split — >=64em: 46% text column / 54% manifest column (brief
   §4.6), placed on the flat DOM children so the eyebrow -> h1 -> CTA ->
   field -> lead -> facts order above is never reordered in markup. */
@media (min-width: 64em) {
  .hero__inner {
    display: grid;
    grid-template-columns: var(--hero-columns-lg);
    align-items: start;
    column-gap: var(--space-xl);
  }

  .hero-field {
    grid-column: 2;
    grid-row: 1 / span 6;
    max-width: var(--measure-hero-field);
    justify-self: end;
  }

  .hero__eyebrow,
  .hero__title,
  .hero__cta-row,
  .hero__lead,
  .hero__facts {
    grid-column: 1;
    max-width: var(--measure-hero-text);
  }
}

/* --- Trust strip ------------------------------------------------------------
   Desktop: 3 equal columns — heading / body / IMG-2 (brief §4.6; the
   drawn width this table states for IMG-2, ~357px, is exactly one column
   of 1152px split three ways with two var(--space-lg) gaps). Single column
   below 64em. */

.trust__body {
  margin-block-start: var(--space-sm);
}

.trust__media {
  margin-block-start: var(--space-lg);
}

@media (min-width: 64em) {
  .trust__grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    align-items: start;
    column-gap: var(--space-lg);
  }

  .trust__body,
  .trust__media {
    margin-block-start: 0;
    max-width: var(--measure-trust);
  }
}

/* --- Estimate CTA + form ----------------------------------------------------
   Desktop: centred single column, form capped at var(--measure-form)
   (40ch) and centred within the 1152px container (brief §4.6). */

.estimate .form {
  margin-block-start: var(--space-md);
}

@media (min-width: 64em) {
  .estimate {
    max-width: var(--measure-form);
    margin-inline: auto;
    text-align: center;
  }

  .estimate .form {
    text-align: start;
  }
}

/* --- Service area (ships thin, honestly — brief §4.2 row 4) ---------------- */

.service-area p {
  font-size: var(--text-lead);
  max-width: var(--measure-service);
}

/* --- Process — own numeral costume, distinct from the hero manifest -------
   The hero uses icons, not numerals, at all (brief §6 nested-counter
   compliance), so this numbered system collides with nothing. Work Sans
   numerals, ink colour, no hairline rule. */

.process-list {
  display: grid;
  gap: var(--space-lg);
  margin: 0;
  padding: 0;
  list-style: none;
}

@media (min-width: 48em) {
  .process-list {
    grid-template-columns: repeat(3, 1fr);
  }
}

.process-list li {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
  max-width: var(--measure-process);
}

.process-list .process-n {
  font-weight: var(--font-weight-body-strong);
  font-size: var(--text-h3);
  color: var(--color-ink);
}

.process-list p {
  margin: 0;
  color: var(--color-ink-muted);
}

/* --- Reviews pull-quote band ------------------------------------------------
   Four verbatim quotes, no aggregate score anywhere (brief §7 item 6).
   Desktop: 2x2 grid, gap var(--space-lg) (brief §4.6). */

.pullquote-list {
  display: grid;
  gap: var(--space-lg);
  margin: 0;
  padding: 0;
  list-style: none;
}

@media (min-width: 48em) {
  .pullquote-list {
    grid-template-columns: repeat(2, 1fr);
  }
}

.pullquote {
  border-inline-start: var(--border-thick) var(--border-style) var(--color-border-strong);
  padding-inline-start: var(--space-md);
  margin: 0;
  max-width: var(--measure-reviews);
}

.pullquote p {
  font-family: var(--font-display);
  font-size: var(--text-lead);
  line-height: var(--leading-snug);
  color: var(--color-ink);
  margin: 0;
}

.pullquote footer {
  margin-block-start: var(--space-2xs);
  font-size: var(--text-small);
  font-weight: var(--font-weight-body-strong);
  color: var(--color-ink-muted);
  font-style: normal;
}

/* --- Hours & contact ---------------------------------------------------------
   Desktop: 2 columns — hours/contact text left, IMG-3 right, gap
   var(--space-xl) (brief §4.6). */

.hours-contact__text p {
  margin-block-start: var(--space-sm);
}

.hours-contact__media {
  margin-block-start: var(--space-lg);
}

@media (min-width: 64em) {
  .hours-contact__grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    align-items: start;
    column-gap: var(--space-xl);
  }

  .hours-contact__text {
    max-width: var(--measure-hours);
  }

  .hours-contact__media {
    margin-block-start: 0;
  }
}

/* --- FAQ ---------------------------------------------------------------------
   Plain question/answer pairs — no accordion chrome, no numeral treatment
   at all (design-rules §5's second limit). Single centred column, capped
   at var(--layout-measure) (62ch), per brief §4.6. */

.faq-list {
  display: grid;
  gap: var(--space-lg);
}

.faq-list h3 {
  font-size: var(--text-h3);
  margin-block-end: var(--space-2xs);
}

.faq-list p {
  margin: 0;
  color: var(--color-ink-muted);
}

/* --- Shared media figure (IMG-2/IMG-3) --------------------------------------- */

.media-figure {
  margin: 0;
}

.media-figure img {
  width: 100%;
  border: var(--border-hairline) var(--border-style) var(--color-border);
}

/* --- Reduced motion: this site's own one-shot/keyframe-driven moments ------
   base.css already collapses --duration-* and --reveal-shift for the
   shared reveal system. This site's bespoke moments (brief §6) get their
   own explicit freeze here, same specificity as the rules that start them,
   so this wins on source order alone (spec §12):
     - the tick's stroke-draw resolves instantly to fully drawn
     - both satellites freeze at rest rotation (0deg)
     - the atmosphere freezes at a fixed .65 opacity (brief §2.1/§6) */
@media (prefers-reduced-motion: reduce) {
  .manifest__mark--tick path {
    animation: none;
    stroke-dashoffset: 0;
  }

  .manifest__mark--screw,
  .manifest__mark--hinge {
    animation: none;
    transform: rotate(0deg);
  }

  .hero-field::before {
    animation: none;
    opacity: .65;
  }
}

/* --- Footer ---------------------------------------------------------------- */

.site-footer {
  padding-block: var(--space-xl);
  border-top: var(--border-hairline) var(--border-style) var(--color-border);
  font-size: var(--text-small);
}

.site-footer a {
  text-underline-offset: var(--space-3xs);
}

.site-footer__nap {
  font-style: normal;
}

/* Desktop: 3 columns — NAP / hours / copyright, gap var(--space-lg)
   (brief §4.6). Single column below the breakpoint, unchanged. */
@media (min-width: 64em) {
  .site-footer__grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    column-gap: var(--space-lg);
  }
}
