/* captable.fund
   Monochrome, paper-first. One accent-free palette; emphasis comes from
   weight, scale and hairlines rather than colour. */

:root {
  --paper:        #FBFAF8;
  --paper-raised: #FFFFFF;
  --paper-sunk:   #F4F2EE;
  --ink:          #15161A;
  --ink-2:        #4A4C54;
  /* --ink-3 carries the small type: labels, years, the disclaimer. It is
     held at >=4.5:1 against paper, paper-sunk AND paper-raised, so the same
     token is safe on every surface. #86888F read 3.2-3.4:1 and failed. */
  --ink-3:        #6C6D73;
  --ink-4:        #B9B8B4;
  --rule:         #E2DFD8;
  --rule-strong:  #CFCBC2;
  --node:         #15161A;
  --node-line:    rgba(21, 22, 26, 0.20);

  /* the one colour on the site — `.fund`. --fund-strong is the deeper value
     used by the small proportion of particles that flare during the burst. */
  --fund:         #2754F5;
  --fund-strong:  #0B2FB0;
  --shadow:       0 1px 2px rgba(20, 20, 25, 0.04), 0 8px 24px rgba(20, 20, 25, 0.05);

  --font-sans: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI",
               "Inter", "Helvetica Neue", Helvetica, Arial, sans-serif;
  --font-mono: ui-monospace, "SF Mono", SFMono-Regular, "JetBrains Mono",
               "Roboto Mono", Menlo, Consolas, monospace;

  /* Display type gets the wider measure; body copy sits just inside it, so
     the paragraphs never look like they are overrunning the sentence above
     them. Large type wraps coarsely — a single word is 80–150px — so the two
     ragged edges can never match exactly, and the body should be the tighter
     of the two. */
  --measure: 660px;
  --measure-body: 600px;
  --pad: clamp(1.25rem, 5vw, 4.5rem);
  --max: 1240px;

  color-scheme: light dark;
}

@media (prefers-color-scheme: dark) {
  :root {
    --paper:        #0D0E10;
    --paper-raised: #141518;
    --paper-sunk:   #101114;
    --ink:          #EDECE8;
    --ink-2:        #A2A29E;
    --ink-3:        #7D7E82;
    --ink-4:        #45464A;
    --rule:         #23252A;
    --rule-strong:  #33363C;
    --node:         #EDECE8;
    --node-line:    rgba(237, 236, 232, 0.20);
    /* the same hue lifted for the dark ground, where #2754F5 goes murky */
    --fund:         #6B87FF;
    --fund-strong:  #AFC0FF;
    --shadow:       0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 24px rgba(0, 0, 0, 0.35);
  }
}

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

/* Held for one beat while the colour scheme swaps — see main.js. Without it
   anything mid-transition on a themed property keeps the outgoing value. */
[data-theme-shift] *,
[data-theme-shift] *::before,
[data-theme-shift] *::after {
  transition: none !important;
}

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* keeps anchored sections clear of the fixed nav */
  scroll-padding-top: 5rem;
}

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

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: 1.0625rem;
  line-height: 1.6;
  letter-spacing: -0.006em;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

a { color: inherit; }

::selection { background: var(--ink); color: var(--paper); }

.shell {
  width: 100%;
  max-width: var(--max);
  margin-inline: auto;
  padding-inline: var(--pad);
}

.label {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-3);
}

/* A label that needs to read as a heading rather than a caption. */
.label--strong { font-weight: 700; }

/* Available to a screen reader, absent from the page. Used for the "opens
   in a new tab" note that used to be crammed into an aria-label — which
   replaced the card's whole accessible name and so silently hid the
   category and description from anyone not looking at the screen. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.skip {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 100;
  padding: 0.75rem 1rem;
  background: var(--ink);
  color: var(--paper);
  font-family: var(--font-mono);
  font-size: 0.75rem;
  text-decoration: none;
  /* Off-canvas by transform rather than by a -9999px offset: the box stays
     inside the page, so it can never widen the scroll area. */
  transform: translateY(-110%);
  transition: transform 0.18s ease;
}
.skip:focus { transform: none; }

