/* ============================================================
   Tyko Tech, Circuit Grove component library
   Consumes site/assets/css/tokens.css exclusively: every color,
   size, radius, and duration below references a token. The only
   raw values are the media-query breakpoints (880px / 560px),
   because custom properties cannot appear in media conditions,
   plus zeros, unitless ratios, and gradient stop percentages.
   ============================================================ */

/* ---- Base ---- */
*,
*::before,
*::after { box-sizing: border-box; }

html { scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

body {
  margin: 0;
  background: var(--color-bg-base);
  color: var(--color-text-primary);
  font: 400 var(--text-body)/var(--leading-body) var(--font-body);
  -webkit-font-smoothing: antialiased;
}

img { max-width: 100%; height: auto; }

h1, h2, h3, h4 {
  font-family: var(--font-display);
  color: var(--color-text-primary);
  margin: 0;
}

p { margin: 0; }

:focus-visible {
  outline: var(--focus-width) solid var(--color-accent);
  outline-offset: var(--focus-offset);
}

.wrap {
  max-width: var(--wrap-max);
  margin: 0 auto;
  padding: 0 var(--space-4);
}

/* ---- Skip link: hidden until keyboard focus ---- */
.skip-link {
  position: absolute;
  top: var(--space-2);
  left: var(--space-2);
  z-index: 30;
  display: inline-flex;
  align-items: center;
  min-height: var(--touch-target);
  padding: 0 var(--space-4);
  border-radius: var(--radius);
  background: var(--color-accent);
  color: var(--color-on-accent);
  font: 600 var(--text-ui)/1 var(--font-display);
  text-decoration: none;
  transform: translateY(calc(-1 * (var(--touch-target) + var(--space-5))));
}
.skip-link:focus-visible {
  transform: none;
  outline-color: var(--color-text-primary);
}

/* ---- Header ---- */
.site-header {
  position: sticky;
  top: 0;
  z-index: 20;
  background: var(--color-bg-raised);
  border-bottom: var(--hairline) solid var(--color-border);
}
.site-header-inner {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-5);
  min-height: var(--space-7);
  padding-block: var(--space-2);
}
/* The lockup never shrinks: as a flex item it would otherwise be
   squeezed narrower than its aspect ratio on small screens, which
   distorts the wordmark rather than scaling it. */
.site-logo {
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
  min-height: var(--touch-target);
}
.site-logo img { height: var(--space-5); width: auto; }

.site-nav { margin-left: auto; }
.site-nav ul {
  list-style: none;
  display: flex;
  align-items: center;
  gap: var(--space-4);
  margin: 0;
  padding: 0;
  flex-wrap: wrap;
}
.site-nav a {
  display: inline-flex;
  align-items: center;
  min-height: var(--touch-target);
  padding: 0 var(--space-1);
  font: 500 var(--text-ui)/1 var(--font-display);
  color: var(--color-text-primary);
  text-decoration: none;
  border-bottom: var(--rule-weight) solid transparent;
  transition: color var(--duration-base) ease, border-color var(--duration-base) ease;
}
.site-nav a:hover { color: var(--color-accent); }
.site-nav a[aria-current="page"] {
  color: var(--color-accent);
  border-bottom-color: var(--color-accent);
}

/* Theme toggle: a bare icon control carrying a sun and a moon,
   with no border or fill of its own (owner decision, revision 8).
   The visible icon names the theme you would GET by pressing it,
   so the moon shows while the default light theme is rendered.
   The hit area keeps the full touch target; only the glyph is
   drawn. Like the nav toggle it ships with the hidden attribute
   and is revealed by theme.js, because with no script it cannot
   work and a dead control is worse than no control. */
.theme-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  width: var(--touch-target);
  height: var(--touch-target);
  flex-shrink: 0;
  padding: 0;
  border: 0;
  border-radius: var(--radius);
  background: transparent;
  color: var(--color-text-primary);
  cursor: pointer;
  transition: color var(--duration-base) ease;
}
.js .theme-toggle:not([hidden]) { display: inline-flex; }
.theme-toggle:hover { color: var(--color-accent); }
.theme-icon {
  /* 80% of the former --space-4 glyph (owner decision, revision 8) */
  width: calc(var(--space-4) * 0.8);
  height: calc(var(--space-4) * 0.8);
  display: block;
}
/* Default (light rendered): offer the moon. An explicit dark choice
   swaps in the sun. Both icons ship in the markup so the swap is
   pure CSS and never flashes the wrong one. */
.theme-icon-sun { display: none; }
:root[data-theme="dark"] .theme-icon-moon { display: none; }
:root[data-theme="dark"] .theme-icon-sun { display: inline; }

/* Mobile nav toggle: hidden attribute in markup keeps it out of
   no-JS flows entirely; theme.js reveals and wires it. */
