/* =====================================================================
   Andrionis Group: "Antikythera"
   ---------------------------------------------------------------------
   The firm's name is Greek; the oldest surviving artifact of "verify the
   cycle by measurement" is a Greek bronze calculating instrument. So the
   page is built as an instrument: a patinated bronze-dark ground, hairline
   engraved geometry, one brass accent used where a pointer would be.

   Depth never comes from a soft gradient wash: it comes from layered
   structure (radial patina, concentric arcs, a fine grain).

   Load order: styles/fonts.css (families) then this file.
   No inline styles anywhere: Task 12 sets style-src 'self'.
   ===================================================================== */

/* ---------------------------------------------------------------------
   1. Tokens
   ------------------------------------------------------------------ */

:root {
  /* Palette: verbatim from the approved design spec. Nothing else. */
  --ground: #0B1413;
  --ground-lift: #10201E;
  --verdigris: #3E8C7A;
  --verdigris-d: #2E6F62;
  --brass: #C08A3E;
  --brass-hot: #E5B25D;
  --text: #DCE6E1;
  --text-dim: #8FA9A1;
  --rule: rgba(220, 230, 225, 0.14);

  /* Alpha derivations of the palette above: same three colours, no new
     hues introduced. --rule is already defined this way in the spec. */
  --rule-soft: rgba(220, 230, 225, 0.07);
  --verdigris-line: rgba(62, 140, 122, 0.30);
  --verdigris-line-hi: rgba(62, 140, 122, 0.46);
  --patina-1: rgba(62, 140, 122, 0.20);
  --patina-2: rgba(46, 111, 98, 0.16);
  --brass-glow: rgba(192, 138, 62, 0.40);
  --well: rgba(6, 12, 11, 0.55);

  --display: "GFS Didot", Georgia, serif;
  --prose: "Newsreader", Georgia, serif;
  --mono: "Martian Mono", ui-monospace, monospace;

  /* Type scale. --step-0/2/4 are the spec's; -1/1/3 fill it in.
     Note: the h1 sits at --step-4, the top of the scale. It held at
     --step-3 for as long as the headline was a 27-word sentence, which
     at --step-4 would have set as a wall of type instead of a masthead.
     The headline is now five words, so the constraint is gone and the
     display face gets the size it was picked for. Put a paragraph back
     in the h1 and this has to go back down with it. */
  --step--1: clamp(0.78rem, 0.75rem + 0.14vw, 0.86rem);
  --step-0: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
  --step-1: clamp(1.125rem, 1.04rem + 0.4vw, 1.35rem);
  --step-2: clamp(1.5rem, 1.3rem + 1vw, 2.25rem);
  --step-3: clamp(1.95rem, 1.42rem + 2.35vw, 3.35rem);
  --step-4: clamp(2.5rem, 1.9rem + 3vw, 4.5rem);

  --gutter: clamp(1.25rem, 5vw, 4rem);
  --section-y: clamp(4.5rem, 10vw, 9rem);
  --rail: minmax(0, 17rem);
  --body: minmax(0, 46rem);
  --col-gap: clamp(2rem, 4vw, 4.5rem);

  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);

  color-scheme: dark;
}

/* ---------------------------------------------------------------------
   2. Reset
   ------------------------------------------------------------------ */

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

html {
  /* Guard against any single element pushing the page sideways. `clip`
     rather than `hidden` so no scroll container is created. */
  overflow-x: clip;
  -webkit-text-size-adjust: 100%;
}

body,
h1,
h2,
h3,
p,
ol,
ul,
figure {
  margin: 0;
}

ol,
ul {
  padding: 0;
}

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

/* app.js sets form.hidden = true on success. Several elements below are
   display:grid, which would otherwise defeat the hidden attribute. */
[hidden] {
  display: none !important;
}

/* ---------------------------------------------------------------------
   3. Ground, patina and grain
   ------------------------------------------------------------------ */

body {
  background-color: var(--ground);
  color: var(--text);
  font-family: var(--prose);
  font-size: var(--step-0);
  font-weight: 380;
  line-height: 1.62;
  overflow-wrap: break-word;
}

