/* Shared shell: header, navigation, buttons, section furniture, modals, footer.
   Every page includes tokens.css then this file, then its own page stylesheet. */

/* ---------- utilities ---------- */
/* The header is fixed, so anchor targets must stop below it. */
[id] { scroll-margin-top: calc(var(--header-h) + 20px); }

.wrap { width: 100%; max-width: var(--maxw); margin-inline: auto; padding-inline: var(--pad-x); }
.wrap--narrow { max-width: var(--maxw-narrow); }
.section { padding-block: var(--section-y); }
.section--alt { background: var(--bg-alt); }
.u-caps { text-transform: uppercase; letter-spacing: var(--ls-mid); }
.u-center { text-align: center; }
/* Thai has no spaces between words, so the browser breaks it with a dictionary.
   Loanwords are not in that dictionary: ไมอามี was splitting as ไมอา / มี at the
   end of a line. Wrap any transliterated name in this. */
.u-nowrap { white-space: nowrap; }
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

.eyebrow {
  font-size: var(--fs-eyebrow);
  text-transform: uppercase;
  letter-spacing: var(--ls-wide);
  color: var(--ink-muted);
  margin-bottom: 18px;
}
.section-title {
  font-family: var(--font-display);
  font-size: var(--fs-h2);
  letter-spacing: 0.01em;
}
.section-title--caps {
  font-family: var(--font-sans);
  font-size: clamp(1.1rem, 2.2vw, 1.6rem);
  text-transform: uppercase;
  letter-spacing: var(--ls-wide);
  font-weight: 400;
}
.lede { max-width: 60ch; color: var(--ink-soft); }
.rule { height: 1px; background: var(--line); border: 0; margin: 0; }

/* ---------- buttons ---------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 48px; padding: 0 34px;
  font-size: var(--fs-small); font-weight: 400;
  text-transform: uppercase; letter-spacing: var(--ls-mid);
  border: 1px solid var(--ink); background: var(--ink); color: var(--on-dark);
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.btn:hover { background: transparent; color: var(--ink); }
.btn--ghost { background: transparent; color: var(--ink); }
.btn--ghost:hover { background: var(--ink); color: var(--on-dark); }
.btn--light { background: transparent; border-color: rgba(255,255,255,0.7); color: #fff; }
.btn--light:hover { background: #fff; border-color: #fff; color: var(--ink); }
.btn--solid-light { background: #fff; border-color: #fff; color: var(--ink); }
.btn--solid-light:hover { background: transparent; color: #fff; }
@media (hover: none) { .btn:hover { background: var(--ink); color: var(--on-dark); } }

/* ---------- announcement strip ---------- */
/* Sits above the header in normal flow, so it scrolls away with the page while
   the header stays. The header therefore starts at --announce-h and drops to 0
   once .is-stuck lands (js/main.js, scrollY > 24), which is exactly when the
   strip has left the viewport. Reference look: the REFY top bar. */
.announce {
  height: var(--announce-h);
  display: flex; align-items: center; justify-content: center;
  padding-inline: var(--pad-x);
  background: var(--bg-alt);
  color: var(--ink);
}
.announce__msg {
  font-size: var(--fs-eyebrow);
  text-transform: uppercase;
  letter-spacing: var(--ls-wide);
  padding-left: var(--ls-wide); /* balance the trailing letterspace */
  text-align: center;
  line-height: 1.3;
}
:root[lang="th"] .announce__msg { text-transform: none; padding-left: 0; }

