/* ==========================================================================
   xiejin.me: shared styles for the home page and the essays.

   How this file is organised (each block folds in VS Code with the ▸ arrow,
   or Ctrl/Cmd+K then Ctrl/Cmd+0 to fold everything):

     1. Tokens          colours, fonts, sizes, motion timings. Restyle here first.
     2. Base            page defaults: body text, headings, links, focus rings
     3. Layout          the centred column and the two-column section grid
     4. Header          the top navigation, pinned while you scroll,
                        and the phone menu
     5. Hero            photo, name, intro and the first row of buttons
     6. Content         project items, experience list, education
     7. Buttons & links
     8. Contact form
     9. Footer
    10. Essays          the /writing/ pages
    11. Motion          scroll entrances, hero drift, loading intro
                        (driven by /assets/motion.js)
    12. Print

   Phone and big-screen rules sit inside the block they belong to, so
   everything about a component is in one place. Breakpoints used:
     ≤700px phones · ≤360px small phones · ≥1600px big screens
     max-height 500px (landscape) landscape phones
   ========================================================================== */


/* #region 1. Tokens ------------------------------------------------------- */

:root {
  /* Colours (light) */
  --bg: #ffffff;
  --fg: #16181d;        /* main text */
  --fg2: #4f5562;       /* secondary text */
  --fg3: #848a96;       /* labels, dates, captions */
  --rule: #e7e9ee;      /* hairlines and borders */
  --soft: #f6f7f9;      /* subtle fills */
  --accent: #1d4e89;    /* links and primary buttons */
  --accent-h: #163d6c;  /* accent on hover */
  --on-accent: #ffffff; /* text on primary buttons */
  --ok: #1f7a4d;
  --err: #b42318;

  /* Type */
  --serif: "Source Serif 4", Georgia, "Times New Roman", serif;
  --sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;

  /* Layout */
  --column: 760px;      /* reading width */
  --column-xl: 820px;   /* reading width on big screens */
  --gutter: 24px;       /* side padding */
  --gutter-phone: 16px;
  --label-col: 150px;   /* width of the section label column */
  --radius: 8px;        /* buttons and inputs */

  /* Buttons (see section 7) */
  --shadow: 16, 24, 40; /* RGB of button shadows */
  --press: .98;         /* how far a button sinks when pressed */
  --ease-out: cubic-bezier(0, 0, .5, 1);

  /* Motion (see section 11) */
  --eio: cubic-bezier(.455, .03, .515, .955); /* ease-in-out, Apple's entrance curve */
  --rise: 24px;         /* how far things rise as they enter */
  --nav-h: 56px;        /* height of the header */

  /* Loading intro on the home page (see the end of section 11) */
  --intro-hold: .9s;    /* how long your name shows before the screen lifts */
  --intro-lift: .5s;    /* how long the lift takes */

  color-scheme: light;
}

/* Colours (dark). Only colours change; everything else is shared. */
@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0f1115;
    --fg: #e9ebef;
    --fg2: #aab0bb;
    --fg3: #7c8290;
    --rule: #23262d;
    --soft: #171a20;
    --accent: #8db4e6;
    --accent-h: #b3cdf0;
    --on-accent: #0f1115;
    --ok: #5cc28f;
    --err: #f07a6e;
    --shadow: 0, 0, 0;
    color-scheme: dark;
  }
}

/* #endregion */


/* #region 2. Base --------------------------------------------------------- */

* { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: 16px;
}
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font: 16px/1.65 var(--sans);
  -webkit-font-smoothing: antialiased;
  overflow-wrap: break-word;
  -webkit-tap-highlight-color: transparent;
}
@media (min-width: 1600px) {
  body { font-size: 17px; }
}

img { max-width: 100%; height: auto; }

h1 {
  margin: 0;
  font: 600 40px/1.1 var(--serif);
  letter-spacing: -.015em;
}
@media (max-width: 700px) { h1 { font-size: 32px; } }
@media (max-width: 360px) { h1 { font-size: 28px; } }