.nav-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  min-height: var(--touch-target);
  padding: 0 var(--space-3);
  border-radius: var(--radius);
  border: var(--hairline) solid var(--color-border-strong);
  background: transparent;
  color: var(--color-text-primary);
  font: 600 var(--text-ui-sm)/1 var(--font-display);
  cursor: pointer;
  transition: border-color var(--duration-base) ease, color var(--duration-base) ease;
}
.nav-toggle:hover { border-color: var(--color-accent); color: var(--color-accent); }

/* ---- Hero (landing page) ---- */
.hero {
  display: grid;
  grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
  gap: var(--space-6);
  align-items: center;
  padding-block: var(--space-7) var(--space-8);
}
.hero-lede {
  margin-top: var(--space-3);
  font-size: var(--text-body-lg);
  color: var(--color-text-secondary);
  max-width: var(--measure-lede);
}
.hero-art {
  justify-self: center;
  width: 100%;
  max-width: var(--measure-field);
}

/* ---- Themed assets (light/dark photograph swap) ----
   A .themed-img holds two slots classed .if-light / .if-dark
   (the class sits on the <img>; <picture> wrappers are fine).
   Visibility follows the EFFECTIVE theme with the same two-state
   resolution as tokens.css: bare root is dark (the default), and
   an explicit light choice flips it. */
.themed-img .if-light { display: block; }
.themed-img .if-dark { display: none; }
:root[data-theme="dark"] .themed-img .if-light { display: none; }
:root[data-theme="dark"] .themed-img .if-dark { display: block; }

/* Hero photograph: radius and the system's one soft shadow. */
.hero-photo {
  position: relative;
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--shadow-soft);
}
.hero-photo img {
  display: block;
  width: 100%;
  height: auto;
}

/* Ambient hero loop: sits above the themed stills. theme.js
   reveals and plays it only when the visitor's system allows
   motion; with no JS, or under reduced motion, it stays hidden
   and (preload="none") never downloads. */
.hero-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.hero-video[hidden] { display: none; }

/* ---- Buttons ---- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--control-height);
  padding: 0 var(--space-4);
  border-radius: var(--radius);
  border: var(--hairline) solid transparent;
  font: 600 var(--text-ui)/1 var(--font-display);
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition:
    background-color var(--duration-base) ease,
    color var(--duration-base) ease,
    border-color var(--duration-base) ease,
    transform var(--duration-fast) ease;
}
.btn:active:not(:disabled) { transform: translateY(var(--press)); }
.btn:disabled { opacity: var(--disabled-opacity); cursor: not-allowed; }

.btn-primary { background: var(--color-accent); color: var(--color-on-accent); }
.btn-primary:hover:not(:disabled) { background: var(--color-accent-hover); }

.btn-secondary {
  background: transparent;
  color: var(--color-text-primary);
  border-color: var(--color-border-strong);
}
.btn-secondary:hover:not(:disabled) {
  border-color: var(--color-accent);
  color: var(--color-accent);
}

.btn-quiet { background: transparent; color: var(--color-accent); }
.btn-quiet:hover:not(:disabled) {
  background: color-mix(in srgb, var(--color-accent) 12%, transparent);
}

/* ---- Text links ---- */
.link,
.prose a {
  color: var(--color-accent);
  text-decoration: underline;
  text-underline-offset: var(--underline-offset);
  text-decoration-thickness: var(--hairline);
  transition: color var(--duration-base) ease;
}
.link:hover,
.prose a:hover { color: var(--color-accent-hover); }

/* ---- Badges ---- */
.badge {
  display: inline-flex;
  align-items: center;
  min-height: var(--badge-height);
  padding: 0 var(--space-3);
  border-radius: var(--radius);
  border: var(--hairline) solid var(--color-border-strong);
  background: color-mix(in srgb, var(--color-accent) 10%, transparent);
  font: 500 var(--text-caption)/1 var(--font-display);
  color: var(--color-text-primary);
}

/* ---- Cards ---- */
.cards {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: var(--space-4);
  align-items: start;
}
.card {
  background: var(--color-bg-base);
  border: var(--hairline) solid var(--color-border);
  border-radius: var(--radius);
  overflow: hidden;
  transition:
    transform var(--duration-base) ease,
    border-color var(--duration-base) ease,
    box-shadow var(--duration-base) ease;
}
.card:hover,
.card:focus-within {
  transform: translateY(calc(-1 * var(--lift)));
  border-color: var(--color-border-strong);
  box-shadow: var(--shadow-soft);
}
.card-featured { grid-column: span 7; }
.card-compact { grid-column: span 5; }
.card-half { grid-column: span 6; }
.card-media {
  aspect-ratio: 16 / 7;
  display: flex;
  align-items: flex-end;
  padding: var(--space-4);
  background:
    radial-gradient(120% 150% at 85% 0%, color-mix(in srgb, var(--color-accent) 22%, transparent) 0%, transparent 55%),
    linear-gradient(150deg, color-mix(in srgb, var(--color-accent) 12%, var(--color-bg-raised)) 0%, var(--color-bg-base) 100%);
  border-bottom: var(--hairline) solid var(--color-border);
}
.card-wordmark {
  font: 700 var(--text-display-m)/1 var(--font-display);
  letter-spacing: var(--tracking-display);
  color: var(--color-accent);
}