/* ---------- header ---------- */
.site-header {
  position: fixed; inset: var(--announce-h) 0 auto 0; z-index: 60;
  height: var(--header-h);
  display: flex; align-items: center;
  padding-top: env(safe-area-inset-top);
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease),
              border-color var(--dur) var(--ease), top var(--dur) var(--ease);
  border-bottom: 1px solid transparent;
}
.site-header--overlay { background: transparent; color: #fff; }
.site-header--solid,
.site-header.is-stuck { background: rgba(255,255,255,0.96); color: var(--ink); border-bottom-color: var(--line); backdrop-filter: saturate(140%) blur(6px); }
.site-header.is-stuck { top: 0; }

.header-inner {
  width: 100%; max-width: var(--maxw); margin-inline: auto; padding-inline: var(--pad-x);
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 12px;
}
.brand {
  grid-column: 2;
  justify-self: center;
  font-family: var(--font-sans);
  font-size: clamp(0.95rem, 1.9vw, 1.35rem);
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: 0.34em;
  padding-left: 0.34em; /* balance the trailing letterspace */
  white-space: nowrap;
}
/* The double I in the wordmark. In Jost a capital I is a bare vertical bar, so
   BROWBESTIIE can read as BROWBESTllE. This used to be solved by switching the
   pair to Cormorant, whose capital I carries serifs. That worked at hero size and
   failed at 14px: two serifed letters inside eight geometric ones read as a glyph
   that failed to load, not as a flourish.
   Decision 2026-09-09: one typeface, the pair separated by COLOUR instead. Silver
   says the two bars are deliberate, and it is the same move as the printed menu.
   Nikodem picked the silver and confirmed it: #a4a4aa, the same value as --accent
   and the same grey as the printed menu sheet, everywhere the wordmark appears.
   Measured, so it is a known trade rather than an accident: on white it is 2.48:1
   and on the grey band 2.22:1, both under the 4.5:1 that text at this size would
   normally need. That is a deliberate choice for two letters of a logo, not body
   copy: the word is still fully legible from the six black letters around them,
   and the pair is meant to sit back. It should NOT be copied to anything a client
   has to read. On the dark sections the same silver measures 7.61:1, so one value
   genuinely does serve every ground.
   Letter-spacing and weight are never set here: both inherit, so the pair always
   carries exactly the tracking and weight of the letters beside it. */
/* ONLY THE SECOND I IS SILVER. The name is Bestie; the wordmark spells it with a
   double I. Greying both leaves the ink letters reading BROWBEST--E, which is not
   a word. Greying the second alone leaves BROWBESTIE in ink and turns the extra I
   into a flourish on a name the eye has already finished. Where colour cannot
   follow (the rubber stamp, the favicon, the 40px avatar, anything single-ink)
   the mark is plain BROWBESTIIE. */
.brand-mark {
  color: var(--accent);
  font-weight: inherit;
  font-style: normal;
}

.primary-nav { grid-column: 1; }
.primary-nav ul { display: flex; align-items: center; gap: clamp(18px, 2.6vw, 40px); }
.primary-nav a {
  position: relative;
  font-size: var(--fs-small); text-transform: uppercase; letter-spacing: var(--ls-mid);
  padding-block: 6px;
}
.primary-nav a::after {
  content: ""; position: absolute; left: 0; bottom: 0; height: 1px; width: 100%;
  background: currentColor; transform: scaleX(0); transform-origin: left;
  transition: transform var(--dur) var(--ease);
}
.primary-nav a:hover::after, .primary-nav a[aria-current="page"]::after { transform: scaleX(1); }

/* Two groups, not three equal items: the account and search icons belong together,
   the language switch stands clearly apart. The outer gap is an order of magnitude
   larger than the inner one, which is what makes the pair read as a pair. */
.header-actions { grid-column: 3; justify-self: end; display: flex; align-items: center; gap: clamp(22px, 3.2vw, 40px); }
.header-icons { display: flex; align-items: center; gap: 2px; }
.icon-btn {
  width: 44px; height: 44px; display: grid; place-items: center;
  transition: opacity var(--dur) var(--ease);
}
.icon-btn:hover { opacity: 0.6; }
.icon-btn svg { width: 19px; height: 19px; fill: none; stroke: currentColor; stroke-width: 1.3; }

.lang-switch { display: flex; align-items: center; gap: 6px; font-size: var(--fs-small); letter-spacing: 0.08em; }
.lang-switch button { padding: 6px 2px; opacity: 0.5; text-transform: uppercase; transition: opacity var(--dur) var(--ease); }
.lang-switch button[aria-pressed="true"] { opacity: 1; text-decoration: underline; text-underline-offset: 4px; }
/* A plain hairline between EN and TH, drawn in CSS so the span stays empty and
   screen readers never announce a stray slash. Height follows the font size, so it
   keeps to roughly a cap height in the header, in the mobile menu and at any scale.
   currentColor means it inherits white over the dark hero and near black once the
   bar turns solid, at one opacity that holds on both. */
.lang-sep {
  flex: 0 0 auto;
  width: 1px;
  height: 0.9em;
  background: currentColor;
  opacity: 0.4;
}

/* hamburger */
.nav-toggle { display: none; width: 44px; height: 44px; padding: 0; }
.nav-toggle span {
  display: block; width: 22px; height: 1px; background: currentColor; margin: 5px auto;
  transition: transform var(--dur) var(--ease), opacity var(--dur) var(--ease);
}
.nav-toggle[aria-expanded="true"] span:nth-child(1) { transform: translateY(6px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.nav-toggle[aria-expanded="true"] span:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }

@media (max-width: 900px) {
  .nav-toggle { display: block; grid-column: 1; justify-self: start; }
  .primary-nav {
    position: fixed; inset: 0; z-index: 70;
    background: var(--bg); color: var(--ink);
    padding: calc(var(--header-h) + 32px) var(--pad-x) 40px;
    padding-bottom: calc(40px + env(safe-area-inset-bottom));
    transform: translateX(-100%); visibility: hidden;
    transition: transform var(--dur) var(--ease), visibility var(--dur);
  }
  .primary-nav.is-open { transform: none; visibility: visible; }
  .primary-nav ul { flex-direction: column; align-items: flex-start; gap: 6px; }
  /* Same setting as the desktop nav: Jost at the body's 300, uppercase, --ls-mid.
     It was Cormorant at clamp(1.9rem, 8vw, 2.6rem) in sentence case, which made
     the phone menu the only place on the site where the nav was a serif.
     Only the size is raised off the desktop's 13px, so the links stay a 44px
     tap target. */
  .primary-nav a {
    font-family: var(--font-sans);
    font-size: 1.05rem;
    font-weight: 300;
    text-transform: uppercase;
    letter-spacing: var(--ls-mid);
    padding-block: 14px;
  }
  .primary-nav a::after { display: none; }
  .nav-close { position: absolute; top: calc(env(safe-area-inset-top) + 9px); right: max(var(--pad-x), 16px); width: 44px; height: 44px; font-size: 26px; line-height: 1; }
}
@media (min-width: 901px) { .nav-close { display: none; } }
/* Narrow phones: the header has to fit hamburger, wordmark, search and language.
   The newsletter icon is dropped here, the same action sits in the footer. */
@media (max-width: 620px) {
  .brand { letter-spacing: 0.16em; padding-left: 0.16em; font-size: 0.82rem; }
  /* only the search icon is left here, so the pair gap no longer matters, but the
     language switch still needs air around it while the row stays inside the bar */
  .header-actions { gap: 4px; }
  /* the newsletter icon moves into the mobile menu and the footer, which frees
     enough room to keep the remaining icon at a full 44px tap target */
  .icon-btn[data-open="signupModal"] { display: none; }
  .lang-switch { font-size: 0.7rem; gap: 3px; margin-left: 4px; }
  .lang-switch button { padding: 6px 1px; min-width: 20px; }
}
@media (max-width: 380px) {
  .brand { font-size: 0.72rem; letter-spacing: 0.12em; padding-left: 0.12em; }
}

.nav-extra { display: none; }
@media (max-width: 900px) {
  .nav-extra {
    display: grid; gap: 20px; justify-items: start;
    margin-top: clamp(28px, 6vh, 56px); padding-top: 26px;
    border-top: 1px solid var(--line);
  }
  /* these sit inside .primary-nav, so they must win over the big serif link style */
  .primary-nav .nav-extra a,
  .primary-nav .nav-extra button {
    font-family: var(--font-sans);
    font-size: var(--fs-small);
    text-transform: uppercase;
    letter-spacing: var(--ls-mid);
    padding-block: 0;
    /* A <button> carries the browser's own 6px side padding, so JOIN THE LIST
       started 6px right of INSTAGRAM and of the links above it, even though
       every box began on the same line. */
    padding-inline: 0;
  }
  .nav-extra .btn { width: 100%; min-height: 54px; }
  .nav-extra-link {
    font-size: var(--fs-small); text-transform: uppercase; letter-spacing: var(--ls-mid);
    min-height: 44px; display: inline-flex; align-items: center;
  }
  /* the switch is cloned out of the header, where it carries a left margin to
     sit off the search icon. In the menu there is nothing to its left. */
  /* standing alone it has no text beside it to set the scale, so 15px looks lost */
  .nav-extra .ig-glyph { width: 21px; height: 21px; }
  .nav-extra .lang-switch { font-size: var(--fs-small); gap: 8px; margin-left: 0; }
  .nav-extra .lang-switch button { padding: 10px 4px; min-width: 32px; }
  .nav-extra .lang-switch button:first-child { padding-left: 0; }
}

/* Touch targets: on phones every link and control needs a comfortable 44px band.
   Row margins shrink to compensate, so the footer keeps its rhythm. */
@media (max-width: 900px) {
  .brand { padding-block: 11px; }
  .lang-switch button {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 34px; min-height: 44px; padding: 0 4px;
  }
  .footer-col li + li { margin-top: 0; }
  .footer-col a { display: inline-flex; align-items: center; min-height: 44px; }
  /* .social a is defined later in this file, so the selector has to be stronger */
  .site-footer .social a { width: 44px; height: 44px; }
}

body.nav-open { overflow: hidden; }
/* backdrop-filter makes an element a containing block for its fixed-position
   descendants, exactly like transform does. .primary-nav is a child of the
   header and is position:fixed inset:0, so once the header went .is-stuck the
   menu stopped filling the viewport and filled the 134px header instead, with
   the page showing through underneath. Only happened after scrolling, which is
   why it looked fine from the top of the page.
   The header sits behind the open menu, so dropping its blur costs nothing. */
body.nav-open .site-header.is-stuck { backdrop-filter: none; }

/* ---------- modal (never use native alert/confirm/prompt) ---------- */
.modal {
  position: fixed; inset: 0; z-index: 90;
  display: grid; place-items: center;
  padding: var(--pad-x);
  background: rgba(17,17,19,0.55);
  opacity: 0; visibility: hidden;
  transition: opacity var(--dur) var(--ease), visibility var(--dur);
}
.modal.is-open { opacity: 1; visibility: visible; }
.modal-card {
  width: min(520px, 100%); background: var(--bg); padding: clamp(28px, 5vw, 52px);
  position: relative; transform: translateY(14px);
  transition: transform var(--dur) var(--ease);
}
.modal.is-open .modal-card { transform: none; }
.modal-close { position: absolute; top: 8px; right: 8px; width: 44px; height: 44px; font-size: 22px; }
.modal-title { font-family: var(--font-display); font-size: clamp(1.6rem, 3vw, 2.2rem); margin-bottom: 10px; }
.modal-text { color: var(--ink-soft); font-size: var(--fs-small); margin-bottom: 22px; }
/* the consent line under the modal's button, matching .join-consent on the
   contact page: same words, same weight, same tone */
.modal-consent { margin: 14px 0 0; color: var(--ink-muted); font-size: var(--fs-eyebrow); line-height: 1.7; }

/* ---------- forms ---------- */
.field { display: block; margin-bottom: 18px; }
.field-label { display: block; font-size: var(--fs-eyebrow); text-transform: uppercase; letter-spacing: var(--ls-mid); color: var(--ink-muted); margin-bottom: 8px; }
.input, .textarea, .select {
  width: 100%; padding: 14px 16px;
  font-family: inherit; font-size: 1rem; /* 16px keeps iOS from zooming */
  font-weight: inherit; /* form controls default to 400, the page body is 300 */
  color: var(--ink); background: var(--bg-alt);
  border: 1px solid var(--line); border-radius: var(--radius);
  transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease);
}
/* Safari draws placeholders in its own grey and weight, so both are set here */
.input::placeholder, .textarea::placeholder { color: var(--ink-muted); font-weight: 300; opacity: 1; }
.input:focus, .textarea:focus, .select:focus { border-color: var(--accent); background: var(--bg); outline: none; }
.textarea { min-height: 140px; resize: vertical; }
.field-error { display: none; margin-top: 6px; font-size: var(--fs-small); color: #a4443a; }
.field.has-error .input, .field.has-error .textarea, .field.has-error .select { border-color: #a4443a; }
.field.has-error .field-error { display: block; }

/* Pułapka na boty: pole poza ekranem, którego człowiek nie widzi i nie dotknie.
   Nie display: none, bo część botów pomija ukryte tak pola. */
.hp {
  position: absolute;
  left: -9999px;
  width: 1px; height: 1px;
  overflow: hidden;
}

/* toast, used instead of alert() */
.toast {
  position: fixed; left: 50%; bottom: calc(24px + env(safe-area-inset-bottom)); z-index: 95;
  transform: translate(-50%, 20px); opacity: 0; pointer-events: none;
  background: var(--ink); color: var(--on-dark);
  padding: 14px 22px; font-size: var(--fs-small); letter-spacing: 0.04em;
  transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease);
  max-width: min(420px, calc(100% - 32px)); text-align: center;
}
.toast.is-visible { opacity: 1; transform: translate(-50%, 0); }

/* Instagram sits in the contact list, not in a tile of its own: it is a way to
   reach the studio and a portfolio, so it belongs beside email and phone. The
   handle is the useful part, the glyph only marks which app it is. */
.footer-contact a,
.contact-ig a { display: inline-flex; align-items: center; gap: 8px; }
.ig-glyph {
  width: 15px; height: 15px; flex: 0 0 auto;
  fill: none; stroke: currentColor; stroke-width: 1.5;
}
.contact-ig { font-size: var(--fs-small); }

/* ---------- footer ---------- */
.site-footer { background: var(--bg-alt); padding-block: clamp(56px, 7vw, 96px) 0; }

/* Lewa kolumna: marka i zapis na liste. Prawa: godziny, kontakt, linki. */
/* One gap value for the whole footer: the space between the signup and the first
   column has to equal the space between the columns, or the right-hand group
   reads as crowded against a roomy left. --footer-gap is used by both grids. */
.footer-grid {
  --footer-gap: clamp(34px, 3.4vw, 44px);
  display: grid; gap: var(--footer-gap); grid-template-columns: 1fr;
}
/* Desktop: the four blocks sit on one flex row and the leftover space is split
   equally between them. Equal grid TRACKS did not do it: the text in the three
   columns is very different in length, so equal slices still left uneven white
   between the blocks. Here each block is as wide as its own content and only the
   air is equal, which is what the eye actually reads.
   .footer-main and .footer-links are display: contents so their children join
   this row directly, instead of forming a second row with its own gaps. */
@media (min-width: 900px) {
  .footer-grid {
    display: flex;
    flex-wrap: nowrap;
    justify-content: space-between;
    align-items: start;
    gap: 0;
  }
  /* .footer-grid prefix on purpose: the plain .footer-links rule sits further
     down this file and would otherwise win on source order and put display back
     to grid, which silently rebuilt the old uneven row. */
  .footer-grid .footer-main,
  .footer-grid .footer-links { display: contents; }
  .footer-lead { flex: 0 1 380px; min-width: 300px; }
  .footer-col { flex: 0 0 auto; }
  .footer-hours { min-width: 230px; } /* the day/time rows need room to split */
}

/* One setting for the wordmark everywhere it appears: Jost 400 at 0.34em. The
   footer used to run 300 at 0.22em and the hero 200 at 0.16em, so the same name
   looked like three logos depending on the page you were on. */
.footer-brand {
  font-size: clamp(1.6rem, 4vw, 2.2rem);
  text-transform: uppercase;
  letter-spacing: 0.34em;
  /* No padding-left here. That trick belongs to the header, where the wordmark is
     CENTRED and the trailing letterspace would push it off centre. In the footer
     the block is left-aligned, so the same padding just indents the B past the
     text beneath it. */
  font-weight: 400;
  line-height: 1;
  margin-top: -0.12em; /* optical: cap height lines up with the column heads */
}
.footer-signup { margin-top: 24px; display: grid; gap: 12px; }
.footer-signup .field-label { color: var(--ink-muted); }
.footer-note { color: var(--ink-soft); font-size: var(--fs-small); line-height: 1.7; margin: -4px 0 4px; }
.footer-signup .btn { width: 100%; }
/* No text-wrap: balance here. Balance was tried and removed: it makes the two
   lines equal to each other, which pulls the block in to about 230px while the
   inputs above it are 380px wide, so the consent looked narrower than everything
   else in the column. Filling the column edge to edge is what lines up. */
.footer-consent { margin-top: 14px; color: var(--ink-muted); font-size: var(--fs-eyebrow); line-height: 1.7; }
.footer-lead .social { margin-top: 22px; }

.footer-motto {
  font-family: var(--font-display);
  font-size: clamp(1.05rem, 1.9vw, 1.3rem);
  letter-spacing: var(--ls-mid);
  text-transform: uppercase;
  color: var(--ink);
  margin-bottom: clamp(26px, 3.4vw, 40px);
}

.footer-links { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--footer-gap); }
/* three equal columns, not 1.1fr 1fr 1fr: Studio hours was wider than Contact
   and Explore, so the three heads did not sit on an even rhythm */
@media (min-width: 700px) { .footer-links { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 460px) { .footer-links { grid-template-columns: 1fr; } }
.footer-col h4 { font-size: var(--fs-eyebrow); text-transform: uppercase; letter-spacing: var(--ls-wide); color: var(--ink-muted); margin-bottom: 16px; }
.footer-col li + li { margin-top: 10px; }
.footer-col a { font-size: var(--fs-small); }
.footer-col a:hover { color: var(--accent); }

/* Godziny: dzien po lewej, zakres po prawej, kazdy wiersz na wlasnej linii. */
.footer-hours li {
  display: flex; justify-content: space-between; gap: 14px;
  padding-block: 7px; border-bottom: 1px solid var(--line);
  font-size: var(--fs-small); color: var(--ink-soft);
}
.footer-hours li + li { margin-top: 0; }
.footer-hours li:last-child { border-bottom: 0; }
.footer-hours span:first-child { color: var(--ink); }
.footer-hours li:last-child span:last-child { color: var(--ink-muted); }

.footer-contact li + li { margin-top: 14px; }
.footer-contact small {
  display: block; margin-bottom: 4px;
  font-size: var(--fs-eyebrow); letter-spacing: var(--ls-mid);
  text-transform: uppercase; color: var(--ink-muted);
}
.footer-contact a, .footer-contact span { font-size: var(--fs-small); color: var(--ink); }
/* No standing rules under the contact links: Nikodem's call, the column reads
   cleaner without them. The line comes back on hover and on keyboard focus, so
   the link is still discoverable with a mouse and reachable by keyboard, and the
   focus ring in tokens.css still marks it for anyone tabbing through. */
.footer-contact a { border-bottom: 1px solid transparent; }
.footer-contact a:hover,
.footer-contact a:focus-visible { border-bottom-color: var(--line-strong); }
.contact-ig a { border-bottom: 1px solid transparent; }
.contact-ig a:hover,
.contact-ig a:focus-visible { border-bottom-color: var(--line-strong); }
.footer-contact a:hover { border-color: var(--accent); color: var(--accent); }

.footer-bottom {
  margin-top: clamp(40px, 6vw, 72px); border-top: 1px solid var(--line);
  padding-block: 22px; padding-bottom: calc(22px + env(safe-area-inset-bottom));
  display: flex; flex-wrap: wrap; gap: 12px 24px; align-items: center; justify-content: space-between;
  font-size: var(--fs-small); color: var(--ink-muted);
}
.footer-bottom a { color: var(--ink-muted); }
.footer-bottom a:hover { color: var(--accent); }

.social { display: flex; gap: 14px; }
.social a { width: 40px; height: 40px; display: grid; place-items: center; border: 1px solid var(--line); }
.social a:hover { border-color: var(--ink); }
.social svg { width: 17px; height: 17px; fill: none; stroke: currentColor; stroke-width: 1.3; }

/* reveal on scroll */
.reveal { opacity: 0; transform: translateY(24px); transition: opacity 0.9s var(--ease), transform 0.9s var(--ease); }
.reveal.is-in { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) { .reveal { opacity: 1; transform: none; } }
/* With scripting off nothing would ever reveal, so show everything. */
@media (scripting: none) { .reveal { opacity: 1; transform: none; } }