/* Links in running text: the underline fades in on hover. Buttons, navigation
   and footer links have their own hover styles in section 7. */
a {
  color: var(--accent);
  text-decoration: underline 1px transparent;
  text-underline-offset: 3px;
  transition: color .2s ease, text-decoration-color .2s ease;
}
a:hover { text-decoration-color: currentColor; color: var(--accent-h); }

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

/* #endregion */


/* #region 3. Layout ------------------------------------------------------- */

/* The centred reading column. */
.wrap {
  max-width: var(--column);
  margin: 0 auto;
  padding: 0 max(var(--gutter), env(safe-area-inset-right)) 0 max(var(--gutter), env(safe-area-inset-left));
}
@media (max-width: 700px) {
  .wrap { padding: 0 max(var(--gutter-phone), env(safe-area-inset-right)) 0 max(var(--gutter-phone), env(safe-area-inset-left)); }
}
@media (min-width: 1600px) {
  .wrap { max-width: var(--column-xl); }
}

/* Home-page sections: a small label on the left, content on the right.
   On phones the label stacks above the content. */
section {
  display: grid;
  grid-template-columns: var(--label-col) 1fr;
  gap: 24px;
  padding: 48px 0;
  border-bottom: 1px solid var(--rule);
}
section:last-of-type { border-bottom: 0; }
@media (max-width: 700px) {
  section { grid-template-columns: 1fr; gap: 14px; padding: 36px 0; }
}
@media (max-height: 500px) and (orientation: landscape) {
  section { padding: 32px 0; }
}

.label {
  padding-top: 4px;
  font-size: 12.5px;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--fg3);
}

/* #endregion */


/* #region 4. Header ------------------------------------------------------ */

/* One header for the whole page. It stays pinned to the top while you
   scroll. At the very top it has no background; once you scroll, a frosted
   backdrop and hairline fade in. Nothing slides or changes height, so the
   page never jumps. Pinning needs JavaScript (the .js class): without it
   this is a plain header that scrolls away, so content can't end up
   behind a see-through bar. */
.top {
  position: relative;
  z-index: 50;
  padding-top: env(safe-area-inset-top);
  font-size: 14px;
}
.js .top { position: sticky; top: 0; }

/* The frosted backdrop, faded in by motion.js adding .is-stuck. */
.top::before {
  content: "";
  position: absolute;
  inset: 0;
  background: color-mix(in srgb, var(--bg) 78%, transparent);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  backdrop-filter: saturate(180%) blur(20px);
  border-bottom: 1px solid var(--rule);
  opacity: 0;
  pointer-events: none;
  transition: opacity .3s ease;
}
.top.is-stuck::before { opacity: 1; }
@supports not ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
  .top::before { background: var(--bg); }
}

/* The row inside: it uses .wrap, so it lines up with the page column. */
.top-in {
  position: relative;
  height: var(--nav-h);
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 16px;
}

/* Your name, on the left. */
.top .home {
  padding: 10px 0;
  font-weight: 600;
  letter-spacing: -.01em;
  color: var(--fg);
  text-decoration: none;
  transition: opacity .2s ease;
}
.top .home:hover { color: var(--fg); }
@media (hover: hover) and (pointer: fine) {
  .top .home:hover { opacity: .72; }
}

/* Links, on the right. Plain grey text that brightens on hover. The last
   link's padding hangs into the margin so its text lines up with the column. */
.top nav {
  position: relative;
  display: flex;
  align-items: center;
  gap: 2px;
  margin-right: -12px;
}
.top nav a {
  position: relative;
  z-index: 1;
  padding: 10px 12px;
  border-radius: 999px;
  color: var(--fg2);
  text-decoration: none;
  transition: color .2s ease;
}
.top nav a:hover,
.top nav a[aria-current="true"] { color: var(--fg); }

/* The gliding highlight. motion.js adds it and moves it behind the section
   you're reading, or behind the link under your pointer. */