/* Photo media variant: an image replaces the gradient; a bottom
   scrim keeps the wordmark legible on any photo in both themes
   (measured 9.87:1 worst case for on-photo type over the scrim). */
.card-media-photo { position: relative; }
.card-photo {
  position: absolute;
  inset: 0;
}
.card-photo img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.card-media-photo::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--photo-scrim);
  pointer-events: none;
}
.card-media-photo .card-wordmark,
.card-media-photo .card-logo {
  position: relative;
  z-index: 1;
}
.card-media-photo .card-wordmark { color: var(--color-on-photo); }

/* Project-logo slots. Graceful fallback contract: a page emits the
   logo markup ONLY once its files exist on disk; until then the
   wordmark treatment above IS the component's fallback state, so
   no broken images can ever ship. The card slot sits bottom-left,
   in the calm corner every card photograph was composed to leave
   empty, so the mark never lands on the subject. */
.card-logo {
  display: block;
  margin-right: auto;
  max-height: var(--space-6);
  max-width: 62%;
  width: auto;
}
.project-logo {
  display: block;
  max-height: var(--space-8);
  width: auto;
  margin-bottom: var(--space-3);
}
.card-body { padding: var(--space-4); }
.card-body h3 {
  font-size: var(--text-title);
  font-weight: 600;
  margin: var(--space-2) 0;
}
.card-compact .card-body h3 { font-size: var(--text-title-sm); }
.card-title-link {
  color: var(--color-text-primary);
  text-decoration: none;
}
.card-title-link:hover { color: var(--color-accent); }
.card-text { color: var(--color-text-secondary); font-size: var(--text-ui); }
.card-meta {
  margin-top: var(--space-3);
  font: 500 var(--text-ui-sm)/var(--leading-ui) var(--font-display);
  color: var(--color-text-secondary);
}

/* ---- Whole-card click target ----
   The title link stretches an invisible overlay across the whole
   card, so a click anywhere opens the project. It stays ONE link
   in the accessibility tree (no duplicate announcements, keyboard
   focus lands on the title), and every other link inside the card
   is lifted above the overlay so it keeps its own destination.
   Text remains selectable outside the overlay's own area. */
.card,
.project-row { position: relative; }
.card-title-link::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
}
.card a:not(.card-title-link),
.project-row a:not(.card-title-link) {
  position: relative;
  z-index: 2;
}

/* ---- Capability rail (.rail) ----
   Wide, hairline-separated rows: the name of a thing on the left,
   what it is on the right. Replaces stacked card grids wherever a
   set of capabilities was reading as two tall narrow columns of
   prose. Rows are full container width, so the text measure stays
   comfortable and the eye scans down one edge. */
.rail {
  margin-top: var(--space-5);
  border-top: var(--hairline) solid var(--color-border);
}
.rail-item {
  display: grid;
  grid-template-columns: minmax(0, 4fr) minmax(0, 8fr);
  gap: var(--space-3) var(--space-6);
  padding-block: var(--space-5);
  border-bottom: var(--hairline) solid var(--color-border);
}
.rail-name h3 {
  font: 600 var(--text-title)/var(--leading-subhead) var(--font-display);
  letter-spacing: var(--tracking-snug);
}
.rail-name .meta-chips { margin-top: var(--space-3); }
.rail-detail p {
  color: var(--color-text-secondary);
  font-size: var(--text-body);
  line-height: var(--leading-body);
  max-width: var(--measure-body);
}
.rail-detail p + p { margin-top: var(--space-3); }
.rail-detail .link { font-family: var(--font-display); font-size: var(--text-ui); }
.rail-cta { margin-top: var(--space-3); }

/* ---- Feature panel ----
   One wide photograph carrying a short block of type in its calm
   half. Used sparingly: it is the loudest device on the site, so
   a page gets at most one. The whole panel is a single link. */
.feature-panel {
  position: relative;
  display: flex;
  align-items: center;
  min-height: clamp(19rem, 32vw, 25rem);
  margin-top: var(--space-5);
  border-radius: var(--radius);
  overflow: hidden;
  color: var(--color-on-photo);
  text-decoration: none;
  transition:
    transform var(--duration-base) ease,
    box-shadow var(--duration-base) ease;
}
.feature-panel:hover,
.feature-panel:focus-visible {
  transform: translateY(calc(-1 * var(--lift)));
  box-shadow: var(--shadow-soft);
}
.feature-panel-photo,
.feature-panel-photo img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.feature-panel::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--photo-scrim-left);
  pointer-events: none;
}
.feature-panel-body {
  position: relative;
  z-index: 1;
  max-width: 36rem;
  padding: var(--space-6);
}
.feature-panel-eyebrow {
  font: 600 var(--text-ui-sm)/1 var(--font-display);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  opacity: 0.82;
}
/* The heading colour is set explicitly: the global h1-h4 rule would
   otherwise beat the panel's inherited on-photo colour and paint
   dark type onto a dark scrim. */