/* Patina. Viewport-fixed so the wash never stretches down a long
   document; three low-alpha radials, no single flat fill. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(58% 44% at 78% 8%, var(--patina-1), transparent 68%),
    radial-gradient(46% 38% at 8% 58%, var(--patina-2), transparent 70%),
    radial-gradient(70% 50% at 50% 108%, rgba(46, 111, 98, 0.13), transparent 72%);
}

/* Grain. 1px engraved hatch across the whole page at an alpha that is
   felt rather than seen. Sits above content; must not eat clicks. */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 40;
  pointer-events: none;
  background-image: repeating-linear-gradient(
    115deg,
    rgba(220, 230, 225, 0.018) 0 1px,
    transparent 1px 3px
  );
}

::selection {
  background: var(--brass);
  color: var(--ground);
}

:focus-visible {
  outline: 2px solid var(--brass-hot);
  outline-offset: 3px;
}

/* ---------------------------------------------------------------------
   4. Type
   ------------------------------------------------------------------ */

h1,
h2 {
  font-family: var(--display);
  font-weight: 400;
  letter-spacing: 0.01em;
  text-wrap: balance;
}

h1 {
  font-size: var(--step-4);
  line-height: 1.05;
  /* Didone at display size needs the tracking pulled back in. */
  letter-spacing: -0.014em;
  /* `balance` rather than `pretty`: pretty exists to stop a paragraph
     ending on a runt, which is the right call for a headline that runs
     four or five lines and the wrong one for a headline that runs two.
     h1/h2 already set balance; this used to override it. */
}

h2 {
  font-size: var(--step-2);
  line-height: 1.14;
}

h3,
label,
button,
input,
textarea,
.stages li::before {
  font-family: var(--mono);
}

p + p {
  margin-top: 1.1em;
}

/* ---------------------------------------------------------------------
   5. Masthead
   ------------------------------------------------------------------ */

#masthead {
  position: relative;
  isolation: isolate;
  overflow: hidden; /* the dial bleeds past the right edge */
  display: grid;
  grid-template-columns: minmax(0, 72rem);
  justify-content: center;
  align-content: center;
  gap: clamp(1.6rem, 3vw, 2.4rem);
  min-height: 100vh;
  min-height: min(100svh, 58rem);
  padding: clamp(5rem, 13vh, 9rem) var(--gutter) clamp(4rem, 9vh, 7rem);
  border-bottom: 1px solid var(--rule);
  background:
    radial-gradient(60% 50% at 50% 0%, var(--patina-1), transparent 70%),
    radial-gradient(40% 40% at 15% 85%, var(--patina-2), transparent 70%),
    linear-gradient(180deg, var(--ground-lift), var(--ground) 70%);
}

/* Fine engraved grain, local to the masthead. Low alpha on purpose. */
#masthead::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image: repeating-linear-gradient(
    115deg,
    rgba(220, 230, 225, 0.035) 0 1px,
    transparent 1px 3px
  );
}

#masthead > h1,
#masthead > .lede,
#masthead > .btn {
  position: relative;
  z-index: 1;
}

#masthead > h1 {
  /* Tight enough that `text-wrap: balance` breaks the line rather than
     running the headline out across the dial's pointer at the narrow end
     of the two-column masthead. Two lines of display type, which is what
     a masthead is. Widen this and the break goes away before the
     collision does. */
  max-width: 12em;
}

#masthead > .lede {
  max-width: 44ch;
  font-size: var(--step-1);
  line-height: 1.55;
  color: var(--text-dim);
}

#masthead > .lede::before {
  content: "";
  display: block;
  width: 3.5rem;
  height: 1px;
  margin-bottom: clamp(1.1rem, 2vw, 1.6rem);
  background: var(--verdigris);
}