.nav-ink {
  position: absolute;
  left: 0;
  top: 50%;
  width: var(--w, 0px);
  height: 32px;
  margin-top: -16px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--fg) 7%, transparent);
  opacity: 0;
  pointer-events: none;
  transform: translateX(var(--x, 0px));
  transition:
    transform .45s cubic-bezier(.2, .8, .2, 1),
    width .45s cubic-bezier(.2, .8, .2, 1),
    opacity .25s ease;
}
.nav-ink.on { opacity: 1; }
.nav-ink.jump { transition: opacity .25s ease; } /* first appearance: fade, don't slide */
@media (prefers-reduced-motion: reduce) {
  .top::before, .nav-ink { transition: opacity .15s ease; }
}

@media (max-width: 700px) {
  .top nav { margin-right: -10px; }
  .top nav a { padding: 10px 10px; }
  .top nav .opt { display: none; } /* class="opt" links hide on phones */
}
@media (max-width: 360px) {
  .top nav { margin-right: -8px; }
  .top nav a { padding: 10px 8px; }
}

/* ---- Phone menu ---------------------------------------------------------
   On phones the header links are swapped for a "Menu" button that opens a
   full-screen panel: big links to every part of the page, then shortcut
   buttons. Markup: the MENU block in index.html. Behaviour: /assets/menu.js.
   Without JavaScript the button is hidden and the plain links show instead. */

.menu-btn { display: none; }
.menu { display: none; }

@media (max-width: 700px) {
  .js .top:has(.menu-btn) nav { display: none; }

  /* The button: a soft pill with a two-line icon that turns into an ×. */
  .js .menu-btn {
    display: inline-flex;
    align-items: center;
    gap: 9px;
    height: 36px;
    margin-right: -4px;
    padding: 0 13px 0 14px;
    font: 500 14px/1 var(--sans);
    color: var(--fg);
    background: color-mix(in srgb, var(--fg) 6%, transparent);
    border: 0;
    border-radius: 999px;
    cursor: pointer;
    transition: background-color .2s ease, scale .3s var(--ease-out);
  }
  .js .menu-btn:active { scale: .96; transition-duration: .08s; }
  .menu-btn:focus-visible { outline-offset: 2px; border-radius: 999px; }

  .menu-icon {
    position: relative;
    width: 14px;
    height: 8px;
  }
  .menu-icon::before,
  .menu-icon::after {
    content: "";
    position: absolute;
    left: 0;
    width: 100%;
    height: 1.5px;
    border-radius: 1px;
    background: currentColor;
    transition: translate .35s var(--ease-out), rotate .35s var(--ease-out);
  }
  .menu-icon::before { top: 0; }
  .menu-icon::after { bottom: 0; }
  .menu-btn[aria-expanded="true"] .menu-icon::before { translate: 0 3.25px; rotate: 45deg; }
  .menu-btn[aria-expanded="true"] .menu-icon::after { translate: 0 -3.25px; rotate: -45deg; }

  /* The panel: covers the page under the header. */
  .js .menu:not([hidden]) {
    position: fixed;
    inset: 0;
    z-index: 40;            /* just under the header (50) */
    display: block;
    padding: calc(var(--nav-h) + env(safe-area-inset-top)) 0 max(32px, env(safe-area-inset-bottom));
    overflow-y: auto;
    overscroll-behavior: contain;
    background: var(--bg);
    opacity: 0;
    transition: opacity .2s ease;
  }
  .js .menu.is-open { opacity: 1; transition-duration: .3s; }

  /* While open: the page underneath can't scroll, and the header's hairline shows. */
  html.menu-open { overflow: hidden; }
  html.menu-open .top::before { opacity: 1; background: var(--bg); }

  /* Big section links. */
  .menu-links {
    list-style: none;
    margin: 12px 0 0;
    padding: 0;
  }
  .menu-links a {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 13px 0;
    font: 600 30px/1.2 var(--serif);
    letter-spacing: -.015em;
    color: var(--fg);
    text-decoration: none;
    border-bottom: 1px solid var(--rule);
  }
  .menu-links a:active { color: var(--accent); }
  /* The section you were reading gets the accent colour and a small dot. */
  .menu-links a[aria-current="true"] { color: var(--accent); }
  .menu-links a[aria-current="true"]::after {
    content: "";
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: currentColor;
  }

  /* Shortcut buttons, two per row. */
  .menu-actions {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
    margin-top: 28px;
  }

  /* Links rise in one after another (--i is set by menu.js); they leave
     together, quickly. */
  .menu-links li,
  .menu-actions {
    opacity: 0;
    translate: 0 14px;
    transition: opacity .15s ease, translate .15s ease;
  }
  .menu.is-open :is(.menu-links li, .menu-actions) {
    opacity: 1;
    translate: 0 0;
    transition:
      opacity .45s var(--eio) calc(60ms + var(--i, 0) * 40ms),
      translate .55s cubic-bezier(.2, .8, .2, 1) calc(60ms + var(--i, 0) * 40ms);
  }
}
@media (max-width: 360px) {
  .menu-links a { font-size: 26px; padding: 11px 0; }
}
@media (max-height: 500px) and (orientation: landscape) {
  .menu-links a { font-size: 22px; padding: 8px 0; }
}
@media (prefers-reduced-motion: reduce) {
  .menu-icon::before, .menu-icon::after { transition: none; }
  .menu-links li, .menu-actions,
  .menu.is-open :is(.menu-links li, .menu-actions) { translate: none; transition: opacity .15s ease; }
}