.feature-panel-title {
  margin-top: var(--space-3);
  font: 600 var(--text-display-m)/var(--leading-heading) var(--font-display);
  letter-spacing: var(--tracking-display);
  color: var(--color-on-photo);
}
.feature-panel-text {
  margin-top: var(--space-3);
  font-size: var(--text-body);
  line-height: var(--leading-body);
  opacity: 0.92;
}
.feature-panel-cue {
  display: inline-block;
  margin-top: var(--space-4);
  font: 600 var(--text-ui)/1 var(--font-display);
  border-bottom: var(--rule-weight) solid currentColor;
  padding-bottom: var(--space-1);
}

/* ---- Project rows ----
   The portfolio entry as a wide horizontal card: photograph on one
   side, the writing on the other, sides alternating down the page.
   Same surface language as .card, but the text sits in a short
   wide column instead of a tall narrow one. */
.project-rows {
  display: grid;
  gap: var(--space-5);
  margin-top: var(--space-5);
}
.project-row {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  align-items: stretch;
  background: var(--color-bg-base);
  border: var(--hairline) solid var(--color-border);
  border-radius: var(--radius);
  overflow: hidden;
  transition:
    transform var(--duration-base) ease,
    border-color var(--duration-base) ease,
    box-shadow var(--duration-base) ease;
}
.project-row:hover,
.project-row:focus-within {
  transform: translateY(calc(-1 * var(--lift)));
  border-color: var(--color-border-strong);
  box-shadow: var(--shadow-soft);
}
.project-row-media {
  display: flex;
  align-items: flex-end;
  min-height: 15rem;
  padding: var(--space-4);
  border-right: var(--hairline) solid var(--color-border);
}
.project-rows-lead .project-row-media { min-height: 18rem; }
.project-row:nth-child(even) > .project-row-media {
  grid-column: 2;
  grid-row: 1;
  border-right: 0;
  border-left: var(--hairline) solid var(--color-border);
}
.project-row:nth-child(even) > .project-row-body {
  grid-column: 1;
  grid-row: 1;
}
.project-row-body {
  padding: var(--space-5);
  align-self: center;
}
.project-row-body h3 {
  font-size: var(--text-title);
  font-weight: 600;
  margin-bottom: var(--space-2);
}
.project-rows-lead .project-row-body h3 { font-size: var(--text-display-m); }

/* ---- Form field: label + input + hint + error/success message ---- */
.field { max-width: var(--measure-field); }
.field label {
  display: block;
  font: 600 var(--text-ui)/var(--leading-ui) var(--font-display);
  margin-bottom: var(--space-2);
}
.field input,
.field select,
.field textarea {
  width: 100%;
  min-height: var(--control-height);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius);
  border: var(--hairline) solid var(--color-border-strong);
  background: var(--color-bg-base);
  color: var(--color-text-primary);
  font: 400 var(--text-body)/var(--leading-ui) var(--font-body);
  transition: border-color var(--duration-base) ease;
}
.field input::placeholder,
.field textarea::placeholder {
  color: var(--color-text-secondary);
  opacity: 1;
}
.field input:focus-visible,
.field select:focus-visible,
.field textarea:focus-visible {
  outline: var(--focus-width) solid var(--color-accent);
  outline-offset: var(--focus-offset);
  border-color: var(--color-accent);
}
.field input:disabled,
.field select:disabled,
.field textarea:disabled {
  opacity: var(--disabled-opacity);
  cursor: not-allowed;
}
.field-hint {
  margin-top: var(--space-2);
  font: 400 var(--text-caption)/var(--leading-ui) var(--font-display);
  color: var(--color-text-secondary);
}
.field-error,
.field-success {
  margin-top: var(--space-2);
  font: 500 var(--text-caption)/var(--leading-ui) var(--font-display);
}
.field-error { color: var(--color-error); }
.field-success { color: var(--color-success); }
.field.is-error input,
.field.is-error select,
.field.is-error textarea { border-color: var(--color-error); }
.field.is-error input:focus-visible,
.field.is-error select:focus-visible,
.field.is-error textarea:focus-visible { outline-color: var(--color-error); }

/* ---- Contact form ----
   Progressive enhancement: the form carries a real action and
   method, so with no JS it validates natively and posts to the
   form service. contact-form.js takes over to validate inline and
   submit in the background. The recipient address lives only in
   the service's opaque key, never in the page. */
.contact-form {
  display: grid;
  gap: var(--space-4);
  margin-top: var(--space-5);
  max-width: var(--measure-lede);
}
.contact-form .field { max-width: none; }
/* Name and email share a row so the form reads as a block of work
   rather than a tall stack of full-width strips. */
.contact-form-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-4);
}
.contact-form textarea {
  min-height: 9rem;
  resize: vertical;
  line-height: var(--leading-body);
}
.contact-form .btn { justify-self: start; }