/* ---------------------------------------------------------------------
   6. The dial
   ---------------------------------------------------------------------
   Seven hairline elements. At every width the instrument is anchored off
   the right edge and cropped by the frame rather than centred like a
   medallion. Above 64em it runs at full strength and the headline, held
   to 16em, crosses only its outermost, faintest rings. It never crosses
   the brass pointer. Below 64em the headline is wider and does cross it, so the
   instrument is muted where it stands rather than moved out of the way;
   see section 19.

   --len is that element's path length in viewBox user units, rounded up
   about 1%: it is both the dash and the starting offset, so the element
   begins fully retracted and finishes closed.

   `vector-effect: non-scaling-stroke` is deliberately NOT used here, and
   must not be added. It resolves all stroke geometry (dasharray and
   dashoffset included) in screen space, while every --len below is in
   user units. The two disagree by the viewBox scale (up to 1.227x), which
   would leave the bezel visibly open in its final state above ~980px wide
   and invert the retracted start state into disconnected inked fragments.
   Without it, path and dash share one coordinate space and the scale
   cancels: strokes render between 0.62px and 1.23px across the range,
   which is what a hairline should do anyway.
   ------------------------------------------------------------------ */

.dial {
  position: absolute;
  z-index: 0;
  top: 50%;
  /* Flat bleed, not a vw fraction. The brass pointer must clear the right
     edge of the headline at every width this rule covers, and the headline
     grows faster than a proportional bleed does: at 1024px a -13vw bleed
     put the pointer 117px inside the h1. 16rem clears it from 64em up. */
  right: -16rem;
  width: min(46rem, 62vw);
  height: auto;
  aspect-ratio: 1;
  pointer-events: none;
  transform: translateY(-50%);
}

.dial > * {
  fill: none;
  stroke: var(--rule);
  stroke-width: 1;
  stroke-dasharray: var(--len);
  stroke-dashoffset: var(--len);
  /* Longhands, not the shorthand: --delay and --dur are inherited custom
     properties, and one bad value in the shorthand invalidates the whole
     declaration at computed-value time, giving `animation: none` and an
     element frozen in its retracted, invisible start state. As longhands a
     bad value costs only that one property. */
  animation-name: draw;
  animation-duration: var(--dur, 900ms);
  animation-delay: var(--delay, 0ms);
  animation-timing-function: var(--ease-out);
  animation-fill-mode: both;
}

/* Outer bezel */
.dial > :nth-child(1) {
  --len: 1800;
  --dur: 1000ms;
  --delay: 0ms;
}

/* Second bezel ring */
.dial > :nth-child(2) {
  --len: 1500;
  --dur: 900ms;
  --delay: 80ms;
  stroke: var(--verdigris-line);
}

/* The 270-degree sweep: the structural line of the whole composition */
.dial > :nth-child(3) {
  --len: 900;
  --dur: 900ms;
  --delay: 170ms;
  stroke: var(--verdigris-line-hi);
  stroke-width: 1.25;
}

/* Twelve index ticks. One path, twelve subpaths: renderers restart the
   dash pattern per subpath, so --len is set above one tick's length and
   a paired fade keeps the timing honest whichever way it resolves. */
.dial .ticks {
  --len: 240;
  --dur: 560ms;
  --delay: 360ms;
  stroke: rgba(220, 230, 225, 0.30);
  animation-name: draw, fade;
  animation-duration: var(--dur), var(--dur);
  animation-delay: var(--delay), var(--delay);
  animation-timing-function: var(--ease-out), linear;
  animation-fill-mode: both, both;
}

/* Inner register */
.dial > :nth-child(5) {
  --len: 890;
  --dur: 820ms;
  --delay: 250ms;
}

/* The pointer. The only brass on the page above the fold. */
.dial .lead {
  --len: 200;
  --dur: 520ms;
  --delay: 620ms;
  stroke: var(--brass);
  stroke-width: 2;
  stroke-linecap: round;
  filter: drop-shadow(0 0 7px var(--brass-glow));
}

/* Pivot */
.dial > :nth-child(7) {
  --len: 285;
  --dur: 620ms;
  --delay: 330ms;
  stroke: rgba(62, 140, 122, 0.45);
}

/* ---------------------------------------------------------------------
   7. Section frame
   ---------------------------------------------------------------------
   A two-track ledger: the heading sits alone in a narrow left rail, the
   argument runs in a measured column beside it. Deliberately not a
   centred stack, and deliberately not a grid of cards.
   ------------------------------------------------------------------ */