/* #endregion */


/* #region 5. Hero --------------------------------------------------------- */

.hero {
  padding: 72px 0 56px;
  border-bottom: 1px solid var(--rule);
}
@media (max-width: 700px) {
  .hero { padding: 44px 0 40px; }
}
@media (max-height: 500px) and (orientation: landscape) {
  .hero { padding: 32px 0; }
}

/* Photo next to name and role. */
.hero-id {
  display: flex;
  align-items: center;
  gap: 20px;
  margin-bottom: 28px;
}
.hero-id img {
  flex: none;
  width: 76px;
  height: 76px;
  border-radius: 50%;
  object-fit: cover;
}
@media (max-width: 700px) {
  .hero-id img { width: 60px; height: 60px; }
}
@media (max-width: 360px) {
  .hero-id { gap: 14px; }
  .hero-id img { width: 52px; height: 52px; }
}

.role {
  margin-top: 6px;
  font-size: 15px;
  color: var(--fg2);
}

/* The intro line under the name (also used on essays). On the home page it is
   a <p> inside .hero, so ".hero p" below sets its colour and bottom margin. */
.lede {
  max-width: 640px;
  margin: 0 0 18px;
  font-size: 19px;
  line-height: 1.6;
  color: var(--fg);
}
@media (max-width: 700px) { .lede { font-size: 17.5px; } }
@media (min-width: 1600px) { .lede { font-size: 20px; } }

.hero p {
  max-width: 640px;
  margin: 0 0 16px;
  color: var(--fg2);
}

/* Row of buttons under the intro. */
.actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 28px;
}
/* Phones: the main button gets its own full-width row, the other two share
   the row under it. */
@media (max-width: 700px) {
  .actions { display: grid; grid-template-columns: 1fr 1fr; }
  .actions .btn.primary { grid-column: 1 / -1; }
}

/* #endregion */


/* #region 6. Content ------------------------------------------------------ */

/* A project, essay or school: serif title, grey meta line, short paragraph. */
.item { margin-bottom: 32px; }
.item:last-child { margin-bottom: 0; }
.item h3 {
  margin: 0 0 4px;
  font: 600 20px/1.35 var(--serif);
  letter-spacing: -.005em;
}
.item h3 a { display: inline-block; padding: 2px 0; color: var(--fg); }
.item h3 a:hover { color: var(--accent); }
.item p { margin: 0 0 8px; color: var(--fg2); }
@media (min-width: 1600px) {
  .item p { font-size: 16.5px; }
}

.meta {
  margin-bottom: 8px;
  font-size: 14px;
  color: var(--fg3);
}