:where(a, button):focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 3px;
  border-radius: 2px;
}

/* ---------------------------------------------------------------- nav --- */

.nav {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 40;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 1.15rem var(--pad);
  transition: background 0.3s ease, border-color 0.3s ease, backdrop-filter 0.3s ease;
  border-bottom: 1px solid transparent;
}

.nav[data-stuck='true'] {
  background: color-mix(in srgb, var(--paper) 82%, transparent);
  backdrop-filter: saturate(1.4) blur(14px);
  -webkit-backdrop-filter: saturate(1.4) blur(14px);
  border-bottom-color: var(--rule);
}

.nav__mark {
  font-size: 0.9375rem;
  font-weight: 500;
  letter-spacing: -0.02em;
  text-decoration: none;
  opacity: 0;
  transform: translateY(-2px);
  /* `visibility`, not just opacity: an opacity-0 link is still in the tab
     order, so a keyboard visitor used to land on an invisible target here.
     The delay holds it visible for the length of the fade out. */
  visibility: hidden;
  transition: opacity 0.35s ease, transform 0.35s ease, visibility 0s linear 0.35s;
}

.nav[data-stuck='true'] .nav__mark {
  opacity: 1;
  transform: none;
  visibility: visible;
  transition-delay: 0s;
}

.nav__links {
  display: flex;
  gap: clamp(1rem, 3vw, 2rem);
  margin-left: auto;
}

.nav__link {
  position: relative;
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.08em;
  color: var(--ink-2);
  text-decoration: none;
  padding-block: 0.25rem;
  transition: color 0.2s ease;
}

/* The painted box stays 27px tall so the underline keeps sitting on the
   text; the *touch* target is grown to ~45x40 by an invisible overlay. The
   8px of horizontal growth exactly meets the 16px minimum gap between
   links, so neighbouring targets touch but never overlap. */
.nav__link::before {
  content: '';
  position: absolute;
  inset: -0.55rem -0.5rem;
}

.nav__link::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.3s cubic-bezier(0.2, 0.7, 0.2, 1);
}

.nav__link:hover { color: var(--ink); }
.nav__link:hover::after { transform: scaleX(1); }

/* --------------------------------------------------------------- hero --- */

.hero {
  position: relative;
  min-height: 100svh;
  display: grid;
  grid-template-rows: 1fr auto;
  overflow: clip;
}

.hero__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  /* the visual sits behind the wordmark and never eats a click meant for it */
  z-index: 0;
}

.hero__inner {
  position: relative;
  z-index: 1;
  grid-row: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding-block: 8rem 3rem;
  pointer-events: none;
}

.hero__wordmark {
  position: relative;
  margin: 0;
  font-size: clamp(2.6rem, 9.5vw, 7rem);
  font-weight: 500;
  line-height: 0.94;
  letter-spacing: -0.045em;
  max-width: 14ch;
}

/* `.fund` is handed over to a canvas by wordmark.js. The text stays in the
   DOM — selectable, readable by screen readers, and fully visible if the
   script never runs — but goes transparent once the canvas takes over.
   --wm-ink survives that, so the canvas can still read its own colour. */
.dot { color: var(--fund); }

/* The dot between the two halves of the wordmark. It sits outside the .dot
   span, so wordmark.js leaves it alone — it stays still while `fund` moves. */
.wordmark__sep { color: var(--ink-3); }

.hero__wordmark .dot {
  --wm-ink: var(--fund);
  --wm-hot: var(--fund-strong);
}


.wm-live .hero__wordmark .dot { color: transparent; }

.wm-canvas {
  position: absolute;
  pointer-events: none;
  z-index: 1;
}

/* Reads as a single line at every width down to ~400px: the size is capped
   for large screens and falls back to a share of the viewport on small ones,
   rather than wrapping. */
.hero__tagline {
  margin: clamp(1.25rem, 2.5vw, 1.75rem) 0 0;
  font-size: min(1.1875rem, 3.7vw);
  line-height: 1.45;
  color: var(--ink-2);
  letter-spacing: -0.012em;
  white-space: nowrap;
}

