/* ══════════════════════════════════════════════════════════════════
   /about — the story, told as a motion comic.

   Reference is Riot's story/release pages: full-viewport panels, one
   beat each, art on layers that move at different speeds, very few
   words, and a palette that CHANGES as you travel so the scroll feels
   like distance rather than length.

   What stands in for key art is the dream canvases, run to full
   development and frozen — kente, adire, the loom's lattice, a dundun
   drum, an ayo board. See capture-dreams.mjs for why they are stills.

   The old /about was 22 sections, 10,013 words and ~33 screens of the
   same card grid. Everything it said about features now lives on the
   five category pages, each of which proves the claim with a shot of
   a real page. What's left here is the part only this page can carry.
   ══════════════════════════════════════════════════════════════════ */

:root {
  --ink: #F4F1EC;
  --ink-2: #A9A49B;
  --ink-3: #6E6961;
  --void: #08070A;
  --display: 'Archivo', 'Arial Narrow', sans-serif;
  --body: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  --mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;

  /* Panel rhythm. Every vertical measure on the page derives from these
     two, so a change to the panel's breathing doesn't need chasing. */
  --gutter: clamp(1.25rem, 5vw, 3.5rem);
  --rail: 132px;
}

* { box-sizing: border-box; }

html { scroll-behavior: smooth; }