section:not(#masthead) {
  display: grid;
  grid-template-columns: var(--body);
  justify-content: center;
  align-content: start;
  gap: clamp(1.4rem, 3vw, 2.2rem) var(--col-gap);
  padding: var(--section-y) var(--gutter);
}

section:not(#masthead) > h2 {
  color: var(--text);
}

section:not(#masthead) > h2::after {
  content: "";
  display: block;
  width: 2.75rem;
  height: 1px;
  margin-top: clamp(0.9rem, 2vw, 1.3rem);
  background: var(--verdigris);
}

section p,
section li {
  max-width: 66ch;
}

/* Direct children of the section grid are spaced by row-gap, not margins. */
section:not(#masthead) > p + p {
  margin-top: 0;
}

@media (min-width: 60em) {
  section:not(#masthead) {
    grid-template-columns: var(--rail) var(--body);
  }

  /* Row 1 only, never a span: spanning implicit rows would insert a
     row-gap for every empty row underneath the heading. */
  section:not(#masthead) > h2 {
    grid-column: 1;
    grid-row: 1;
    align-self: start;
  }

  section:not(#masthead) > :not(h2) {
    grid-column: 2;
  }
}

/* ---------------------------------------------------------------------
   8. Button
   ---------------------------------------------------------------------
   One component, two callers: the masthead's call to action (an anchor)
   and the form's submit (a button). Extracted from #contact-form button
   so the two cannot drift apart. Note that `button` inherits --mono from
   section 4 but an anchor does not, so the family is set here rather
   than relied on.
   ------------------------------------------------------------------ */

.btn {
  justify-self: start;
  display: inline-block;
  padding: 0.85rem 2.2rem;
  font-family: var(--mono);
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--brass-hot);
  background-color: transparent;
  border: 1px solid var(--brass);
  border-radius: 0;
  cursor: pointer;
  transition:
    color 180ms var(--ease-out),
    background-color 180ms var(--ease-out),
    box-shadow 180ms var(--ease-out);
}

.btn:hover:not(:disabled) {
  color: var(--ground);
  background-color: var(--brass-hot);
  box-shadow: 0 0 22px rgba(192, 138, 62, 0.22);
}

.btn:active:not(:disabled) {
  background-color: var(--brass);
}

.btn:disabled {
  color: var(--text-dim);
  border-color: var(--rule);
  cursor: progress;
}

/* ---------------------------------------------------------------------
   9. The default this exists to fix
   ------------------------------------------------------------------ */

#default > p {
  font-size: var(--step-1);
  line-height: 1.58;
}

/* ---------------------------------------------------------------------
   10. Value: four columns, ruled and not carded
   ---------------------------------------------------------------------
   The shape here is borrowed from a product page's four-up benefit grid,
   which renders as cards: rounded panels, a drop shadow, an icon apiece.
   That kit is the default this page's art direction was written against,
   and section 12 already turns it down once for the deliverables. So the
   scannability is taken and the chrome is not. The columns are divided by
   the same hairline used everywhere else on the page, which is what an
   engraved scale looks like in any case.

   Column counts are explicit rather than auto-fit, because the dividing
   rules have to know which cells start a row: with auto-fit, the first
   cell of a wrapped second row keeps a left rule that should not be
   there and the block reads as broken.
   ------------------------------------------------------------------ */

/* `section#value` and not `#value`, here and below. Section 7 selects
   `section:not(#masthead)`, and :not() takes the specificity of its
   argument, so that rule carries an id and a bare #value would silently
   lose to it. Matching the element as well makes this a tie that source
   order settles the right way. */
section#value {
  /* Opt out of the two-track rail in section 7. Four columns need the
     full measure, and this heading belongs above its grid, not beside
     it. */
  grid-template-columns: minmax(0, 72rem);
}

.value-grid {
  list-style: none;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
}

.value-grid li {
  display: grid;
  align-content: start;
  gap: 0.55rem;
  /* Overrides the 66ch measure on `section li` from section 7, which is
     a prose measure and far wider than a column here. */
  max-width: none;
  padding: clamp(1.4rem, 2.6vw, 1.9rem) 0;
  border-top: 1px solid var(--rule);
}

