/* ============================================================
   Ryker Graves — foundation. Colors sampled from the mockup
   screenshots per-pixel; do not "tidy" them lighter.
   ============================================================ */
:root {
  --bg:          #040404;
  --bg-raised:   #07080d;
  --border:      rgba(255,255,255,.07);
  --text:        #a6a6ac;
  --text-dim:    #6e6e74;
  --text-bright: #d8d8dc;

  --seal-oxblood: #4a2423;  --seal-oxblood-lit: #c8afaa;
  --seal-navy:    #2d2f3e;  --seal-navy-lit:    #b5b3b8;
  --seal-plum:    #3c2432;  --seal-plum-lit:    #bfabb4;
  --seal-crimson: #5c2e2e;  --seal-crimson-lit: #ccafab;

  --tint-crimson-dim: #220809; --tint-crimson-lit: #422021;
  --tint-navy-dim:    #060610; --tint-navy-lit:    #0e1728;
  --tint-amber-dim:   #140b04; --tint-amber-lit:   #301b0a;
  --tint-forest-dim:  #010a05; --tint-forest-lit:  #021e10;

  --tag: #441a1c;
}

* { box-sizing: border-box; }

/* Hiding must not depend on the UA sheet alone. Any later rule setting
   `display` on a section's class would silently defeat [hidden] and leave a
   heading with nothing under it — which is the exact failure hide-when-empty
   exists to prevent. Task 6 adds display rules at section level. */
[hidden] { display: none !important; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: "Cormorant Garamond", Georgia, serif;
  font-weight: 300;
  font-size: 19px;
  line-height: 1.75;
  -webkit-font-smoothing: antialiased;
}

/* Display type. Cormorant SC renders true small caps natively —
   "Whispers in the Dark" becomes full-cap W + small-cap HISPERS.
   Substituting Cormorant Garamond here loses the effect entirely. */
.display {
  font-family: "Cormorant SC", "Cormorant Garamond", Georgia, serif;
  font-weight: 500;
  line-height: 1.15;
  letter-spacing: .01em;
  color: #d8d8dc;                  /* MUST come first — see below */
  background: linear-gradient(180deg, #e9e9ec 0%, #9b9ba1 52%, #5f5f66 100%);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
}
/* `color` is declared before the clip on purpose: where background-clip:text
   is unsupported, -webkit-text-fill-color is also unsupported, so the heading
   falls back to #d8d8dc instead of rendering invisible. */

.eyebrow {
  font-size: .7rem;
  font-weight: 300;
  text-transform: uppercase;
  letter-spacing: .34em;
  color: var(--text-dim);
  margin: 0 0 1.2rem;
}

.wordmark {
  font-size: 1.05rem;
  text-transform: uppercase;
  letter-spacing: .28em;
  text-decoration: none;
}

.lede, .subhead, blockquote p, .credit-author, .narrate-points li {
  font-style: italic;
}

.section { padding: 5.5rem 1.25rem; max-width: 1100px; margin: 0 auto; }
.section-head { text-align: center; margin-bottom: 3.5rem; }
.section-head h2 { font-size: clamp(2.1rem, 7vw, 3.4rem); margin: 0 0 1.2rem; }
.subhead { font-size: 1.15rem; color: var(--text-dim); margin: 0 auto; max-width: 34ch; }

.card {
  background: var(--bg-raised);
  border: 1px solid var(--border);
  padding: 2rem 1.6rem;
}

.skip { position: absolute; left: -9999px; }
/* z-index must beat the nav's 20. At 10 the site's only keyboard bypass
   focused correctly and rendered *underneath* the sticky header — present,
   announced by a screen reader, and invisible to the sighted keyboard user
   it exists for. */
.skip:focus { left: 1rem; top: 1rem; background: var(--bg-raised); padding: .6rem 1rem; z-index: 30; }

:focus-visible { outline: 1px solid var(--text-bright); outline-offset: 3px; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    /* Neutralising duration alone does NOT remove the animation, because it
       leaves the DELAY. `.contract` animates `rise` with fill-mode `both`,
       whose backwards fill holds the `from` keyframe — opacity 0 — for the
       whole delay, and app.js stages each card by 70ms. So a reduced-motion
       user still watched the ledger cascade in, invisible card by invisible
       card, and watched it again on every shuffle. The constraint is
       "removes", not "shortens". transition-delay is the same hazard with
       nothing using it yet. */
    animation-delay: 0s !important;
    transition-delay: 0s !important;
  }
}