body {
  margin: 0;
  background: var(--void);
  color: var(--ink);
  font-family: var(--body);
  font-size: 16px;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

/* ══ The cloths ════════════════════════════════════════════════════
   One per chapter, set with data-cloth on the panel. These are the
   dye palettes, not decoration: indigo is the adire vat against
   undyed cotton, harmattan is the dust season, the loom is Asante
   green and gold. Each panel redefines the same four tokens, so the
   panel's internals never name a colour.
   ════════════════════════════════════════════════════════════════ */
/* Each of these is read off its own plate, not chosen next to it — the
   ground is the plate's darkest field and the spark is the colour it
   actually prints in. That's why the type never looks laid on top.

   --wash and --plate-op are the two knobs that make one formula work
   across plates of wildly different brightness. Adire is pale cream on
   pale blue and swallows white type, so it takes a heavy scrim; kente
   is already dark and needs almost none or it disappears. Setting these
   per cloth is the whole difference between legible and not. */
[data-cloth="dundun"] {      /* dream-17 — a drum head lit from above */
  --ground: #2A1C10;
  --glow: #C2571F;
  --spark: #F0C98A;
  --wash: .46;
  --plate-op: 1;
}
[data-cloth="drum"] {        /* dream-10 — gold dust over drum red */
  --ground: #2A1210;
  --glow: #E63946;
  --spark: #F5C542;
  /* Full strength and the lightest wash on the page. This is the
     sparsest plate — a few hundred specks on black — and it's the one
     carrying the panel about people scattered and finding each other,
     so it has to be seen rather than sensed. */
  --wash: .36;
  --plate-op: 1;
}
[data-cloth="adire"] {       /* dream-15 — cream resist on grey-blue */
  --ground: #141A28;
  --glow: #5B6B8C;
  --spark: #EAE6DA;
  --wash: .7;
  --plate-op: .5;
}
[data-cloth="loom"] {        /* dream-09 — teal lattice on black */
  --ground: #0E1F16;
  --glow: #1E7A3C;
  --spark: #6FC48C;
  --wash: .42;
  --plate-op: .95;
}
[data-cloth="stripweave"] {  /* dream-12 — bands turning around a centre */
  --ground: #050D22;
  --glow: #1E7A3C;
  --spark: #F5C542;
  --wash: .6;
  --plate-op: .7;
}
[data-cloth="wax"] {         /* dream-18 — wax print, crackle and all */
  --ground: #2A0A0F;
  --glow: #C4213A;
  --spark: #F0E4CC;
  --wash: .6;
  --plate-op: .66;
}
[data-cloth="bogolan"] {     /* dream-19 — mud cloth that stops partway */
  --ground: #241A11;
  --glow: #8A5A33;
  --spark: #E4D8BC;
  --wash: .58;
  --plate-op: .7;
  /* High-contrast checkerboard directly behind body copy, so a little
     more shade than the default — but only a little. At 94 the pool
     swallowed the cloth entirely, which defeats the point of having it. */
  --shade: 88;
  /* The weaving stops around two-thirds down and the rest is bare
     cloth — a piece still on the loom, which is exactly the chapter it
     carries. Hold the crop to the woven part all the same: as a
     background the bare end just bleaches the type. */
  --plate-pos: center top;
}
[data-cloth="kente"] {       /* dream-14 — red, gold and black strips */
  --ground: #1C0906;
  --glow: #E63946;
  --spark: #F5C542;
  --wash: .58;
  --plate-op: .74;
}

/* ══ Chapter rail ══════════════════════════════════════════════════
   A story has an order, so the marks are numbered — the number is the
   sequence, not an ornament borrowed from one. Fixed, so you always
   know how far in you are.
   ════════════════════════════════════════════════════════════════ */
.rail {
  position: fixed;
  top: 50%;
  left: clamp(.9rem, 2.4vw, 1.9rem);
  transform: translateY(-50%);
  z-index: 40;
  display: flex;
  flex-direction: column;
  gap: .1rem;
  font-family: var(--mono);
  font-size: .58rem;
  letter-spacing: .1em;
}
.rail a {
  display: flex;
  align-items: center;
  gap: .6rem;
  padding: .34rem 0;
  color: var(--ink-3);
  text-decoration: none;
  transition: color .3s ease;
}
.rail a::before {
  content: "";
  width: 15px;
  height: 1px;
  background: currentColor;
  transition: width .35s cubic-bezier(.22,1,.36,1), background .3s ease;
}
.rail a .nm {
  opacity: 0;
  transform: translateX(-5px);
  transition: opacity .3s ease, transform .35s cubic-bezier(.22,1,.36,1);
  text-transform: uppercase;
  white-space: nowrap;
}
.rail a:hover,
.rail a[aria-current="true"] { color: var(--ink); }
.rail a:hover::before,
.rail a[aria-current="true"]::before { width: 30px; }
.rail a:hover .nm,
.rail a[aria-current="true"] .nm { opacity: 1; transform: none; }
.rail a:focus-visible { outline: 2px solid var(--ink); outline-offset: 4px; }

/* ══ Panel ═════════════════════════════════════════════════════════
   Each is its own stacking context so the art can sit at z-index 0
   without punching through the panel above it.
   ════════════════════════════════════════════════════════════════ */
.panel {
  position: relative;
  isolation: isolate;
  min-height: 100svh;
  display: grid;
  /* minmax(0,1fr), not the default auto: an auto track is sized by its
     content's min-content, and a nested grid can claim far more of that
     than the viewport has. A card grid here once made the panel lay out
     521px wide on a 390px phone, and overflow:hidden silently ate the
     difference. `safe` is the same defence vertically — a centred grid
     that outgrows its box overflows at BOTH ends, so the clip takes the
     top too. Both stay whether or not anything currently needs them. */
  grid-template-columns: minmax(0, 1fr);
  align-content: safe center;
  padding: clamp(5rem, 12vh, 8rem) var(--gutter) clamp(4rem, 10vh, 7rem);
  overflow: hidden;
}
@media (min-width: 900px) {
  .panel { padding-left: var(--rail); }
}

/* ══ Layer 0 — the plate ═══════════════════════════════════════════
   A still, not a live canvas. The dreams are slow simulations built
   for a page you sit on; at the three seconds a visitor spends inside
   a panel, half of them are still a black rectangle. So they are run
   to full development once by capture-dreams.mjs and shipped as images
   — instant, identical every time, and seven of them cost a phone
   nothing where seven sandboxed iframes would have cooked it.

   Three kinds, because the dreams are three kinds:

     field  — cloth that fills the frame (the kente, the adire). It can
              sit behind the type as a ground and wants covering.
     ink    — line art on black (the loom's lattice, the drifting
              specks). Screened over a flat cloth ground it reads like
              ink printed on a colour plate, which is the comic move.
              Laid behind type at low opacity it reads like nothing.
     object — one thing photographed against nothing: the drum, the ayo
              board. Covering these crops them into abstraction, so they
              are contained and pushed to the side the type isn't on.
   ════════════════════════════════════════════════════════════════ */
/* No will-change here. It was on both .plate and .plate img, which
   permanently promoted sixteen layers of ~1200x800 image and never
   released them — will-change is a hint you hand back, not a setting.
   The scroll transform and the keyframes promote what they need for as
   long as they need it. */
.plate {
  position: absolute;
  inset: -8% -4%;
  z-index: 0;
  pointer-events: none;
}
.plate img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Some cloths are only woven across part of their length, so which
     part survives the crop is a per-cloth decision, not a default. */
  object-position: var(--plate-pos, center);
  opacity: var(--plate-op, .7);
}
[data-plate="ink"] .plate img { mix-blend-mode: screen; }
/* An ink panel needs a cloth under the ink; a field panel already is one. */
[data-plate="ink"] { background: var(--ground); }

