/* =========================================================================
   renatalee.com — editorial / restrained
   -------------------------------------------------------------------------
   Everything is driven by the tokens in :root. Change a colour or a size
   once there and it propagates through the whole page. Dark mode only
   redefines those same tokens — no component rules are duplicated.
   ========================================================================= */

:root {
  /* Theme switching lives here and nowhere else.
     ------------------------------------------------------------------
     `color-scheme: light dark` says this page supports both and has no
     opinion — so with no data-theme attribute the browser picks from the
     OS, and follows it live if the OS flips at sunset. The toggle sets
     data-theme, which pins color-scheme to one value. Every colour below
     is declared ONCE as light-dark(lightValue, darkValue) and resolves
     against whichever is active, so there is no second dark palette to
     keep in sync. */
  color-scheme: light dark;

  /* Colour — warm paper, near-black ink, one clay accent */
  --paper:      light-dark(#FBF9F5, #14120F);
  --paper-2:    light-dark(#F4F0E9, #1D1A16);  /* cards, deeper than the page */
  --ink:        light-dark(#1A1815, #F2EEE7);
  --ink-soft:   light-dark(#4A453D, #C4BDB2);  /* body copy */
  --muted:      light-dark(#857E73, #8A8377);  /* labels, dates, meta */
  --rule:       light-dark(#E3DCD1, #2E2A24);  /* hairlines */
  --accent:     light-dark(#A94F32, #E08C63);  /* clay — links, numerals, CTA */
  --accent-dim: light-dark(#A94F3218, #E08C6320);
  --flag:       light-dark(#B8860B, #D9B45A);  /* [ADD: ...] placeholders */
  --flag-bg:    light-dark(#F0E2B833, #D9B45A1F);

  /* Type — one family, differentiated by weight and tracking rather than
     by a second typeface. Inter tightens as it gets bigger: the larger the
     text, the more negative the tracking, which is what stops big sans
     headings reading loose and generic. */
  --text:    "Inter", "Inter Fallback", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --display: var(--text);
  --w-display: 600;          /* headings */
  --w-lede:    400;          /* large intro paragraphs */

  --tr-xl:  -.045em;         /* the name, ~5rem+ */
  --tr-lg:  -.032em;         /* section titles, ~2-3rem */
  --tr-md:  -.024em;         /* card + timeline titles, ~1.3-1.8rem */
  --tr-sm:  -.016em;         /* ledes at ~1.2-1.9rem */

  /* Rhythm — one spacing scale, used everywhere */
  --s1: 0.5rem;  --s2: 0.875rem; --s3: 1.25rem;
  --s4: 2rem;    --s5: 3.25rem;  --s6: 5rem;   --s7: 8rem;

  --measure: 36rem;   /* readable line length for prose (~65 characters) */
  --page:    82rem;   /* max content width */
  --rail:    15rem;   /* the section-heading column on wide screens */
}

/* An explicit choice from the toggle. Pinning color-scheme is the whole
   mechanism — no colour is restated. */
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"]  { color-scheme: dark;  }

/* Easing the switch between a near-white page and a near-black one, rather
   than cutting between them in a single frame.
   ------------------------------------------------------------------
   There is nothing to animate at the token level: every colour comes from
   light-dark(), which re-resolves the instant color-scheme flips, and a
   custom property holding a light-dark() pair cannot be transitioned. So
   instead the switch briefly puts a transition on the *resolved* colour
   properties everywhere, and the script removes it once it has served.

   The blunt instruments — the universal selector and !important — are the
   point rather than an oversight. Every element has to cross-fade on the
   same curve over the same window or the page tears itself apart in
   pieces, and that means overriding each component's own transition for
   the ~320ms this is applied. Nothing here is in effect at any other
   time. */
:root.is-theming,
:root.is-theming *,
:root.is-theming *::before,
:root.is-theming *::after {
  transition: background-color .32s ease,
              color            .32s ease,
              border-color     .32s ease !important;
}

/* A full-page brightness cross-fade is exactly the kind of motion this
   asks to be spared. The switch stays instant. */
@media (prefers-reduced-motion: reduce) {
  :root.is-theming,
  :root.is-theming *,
  :root.is-theming *::before,
  :root.is-theming *::after { transition: none !important; }
}

/* Fallback for browsers without light-dark(). A custom property will hold
   any value, so an unsupported light-dark() only fails when it is *used* —
   which would leave the page unpainted. This overrides the tokens with
   plain light values so those browsers get a working light-only site. */
@supports not (color: light-dark(#000, #fff)) {
  :root {
    --paper: #FBF9F5;  --paper-2: #F4F0E9;
    --ink: #1A1815;    --ink-soft: #4A453D;
    --muted: #857E73;  --rule: #E3DCD1;
    --accent: #A94F32; --accent-dim: #A94F3218;
    --flag: #B8860B;   --flag-bg: #F0E2B833;
  }
  .theme-toggle { display: none; }
}

/* Inter arrives over the network with font-display: swap, so the page first
   paints in a fallback and then swaps. Unadjusted, that swap visibly reflows
   the name — at clamp(3rem, 10vw, 7rem) the metric difference between Inter
   and the system sans is worth tens of pixels of width and a shifted
   baseline. Overriding the fallback's metrics to match Inter's makes the two
   occupy the same space, so the swap changes the letterforms and nothing
   else. Values are the standard Inter-over-Arial set.

   If none of the local faces resolve, the @font-face simply fails and the
   stack falls through to -apple-system as before. */
@font-face {
  font-family: "Inter Fallback";
  src: local("Arial"), local("Helvetica Neue"), local("Liberation Sans");
  ascent-override:   90.20%;
  descent-override:  22.48%;
  line-gap-override:  0.00%;
  size-adjust:      107.40%;
}

/* ----------------------------------------------------------- base reset */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink-soft);
  font-family: var(--text);
  font-size: 1rem;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4 { color: var(--ink); font-weight: var(--w-display); margin: 0; }
p { margin: 0; }
a { color: var(--accent); text-decoration: none; }
ol, ul { margin: 0; padding: 0; list-style: none; }
strong { color: var(--ink); font-weight: 600; }

.skip {
  position: absolute; left: -9999px;
  background: var(--ink); color: var(--paper); padding: var(--s2) var(--s3);
}
.skip:focus { left: var(--s3); top: var(--s3); z-index: 100; }

/* Off the screen but still in the accessibility tree. clip-path rather than
   display:none or visibility:hidden, both of which would take it out. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 2px; }

/* --------------------------------------------------------------- topbar */
.topbar {
  position: sticky; top: 0; z-index: 50;
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s4);
  padding: var(--s3) var(--s4);
  background: color-mix(in srgb, var(--paper) 88%, transparent);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid transparent;
  transition: border-color .3s ease;
}
/* A soft edge where content meets the floating chrome, rather than a hard
   rule under it. Content dissolves into the bar instead of being cut off by
   a line, which is what the translucency is trying to say in the first
   place. It only appears once there is actually something underneath.

   The 1px border above stays transparent and is kept purely so the bar's
   height never changes — the reduced-transparency and high-contrast blocks
   further down colour it in, and they must not shift the layout by 1px when
   they do. */
.topbar::after {
  content: '';
  position: absolute;
  left: 0; right: 0; top: 100%;
  height: 1.75rem;
  pointer-events: none;
  opacity: 0;
  transition: opacity .3s ease;
  background: linear-gradient(
    to bottom,
    color-mix(in srgb, var(--paper) 80%, transparent),
    transparent);
}
.topbar.is-stuck::after { opacity: 1; }

.topbar__mark {
  font-family: var(--display);
  font-size: 1.05rem;
  font-weight: var(--w-display);
  color: var(--ink);
  letter-spacing: var(--tr-md);
  white-space: nowrap;
}
.topbar__right { display: flex; align-items: center; gap: var(--s4); }
.topbar__nav { display: flex; gap: var(--s4); }
.topbar__nav a {
  position: relative;
  font-size: .78rem;
  /* Text over a blurred surface sits on a background that is constantly
     changing as content scrolls beneath it. Slightly heavier and slightly
     more open than body-weight grey would be, which is what keeps it
     legible without darkening the colour and making the bar shout. */
  font-weight: 500;
  letter-spacing: .11em;
  text-transform: uppercase;
  color: var(--muted);
  transition: color .2s ease;
  /* "Side quests" is the only two-word item; without this it is the one
     label that can break across lines and push the bar to two rows. */
  white-space: nowrap;
}
.topbar__nav a.is-active { color: var(--ink); }

/* The underline is always present and always full width — only its scale
   changes. Animating a transform rather than the width means it wipes in
   from the left without the browser reflowing the nav on every frame, and
   it inherits the link's colour so it picks up the hover shift for free. */
.topbar__nav a::after {
  content: '';
  position: absolute;
  left: 0; right: 0; bottom: -.4em;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: 0 50%;
  transition: transform .35s cubic-bezier(.2, .7, .2, 1);
}
.topbar__nav a.is-active::after { transform: scaleX(1); }

/* Hover is for real pointers only. Tapping a link on a touch screen leaves
   it in :hover until something else is tapped, so the link you just used
   stayed underlined next to the section you then scrolled to — two
   underlines, one of them stale. A phone has no way to un-hover it. */
@media (hover: hover) and (pointer: fine) {
  .topbar__nav a:hover { color: var(--ink); }
  .topbar__nav a:hover::after { transform: scaleX(1); }
}

/* Still marks the current section, it just arrives instead of sweeping. */
@media (prefers-reduced-motion: reduce) {
  .topbar__nav a::after { transition: none; }
}

/* Translucency is a preference, not a given. Someone who has asked for less
   of it gets a solid bar and the hard rule back — the soft edge is only an
   improvement while you can actually see through the bar; over an opaque one
   it is a smudge that separates nothing. */
@media (prefers-reduced-transparency: reduce) {
  .topbar {
    background: var(--paper);
    backdrop-filter: none;
  }
  .topbar::after { content: none; }
  .topbar.is-stuck { border-bottom-color: var(--rule); }
}

/* Asked for more contrast: a defined border at all times rather than one
   that waits for scroll, and body-weight ink for the nav instead of grey. */
@media (prefers-contrast: more) {
  .topbar {
    background: var(--paper);
    backdrop-filter: none;
    border-bottom-color: var(--rule);
  }
  .topbar::after { content: none; }
  .topbar__nav a { color: var(--ink-soft); }
}

/* Theme toggle. Both icons stack in one grid cell; which one shows is the
   only thing that needs a light/dark selector, since light-dark() handles
   colours but not display. */
.theme-toggle {
  display: grid;
  place-items: center;
  width: 2rem; height: 2rem;
  padding: 0;
  border: 1px solid var(--rule);
  border-radius: 999px;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  flex: none;
  transition: color .2s ease, border-color .2s ease;
}
.theme-toggle:hover { color: var(--ink); border-color: var(--ink); }
.theme-toggle svg { grid-area: 1 / 1; width: 15px; height: 15px; }

.theme-toggle .i-sun { display: none; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .theme-toggle .i-moon { display: none; }
  :root:not([data-theme="light"]) .theme-toggle .i-sun  { display: block; }
}
:root[data-theme="dark"] .theme-toggle .i-moon { display: none; }
:root[data-theme="dark"] .theme-toggle .i-sun  { display: block; }

@media (max-width: 640px) {
  .topbar { padding: var(--s2) var(--s3); }
  .topbar__nav { gap: var(--s3); }
  .topbar__nav a { font-size: .68rem; letter-spacing: .06em; }
}

/* Below ~560px the wordmark and the nav items can't share a line without
   the wordmark wrapping. The <h1> is directly underneath, so drop the
   wordmark and give the nav the full width instead. Home carries the
   wordmark's job from here down — it is the only way back to the top once
   the wordmark is gone. */
@media (max-width: 560px) {
  .topbar__mark { display: none; }
  .topbar__right { width: 100%; gap: var(--s2); }
  .topbar__nav { flex: 1; justify-content: space-between; gap: var(--s1); }
  .topbar__nav a { font-size: .64rem; letter-spacing: .04em; }
  .theme-toggle { width: 1.75rem; height: 1.75rem; }
  .theme-toggle svg { width: 13px; height: 13px; }
}

/* Five nav items fit down to about 340px. Narrower than that — an iPhone SE
   at 320 — and the row runs out of space, so trade a little more tracking
   and size rather than let it wrap or overflow. */
@media (max-width: 370px) {
  .topbar__nav a { font-size: .58rem; letter-spacing: .02em; }
}

/* ----------------------------------------------------------------- hero */
.hero {
  max-width: var(--page);
  margin: 0 auto;
  padding: var(--s7) var(--s4) var(--s6);

  /* Home points at this section, and without this the browser aligns the
     hero's top edge with the top of the viewport — which puts it *behind*
     the sticky header, clipping the eyebrow. On mobile the hero's own top
     padding (--s5, 52px) is shorter than the header (57px), so there is no
     spare room to absorb it.

     Any value larger than the header makes the target position negative,
     and scrolling clamps at zero — so Home lands on the actual top of the
     page. That holds only because the hero is the first thing in <main>;
     anywhere else this would simply offset by 6rem. */
  scroll-margin-top: 6rem;
}
@media (max-width: 640px) { .hero { padding: var(--s5) var(--s3) var(--s5); } }

.eyebrow {
  font-size: .74rem;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: var(--s4);
}
@media (max-width: 560px) {
  .eyebrow { font-size: .66rem; letter-spacing: .12em; }
}

/* The caret exists only while JS is actually driving the text — .is-typing
   is added by the script. A blinking bar on a line that never changes would
   be claiming something the page isn't doing.

   It also does the job of holding the line open: mid-erase the text is empty,
   and without an inline box of its own the paragraph would collapse and shunt
   the whole hero up a line on every cycle. */
.eyebrow.is-typing .eyebrow__text::after {
  content: '';
  display: inline-block;
  width: 1px;
  height: 1em;
  margin-left: .18em;
  vertical-align: -.12em;
  background: var(--accent);
  animation: caret-blink 1.1s steps(1, end) infinite;
}
@keyframes caret-blink {
  0%, 50%      { opacity: 1; }
  50.01%, 100% { opacity: 0; }
}

.hero h1 {
  font-family: var(--display);
  /* fluid: 3.5rem at 360px up to 8rem at 1200px */
  font-size: clamp(3rem, 10vw, 7rem);
  line-height: .94;
  font-weight: var(--w-display);
  letter-spacing: var(--tr-xl);
  margin-bottom: var(--s4);
}

.hero__lede {
  font-family: var(--display);
  font-size: clamp(1.22rem, 2.2vw, 1.6rem);
  line-height: 1.5;
  font-weight: var(--w-lede);
  letter-spacing: var(--tr-sm);
  color: var(--ink);
  max-width: 44rem;
}
.hero__lede--second {
  font-size: 1.02rem;
  letter-spacing: 0;
  line-height: 1.7;
  color: var(--ink-soft);
  margin-top: var(--s3);
  max-width: var(--measure);
}

.hero__actions {
  display: flex; flex-wrap: wrap; gap: var(--s2);
  margin-top: var(--s5);
}

.btn {
  display: inline-flex; align-items: center; gap: .5em;
  padding: .7em 1.2em;
  border: 1px solid var(--rule);
  border-radius: 2px;
  font-size: .88rem;
  color: var(--ink);
  background: transparent;
  transition: border-color .2s ease, background .2s ease, transform .2s ease;
}
.btn:hover { border-color: var(--ink); transform: translateY(-1px); }
.btn--primary {
  background: var(--ink); color: var(--paper); border-color: var(--ink);
}
.btn--primary:hover { background: var(--accent); border-color: var(--accent); }

/* ------------------------------------------------------------- sections */
.section {
  max-width: var(--page);
  margin: 0 auto;
  padding: var(--s6) var(--s4);
  border-top: 1px solid var(--rule);

  /* A nav jump aligns the section's top edge with the top of the viewport,
     which is *behind* the sticky header. On mobile the section's own top
     padding (--s5, 52px) is shorter than the header (~57px), so the title
     itself landed under the bar; on desktop the section's top hairline did.
     Clearing the header outright is the whole fix. Matches the hero. */
  scroll-margin-top: 6rem;
}
@media (max-width: 640px) { .section { padding: var(--s5) var(--s3); } }

.section__head {
  display: flex; align-items: baseline; gap: var(--s3);
  margin-bottom: var(--s5);
}
.section__num {
  font-size: .74rem;
  letter-spacing: .16em;
  color: var(--accent);
  padding-top: .4em;
}
.section__title {
  font-family: var(--display);
  font-size: clamp(1.7rem, 3.6vw, 2.5rem);
  line-height: 1.12;
  letter-spacing: var(--tr-lg);
}
.section__note {
  max-width: var(--measure);
  font-size: .9rem;
  color: var(--muted);
  border-left: 2px solid var(--rule);
  padding-left: var(--s3);
  margin-bottom: var(--s5);
}

/* ------------------------------------------------- wide-screen rail layout
   Below this width the heading sits above its content in normal flow. Above
   it, the section splits into a heading rail and a body column. This is what
   uses the horizontal space on a large display — the alternative, stretching
   paragraphs across the full width, would push lines past 100 characters and
   make them harder to read, not easier.

   The heading is sticky inside its grid area. Because the section is exactly
   two grid children in a single row, that area is the full height of the
   section, so the heading stays alongside its content the whole way down. */
@media (min-width: 62rem) {
  .section {
    display: grid;
    grid-template-columns: var(--rail) minmax(0, 1fr);
    column-gap: var(--s5);
    align-items: start;
  }
  .section__head {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--s1);
    margin-bottom: 0;
    position: sticky;
    top: 5.5rem;
  }
  .section__num { padding-top: 0; }
  .section__title { font-size: 2.1rem; }
}

/* On a very large display, scale the type up rather than the line length.
   The measure grows in rem, the font grows with it, so characters per line
   stay put while the block occupies more of the screen. */
@media (min-width: 100rem) {
  :root { --measure: 40rem; --rail: 17rem; }
  .prose p, .card__body dd { font-size: 1.08rem; }
  .section__title { font-size: 2.4rem; }
}

/* ---------------------------------------------------------------- prose */
.prose { max-width: var(--measure); display: grid; gap: var(--s3); }
.prose p { font-size: 1.03rem; }

/* ----------------------------------------------------------- work cards */
.grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(21rem, 1fr));
  gap: var(--s3);
}
@media (max-width: 640px) { .grid { grid-template-columns: 1fr; } }

.card {
  display: flex; flex-direction: column;
  background: var(--paper-2);
  border: 1px solid var(--rule);
  border-radius: 3px;
  padding: var(--s4);
  transition: border-color .25s ease;
}
.card:hover { border-color: color-mix(in srgb, var(--accent) 45%, var(--rule)); }

.card__meta {
  display: flex; align-items: center; gap: var(--s2);
  margin-bottom: var(--s3);
}
.card__idx {
  font-family: var(--display);
  font-size: 1.15rem;
  font-weight: var(--w-display);
  line-height: 1;
  letter-spacing: var(--tr-md);
  font-variant-numeric: tabular-nums;
  color: var(--accent);
}
.card__kind {
  font-size: .7rem; letter-spacing: .13em; text-transform: uppercase;
  color: var(--muted);
}
.card__title {
  font-family: var(--display);
  font-size: 1.45rem;
  line-height: 1.2;
  letter-spacing: var(--tr-md);
}
.card__role {
  font-size: .85rem;
  color: var(--muted);
  margin-top: .35em;
  margin-bottom: var(--s3);
}

/* dt/dd give each card the same three-beat rhythm: problem → did → outcome */
.card__body { margin: 0; display: grid; gap: var(--s2); }
.card__body dt {
  font-size: .68rem; letter-spacing: .16em; text-transform: uppercase;
  color: var(--muted);
}
.card__body dd { margin: 0 0 var(--s2); font-size: .95rem; }
.card__body dd:last-child { margin-bottom: 0; }

.tags {
  display: flex; flex-wrap: wrap; gap: .4rem;
  margin-top: var(--s3);
  padding-top: var(--s3);
  border-top: 1px solid var(--rule);
}
.tags li {
  font-size: .72rem;
  letter-spacing: .04em;
  color: var(--muted);
  border: 1px solid var(--rule);
  border-radius: 999px;
  padding: .25em .7em;
}

.card__link {
  margin-top: var(--s3);
  font-size: .88rem;
  align-self: flex-start;
  border-bottom: 1px solid var(--accent-dim);
  transition: border-color .2s ease;
}
.card__link:hover { border-bottom-color: var(--accent); }

/* placeholder markers — deliberately loud so they can't ship by accident */
.todo {
  display: inline;
  background: var(--flag-bg);
  color: var(--flag);
  border-bottom: 1px dashed var(--flag);
  font-size: .85em;
  padding: .1em .25em;
}

/* ------------------------------------------------------------- timeline */
.timeline { display: grid; gap: 0; }
.timeline__item {
  display: grid;
  grid-template-columns: 12rem 1fr;
  gap: var(--s4);
  padding: var(--s3) 0;
  border-bottom: 1px solid var(--rule);
}
.timeline__item:first-child { border-top: 1px solid var(--rule); }
@media (max-width: 640px) {
  .timeline__item { grid-template-columns: 1fr; gap: var(--s1); }
}

/* Rows with no date to show. Without this they keep the 12rem date column
   and sit indented against an empty gutter. */
.timeline--undated .timeline__item { grid-template-columns: 1fr; }

.timeline__when {
  font-size: .78rem;
  letter-spacing: .06em;
  color: var(--muted);
  padding-top: .35em;
}
.timeline__what h3 {
  font-family: var(--display);
  font-size: 1.12rem;
  line-height: 1.3;
  letter-spacing: var(--tr-md);
}
.timeline__what h3 span {
  color: var(--muted); font-size: .84rem;
  font-weight: 400; letter-spacing: 0;
}
.timeline__what p { font-size: .93rem; margin-top: .25em; }
/* A card link reused inside a timeline row. In a card it is a flex child;
   here it is inline, so it needs a box before its top margin means anything. */
.timeline__what .card__link { display: inline-block; margin-top: var(--s2); }
/* Two links on one row. Flex rather than inline-block so the gap is explicit
   and they stack instead of colliding when the row gets narrow. */
.timeline__links { display: flex; flex-wrap: wrap; gap: var(--s3); margin-top: var(--s2); }
.timeline__links .card__link { margin-top: 0; }

/* A labelled sub-block inside a section — same visual weight as the
   credential labels, so groups within a section read as one system. */
.subhead {
  font-size: .7rem;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--muted);
  font-weight: 500;
  margin-top: var(--s5);
  margin-bottom: var(--s3);
}

/* ---------------------------------------------------------- credentials */
.creds {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: var(--s4);
  margin-top: var(--s5);
  padding-top: var(--s4);
  border-top: 1px solid var(--rule);
}
.creds__col h4 {
  font-size: .7rem; letter-spacing: .16em; text-transform: uppercase;
  color: var(--muted); margin-bottom: var(--s1);
}
.creds__col p { font-size: .93rem; color: var(--ink); }

/* -------------------------------------------------------------- contact */
.section--contact { padding-bottom: var(--s6); }
.contact__list {
  display: grid;
  gap: 0;
  max-width: 40rem;
}
.contact__list li {
  display: grid;
  grid-template-columns: 7rem 1fr;
  gap: var(--s3);
  padding: var(--s2) 0;
  border-bottom: 1px solid var(--rule);
  font-size: .98rem;
}
.contact__list li:first-child { border-top: 1px solid var(--rule); }
.contact__list span {
  font-size: .7rem; letter-spacing: .16em; text-transform: uppercase;
  color: var(--muted); padding-top: .5em;
}

/* --------------------------------------------------------------- footer */
.footer {
  max-width: var(--page);
  margin: 0 auto;
  padding: var(--s4);
  border-top: 1px solid var(--rule);
  font-size: .8rem;
  color: var(--muted);
  text-align: center;
}

/* ------------------------------------------------------- reveal on scroll
   JS adds .is-in when the element enters the viewport. If JS never runs,
   or motion is reduced, everything is visible by default via these rules. */
.reveal { opacity: 1; }
@media (prefers-reduced-motion: no-preference) {
  html.js .reveal {
    opacity: 0;
    transform: translateY(12px);
    transition: opacity .6s ease, transform .6s ease;
  }
  html.js .reveal.is-in { opacity: 1; transform: none; }
}

/* --------------------------------------------------------- press feedback
   The first rule of a fluid interface: respond on pointer-down, not on
   release — waiting for touch-up to acknowledge a press "feels dead".

   This matters more than it looks here. Hover is deliberately limited to
   real pointers (see the nav rules above), so before this a tap on a phone
   produced no acknowledgement whatsoever until the page began to move.

   Deliberately placed last in the file. These selectors tie with the
   :hover rules they need to beat — .btn:active and .btn:hover are both one
   class and one pseudo-class — so source order is what decides which wins
   while a pointer is both hovering and pressing.

   Two shapes of press. Bordered controls dip in scale; text links, where a
   scale would visibly shift the words, dip in opacity. Both are fast going
   in and settle back out through each element's existing transition. */
.btn:active,
.theme-toggle:active {
  transform: scale(.97);
  transition: transform 100ms ease-out;
}

.topbar__mark:active,
.topbar__nav a:active,
.card__link:active,
.contact__list a:active {
  opacity: .55;
  transition: opacity 80ms ease-out;
}

/* Safari paints its own grey tap flash, which would land on top of the
   states above at a different size and a different moment. Suppressed only
   because something better replaces it — never removed leaving nothing. */
.btn, .theme-toggle, .topbar__mark, .topbar__nav a,
.card__link, .contact__list a, .skip {
  -webkit-tap-highlight-color: transparent;
}

/* Scale is motion; opacity is not. Reduced motion should still get an
   acknowledgement — it just shouldn't move. */
@media (prefers-reduced-motion: reduce) {
  .btn:active,
  .theme-toggle:active { transform: none; opacity: .7; }
}

@media print {
  .topbar { position: static; }
  /* .reveal hides with opacity, not display — resetting display did nothing,
     so anything not yet revealed printed blank. */
  .reveal { opacity: 1 !important; transform: none !important; }
  body { background: #fff; color: #000; }
}