/* ---- Contact panel ----
   The form is the point of the Contact page, so it sits at the top
   of the body on its own plate, at full column width, with the
   short practical notes beside it rather than stacked above it.
   The plate uses the PAGE ground inside the raised band, which is
   what lifts it off the surrounding surface. */
.contact-panel {
  display: grid;
  grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
  gap: var(--space-6);
  align-items: start;
}
.contact-panel-main {
  background: var(--color-bg-base);
  border: var(--hairline) solid var(--color-border);
  border-radius: var(--radius);
  padding: var(--space-5);
  box-shadow: var(--shadow-soft);
}
.contact-panel-main .contact-form { max-width: none; }
.contact-panel-side {
  display: grid;
  gap: var(--space-4);
  align-content: start;
}
.form-status {
  font: 500 var(--text-ui-sm)/var(--leading-ui) var(--font-display);
  color: var(--color-text-secondary);
}
.form-status:empty { display: none; }
.form-status.is-error { color: var(--color-error); }
.form-status.is-success { color: var(--color-success); }

/* ---- Quote rotator ----
   A set of situations shown one at a time on the stone tablet.
   These are illustrative situations written in a client's voice,
   never attributed quotes, so nothing here reads as a testimonial.

   The panel is deliberately quiet: no controls, no accent bar, the
   quote set in the body italic rather than in display weight, and
   the stone held under a heavy scrim so it reads as grain rather
   than as a photograph. It changes on its own and asks nothing.

   FAIL-OPEN: with no JS, and under reduced motion, every item is
   visible as a plain stacked list, so the panel is complete and
   readable and nothing moves. theme.js adds .is-live to switch to
   one-at-a-time only when motion is welcome. */
.quote-rotator {
  position: relative;
  border-radius: var(--radius);
  overflow: hidden;
  padding: var(--space-4);
  border: var(--hairline) solid var(--color-border);
}
/* One plate in both themes: the stone tablet, veiled by the
   theme-toned scrim above it. */
.quote-rotator::before {
  content: "";
  position: absolute;
  inset: 0;
  background: url("../img/quote-plate.webp") center / cover no-repeat;
}
.quote-rotator::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--quote-plate-scrim);
}
.quote-rotator > * { position: relative; z-index: 1; }
.quote-eyebrow {
  font: 600 var(--text-caption)/1 var(--font-display);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-text-secondary);
  margin-bottom: var(--space-3);
}
.quote-items { display: grid; gap: var(--space-4); }
.quote-item blockquote {
  margin: 0;
  font: italic 500 var(--text-body-lg)/var(--leading-subhead) var(--font-body);
  color: var(--color-text-primary);
}
.quote-answer {
  margin-top: var(--space-2);
  font-size: var(--text-ui-sm);
  line-height: var(--leading-body);
  color: var(--color-text-secondary);
}

/* Live mode: every item stacks in one grid cell, so the plate is
   as tall as its longest situation and never resizes mid-cycle.
   The cross-fade runs in two halves: the outgoing item fades out
   over --duration-fade, then the incoming one fades in over the
   same span, which is why .is-current carries the matching delay.

   The off-screen items are faded, NOT hidden: no display:none and
   no visibility:hidden. There are no controls on this panel, so
   hiding them would leave a screen reader with one situation out
   of five and no way to reach the rest. At opacity 0 every one of
   them stays in the accessibility tree, which is the same complete
   list the no-JS and reduced-motion visitor sees. pointer-events
   and user-select keep them from catching a mouse. */
.quote-rotator.is-live .quote-item {
  grid-area: 1 / 1;
  opacity: 0;
  pointer-events: none;
  user-select: none;
  transition: opacity var(--duration-fade) ease;
}
.quote-rotator.is-live .quote-item.is-current {
  opacity: 1;
  pointer-events: auto;
  user-select: auto;
  transition: opacity var(--duration-fade) ease var(--duration-fade);
}
.field.is-success input,
.field.is-success select,
.field.is-success textarea { border-color: var(--color-success); }