[data-plate="object"] { background: var(--void); }
[data-plate="object"] .plate {
  /* Off to the right, clear of the words, and whole rather than cropped. */
  inset: 4% -2% 4% 34%;
  /* The piece was photographed on its own black, which is not the same
     black as the panel — without this the plate reads as a pasted
     rectangle with a visible edge. Feather it and the object floats. */
  -webkit-mask-image: radial-gradient(70% 74% at 58% 50%, #000 42%, transparent 80%);
  mask-image: radial-gradient(70% 74% at 58% 50%, #000 42%, transparent 80%);
}
[data-plate="object"] .plate img {
  object-fit: contain;
  object-position: right center;
}
/* A split beat already has something on the right. Two things competing
   for that side reads as a collision, so the object drops back to being
   a watermark under the whole panel. */
[data-plate="object"]:has(.beat--split) .plate { inset: 0; }
[data-plate="object"]:has(.beat--split) .plate img {
  object-position: center;
  opacity: calc(var(--plate-op, .7) * .32);
}
@media (max-width: 899px) {
  /* No room to sit beside anything — put it behind, faintly. */
  [data-plate="object"] .plate { inset: 0; }
  [data-plate="object"] .plate img { object-position: center; opacity: .3; }
}

/* Layer 1 — the ground. Darkest under the words and thinning fast to
   the right, so the type has something to sit on while the dream stays
   the picture. An even wash over the whole panel just smothers it. */
.panel::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  --w: calc(var(--wash, .6) * 100%);
  background:
    linear-gradient(100deg,
      color-mix(in srgb, var(--ground) var(--w), transparent) 0%,
      color-mix(in srgb, var(--ground) calc(var(--w) * .68), transparent) 34%,
      transparent 72%),
    radial-gradient(44% 38% at 86% 14%, color-mix(in srgb, var(--glow) 26%, transparent), transparent 74%),
    linear-gradient(180deg, var(--void) 0%, transparent 20%, transparent 80%, var(--void) 100%);
}
/* Ink already sits on a flat cloth, so the wash only has to protect the
   words — laying the full ground over it a second time buries the ink. */
[data-plate="ink"]::before {
  background:
    linear-gradient(100deg,
      color-mix(in srgb, var(--void) calc(var(--w) * 1.3), transparent) 0%,
      color-mix(in srgb, var(--void) calc(var(--w) * .6), transparent) 36%,
      transparent 68%),
    linear-gradient(180deg, var(--void) 0%, transparent 24%, transparent 78%, var(--void) 100%);
}

/* Corner marks — the comic-panel cue, drawn rather than illustrated.
   Eight bars rather than a masked border: mask-composite still differs
   enough across engines that a bordered box leaks whole rules. */
.panel::after {
  content: "";
  position: absolute;
  z-index: 2;
  inset: clamp(.9rem, 2.4vh, 1.6rem) clamp(.9rem, 2.4vw, 1.6rem);
  pointer-events: none;
  --mk: color-mix(in srgb, var(--spark) 20%, transparent);
  --mk-len: 54px;
  background-image:
    linear-gradient(var(--mk), var(--mk)), linear-gradient(var(--mk), var(--mk)),
    linear-gradient(var(--mk), var(--mk)), linear-gradient(var(--mk), var(--mk)),
    linear-gradient(var(--mk), var(--mk)), linear-gradient(var(--mk), var(--mk)),
    linear-gradient(var(--mk), var(--mk)), linear-gradient(var(--mk), var(--mk));
  background-repeat: no-repeat;
  background-size:
    var(--mk-len) 1px, 1px var(--mk-len),
    var(--mk-len) 1px, 1px var(--mk-len),
    var(--mk-len) 1px, 1px var(--mk-len),
    var(--mk-len) 1px, 1px var(--mk-len);
  background-position:
    left top, left top,
    right top, right top,
    left bottom, left bottom,
    right bottom, right bottom;
}

.beat {
  position: relative;
  z-index: 3;
  max-width: 1080px;
  margin: 0 auto;
  width: 100%;
  display: grid;
  /* Same reason as .panel: an auto track hands its width to whatever
     child claims the most min-content, and the clip does the rest. */
  grid-template-columns: minmax(0, 1fr);
  gap: clamp(1.5rem, 4vw, 3.25rem);
  align-items: center;
}
.beat--split { grid-template-columns: minmax(0, 1fr) minmax(0, 340px); }
@media (max-width: 899px) { .beat--split { grid-template-columns: 1fr; } }

/* align-items:flex-start sizes each child to its own content, so without
   the cap a 46ch paragraph and a 158px-track grid both run off a phone. */
.words {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 1.15rem;
  align-items: flex-start;
  width: 100%;
}
.words > * { max-width: 100%; }

/* A pool of shade under the words only.
   The headline is 4rem of white Archivo and survives anything, but the
   body paragraph does not — it was disappearing into the bogolan
   checkerboard and the kente. Darkening the whole panel to rescue it
   costs the cloth, which is the one thing worth looking at, so the
   shade is local and soft-edged: light falling off, not a card. */
.words::before {
  content: "";
  position: absolute;
  inset: -9% -16% -11% -11%;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(70% 64% at 38% 50%,
    color-mix(in srgb, var(--void) calc(var(--shade, 84) * 1%), transparent) 34%,
    transparent 84%);
}

.mark {
  display: flex;
  align-items: baseline;
  gap: .75rem;
  font-family: var(--mono);
  font-size: .63rem;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--spark);
}
.mark .no {
  font-size: .95rem;
  letter-spacing: .04em;
  color: color-mix(in srgb, var(--spark) 55%, transparent);
  font-variant-numeric: tabular-nums;
}