/* Small grey pill, e.g. <span class="tag">New</span> after a title. */
.tag {
  display: inline-block;
  margin-left: 6px;
  padding: 0 7px;
  font-size: 12px;
  font-weight: 500;
  vertical-align: 2px;
  color: var(--fg2);
  background: var(--soft);
  border: 1px solid var(--rule);
  border-radius: 4px;
}

/* Experience list: organisation and position, dates on the right, then a
   one-line description. Inside "Earlier roles" the list is .xp.compact. */
.xp { list-style: none; margin: 0; padding: 0; }
.xp li {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 2px 16px;
  padding: 14px 0;
  border-top: 1px solid var(--rule);
}
.xp li:first-child { border-top: 0; padding-top: 0; }
.xp .org { font-weight: 600; }
.xp .pos { color: var(--fg2); font-weight: 400; }
.xp .when {
  font-size: 14px;
  color: var(--fg3);
  white-space: nowrap;
  text-align: right;
}
.xp .what {
  grid-column: 1 / -1;
  font-size: 15px;
  color: var(--fg2);
}
.xp.compact li { padding: 9px 0; font-size: 15px; }
@media (max-width: 700px) {
  .xp li { grid-template-columns: 1fr; }
  .xp .when { text-align: left; }
}
@media (min-width: 1600px) {
  .xp .what { font-size: 16.5px; }
}

/* "Earlier roles and volunteering" disclosure. */
details {
  margin-top: 18px;
  padding-top: 14px;
  border-top: 1px solid var(--rule);
}
details .xp { margin-top: 12px; }
summary {
  list-style: none;
  margin: -10px 0;
  padding: 10px 0;
  font-size: 14.5px;
  font-weight: 500;
  color: var(--accent);
  cursor: pointer;
}
summary::-webkit-details-marker { display: none; }
/* Its +/× icon is in section 7. */

/* Education */
.edu p { margin: 0; color: var(--fg2); }

/* #endregion */


/* #region 7. Buttons & links ---------------------------------------------- */

/* How links and buttons behave, everywhere:
   - Every link that goes to another page or file opens in a new tab:
     give it target="_blank" rel="noopener". Links within the page (#work),
     email links (mailto:) and links back to the home page don't need it.
   - Mouse and trackpad get hover effects; touch screens get press feedback only.
   - Arrows are added for you, so never type them in the HTML:
       links that open a new tab (target="_blank") on .btn and .more get ↗
       other .more links get →, and .back links get ←
     Each arrow nudges in its direction on hover. */

/* Buttons: <a class="btn"> or <button class="btn">. Add "primary" for the
   filled one; use one primary per row. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: 44px;              /* comfortable tap target */
  padding: 0 18px;
  font-family: inherit;
  font-size: 14.5px;
  font-weight: 500;
  letter-spacing: -.005em;
  color: var(--fg);
  text-decoration: none;
  background: var(--bg);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  box-shadow: 0 1px 2px rgba(var(--shadow), .05);
  cursor: pointer;
  transition:
    background-color .2s ease,
    border-color .2s ease,
    color .2s ease,
    box-shadow .3s var(--ease-out),
    translate .3s var(--ease-out),
    scale .3s var(--ease-out);
}
.btn:hover { color: var(--fg); text-decoration: none; }
/* Phones: slightly tighter padding. */
@media (max-width: 700px) {
  .btn { padding: 0 14px; }
}

.btn.primary {
  color: var(--on-accent);
  background: var(--accent);
  border-color: var(--accent);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .14), 0 1px 2px rgba(var(--shadow), .14);
}
.btn.primary:hover { color: var(--on-accent); }

/* Hover (mouse and trackpad only): lift 1px and deepen the shadow. */
@media (hover: hover) and (pointer: fine) {
  .btn:hover {
    translate: 0 -1px;
    background: var(--soft);
    border-color: color-mix(in srgb, var(--fg) 22%, transparent);
    box-shadow: 0 6px 14px -8px rgba(var(--shadow), .28);
  }
  .btn.primary:hover {
    background: var(--accent-h);
    border-color: var(--accent-h);
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, .14), 0 8px 18px -8px color-mix(in srgb, var(--accent) 70%, transparent);
  }
}