.value-grid li:last-child {
  border-bottom: 1px solid var(--rule);
}

.value-grid h3 {
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: 0.14em;
  line-height: 1.4;
  text-transform: uppercase;
  color: var(--verdigris);
}

.value-grid p {
  max-width: 38ch;
  color: var(--text);
}

@media (min-width: 40em) {
  .value-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .value-grid li {
    padding: clamp(1.4rem, 2.6vw, 1.9rem) clamp(1.1rem, 2vw, 1.6rem);
  }

  /* Border-box is set in the reset, so a left rule is drawn inside the
     track and the four columns stay exactly equal. */
  .value-grid li:nth-child(2n) {
    border-left: 1px solid var(--rule);
  }

  .value-grid li:nth-child(-n + 2) {
    border-bottom: 1px solid var(--rule);
  }
}

@media (min-width: 64em) {
  .value-grid {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }

  .value-grid li:not(:first-child) {
    border-left: 1px solid var(--rule);
  }

  .value-grid li {
    border-bottom: 1px solid var(--rule);
  }
}

@media (min-width: 60em) {
  section#value {
    grid-template-columns: minmax(0, 72rem);
  }

  /* Section 7 puts the h2 in column 1 and everything else in column 2.
     With one track here, both belong in column 1. */
  section#value > h2,
  section#value > .value-grid {
    grid-column: 1;
  }
}

/* ---------------------------------------------------------------------
   11. Method: five stages
   ------------------------------------------------------------------ */

#method {
  background-image: radial-gradient(
    38% 44% at 4% 40%,
    rgba(46, 111, 98, 0.10),
    transparent 70%
  );
}

.stages {
  list-style: none;
  counter-reset: stage;
}

.stages li {
  counter-increment: stage;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 0.55rem;
  max-width: none;
  padding-block: clamp(1.6rem, 3vw, 2.4rem);
  border-top: 1px solid var(--rule);
}

.stages li:last-child {
  border-bottom: 1px solid var(--rule);
}

.stages li::before {
  content: counter(stage, decimal-leading-zero);
  font-size: var(--step--1);
  font-weight: 400;
  letter-spacing: 0.1em;
  line-height: 1;
  color: var(--text-dim);
}

.stages h3 {
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--verdigris);
}

.stages p {
  max-width: 62ch;
  color: var(--text);
}

@media (min-width: 40em) {
  .stages li {
    grid-template-columns: 4.5rem minmax(0, 1fr);
    column-gap: clamp(1rem, 2vw, 2rem);
    row-gap: 0.7rem;
  }

  .stages li::before {
    grid-column: 1;
    grid-row: 1 / span 2;
    padding-top: 0.35em;
  }

  .stages h3,
  .stages p {
    grid-column: 2;
  }
}

/* ---------------------------------------------------------------------
   12. Deliverables: a ledger, not cards
   ------------------------------------------------------------------ */

#deliverables ul {
  list-style: none;
}

#deliverables li {
  display: grid;
  grid-template-columns: 1.9rem minmax(0, 1fr);
  align-items: start;
  max-width: 68ch;
  padding-block: clamp(1.05rem, 2vw, 1.4rem);
  border-top: 1px solid var(--rule);
}

#deliverables li:last-child {
  border-bottom: 1px solid var(--rule);
}

#deliverables li::before {
  content: "";
  width: 0.5rem;
  height: 0.5rem;
  margin-top: 0.62em;
  border: 1px solid var(--verdigris);
  transform: rotate(45deg);
}

/* ---------------------------------------------------------------------
   13. What we don't claim: the inset plate
   ------------------------------------------------------------------ */

#limits {
  background-color: var(--ground-lift);
  border-block: 1px solid var(--rule);
  box-shadow: inset 0 1px 0 var(--rule-soft);
}

#limits p {
  color: var(--text-dim);
}

#limits p:first-of-type {
  color: var(--text);
}

/* ---------------------------------------------------------------------
   14. Contact
   ---------------------------------------------------------------------
   Concentric engraved rings rise behind the form, echoing the dial.
   ------------------------------------------------------------------ */