/* Archivo carries a width axis; opening it up is what makes the
   display voice poster rather than dashboard. */
.say {
  font-family: var(--display);
  font-variation-settings: 'wdth' 112;
  font-weight: 800;
  font-size: clamp(2rem, 5.6vw, 4.15rem);
  line-height: .99;
  letter-spacing: -.028em;
  text-wrap: balance;
  max-width: 17ch;
  margin: 0;
}
.say em {
  font-style: normal;
  color: var(--spark);
}

.tell {
  color: var(--ink-2);
  max-width: 46ch;
  font-size: clamp(1rem, 1.35vw, 1.09rem);
  margin: 0;
}
.tell strong { color: var(--ink); font-weight: 600; }
.tell a { color: var(--spark); text-decoration-thickness: 1px; text-underline-offset: 3px; }
.tell a:hover { color: var(--ink); }

/* The story is signed, because it's one person's and the whole page
   has been saying so. */
.sign {
  margin: .4rem 0 0;
  font-family: var(--mono);
  font-size: .66rem;
  letter-spacing: .17em;
  text-transform: uppercase;
  color: var(--spark);
}

/* ══ Artifacts ═════════════════════════════════════════════════════
   What a panel holds up. Never an illustration of a thing — the thing.

   There was a huge "5% + 49¢" here. It's gone: what a thing costs is
   a question you ask on the page that sells it, and all five of those
   answer it already. An about page that quotes you a price is a
   landing page wearing a hat.
   ════════════════════════════════════════════════════════════════ */