/* Press: sink slightly. Quick in, eased out. */
.btn:active {
  translate: 0 0;
  scale: var(--press);
  box-shadow: 0 1px 1px rgba(var(--shadow), .08);
  transition-duration: .08s;
}

.btn:focus-visible { outline-offset: 2px; border-radius: var(--radius); }

.btn:disabled,
.btn[aria-disabled="true"] { opacity: .6; cursor: default; translate: none; scale: none; box-shadow: none; }

/* While the contact form sends, its button shows a small spinner. */
.btn[aria-busy="true"] { opacity: .85; cursor: progress; }
.btn[aria-busy="true"]::before {
  content: "";
  width: 14px;
  height: 14px;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: spin .7s linear infinite;
}
@keyframes spin { to { rotate: 360deg; } }
@media (prefers-reduced-motion: reduce) {
  .btn[aria-busy="true"]::before { animation: none; border-right-color: currentColor; opacity: .5; }
}

/* Small text links under an item, e.g. "Read the analysis". */
.more {
  display: inline-block;
  padding: 8px 0;
  font-size: 14.5px;
  font-weight: 500;
  text-decoration: none;
}

/* The "Back to xiejin.me" link at the end of an essay. */
.back {
  display: inline-block;
  margin-top: 16px;
  padding: 10px 0;
  font-size: 14.5px;
  font-weight: 500;
  text-decoration: none;
}

/* Automatic arrows. The text after the "/" is what screen readers hear. */
.more::after,
.back::before,
.btn[target="_blank"]::after {
  display: inline-block;
  transition: translate .3s var(--ease-out);
}
.more::after {
  content: "→";
  content: "→" / "";
  margin-left: 6px;
}
.back::before {
  content: "←";
  content: "←" / "";
  margin-right: 6px;
}
.more[target="_blank"]::after,
.btn[target="_blank"]::after {
  content: "↗";
  content: "↗" / " (opens in a new tab)";
  font-size: .95em;
}
.more[target="_blank"]::after { margin-left: 4px; }

/* Other links that open a new tab (titles, header, footer) show no arrow,
   but screen readers still hear "opens in a new tab". */
:is(.item h3, .top nav, footer) a[target="_blank"]::after {
  content: "";
  content: "" / " (opens in a new tab)";
}
.btn[target="_blank"]::after { margin-left: -2px; opacity: .7; }

@media (hover: hover) and (pointer: fine) {
  .more:hover,
  .back:hover { text-decoration: none; }
  .more:hover::after { translate: 3px 0; }
  .back:hover::before { translate: -3px 0; }
  .more[target="_blank"]:hover::after,
  .btn[target="_blank"]:hover::after { translate: 1.5px -1.5px; }
}

/* Footer links: a thin underline draws in from the left on hover. */
footer a {
  text-decoration: none;
  background: linear-gradient(currentColor, currentColor) no-repeat 0 100% / 0 1px;
  background-origin: content-box;
  background-clip: content-box;
  transition: color .2s ease, background-size .35s cubic-bezier(.2, .7, .1, 1);
}
footer a:hover { text-decoration: none; background-size: 100% 1px; }

/* Item titles that are links: the underline fades in, and works across
   titles that wrap onto two lines. */
.item h3 a {
  text-decoration: underline 1px transparent;
  text-underline-offset: 4px;
}
.item h3 a:hover { text-decoration-color: currentColor; }

/* "Earlier roles and volunteering": a + that turns into × when open. */
summary { transition: color .2s ease; }
summary:hover { color: var(--accent-h); }
summary::after,
details[open] summary::after,
details.closing summary::after {
  content: "";
  display: inline-block;
  width: 9px;
  height: 9px;
  margin-left: 8px;
  vertical-align: 1px;
  background:
    linear-gradient(currentColor, currentColor) center / 100% 1.5px no-repeat,
    linear-gradient(currentColor, currentColor) center / 1.5px 100% no-repeat;
  transition: rotate .35s var(--ease-out);
}
details[open] summary::after { rotate: 45deg; }
details.closing summary::after { rotate: 0deg; }