/* ── Demos — Choose Your Temptation (Task 4) ─────────────── */
.demo-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem; }
@media (max-width: 560px) { .demo-grid { grid-template-columns: 1fr; } }

.temptation { position: relative; text-align: center; padding: 2.4rem 1.2rem; overflow: hidden; }
.temptation::before, .temptation::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(135deg, transparent 49.6%, rgba(255,255,255,.05) 50%, transparent 50.4%);
  transition: opacity .5s ease; pointer-events: none;
}
.temptation::after { transform: scaleX(-1); }
.temptation.is-broken::before, .temptation.is-broken::after { opacity: 0; }

.num { font-size: .72rem; letter-spacing: .3em; color: var(--text-dim); margin: 0 0 1.6rem; }

.seal {
  width: 74px; height: 74px; border-radius: 50%; display: grid; place-items: center;
  border: 1px solid rgba(255,255,255,.14); padding: 0; cursor: pointer;
  transition: transform .45s ease, opacity .45s ease;
}
.seal .sigil { font-size: 1.5rem; color: #efe7e2; }
.seal-oxblood { background: radial-gradient(circle at 34% 30%, var(--seal-oxblood-lit), var(--seal-oxblood) 62%); }
.seal-navy    { background: radial-gradient(circle at 34% 30%, var(--seal-navy-lit),    var(--seal-navy) 62%); }
.seal-plum    { background: radial-gradient(circle at 34% 30%, var(--seal-plum-lit),    var(--seal-plum) 62%); }
.seal-crimson { background: radial-gradient(circle at 34% 30%, var(--seal-crimson-lit), var(--seal-crimson) 62%); }

.temptation .seal-btn { margin: 0 auto 1.5rem; }
.temptation.is-broken .seal-btn { transform: rotate(-18deg) scale(.82); opacity: .35; }
.temptation.is-sealed-empty .seal-btn { cursor: default; opacity: .55; }

.temptation-title { font-size: 1.5rem; margin: 0 0 .5rem; }
.temptation-genre { font-style: italic; color: var(--text-dim); margin: 0; }

/* `max-height: 0` + `overflow: hidden` hides the player visually and leaves
   every control in the tab order — you could tab into a play button you
   cannot see, on a card whose seal is still unbroken. `visibility: hidden`
   is what actually removes a subtree from focus. It is transitioned at 0s
   with a delay equal to the collapse, so it only takes effect once the card
   has finished closing; opening flips it immediately so the controls are
   focusable for the whole reveal. app.js already pauses the audio on
   collapse for the same reason — this is the focus half of that. */
.reveal { max-height: 0; overflow: hidden; visibility: hidden;
  transition: max-height .5s ease, visibility 0s linear .5s; }
.temptation.is-broken .reveal { max-height: 220px; visibility: visible;
  transition: max-height .5s ease, visibility 0s linear 0s; }

.player { display: flex; align-items: center; gap: .75rem; margin-top: 1.5rem; }
.demo-play { width: 40px; height: 40px; border-radius: 50%; flex: none;
  background: transparent; border: 1px solid var(--border); color: var(--text-bright); cursor: pointer; }
.wave { flex: 1; display: flex; align-items: center; gap: 2px; height: 34px;
  background: none; border: 0; padding: 0; cursor: pointer; }
.wave i { flex: 1; background: #2a2a30; border-radius: 1px; transition: background .15s; }
.wave i.on { background: var(--seal-crimson-lit); }
.demo-time { font-size: .8rem; color: var(--text-dim); min-width: 3ch; }

/* ── Ledger — Signed & in the Works (Task 5) ─────────────── */
.ledger-grid { display: grid; gap: 1rem; }
.contract { animation: rise .45s ease both; }
@keyframes rise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }

.contract-head { display: flex; align-items: flex-start; justify-content: space-between; }
.contract-head .seal { width: 54px; height: 54px; }
.contract-head .sigil { font-size: 1.1rem; }
.contract-title { font-size: 1.9rem; margin: 1.5rem 0 .3rem; }
.contract-author { font-style: italic; color: var(--text-dim); margin: 0 0 1.6rem; }

.flip-btn {
  background: none; border: 0; padding: 0; cursor: pointer;
  font-family: inherit; font-style: italic; font-size: .95rem; color: var(--text-dim);
}
/* The flip from section 5.2 of the design doc, which app.js has been
   toggling `.is-turned` for since it was written — the class appeared zero
   times in this file, so the mechanic worked (aria, terms and label all
   changed) while the card never moved.

   NOT a persistent rotateY(180deg). app.js reveals the terms in place as a
   sibling rather than building a second face, so holding the card at 180
   would render every word mirrored. This turns it edge-on and back, which
   is the illusion a real flip gives without a backside existing. Keyed on
   a transient `.is-turning` because toggling a class only restarts an
   animation on add, and the card has to turn in both directions.

   `.contract` already animates `rise`; this rule is two classes, so it wins
   and replaces that shorthand — safe only because `rise` has finished long
   before anything can be clicked. The reduced-motion block neutralises this
   along with everything else via its universal !important rules. */
.ledger-grid { perspective: 1200px; }
.contract.is-turning { animation: turn-over .46s ease both; }
@keyframes turn-over {
  0%   { transform: rotateY(0deg); }
  50%  { transform: rotateY(-84deg); }
  100% { transform: rotateY(0deg); }
}
.terms { margin: 1rem 0 0; padding-top: 1rem; border-top: 1px solid var(--border); }

#shuffle-ledger { margin: 1.5rem auto .75rem; display: block; }
.subhead.small { font-size: .95rem; }

/* ── Header (Task 6) ──────────────────────────────────────── */
.nav {
  position: sticky; top: 0; z-index: 20;
  display: flex; align-items: center; gap: 1rem;
  padding: .75rem 1.25rem;
  background: #030303; border-bottom: 1px solid var(--border);
}
.brand { display: flex; align-items: center; gap: 1rem; text-decoration: none; }
.monogram {
  width: 48px; height: 48px; flex: none;
  display: grid; place-items: center;
  border: 1px solid var(--border);
  font-family: "Cormorant Garamond", Georgia, serif;
  font-weight: 300; font-size: 1.05rem; letter-spacing: .06em;
  color: var(--text);
}
.nav-toggle {
  margin-left: auto; width: 40px; height: 40px;
  display: grid; align-content: center; gap: 5px;
  background: none; border: 0; cursor: pointer; padding: 0 8px;
}
.nav-toggle span { display: block; height: 1px; background: var(--text); }
.nav-links { display: none; }
.nav-links.open {
  display: flex; flex-direction: column; gap: .25rem;
  position: absolute; top: 100%; left: 0; right: 0;
  background: #030303; border-bottom: 1px solid var(--border);
  padding: 1rem 1.25rem 1.5rem;
}
.navlink {
  color: var(--text-dim); text-decoration: none;
  text-transform: uppercase; letter-spacing: .24em; font-size: .78rem;
  padding: .6rem 0;
}
@media (min-width: 760px) {
  .nav-toggle { display: none; }
  /* `.nav-links.open` has to be named here too. It is two classes and this
     rule was one, so at desktop width `.open` won on specificity and the
     absolutely-positioned black dropdown panel survived into a layout where
     the hamburger is `display: none` — leaving nothing on screen able to
     close it. Rotating a phone to landscape with the menu open was enough
     to strand it over the page. Every property `.open` sets is reset, not
     just display: it also sets flex-direction, position, padding,
     background and a border. */
  .nav-links,
  .nav-links.open {
    display: flex; flex-direction: row; gap: 2rem; margin-left: auto;
    position: static; padding: 0;
    background: none; border-bottom: 0;
  }
}

/* ── Hero (Task 6) ────────────────────────────────────────── */
.hero {
  position: relative; min-height: 86vh;
  display: grid; align-content: center; justify-items: center;
  text-align: center;
}
.hero h1 { font-size: clamp(2.6rem, 11vw, 5rem); margin: 0 0 2rem; max-width: 16ch; }
/* The headline is the NAME now, not a sentence — two short words that want to
   stack and run wider than prose would. 16ch wraps "Ryker Graves" awkwardly. */
.hero .hero-name {
  font-size: clamp(3rem, 14vw, 6.5rem);
  max-width: 14ch; letter-spacing: .02em; margin: 0 0 1.6rem;
  /* A brighter ramp than the shared .display one. That gradient ends at
     #5f5f66, which is fine on flat near-black and turns the bottom half of
     the letterforms to mud over the backdrop's own mid-greys — worst exactly
     where the vault ribs sit. Starts and ends lighter; still a gradient, so
     it reads as the same family as the section headings below.
     `color` stays ahead of background-clip for the no-clip fallback, same
     rule as .display. */
  color: #e4e4e8;
  background: linear-gradient(180deg, #f4f4f7 0%, #c6c7cf 54%, #9a9ba4 100%);
  -webkit-background-clip: text;
          background-clip: text;
}
.hero .lede {
  max-width: 42ch; color: var(--text-bright); margin: 0 0 1rem;
  font-size: 1.15rem;
}
.hero .hero-sub {
  max-width: 44ch; color: var(--text); margin: 0 0 2.5rem;
  font-size: .95rem;
}
/* Lifts the hero copy off a backdrop that is busy behind the type and flat
   nowhere. Only the hero — every other section sits on plain near-black and
   needs nothing. */
.hero .lede, .hero .hero-sub, .hero .eyebrow {
  text-shadow: 0 1px 14px rgba(0,0,0,.85);
}

/* ❦ flanking the eyebrow. Decorative, so the glyphs live in the markup as
   aria-hidden spans and never reach content.js — TJ edits the words, not the
   ornament. gap on the flex row is what keeps the spacing even; letter-spacing
   on .eyebrow would otherwise push a trailing space after the last glyph. */
.eyebrow-flanked {
  display: flex; align-items: center; justify-content: center;
  gap: .9rem;
}
.eyebrow-flanked .flank { font-size: .9rem; opacity: .55; letter-spacing: 0; }

.seal-rule {
  position: relative; width: min(340px, 70%); height: 1px;
  background: var(--border); margin: 0 0 2.5rem;
  display: grid; place-items: center;
}
.seal-rule .seal { width: 46px; height: 46px; }

.btn-row { display: flex; gap: 1rem; flex-wrap: wrap; justify-content: center; }

/* The backdrop sits behind the type, full bleed. An unfilled hero photo must
   leave a clean type-only hero, NOT the dashed placeholder used elsewhere.

   Opacity is .9, not the .22 this slot used when it held a dim headshot: the
   shipped image is already graded down (median luminance 21 of 255), so the
   darkening is done in the asset where it costs nothing at runtime, rather
   than by washing out a bright photo in CSS. Both levers exist on purpose —
   a brighter image dropped in here is still tamed by the scrim below. */
.hero-photo {
  position: absolute; inset: 0; z-index: -1;
  opacity: .9; overflow: hidden;
  /* Fades into the page bottom so there is no seam where the hero ends and
     the about section begins. Vertical, not the radial mask a portrait
     headshot wanted — this image is architecture and reads edge to edge. */
  -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 52%, transparent 96%);
          mask-image: linear-gradient(180deg, #000 0%, #000 52%, transparent 96%);
}
/* 38%, not center: the composition's light source is the window above the
   midline, and a plain centre crop pushes it off the top on a short viewport. */
.hero-photo img { width: 100%; height: 100%; object-fit: cover; object-position: center 38%; }
/* Legibility scrim. ::after rather than a rule on .hero, so the whole
   backdrop stays one element with one z-index — and so it disappears
   automatically with the image when no photo is set, instead of darkening a
   hero that has nothing behind it. */
.hero-photo::after {
  content: ""; position: absolute; inset: 0;
  background:
    radial-gradient(ellipse at 50% 40%, rgba(4,4,4,.10), rgba(4,4,4,.62) 78%),
    linear-gradient(180deg, rgba(4,4,4,.48), rgba(4,4,4,0) 34%);
}
/* Three classes, not two, and deliberately so. The element is
   `class="photo hero-photo empty"`, so `.photo.empty` below matches it too —
   and that rule sets `display: grid` to build the dashed placeholder. At two
   classes each the two rules tie on specificity, source order decides, and
   `.photo.empty` comes later: the hero rendered the placeholder with its
   `::after` hint text ("Headshot — moody, low-key lighting") laid over the
   lede, which is the exact outcome the comment below says must not happen.
   Winning on specificity instead of position keeps it fixed if either rule
   moves. */
.photo.hero-photo.empty { display: none; }

/* The dashed placeholder, used everywhere EXCEPT the hero. */
.photo.empty {
  border: 1px dashed rgba(255,255,255,.12);
  min-height: 220px; display: grid; place-items: center;
}
.photo.empty::after {
  content: attr(data-hint);
  font-size: .78rem; letter-spacing: .14em; text-transform: uppercase;
  color: var(--text-dim); padding: 1rem; text-align: center;
}

/* `.photo` only ever had rules for its EMPTY state. A container that actually
   received an image had none at all, so the <img> fillPhoto appends rendered
   at its NATURAL size — and since `.about` is a grid whose tracks size to
   content, a 1178px upload became the width of the column and pushed the
   whole section past the viewport at every breakpoint, not just one.
   The hero escaped it only because `.hero-photo` is absolutely positioned
   and has its own img rule.

   Latent for the same reason the credit covers were: nothing had ever been
   uploaded, so the filled state was unreachable. It appeared the moment a
   real about photo was set in wp-admin. Scoped to `.about-photo` rather than
   a bare `.photo img` on purpose — the latter would tie on specificity with
   `.hero-photo img` and, coming later in the file, would silently win and
   drop the hero's `object-position: center 38%`. */
.about-photo { aspect-ratio: 4 / 5; overflow: hidden; border: 1px solid var(--border); }
.about-photo img { display: block; width: 100%; height: 100%; object-fit: cover; }
/* The dashed placeholder keeps its own border and sizing, so the filled-state
   border above must not double up on it. */
.about-photo.empty { aspect-ratio: auto; border: 1px dashed rgba(255,255,255,.12); }

/* ── About + narrate panel (Task 6) ───────────────────────── */
.about { display: grid; gap: 2.5rem; }
.about h2 { font-size: clamp(2rem, 8vw, 3.4rem); margin: 0; }
.about-copy p.body { margin: 0 0 1.5rem; }

.hairline { border: 0; border-top: 1px solid var(--border); margin: 1.8rem 0; }
.hairline.short { width: 72px; margin: 1.6rem 0; }

.narrate-genres, .narrate-points { list-style: none; margin: 0; padding: 0; }
.narrate-genres li { display: flex; align-items: center; gap: 1rem; margin-bottom: 1.1rem; }
.narrate-genres .tag { width: 6px; height: 28px; flex: none; background: var(--tag); }
.genre-name { font-size: 1.45rem; color: var(--text-bright); }
.narrate-points li { font-style: italic; color: var(--text-dim); margin-bottom: .7rem; }

@media (min-width: 900px) {
  .about { grid-template-columns: 1fr 1fr; align-items: start; }
  .about h2 { grid-column: 1 / -1; }
}

/* ── Credit tiles (Task 6) ────────────────────────────────── */
.credit-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem; }
@media (max-width: 560px) { .credit-grid { grid-template-columns: 1fr; } }
/* `isolation: isolate` is load-bearing, not tidiness. The cover sits at a
   negative z-index so it paints above the tile's own gradient but below the
   title, author and meta — and a negative z-index escapes to the nearest
   ANCESTOR stacking context. `position: relative` with `z-index: auto` does
   not make one, so without this the cover would slide behind the tile's
   background and vanish entirely. */
.tile { position: relative; isolation: isolate; aspect-ratio: 2/3;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center; text-align: center;
  padding: 1.5rem 1rem; border: 1px solid var(--border); overflow: hidden; }
/* The spec says the cover "replaces the gradient" — it is a backdrop, not a
   replacement for the whole tile. app.js appends the <img> LAST, after the
   title/author/meta, and a positioned descendant paints over non-positioned
   ones regardless of source order, so at z-index: auto the cover buried every
   word on the tile. Only the empty credits array kept that off the live site.
   z-index: -2 puts it under the scrim, which is under the text. */
.tile img { position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; z-index: -2; }
/* Legibility scrim, same lever as .hero-photo::after. Keyed on .has-photo so
   it only exists when a cover does: a gradient-only tile must keep reading as
   the deliberate art direction the screenshots specify, not get quietly
   darkened. A class rather than :has(img) because app.js already sets one on
   this element (.empty on failure), and because a class is observable in
   jsdom — paint order is not, which is how this defect survived 118 tests. */
.tile.has-photo::after {
  content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none;
  /* Darkest in the MIDDLE, which is the opposite of .hero-photo::after and
     deliberate: the hero keeps its centre clear because the image is the
     subject there and the type sits low. On a tile the type is centred, so
     the scrim has to be heaviest exactly where the hero's is lightest.
     Copying the hero's shape here left the title sitting in the one bright
     spot the gradient preserved — legible on dark cover art and not on
     bright, which is the half of the range real book covers occupy. */
  background:
    radial-gradient(ellipse at 50% 46%, rgba(4,4,4,.80), rgba(4,4,4,.52) 74%),
    linear-gradient(180deg, rgba(4,4,4,.38), rgba(4,4,4,.22) 34%, rgba(4,4,4,.54));
}
.spark { position: absolute; top: 1.4rem; color: var(--text-dim); }
.credit-title { font-size: 1.4rem; margin: 0; }
.credit-author { font-style: italic; margin: .2rem 0 .4rem; }
.credit-meta { font-size: .72rem; letter-spacing: .22em; text-transform: uppercase;
  color: var(--text-dim); margin: 0; }
.tile-crimson { background: radial-gradient(ellipse at 50% 38%, var(--tint-crimson-lit), var(--tint-crimson-dim) 72%); }
.tile-navy    { background: radial-gradient(ellipse at 50% 38%, var(--tint-navy-lit),    var(--tint-navy-dim) 72%); }
.tile-amber   { background: radial-gradient(ellipse at 50% 38%, var(--tint-amber-lit),   var(--tint-amber-dim) 72%); }
.tile-forest  { background: radial-gradient(ellipse at 50% 38%, var(--tint-forest-lit),  var(--tint-forest-dim) 72%); }

/* ── Testimonial cards (Task 6) ───────────────────────────── */
.voice-grid { display: grid; gap: 1rem; }
.voice { margin: 0; }
.quote-mark {
  display: block; font-size: 3rem; line-height: 1;
  color: #424347; margin-bottom: 1.5rem;
}
.voice blockquote { margin: 0; }
.voice blockquote p { font-style: italic; font-size: 1.2rem; margin: 0; }
.voice-name { font-size: 1.25rem; margin: 0 0 .2rem; }
.voice-meta { font-style: italic; color: var(--text-dim); font-size: .95rem; margin: 0; }

/* ── Booking form (Task 6) ────────────────────────────────── */
.booking-form { display: grid; gap: 1.6rem; max-width: 620px; margin: 0 auto; }
.field { display: grid; gap: .5rem; }
.field span {
  font-size: .7rem; text-transform: uppercase;
  letter-spacing: .24em; color: var(--text-dim);
}
.field input, .field textarea {
  font-family: inherit; font-size: 1rem; color: var(--text);
  background: transparent; border: 0;
  border-bottom: 1px solid var(--border);
  padding: .5rem 0; border-radius: 0;
}
.field textarea { resize: vertical; }
.field.invalid input, .field.invalid textarea { border-bottom-color: var(--seal-crimson-lit); }
.field-pair { display: grid; gap: 1.6rem; }
@media (min-width: 560px) { .field-pair { grid-template-columns: 1fr 1fr; } }

.btn {
  display: inline-block; cursor: pointer;
  font-family: inherit; font-size: .78rem;
  text-transform: uppercase; letter-spacing: .22em; text-decoration: none;
  color: var(--text-bright); background: transparent;
  border: 1px solid var(--border); padding: .9rem 2rem;
  transition: border-color .25s ease, color .25s ease;
}
.btn:hover { border-color: var(--seal-crimson-lit); }
.btn-ghost { color: var(--text-dim); }
/* The one filled control on the page. Over the backdrop two outline buttons
   read as equal weight and neither says "start here"; the mockup resolves
   that by making the demos button solid. Deliberately pale rather than a
   brand colour — crimson against the cool blue backdrop reads as a warning,
   and the demos are the thing a visitor actually came for. */
.btn-solid {
  background: #e6e6ea; border-color: #e6e6ea; color: #0a0a0c;
}
.btn-solid:hover { background: #fff; border-color: #fff; color: #0a0a0c; }
.btn-block { width: 100%; }

.form-note { font-size: .9rem; margin: 0; min-height: 1.4em; }
.form-note.err { color: var(--seal-crimson-lit); }
.form-note.ok  { color: var(--text-dim); }

.follow { text-align: center; margin-top: 3rem; }
.social-row { display: flex; gap: 1.5rem; justify-content: center; flex-wrap: wrap; }
.social-row a { color: var(--text-dim); text-decoration: none; font-style: italic; }

/* ── Footer (Task 6) ──────────────────────────────────────── */
.footer {
  display: grid; gap: 1.2rem; justify-items: center;
  padding: 4rem 1.25rem 3rem;
  border-top: 1px solid var(--border);
}
.footer-logo { opacity: .5; filter: invert(1); }
.copyright { font-size: .8rem; color: var(--text-dim); }