/* No overflow clipping here: Turnstile's challenge widget expands
   its own box and must never be cut off. */
#contact {
  position: relative;
  background-image:
    repeating-radial-gradient(
      circle at 50% 132%,
      var(--rule-soft) 0 1px,
      transparent 1px 46px
    ),
    radial-gradient(52% 42% at 50% 128%, rgba(62, 140, 122, 0.14), transparent 70%);
}

#contact-form {
  position: relative;
  display: grid;
  max-width: 34rem;
}

#contact-form > label:first-of-type {
  margin-top: 0.5rem;
}

#contact-form label {
  display: block;
  margin-top: 1.9rem;
  margin-bottom: 0.55rem;
  font-size: var(--step--1);
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-dim);
}

/* No alpha on the optional marker: at this size it fell to 3.8:1. It is
   distinguished by case and tracking, not by being harder to read. */
#contact-form label span {
  text-transform: none;
  letter-spacing: 0.06em;
}

#contact-form input,
#contact-form textarea {
  width: 100%;
  max-width: 100%;
  padding: 0.72rem 0.85rem;
  /* 16px floor: anything smaller and mobile Safari zooms the page on
     focus, which throws the layout for the length of the enquiry. */
  font-size: 1rem;
  letter-spacing: 0.02em;
  line-height: 1.55;
  color: var(--text);
  background-color: var(--well);
  /* WCAG 1.4.11 wants 3:1 for the boundary of a UI component, and this is
     the only conversion point on the site. --rule reaches 1.41:1 against
     the ground; --verdigris-d reaches 3.17:1. The well stays dark so the
     field still reads as a recess: the border, not the fill, carries the
     boundary. */
  border: 1px solid var(--verdigris-d);
  border-radius: 0;
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.35);
  transition:
    border-color 160ms var(--ease-out),
    background-color 160ms var(--ease-out);
}

#contact-form textarea {
  resize: vertical;
  min-height: 9rem;
}

#contact-form input:hover,
#contact-form textarea:hover {
  border-color: var(--verdigris); /* 4.66:1 */
}

#contact-form input:focus-visible,
#contact-form textarea:focus-visible {
  border-color: var(--brass);
  outline: 1px solid var(--brass);
  outline-offset: 1px;
}

/* Chrome paints autofilled fields pale blue; on this ground that reads as
   a bug. Hold the well colour and the text colour. */
#contact-form input:-webkit-autofill,
#contact-form input:-webkit-autofill:focus {
  -webkit-text-fill-color: var(--text);
  -webkit-box-shadow: 0 0 0 1000px var(--ground-lift) inset;
  caret-color: var(--text);
}

.cf-turnstile {
  margin-top: 2rem;
  min-height: 65px;
}

/* Appearance comes from .btn in section 8; only the spacing inside the
   form grid is local. */
#contact-form button {
  margin-top: 1.9rem;
}

.contact-lede {
  font-size: var(--step-1);
  line-height: 1.55;
  color: var(--text-dim);
}

/* Deliberately a sibling of the form, not a child of it: app.js sets
   form.hidden = true on success, and a status paragraph inside the form
   would be hidden along with it. The visitor would watch the form vanish
   with no confirmation, and nothing would reach assistive technology
   either. Kept in the DOM and rendered at all times, since it is the live
   region; spacing comes from the section's own row-gap, so an empty one
   adds nothing to look at. */
#form-status {
  max-width: 34rem;
  margin-top: 0;
  font-family: var(--mono);
  font-size: var(--step--1);
  line-height: 1.7;
  letter-spacing: 0.03em;
  color: var(--brass-hot); /* 9.66:1. Something still needs the visitor. */
}

/* Success and failure must not look identical. app.js exposes no state
   class, but it does hide the form on success and only on success, so the
   two outcomes are already distinguishable from the markup: every error
   path leaves the form standing. Brass reads as "your turn"; verdigris
   (4.66:1) reads as "done, nothing further". */
#contact-form[hidden] + #form-status {
  color: var(--verdigris);
}