/* Reduced motion: colours still change, but nothing lifts, sinks or nudges. */
@media (prefers-reduced-motion: reduce) {
  .btn, .btn:hover, .btn:active { translate: none; scale: none; }
  .more::after, .back::before, .btn[target="_blank"]::after, summary::after { transition: none; translate: none !important; }
}

/* #endregion */


/* #region 8. Contact form ------------------------------------------------- */

.contact-intro { margin: 0; color: var(--fg2); }

.contact-row {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin: 16px 0 28px;
}
@media (max-width: 700px) {
  .contact-row .btn { flex: 1 1 100%; }
}

form { display: grid; gap: 12px; max-width: 520px; }
.two { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
@media (max-width: 700px) {
  .two { grid-template-columns: 1fr; }
  form .btn { width: 100%; }   /* full-width "Send message" on phones */
}

label {
  display: block;
  margin-bottom: 5px;
  font-size: 13.5px;
  font-weight: 500;
  color: var(--fg2);
}
input,
textarea {
  width: 100%;
  padding: 10px 12px;
  font: inherit;
  font-size: 16px; /* 16px stops iPhones zooming in on focus */
  color: var(--fg);
  background: var(--bg);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  -webkit-appearance: none;
  appearance: none;
}
input:focus,
textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
}
textarea { min-height: 130px; resize: vertical; }

/* Hidden spam trap. Bots fill it in; people never see it. */
.hp { display: none !important; }

/* Message under the form: "Sending…", success or error. */
.status { min-height: 1.4em; margin: 0; font-size: 14.5px; }
.status.ok { color: var(--ok); }
.status.err { color: var(--err); }

/* #endregion */


/* #region 9. Footer ------------------------------------------------------- */

footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: 12px;
  padding: 18px 0 max(40px, calc(env(safe-area-inset-bottom) + 24px));
  font-size: 13.5px;
  color: var(--fg3);
  border-top: 1px solid var(--rule);
}
footer a { display: inline-block; padding: 10px 2px; color: var(--fg3); }
footer a:hover { color: var(--fg2); }

/* #endregion */


/* #region 10. Essays ------------------------------------------------------ */

.article { padding: 56px 0 64px; }
.article .kicker { margin-bottom: 14px; font-size: 13px; color: var(--fg3); }
.article h1 { margin-bottom: 18px; font-size: 38px; }
.article .lede { color: var(--fg2); }
.article p {
  max-width: 660px;
  margin: 0 0 20px;
  font-size: 17px;
  line-height: 1.75;
}
.article h2 { margin: 36px 0 10px; font: 600 22px/1.35 var(--serif); }
@media (max-width: 700px) {
  .article h1 { font-size: 30px; }
  .article p { font-size: 16.5px; }
}

/* #endregion */


/* #region 11. Motion ------------------------------------------------------ */
/* Inspired by Apple's product pages, kept quiet: motion is earned by
   scrolling, nothing moves while you stand still, and position settles
   before light (rise 0.7s, fade 0.9s).

   Which elements animate is decided in ONE place: the REVEAL list at the
   top of /assets/motion.js. That script tags each one with data-rv="rise",
   "zoom" or "drop", and the rules below style whatever is tagged. To animate
   something new, add its selector to REVEAL. Nothing to change here.

   Safe by default: nothing is hidden unless the page has the .js class
   (added in each page's <head>) and the visitor allows motion. If the
   script fails, the .js class is removed and the page shows as is. */

html.js { scroll-padding-top: calc(var(--nav-h) + 16px); }

@keyframes rv-o    { from { opacity: 0; } }
@keyframes rv-y    { from { translate: 0 var(--rise); } }
@keyframes rv-s    { from { scale: 1.15; } }
@keyframes rv-drop { from { translate: 0 -4px; } }