/* The three real pages, held up together. */
.proof {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(.7rem, 1.8vw, 1.3rem);
  width: 100%;
}
@media (max-width: 640px) { .proof { grid-template-columns: 1fr; max-width: 260px; } }
.proof a {
  display: block;
  text-decoration: none;
  color: inherit;
  transition: transform .35s cubic-bezier(.22,1,.36,1);
}
.proof a:hover { transform: translateY(-6px); }
.proof .fr {
  display: block;
  border-radius: 18px;
  padding: 4px;
  background: linear-gradient(160deg, #2A2A33, #0D0D11 62%);
  border: 1px solid rgba(255,255,255,.13);
  box-shadow: 0 22px 44px rgba(0,0,0,.6);
}
.proof .cr {
  display: block;
  overflow: hidden;
  border-radius: 14px;
  aspect-ratio: 39 / 62;
  -webkit-mask-image: linear-gradient(180deg, #000 84%, transparent 100%);
  mask-image: linear-gradient(180deg, #000 84%, transparent 100%);
}
.proof img { display: block; width: 100%; height: auto; }
.proof .lb {
  display: block;
  margin-top: .6rem;
  font-family: var(--mono);
  font-size: .6rem;
  letter-spacing: .1em;
  color: var(--ink-3);
}
.proof a:hover .lb { color: var(--ink-2); }

/* A short ledger — used for the honest panel and the by-the-numbers. */
.ledger {
  display: flex;
  flex-direction: column;
  width: 100%;
  /* Its own scrim — a bare list on woven cloth is a list nobody reads.
     This used to blur what was behind it, which is the single most
     expensive thing on the page: backdrop-filter over a background that
     is ALSO animating forces the blur to be recomputed every frame, and
     it sits directly on top of a moving plate. A more opaque flat panel
     reads the same and costs nothing. */
  padding: .3rem 1.05rem;
  border-radius: 12px;
  background: color-mix(in srgb, var(--void) 90%, transparent);
}
.ledger div {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 1rem;
  align-items: baseline;
  padding: .8rem 0;
  border-bottom: 1px solid rgba(255,255,255,.1);
}
.ledger div:last-child { border-bottom: 0; }
.ledger dt { color: var(--ink); font-weight: 500; margin: 0; }
.ledger dd {
  margin: 0;
  font-family: var(--mono);
  font-size: .72rem;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ink-2);
  text-align: right;
}


/* ══ Motion ════════════════════════════════════════════════════════
   Two moves only. Layers translate at different rates while the panel
   crosses the viewport (set as --p, -1 → 1, by the page script), and
   the words lift in once when the panel arrives. Restraint here is the
   difference between cinematic and carousel.
   ════════════════════════════════════════════════════════════════ */
.plate { transform: translate3d(0, calc(var(--p, 0) * 46px), 0) scale(1.06); }
.beat  { transform: translate3d(0, calc(var(--p, 0) * -26px), 0); }

/* The plates are stills, and a still hanging dead behind the type reads
   as a background rather than as art. So the image moves inside its own
   frame.

   The first version of this ran 46s across a 4% zoom — under a tenth of
   a percent per second. Technically animating, and completely invisible.
   Aiming for "slow enough you never catch it moving" overshot into
   "never appears to move at all." These numbers are set to be SEEN: a
   cloth crosses its own frame in under half a minute, which is still
   calm next to anything else on a screen. */
@keyframes drift {
  from { transform: scale(1.02) translate3d(-2.6%, -1.7%, 0); }
  to   { transform: scale(1.16) translate3d(2.6%, 1.7%, 0); }
}
/* Paused unless the panel is near the viewport. Eight large plates all
   animating at once — plus a blend mode on two of them and a mask on
   two more — is a lot of continuous compositing for a page where seven
   of the eight are off screen. `near` is set by the page's observer. */
.plate img {
  animation: drift 26s ease-in-out infinite alternate;
  animation-play-state: paused;
}
.panel.near .plate img { animation-play-state: running; }
/* The objects are a drum and a board — things, not cloth. Zooming them
   looks like a camera fault, so they only sway. */
/* Longhand, deliberately. The `animation` shorthand resets every
   sub-property it doesn't mention, including animation-play-state —
   which quietly un-paused the object plates and left them running off
   screen no matter what the observer said. */
[data-plate="object"] .plate img {
  animation-name: sway;
  animation-duration: 19s;
}

/* Strip weave is concentric rings around a fixed centre, which makes it
   very nearly self-similar under a zoom: it measured as the FASTEST
   moving plate on the page and still read as a photograph. A pattern
   with a centre has to have its centre moved. So this one holds its
   scale and pans instead — the same budget spent where the eye can see
   it. (Scale stays high enough that ±6% never exposes an edge.) */
@keyframes sweep {
  from { transform: scale(1.18) translate3d(-6%, -3.2%, 0); }
  to   { transform: scale(1.18) translate3d(6%, 3.2%, 0); }
}
[data-cloth="stripweave"] .plate img { animation-name: sweep; }
@keyframes sway {
  from { transform: translate3d(-3.6%, -2.4%, 0); }
  to   { transform: translate3d(3.6%, 2.4%, 0); }
}

/* Every plate starts its cycle at page load, so without this all eight
   zoom and pan in lockstep — measured identical to the tenth of a pixel.
   Negative delays start each one already partway through, so a panel is
   never doing the same thing as the one above it. Prime-ish offsets so
   the pattern doesn't resynchronise. */
.panel:nth-child(2) .plate img { animation-delay: -3.1s; }
.panel:nth-child(3) .plate img { animation-delay: -7.3s; }
.panel:nth-child(4) .plate img { animation-delay: -11.9s; }
.panel:nth-child(5) .plate img { animation-delay: -5.2s; }
.panel:nth-child(6) .plate img { animation-delay: -16.4s; }
.panel:nth-child(7) .plate img { animation-delay: -9.7s; }
.panel:nth-child(8) .plate img { animation-delay: -20.3s; }

.words > *,
.beat--split > .art > * {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity .75s ease, transform .85s cubic-bezier(.22,1,.36,1);
}
.panel.lit .words > *,
.panel.lit .beat--split > .art > * { opacity: 1; transform: none; }
/* Stagger, so the beat reads in the order it was written. */
.panel.lit .words > *:nth-child(2) { transition-delay: .09s; }
.panel.lit .words > *:nth-child(3) { transition-delay: .18s; }
.panel.lit .words > *:nth-child(4) { transition-delay: .27s; }
.panel.lit .beat--split > .art > * { transition-delay: .2s; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .plate, .beat { transform: none; }
  .plate img { animation: none; }
  .words > *, .beat--split > .art > * { opacity: 1; transform: none; transition: none; }
  .proof a, .rail a, .rail a::before, .rail a .nm { transition: none; }
}

/* ══ Ends ══════════════════════════════════════════════════════════ */
.topbar {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 1rem var(--gutter);
  font-size: .87rem;
  background: linear-gradient(180deg, rgba(8,7,10,.9), transparent);
}
.topbar a { color: var(--ink-2); text-decoration: none; font-weight: 600; }
.topbar a:hover { color: var(--ink); }

/* Deliberately whisper-quiet, and in the chapter marks' typeface rather
   than the topbar's. Pills here would read as a second navigation on a
   page whose whole argument is that it's one continuous thing. */
.topbar-out { display: flex; gap: clamp(.9rem, 2.4vw, 1.4rem); }
.topbar-out a {
  font-family: var(--mono);
  font-size: .63rem;
  font-weight: 500;
  letter-spacing: .15em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--ink-3);
}
.topbar-out a:hover { color: var(--ink); }

footer {
  position: relative;
  z-index: 3;
  padding: 2.75rem var(--gutter) 3.25rem;
  text-align: center;
  font-size: .84rem;
  color: var(--ink-3);
  border-top: 1px solid rgba(255,255,255,.08);
}
footer a { color: var(--ink-2); text-decoration: none; }
footer a:hover { color: var(--ink); }

@media (max-width: 899px) {
  .rail { display: none; }
}