/* Below 400px, holding one line meant shrinking the type to 11.8px. Two
   readable lines beat one unreadable one, and the mobile hero has the room. */
@media (max-width: 400px) {
  .hero__tagline {
    white-space: normal;
    font-size: 0.9375rem;
    max-width: 24ch;
    text-wrap: pretty;
  }
}

.hero__foot {
  position: relative;
  z-index: 1;
  grid-row: 2;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  padding-block: 1.5rem 1.75rem;
  border-top: 1px solid var(--rule);
  pointer-events: none;
}

.hero__hint { pointer-events: none; }

/* "Move to explore" is meaningless without a cursor, so the touch copy is
   swapped in wholesale rather than left to mislead. Both strings ship; only
   one is ever displayed. */
.hero__hint--touch { display: none; }

@media (hover: none), (pointer: coarse) {
  .hero__hint--pointer { display: none; }
  .hero__hint--touch { display: inline; }
}

/* On narrow screens the wordmark moves to the top third so the visual gets
   the lower half to itself instead of running underneath the type. */
@media (max-width: 860px) {
  .hero__inner {
    justify-content: flex-start;
    padding-block: 7.5rem 2rem;
  }
}

/* The hint used to be dropped wholesale below 640px, which removed it from
   precisely the devices that need it: tap-to-select is the one interaction
   here with no visible affordance, where a cursor announces itself. It stays,
   and only shrinks its tracking to fit alongside the company count. */
@media (max-width: 460px) {
  .hero__foot .hero__hint { letter-spacing: 0.08em; }
}

/* -------------------------------------------------------------- intro --- */

.intro {
  padding-block: clamp(5rem, 12vw, 9rem);
}

/* No `text-wrap: balance` here on purpose: balancing evens the lines out but
   pulls them well short of the measure, which left the headline visibly
   narrower than the paragraphs beneath it. Filling the same measure as the
   body keeps both blocks on one edge whatever the copy says. */
.intro__lead {
  margin: 0;
  max-width: var(--measure);
  font-size: clamp(1.5rem, 3.4vw, 2.375rem);
  font-weight: 400;
  line-height: 1.22;
  letter-spacing: -0.03em;
  text-wrap: pretty;
  /* Display type at 38px: a single long word runs a long way. Without this
     it overflowed its column and stretched the document to twice the
     viewport width. */
  overflow-wrap: anywhere;
}

.intro__body {
  max-width: var(--measure-body);
  margin-top: clamp(1.75rem, 4vw, 2.5rem);
}

.intro__body p {
  margin: 0 0 1.1em;
  color: var(--ink-2);
  text-wrap: pretty;
  overflow-wrap: anywhere;
}

.intro__body p:last-child { margin-bottom: 0; }

/* ---------------------------------------------------------- portfolio --- */

.section-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem 2rem;
  padding-bottom: 1.5rem;
}

.section-head h2 {
  margin: 0;
  font-size: clamp(1.5rem, 2.6vw, 2rem);
  font-weight: 500;
  letter-spacing: -0.03em;
}

.section-head p {
  margin: 0;
  color: var(--ink-3);
  font-size: 0.9375rem;
}

/* Hairlines come from each card's own 1px ring rather than a background
   showing through the gaps — so a short final row leaves clean paper
   instead of a stray filled cell. */
/* minmax(0, 1fr), not 1fr: a grid track's default minimum is `auto`, which
   is the width of its longest unbreakable word. One long run of characters
   in a company name or description was enough to push a track — and with it
   the whole page — past the viewport. Pinning the minimum to 0 lets the
   track stay in its share of the row and the text wrap inside it. */
.grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 1px;
  padding: 1px;
}

@media (max-width: 980px) { .grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 620px) { .grid { grid-template-columns: minmax(0, 1fr); } }

.card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
  min-height: 16.5rem;
  padding: clamp(1.25rem, 2.4vw, 1.75rem);
  background: var(--paper);
  box-shadow: 0 0 0 1px var(--rule);
  text-decoration: none;
  isolation: isolate;
  transition: background 0.35s ease;
}