/* ---- Prose: long-form pages ---- */
.prose { max-width: var(--measure-body); }
.prose h2 {
  font-size: var(--text-display-m);
  font-weight: 600;
  letter-spacing: var(--tracking-snug);
  line-height: var(--leading-heading);
  margin: var(--space-6) 0 var(--space-3);
}
.prose h3 {
  font-size: var(--text-title);
  font-weight: 600;
  line-height: var(--leading-subhead);
  margin: var(--space-5) 0 var(--space-3);
}
.prose p { margin: var(--space-3) 0; }
.prose ul,
.prose ol {
  margin: var(--space-3) 0;
  padding-left: var(--space-4);
}
.prose li { margin-bottom: var(--space-2); }
.prose blockquote {
  margin: var(--space-5) 0;
  padding-left: var(--space-4);
  border-left: var(--accent-rule) solid var(--color-accent);
  font-style: italic;
  color: var(--color-text-secondary);
}
.prose blockquote p { padding-bottom: var(--space-1); }
.table-scroll {
  overflow-x: auto;
  margin: var(--space-4) 0;
  border: var(--hairline) solid var(--color-border);
  border-radius: var(--radius);
}
.prose table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-ui);
}
.prose th {
  text-align: left;
  font: 600 var(--text-ui-sm)/var(--leading-ui) var(--font-display);
  padding: var(--space-3);
  border-bottom: var(--hairline) solid var(--color-border-strong);
  background: color-mix(in srgb, var(--color-accent) 6%, transparent);
}
.prose td {
  padding: var(--space-3);
  border-bottom: var(--hairline) solid var(--color-border);
  vertical-align: top;
}
.prose tr:last-child td { border-bottom: none; }
.prose code,
.prose kbd {
  font: 400 var(--text-ui-sm)/var(--leading-ui) var(--font-mono);
  border-radius: calc(var(--radius) / 2);
  padding: var(--space-1) var(--space-2);
}
.prose code {
  background: color-mix(in srgb, var(--color-text-primary) 7%, transparent);
}
.prose kbd {
  border: var(--hairline) solid var(--color-border-strong);
  background: var(--color-bg-raised);
}

/* ---- Section rhythm and the grove band ---- */
.section { padding-block: var(--space-8); }
.section-title {
  font-size: var(--text-section);
  font-weight: 600;
  letter-spacing: var(--tracking-snug);
  line-height: var(--leading-heading);
}
.section-lede {
  margin-top: var(--space-2);
  color: var(--color-text-secondary);
  max-width: var(--measure-lede);
}

/* Page-level title and intro patterns (canonical for every page's
   single h1, including the landing hero and project pages). */
.page-title {
  font-size: var(--text-h1);
  font-weight: 600;
  line-height: var(--leading-heading);
  letter-spacing: var(--tracking-display);
}
.badge + .page-title { margin-top: var(--space-3); }
.page-intro { padding-block: var(--space-7) var(--space-8); }
.section .cards { margin-top: var(--space-5); }

/* ---- Ambient media plates ----
   A full-width framed still between a page's intro and its first
   band, optionally carrying a quiet video loop above it. The still
   is the ground truth: videos ship with the hidden attribute and
   preload="none", and theme.js reveals and plays them only when
   the visitor's system allows motion. Reduced-motion and no-JS
   visitors get the photograph and never download a frame. */
.page-media { padding-block: 0 var(--space-8); }
.media-frame {
  position: relative;
  aspect-ratio: 5 / 2;
  margin: 0;
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--shadow-soft);
}
.media-frame-tall { aspect-ratio: 2 / 1; }
.media-frame img,
.media-frame video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.media-frame video[hidden] { display: none; }
@media (max-width: 720px) {
  .media-frame,
  .media-frame-tall { aspect-ratio: 16 / 10; }
}

/* CTA helpers: a wrapping button row after a lede, and a CTA block
   separated from preceding prose or card grids. Both sit outside
   .prose so .prose a link styling cannot restyle buttons. */
.cta-row {
  margin-top: var(--space-5);
  display: flex;
  gap: var(--space-3);
  flex-wrap: wrap;
}
.section-cta { margin-top: var(--space-6); }

/* ---- Split section: body left, aside right (7/5) ---- */
.section-split {
  display: grid;
  grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
  gap: var(--space-6);
  align-items: start;
  margin-top: var(--space-5);
}
.aside-panel {
  background: var(--color-bg-raised);
  border: var(--hairline) solid var(--color-border);
  border-radius: var(--radius);
  padding: var(--space-4);
}
.aside-panel h3 {
  font-size: var(--text-title-sm);
  font-weight: 600;
  margin-bottom: var(--space-3);
}
.aside-panel ul,
.aside-panel ol {
  list-style: none;
  margin: 0;
  padding: 0;
}
.aside-panel li {
  padding-block: var(--space-2);
  font-size: var(--text-ui);
  color: var(--color-text-secondary);
}
.aside-panel li + li { border-top: var(--hairline) solid var(--color-border); }
.aside-panel li strong { color: var(--color-text-primary); }

/* ---- Steps: numbered process rail (ol.steps) ---- */
.steps {
  list-style: none;
  counter-reset: step;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--space-5);
  margin: var(--space-5) 0 0;
  padding: 0;
}
.steps li {
  counter-increment: step;
  border-top: var(--rule-weight) solid var(--color-border);
  padding-top: var(--space-3);
}
.steps li::before {
  content: counter(step);
  display: block;
  font: 600 var(--text-title)/1 var(--font-display);
  color: var(--color-accent);
  margin-bottom: var(--space-2);
}
.steps h3 {
  font-size: var(--text-title-sm);
  font-weight: 600;
  margin-bottom: var(--space-2);
}
.steps p {
  font-size: var(--text-ui);
  color: var(--color-text-secondary);
}

/* ---- Problems: problem-and-answer pairs ----
   The problem line speaks in the customer's voice against an
   accent hairline; the answer follows in body color. Even items
   drop by one spacing step for asymmetric rhythm, so the grid
   never reads as identical cards. */