/* ---------------------------------------------------------------------
   15. Honeypot
   ---------------------------------------------------------------------
   The endpoint silently discards any submission where `website` is
   non-empty. If a human can see this field they will fill it in honestly
   and their enquiry will be thrown away while the page tells them it
   succeeded. So it is removed from the flow, parked far off-canvas,
   clipped to nothing, made fully transparent and made unclickable: five
   independent reasons it can never be seen or hit. It is already
   tabindex="-1" and aria-hidden in the markup, so keyboard and screen
   reader users cannot reach it either.
   Off-canvas to the left does not create a scrollable area in LTR.

   Deliberately NOT `display: none`: bots skip fields hidden that way, and a
   honeypot they skip catches nobody. Do not "simplify" this to display:none.
   It would quietly disable the trap. The axe `aria-hidden-focus` finding
   on the wrapper is an accepted, recorded trade-off, not an oversight.
   ------------------------------------------------------------------ */

.trap {
  position: absolute !important;
  left: -9999px !important;
  top: auto !important;
  width: 1px !important;
  height: 1px !important;
  overflow: hidden !important;
  clip-path: inset(50%) !important;
  opacity: 0 !important;
  pointer-events: none !important;
}

/* ---------------------------------------------------------------------
   16. Footer
   ---------------------------------------------------------------------
   A sibling of <main>, so none of the `section:not(#masthead)` rules in
   section 7 reach it and the frame has to be restated here. It is
   restated rather than approximated: the footer borrows section 7's exact
   tracks so the notice starts on the same left edge as every h2 above it.
   Eyeballing a padding value instead would land it a few pixels off the
   rail, which is the kind of thing that reads as sloppy without the
   viewer being able to say why.
   ------------------------------------------------------------------ */

footer {
  display: grid;
  grid-template-columns: var(--body);
  justify-content: center;
  padding: clamp(2rem, 4vw, 2.75rem) var(--gutter);
  border-top: 1px solid var(--rule);
}

footer p {
  max-width: none;
  font-family: var(--mono);
  font-size: var(--step--1);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-dim);
}

@media (min-width: 60em) {
  footer {
    grid-template-columns: var(--rail) var(--body);
  }

  /* Spans both tracks: the notice belongs at the rail's left edge, where
     the headings are, not indented into the body column. */
  footer p {
    grid-column: 1 / -1;
  }
}

/* ---------------------------------------------------------------------
   17. Motion: one orchestrated load, CSS only
   ---------------------------------------------------------------------
   The dial draws from the bezel inward, the ticks strike, the pointer
   lands last; the headline, lede and five stages rise underneath on a
   65ms stagger. Everything is settled inside ~1.2s and nothing loops.
   ------------------------------------------------------------------ */

@keyframes draw {
  to {
    stroke-dashoffset: 0;
  }
}