.card::before {
  /* hairline that draws itself across the top on hover */
  content: '';
  position: absolute;
  inset: -1px -1px auto -1px;
  height: 2px;
  background: var(--ink);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.45s cubic-bezier(0.2, 0.7, 0.2, 1);
}

.card:hover,
.card:focus-visible { background: var(--paper-raised); }

.card:hover::before,
.card:focus-visible::before { transform: scaleX(1); }

.card__top {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
}

.card__cat {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-2);
}

.card__year {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.08em;
  color: var(--ink-3);
}

/* Fixed height with the overflow clipped: a logo file that carries padding
   around its mark is drawn oversized (--logo-h) so the mark itself reaches a
   consistent size, and the surrounding transparent margin is simply cut off. */
.card__logo {
  display: flex;
  align-items: center;
  height: 3.5rem;
  margin-top: auto;
  overflow: hidden;
}

.card__logo img {
  max-width: min(200px, 78%);
  max-height: var(--logo-h, 3.25rem);
  width: auto;
  height: auto;
  object-fit: contain;
  object-position: left center;
  flex: none;
  /* Pulls the image left by however much empty canvas sits to the left of
     its mark, so every logo in the grid starts on the same edge. The logo
     box clips what that pushes out. */
  margin-left: calc(-1 * var(--logo-x, 0px));
}

/* Logos are recoloured to the page's ink so fifteen sets of brand colours
   don't fight each other. How that is done depends on the artwork, set per
   company with `logoMode` in content.js:

     (default)   single-tone art on a transparent background — flatten it
     light-tile  art baked onto a light background
     dark-tile   art baked onto a dark background
     knockout    monochrome art with detail knocked out of it — never flatten,
                 or the knockout fills in
     keepColor   leave the artwork alone

   The tile modes lean on two filter chains and a blend mode. `--logo-crush`
   drives everything dark to black and leaves white alone; `--logo-lift` is
   the same curve inverted. Paired with multiply on a light page (white
   disappears) or screen on a dark one (black disappears), a baked-in
   background drops out and only the mark survives. */
.card__logo img {
  /* Both chains land the same way: mark near-black, background near-white,
     which multiply then drops onto a light page. Appending invert(1) flips
     that for the dark theme, where screen drops the black instead. The
     contrast is what actually separates a mid-luminance mark from its tile —
     a gentler curve leaves marks like FleetFox's orange stranded in grey.
     --logo-b moves where that split falls, for artwork with more than two
     tones — Qminder's icon is a light mark on a dark tile on white, and only
     a raised pivot puts the tile in ink and drops both the others out. */
  --logo-on-light: grayscale(1) brightness(var(--logo-b, 0.85)) contrast(6);
  --logo-on-dark: grayscale(1) invert(1) brightness(var(--logo-b, 1)) contrast(6);
  filter: brightness(0);
}

.card__logo img[data-logo='light-tile'] { filter: var(--logo-on-light); }
.card__logo img[data-logo='dark-tile'] { filter: var(--logo-on-dark); }
.card__logo img[data-logo='knockout'] { filter: none; }
.card__logo img[data-keep-color] { filter: none; }

/* The blend sits on the box, not on the image inside it. Both properties on
   one element is what mobile Safari drops: a filtered element is rendered
   into its own isolated group, and the blend against the card behind it is
   then skipped — the baked-in tile survives as a white square. Splitting them
   over two elements gives the compositor an unfiltered box to blend and a
   plain filtered image inside it, which every engine handles. The wrapper is
   transparent wherever the logo isn't, and multiply leaves a transparent
   source alone, so only the tile is affected. */
.card__logo[data-logo='light-tile'],
.card__logo[data-logo='dark-tile'] { mix-blend-mode: multiply; }

.card__logo[data-keep-color] { mix-blend-mode: normal; }