.problems {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-6);
  margin-top: var(--space-5);
}
.problem {
  border-left: var(--accent-rule) solid var(--color-accent);
  padding-left: var(--space-4);
}
.problem:nth-child(even) { margin-top: var(--space-6); }
.problem-lead {
  font: 600 var(--text-title)/var(--leading-subhead) var(--font-display);
  letter-spacing: var(--tracking-snug);
  color: var(--color-text-primary);
}
.problem p {
  margin-top: var(--space-2);
  color: var(--color-text-secondary);
}

/* ---- Feature grid: definition-style term + description ---- */
.feature-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-4) var(--space-6);
  margin: var(--space-5) 0 0;
}
.feature-grid dt {
  font: 600 var(--text-ui)/var(--leading-ui) var(--font-display);
  color: var(--color-text-primary);
}
.feature-grid dd {
  margin: var(--space-1) 0 0;
  font-size: var(--text-ui);
  color: var(--color-text-secondary);
}

/* ---- Notes page: jump pills, article entries, meta chips ---- */
.toc-pills {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  list-style: none;
  margin: var(--space-4) 0 0;
  padding: 0;
}
.toc-pills a {
  display: inline-flex;
  align-items: center;
  min-height: var(--touch-target);
  padding: 0 var(--space-3);
  border-radius: var(--radius);
  border: var(--hairline) solid var(--color-border-strong);
  font: 500 var(--text-ui-sm)/1 var(--font-display);
  color: var(--color-text-primary);
  text-decoration: none;
  transition: border-color var(--duration-base) ease, color var(--duration-base) ease;
}
.toc-pills a:hover { border-color: var(--color-accent); color: var(--color-accent); }

.article {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: var(--space-3) var(--space-6);
  padding-block: var(--space-5);
  scroll-margin-top: var(--space-8);
}
.article + .article { border-top: var(--hairline) solid var(--color-border); }
.article:nth-of-type(even) { grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); }
.article h3 {
  font-size: var(--text-title);
  font-weight: 600;
  letter-spacing: var(--tracking-snug);
}
.article-body p { color: var(--color-text-secondary); }
.article-body p + p { margin-top: var(--space-2); }

.meta-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  list-style: none;
  margin: var(--space-2) 0 0;
  padding: 0;
}
.meta-chip {
  display: inline-flex;
  align-items: center;
  min-height: var(--badge-height);
  padding: 0 var(--space-2);
  border-radius: var(--radius);
  border: var(--hairline) solid var(--color-border);
  background: color-mix(in srgb, var(--color-text-primary) 4%, transparent);
  font: 500 var(--text-caption)/1 var(--font-display);
  color: var(--color-text-secondary);
}

/* Raised ground with the approved asymmetric curved transition.
   ::before lays a whisper of stone grain over the surface color;
   the tile is a mirrored 2x2 cut, so it repeats seamlessly, and
   the texture swap follows the same two-state theme resolution as
   the tokens. Content sits above it via the positioned .wrap. */
.band {
  position: relative;
  overflow: hidden;
  background: var(--color-bg-raised);
  border-top: var(--hairline) solid var(--color-border);
  border-radius: var(--band-curve);
  padding-block: var(--space-8);
}
/* On pages that CLOSE on a band, the footer shares this grain so
   band and footer read as ONE surface (owner decision, revision 8):
   same ground, same texture, no rule and no gap between them. Pages
   that close on the page ground get an untextured footer on that
   same ground, so the junction is invisible either way. */
.band::before,
main:has(> .band:last-child) + .site-footer::before {
  content: "";
  position: absolute;
  inset: 0;
  background: url("../img/texture-light.webp") repeat;
  background-size: 560px;
  opacity: 0.4;
  pointer-events: none;
}
:root[data-theme="dark"] .band::before,
:root[data-theme="dark"] main:has(> .band:last-child) + .site-footer::before {
  background-image: url("../img/texture-dark.webp");
}
.band > .wrap { position: relative; }
.band .section { padding-block: 0; }
.band .section + .section { margin-top: var(--space-8); }

/* ---- Reveal on scroll, FAIL-OPEN ----
   Base state is fully visible: with no JS, without
   IntersectionObserver, or under reduced motion the page renders
   complete and static. theme.js only ADDS the transient .anim
   class; the keyframes exist only when motion is welcome and end
   at the element's natural visible state. */
@media (prefers-reduced-motion: no-preference) {
  .reveal.anim {
    animation: reveal-in var(--duration-slow) var(--ease-organic) both;
    animation-delay: var(--d, 0s);
  }
  @keyframes reveal-in {
    from { opacity: 0; transform: translateY(var(--reveal-shift)); }
    to { opacity: 1; transform: none; }
  }
}