@media (prefers-reduced-motion: no-preference) {
  /* The split second between the page loading and motion.js tagging what
     animates: keep the content hidden so nothing flashes in and out. */
  .js:not(.motion-ready) :is(main, footer) { opacity: 0; }

  /* Waiting to enter. */
  .js [data-rv="rise"]:not(.rv-in) { opacity: 0; translate: 0 var(--rise); }
  .js [data-rv="zoom"]:not(.rv-in) { opacity: 0; scale: 1.15; }
  .js [data-rv="drop"]:not(.rv-in) { opacity: 0; translate: 0 -4px; }

  /* Entering. --d is the stagger delay, set by motion.js. */
  .js [data-rv="rise"].rv-in {
    animation:
      rv-o .9s var(--eio) var(--d, 0s) backwards,
      rv-y .7s var(--eio) var(--d, 0s) backwards;
  }
  /* The headshot settles from slightly oversized; the scale outlasts the fade. */
  .js [data-rv="zoom"].rv-in {
    animation:
      rv-o .9s cubic-bezier(.45, 0, .55, 1) var(--d, 0s) backwards,
      rv-s 1.5s cubic-bezier(.33, 0, .83, 1) var(--d, 0s) backwards;
  }
  /* The footer drops in rather than rising. */
  .js [data-rv="drop"].rv-in {
    animation:
      rv-o .5s ease-out var(--d, 0s) backwards,
      rv-drop .5s ease-out var(--d, 0s) backwards;
  }

  /* Hero drift: motion.js sets --ro (opacity) and --ry (offset) from the
     eased scroll position as you scroll past the intro. */
  .js .hero > * {
    opacity: var(--ro, 1);
    transform: translateY(var(--ry, 0px));
  }

  .status.ok, .status.err { animation: rv-o .35s ease; }
}

/* Section labels ride alongside their section on wider screens. */
@media (min-width: 701px) {
  .js section > .label {
    position: sticky;
    top: calc(var(--nav-h) + 20px);
    align-self: start;
  }
}

/* Loading intro (home page only). Your name fades up, a thin line draws
   under it, then the screen lifts away and the page rises in behind it.
   The markup is the INTRO block at the top of index.html, which also decides
   whether it plays (once per visit). Timings are --intro-hold and
   --intro-lift in Tokens. Skipped entirely with reduced motion or no JS. */
.intro { display: none; }

@media (prefers-reduced-motion: no-preference) {
  .intro-on .intro {
    position: fixed;
    inset: 0;
    z-index: 100;
    display: grid;
    place-items: center;
    background: var(--bg);
    animation: intro-lift var(--intro-lift) var(--eio) var(--intro-hold) forwards;
  }
}

.intro-mark {
  display: grid;
  justify-items: center;
  gap: 14px;
  animation: intro-mark-out var(--intro-lift) var(--eio) var(--intro-hold) forwards;
}
.intro-name {
  font: 600 22px/1.2 var(--serif);
  letter-spacing: -.01em;
  color: var(--fg);
  animation: intro-in .6s var(--eio) .05s backwards;
}
/* The line: a faint track with the accent colour drawing across it. */
.intro-line {
  position: relative;
  width: 96px;
  height: 1px;
  overflow: hidden;
  background: var(--rule);
}
.intro-line::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--accent);
  transform-origin: left;
  scale: 0 1;
  animation: intro-draw calc(var(--intro-hold) - .1s) cubic-bezier(.65, 0, .35, 1) .1s forwards;
}

@keyframes intro-in       { from { opacity: 0; translate: 0 6px; } }
@keyframes intro-draw     { to { scale: 1 1; } }
@keyframes intro-mark-out { to { opacity: 0; translate: 0 -8px; } }
@keyframes intro-lift     { to { opacity: 0; visibility: hidden; } }

/* #endregion */


/* #region 12. Print ------------------------------------------------------- */

@media print {
  .top, .js .top { position: static; }
  .top::before, .top nav, .actions, form, footer, .intro, .menu, .menu-btn { display: none !important; }
  section { break-inside: avoid; }
  .js * { opacity: 1 !important; translate: none !important; scale: none !important; transform: none !important; }
}

/* #endregion */