@media (prefers-color-scheme: dark) {
  .card__logo img { filter: brightness(0) invert(1); }
  .card__logo img[data-logo='light-tile'] { filter: var(--logo-on-light) invert(1); }
  .card__logo img[data-logo='dark-tile'] { filter: var(--logo-on-dark) invert(1); }
  .card__logo img[data-logo='knockout'] { filter: invert(1); }
  .card__logo img[data-keep-color] { filter: none; }

  .card__logo[data-logo='light-tile'],
  .card__logo[data-logo='dark-tile'] { mix-blend-mode: screen; }

  .card__logo[data-keep-color] { mix-blend-mode: normal; }
}

/* Stands in for a logo until one is supplied: the name set as a logotype. */
.card__wordmark {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  font-size: clamp(1.25rem, 1.5vw, 1.4375rem);
  font-weight: 500;
  line-height: 1.15;
  letter-spacing: -0.032em;
  color: var(--ink);
  text-wrap: balance;
  overflow-wrap: anywhere;
}

.card__wordmark .card__arrow { width: 0.62em; height: 0.62em; }

.card__body { display: flex; flex-direction: column; gap: 0.4rem; }

.card__name {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 1.0625rem;
  font-weight: 500;
  letter-spacing: -0.02em;
  min-width: 0;
  overflow-wrap: anywhere;
}

.card__arrow {
  width: 0.7em;
  height: 0.7em;
  flex: none;
  opacity: 0;
  transform: translate(-3px, 3px);
  transition: opacity 0.3s ease, transform 0.3s cubic-bezier(0.2, 0.7, 0.2, 1);
}

.card:hover .card__arrow,
.card:focus-visible .card__arrow { opacity: 0.55; transform: none; }

.card__desc {
  margin: 0;
  font-size: 0.9375rem;
  line-height: 1.5;
  color: var(--ink-2);
  text-wrap: pretty;
  overflow-wrap: anywhere;
  /* The card's contents are anchored to its bottom, so a one-line
     description would otherwise push its logo lower than the two-line cards
     beside it. Reserving two lines keeps every logo in a row on one line. */
  min-height: 3em;
}

/* --------------------------------------------------------- syndicates --- */

/* The portfolio grid ends flush with its own bottom edge, so without this
   the heading below sat jammed against it. A single line of logos does not
   carry the visual weight the grid above it does, so this is a lighter gap
   than .intro's clamp — enough air to read as its own section without
   opening the same cavern that used to sit between the grid and the footer. */
#syndicates {
  padding-top: clamp(3rem, 7vw, 5rem);
}

/* One row, not a grid of tiles: logos sit at their own optical size and the
   row simply wraps if it runs out of width, the same way .footer__links
   does. No card, no border, no fill — just marks in a line. */
.syndicate-grid {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 2rem clamp(2rem, 5vw, 3.5rem);
}

.syndicate-card {
  display: inline-flex;
  align-items: center;
  gap: 0.75rem;
  text-decoration: none;
  opacity: 0.82;
  transition: opacity 0.25s ease;
}

.syndicate-card:hover,
.syndicate-card:focus-visible { opacity: 1; }

.syndicate__logo {
  display: flex;
  align-items: center;
}

/* Same recolour approach as .card__logo, trimmed to the cases these marks
   actually need — every syndicate logo so far is a single tone on a
   transparent background, so there is no tile/knockout handling to carry
   over. `keepColor` still escapes it if that ever changes. */
.syndicate__logo img {
  max-width: min(180px, 100%);
  max-height: var(--logo-h, 1.75rem);
  width: auto;
  height: auto;
  object-fit: contain;
  flex: none;
  margin-left: calc(-1 * var(--logo-x, 0px));
  filter: brightness(0);
}

.syndicate__logo img[data-keep-color] { filter: none; }

@media (prefers-color-scheme: dark) {
  .syndicate__logo img { filter: brightness(0) invert(1); }
  .syndicate__logo img[data-keep-color] { filter: none; }
}

.syndicate__wordmark {
  font-size: clamp(1.125rem, 1.4vw, 1.3125rem);
  font-weight: 500;
  letter-spacing: -0.03em;
  color: var(--ink);
}

.syndicate__name {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-3);
}

/* ------------------------------------------------------------- footer --- */