/* ---- Footer ----
   The footer is CONTINUOUS with the page above it (owner decision,
   revision 8): no gold rule and no gap, so the closing band flows
   straight into the footer as one unbroken surface. The band's own
   bottom padding supplies the breathing room the removed margin
   used to add. Do not reintroduce a border or a margin here. */
.site-footer {
  position: relative;
  overflow: hidden;
  background: var(--color-bg-base);
  padding: var(--space-4) 0; /* rev 8: shorter footer */
}
/* A page that ends on a band hands the footer that band's surface. */
main:has(> .band:last-child) + .site-footer {
  background: var(--color-bg-raised);
}
.site-footer > .wrap { position: relative; }
/* Everything in the footer sits on ONE baseline: the standing line
   the copyright sets. The row aligns on baselines rather than on
   centres, so the strapline and "(c) 2026" beside it, both pushed
   to the right (rev 8), read as a single line of type, with the
   mark rising above it. */
.site-footer-inner {
  display: flex;
  align-items: baseline;
  gap: var(--space-2) var(--space-3);
  flex-wrap: wrap;
}
.site-footer p {
  font: 400 var(--text-ui)/var(--leading-ui) var(--font-body);
  color: var(--color-text-secondary);
  margin: 0 0 0 auto; /* strapline sits immediately left of the copyright */
}
/* The copyright line ends in the full mark: the logo IS the
   company name here, so it carries the alt text and no wordmark
   is repeated beside it. Baseline alignment sits the mark's foot
   on the copyright line and lets it rise above, rather than
   centring it across the line. */
.site-footer-copy {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-2);
  font: 500 var(--text-ui)/var(--leading-ui) var(--font-display);
  color: var(--color-text-secondary);
  white-space: nowrap;
}
.site-footer-mark {
  height: calc(var(--space-7) + 10px); /* rev 8: +10px */
  width: auto;
}

/* ---- Utility ---- */
.visually-hidden {
  position: absolute;
  width: var(--space-1);
  height: var(--space-1);
  margin: calc(-1 * var(--space-1));
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---- Responsive: tablet collapse ---- */
@media (max-width: 880px) {
  .section { padding-block: var(--space-7); }
  .band { padding-block: var(--space-7); }
  .band .section + .section { margin-top: var(--space-7); }
  .hero {
    grid-template-columns: 1fr;
    gap: var(--space-5);
  }
  .hero-art {
    order: -1;
    max-width: calc(var(--measure-field) / 2);
  }
  .cards { grid-template-columns: 1fr; }
  .card-featured,
  .card-compact,
  .card-half { grid-column: auto; }
  .section-split,
  .steps,
  .problems,
  .rail-item,
  .contact-panel,
  .contact-form-row,
  .feature-grid { grid-template-columns: 1fr; }

  /* Project rows stack: photograph on top, writing beneath, and
     the alternating placement resets so every entry reads the
     same way down a single column. */
  .project-row { grid-template-columns: 1fr; }
  .project-row-media,
  .project-rows-lead .project-row-media {
    min-height: 0;
    aspect-ratio: 16 / 9;
    border-right: 0;
    border-bottom: var(--hairline) solid var(--color-border);
  }
  .project-row:nth-child(even) > .project-row-media {
    grid-column: 1;
    grid-row: 1;
    border-left: 0;
    border-bottom: var(--hairline) solid var(--color-border);
  }
  .project-row:nth-child(even) > .project-row-body {
    grid-column: 1;
    grid-row: 2;
  }
  .feature-panel-body { padding: var(--space-5); }
  .site-header-inner { gap: var(--space-3); }
  .steps { gap: var(--space-4); }
  .problems { gap: var(--space-5); }
  .problem:nth-child(even) { margin-top: 0; }
  .article,
  .article:nth-of-type(even) { grid-template-columns: 1fr; }

  /* With JS present, the nav list collapses behind the toggle.
     Without JS the root never gets the .js class, the toggle stays
     hidden, and the list simply wraps: degraded but usable. */
  .js .nav-toggle:not([hidden]) { display: inline-flex; margin-left: auto; }
  .js .site-nav {
    order: 10;
    flex-basis: 100%;
    margin-left: 0;
  }
  .js .site-nav ul {
    display: none;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding-block: var(--space-2) var(--space-3);
  }
  .js .site-nav ul.is-open { display: flex; }
  .js .site-nav a {
    border-bottom: none;
    padding: var(--space-2) var(--space-1);
  }
  .js .site-nav a[aria-current="page"] {
    border-left: var(--rule-weight) solid var(--color-accent);
    padding-left: var(--space-3);
  }
}

/* ---- Responsive: mobile collapse ---- */
@media (max-width: 560px) {
  .site-header-inner { gap: var(--space-3); }
  /* The lockup never shrinks, so on the narrowest screens it steps
     down a size rather than pushing the controls onto a new row. */
  .site-logo img { height: var(--space-4); }
  .site-nav ul { gap: var(--space-3); }
  .site-footer-inner {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-3);
  }
  .site-footer-copy { margin-left: 0; }
  .contact-panel { gap: var(--space-5); }
}