@keyframes fade {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes rise {
  from {
    opacity: 0;
    transform: translateY(14px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

#masthead > h1,
#masthead > .lede,
#masthead > .btn,
.stages li {
  opacity: 0;
  /* Longhands for the same reason as the dial: a bad --delay must not be
     able to cancel the animation and strand the headline at opacity 0. */
  animation-name: rise;
  animation-duration: 640ms;
  animation-delay: var(--delay, 0ms);
  animation-timing-function: var(--ease-out);
  animation-fill-mode: forwards;
}

#masthead > h1 { --delay: 140ms; }
#masthead > .lede { --delay: 250ms; }
#masthead > .btn { --delay: 345ms; }
.stages li:nth-child(1) { --delay: 320ms; }
.stages li:nth-child(2) { --delay: 385ms; }
.stages li:nth-child(3) { --delay: 450ms; }
.stages li:nth-child(4) { --delay: 515ms; }
.stages li:nth-child(5) { --delay: 580ms; }

/* ---------------------------------------------------------------------
   18. Reduced motion
   ---------------------------------------------------------------------
   Not merely shorter: the reveal above starts at opacity 0 and the dial
   starts fully retracted, so collapsing durations alone would leave the
   headline, the lede, the five stages and the whole instrument invisible
   for anyone with the preference set. Every animated element is put back
   into its finished state explicitly.

   Note for anyone changing the dial: the mobile mute in section 19 sits on
   `.dial` itself, never on `.dial > *`. The `opacity: 1 !important` below
   is what restores the ticks' `fade`, so any per-child opacity would be
   silently discarded here and the dial would render at full strength for
   exactly the users who cannot see it draw.
   ------------------------------------------------------------------ */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 1ms !important;
    animation-delay: 0ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    transition-delay: 0ms !important;
  }

  /* Final state of `rise`. */
  #masthead > h1,
  #masthead > .lede,
  #masthead > .btn,
  .stages li {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
  }

  /* Final state of `draw` and `fade`, for all seven dial elements. */
  .dial > * {
    animation: none !important;
    stroke-dashoffset: 0 !important;
    opacity: 1 !important;
  }
}

/* ---------------------------------------------------------------------
   19. Narrow and small screens
   ---------------------------------------------------------------------
   Two separate concerns, two separate queries.

   (a) Below 64em the headline is set wider and lands on top of the
   instrument instead of beside it. The instrument is not moved out of the
   way for it: a dial pushed off to a corner and stacked under the text
   stops reading as the thing the page is built on and starts reading as a
   picture of one (which is what a phone showed, ~295x266 of clip art
   sitting under the lede). It stays anchored off the right edge, cropped by
   the frame, and is muted instead until it reads as engraving under the
   words. The earlier collision this replaces was a full-strength brass
   pointer competing with display type; low-contrast and deliberately
   behind is a different thing from accidental and full-strength.

   (b) Below 40em the dial additionally simplifies. It never disappears.
   ------------------------------------------------------------------ */

@media (max-width: 63.99em) {
  /* Position and vertical centring are inherited from the base rule
     unchanged, so the composition is the one above 64em (display type set
     against a cropped instrument), and only the crop and the strength
     differ.

     The two pairs cross over at the same viewport width (467.7px: 52vw
     hits 15.2rem exactly where 130vw hits 38rem), so the visible slice is
     a constant 60% of the dial's width at every width this query covers:
     the same fraction the base rule shows above 64em, where the frame cuts
     the pointer at its outermost point. Proportional below the crossover
     so a 320px phone still gets a large arc rather than a token one;
     capped in rem above it so the 1023px end does not outgrow the frame.

     Geometry: the dial's left edge lands at 70px (320 wide), 79px (360),
     86px (390) and 403px (768), so it never reaches the left edge and
     bleeds off the right only. That bleed is clipped by #masthead's
     own `overflow: hidden`, not by the `overflow-x: clip` guard on `html`.
     No width here can put the page into horizontal scroll. */
  .dial {
    right: max(-15.2rem, -52vw);
    width: min(38rem, 130vw);

    /* At this size the base 0.14 --rule hairline reads as invisible behind display type, so raise it to the 0.34 the design already sanctions in prefers-contrast: more. */
    --rule: rgba(220, 230, 225, 0.34);
  }

  /* Strokes scale with the dial now that non-scaling-stroke is gone; the
     scale runs 0.69 (320px) to 1.01 (1023px) here, and the slightly
     heavier user-unit width holds the rendered hairline near 1px across
     that range. The sweep and the pointer set their own widths at higher
     specificity. */
  .dial > * {
    stroke-width: 1.15;
  }
}

@media (max-width: 39.99em) {
  /* Drop the second bezel and the index ticks: at this size they collapse
     into noise. Bezel, sweep, register, pointer and pivot remain. */
  .dial > :nth-child(2),
  .dial .ticks {
    display: none;
  }

  #masthead > h1 {
    max-width: none;
  }
}

/* ---------------------------------------------------------------------
   20. Preference refinements
   ------------------------------------------------------------------ */

@media (prefers-contrast: more) {
  :root {
    --rule: rgba(220, 230, 225, 0.34);
    --text-dim: #B6C9C2;
  }

  #contact-form input,
  #contact-form textarea {
    border-color: var(--verdigris); /* 3.17:1 -> 4.66:1 */
  }

  body::after {
    display: none;
  }
}