.footer {
  /* Matches #syndicates' own top padding: the footer now always follows
     that single-line section rather than the much taller portfolio grid,
     so the old clamp(5rem, 12vw, 9rem) read as a cavern after it. */
  margin-top: clamp(3rem, 7vw, 5rem);
  border-top: 1px solid var(--rule);
  background: var(--paper-sunk);
}

.footer__inner {
  display: grid;
  gap: clamp(2.5rem, 6vw, 4rem);
  /* minmax(0, …) here for the same reason as the portfolio grid: an
     unbreakable run in the disclaimer would otherwise set the track's
     minimum width and take the page with it. */
  grid-template-columns: minmax(0, 1fr);
  padding-block: clamp(3.5rem, 9vw, 6rem) 2rem;
}

/* Stacked on a phone, "Connect with me" is the more actionable of the two —
   it leads. Source order stays disclaimer-then-links throughout, so a screen
   reader hears the same sequence at every width; this only moves where each
   block paints. */
.footer__col--links { order: -1; }

@media (min-width: 860px) {
  .footer__inner { grid-template-columns: minmax(0, 1.7fr) minmax(0, 1fr); align-items: start; }
  .footer__col--links { order: 0; }
}

/* Fine print, deliberately: small, quiet, and set to a comfortable measure
   rather than run across the full column. */
/* Fine print, deliberately: small, quiet, and held to a measure that suits
   14px type — roughly 60 characters. It is NOT set to the intro's measure;
   at this size 660px would run to ~85 characters a line, well past
   comfortable. `balance` rather than `pretty` because the block is only four
   lines: browsers balance up to around six, and evening them out stops it
   ending on a stub. */
.footer__disclaimer {
  margin: 0.9rem 0 0;
  max-width: 56ch;
  font-size: 0.875rem;
  line-height: 1.65;
  color: var(--ink-3);
  text-wrap: balance;
  overflow-wrap: anywhere;
}

.footer__links {
  display: flex;
  flex-wrap: wrap;
  gap: 0 1.5rem;
  margin-top: 0.25rem;
  padding: 0;
  list-style: none;
}

/* Each link was a 15px-tall strip — "X" was a 25x15 target. Centring the
   text in a 2.75rem box makes every one of them thumb-sized without moving
   where the text itself sits. */
.footer__links a {
  display: inline-flex;
  align-items: center;
  min-height: 2.75rem;
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  letter-spacing: 0.04em;
  color: var(--ink-2);
  text-decoration: none;
  transition: color 0.2s ease;
}

.footer__links a:hover { color: var(--ink); }

.footer__base {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem 1.5rem;
  padding-block: 1.5rem;
  border-top: 1px solid var(--rule);
}

/* ------------------------------------------------------------ reveals --- */

[data-reveal] {
  opacity: 0;
  transform: translateY(10px);
  transition: opacity 0.45s cubic-bezier(0.2, 0.7, 0.2, 1),
              transform 0.45s cubic-bezier(0.2, 0.7, 0.2, 1);
  transition-delay: var(--reveal-delay, 0ms);
  /* opacity + transform only, both compositor properties */
  will-change: opacity, transform;
}

[data-reveal='in'] { opacity: 1; transform: none; will-change: auto; }

/* Anything already on screen when the page loads is placed, not animated —
   an entrance the visitor never saw begin just reads as content arriving
   late. Only what scrolls into view afterwards gets the transition. */
[data-reveal='now'] {
  opacity: 1;
  transform: none;
  transition: none;
  will-change: auto;
}

/* No JS: nothing should ever stay invisible. */
.no-js [data-reveal] { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  [data-reveal] { opacity: 1 !important; transform: none !important; }
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    transition-delay: 0ms !important;
    scroll-behavior: auto !important;
  }
}

/* Hero text stays selectable; everything else in the hero lets pointer
   events fall through to the canvas underneath. */
.hero__wordmark,
.hero__tagline,
.hero__foot .label { pointer-events: auto; }

.hero__canvas[data-hit='true'] { cursor: pointer; }

@media (max-width: 620px) {
  .card { min-height: 13.5rem; }
}
