/*
 * ════════════════════════════════════════════════════════════════════
 *  CITY OPTIMISE SOCIETY — layout & components
 * ════════════════════════════════════════════════════════════════════
 *  Sections, in order:
 *    A. Reset & base typography
 *    B. Metaball canvas layer
 *    C. iOS liquid-glass navbar
 *    D. Page shell & masthead
 *    E. Bento tile grid
 *    F. Tile → sheet expansion (the iOS "app opening" morph)
 *    G. Collection lists (blog, reading)
 *    H. Post reader
 *    I. Footer
 *    J. Motion & reduced-motion gates
 *    K. Bubble-aware copy (legibility over the metaballs)
 * ════════════════════════════════════════════════════════════════════
 */

/* ── A. Reset & base ────────────────────────────────────────────── */

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

html {
  scroll-behavior: smooth;
  color-scheme: light dark;
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  min-height: 100vh;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-text);
  font-size: var(--step-0);
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
}

/*
 * A soft radial wash behind everything, so the page has depth even
 * before the WebGL metaballs come up (and on devices that skip them).
 */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background:
    radial-gradient(90% 60% at 12% -6%, var(--accent-soft), transparent 62%),
    radial-gradient(70% 50% at 96% 8%, rgba(224, 102, 47, 0.09), transparent 60%),
    linear-gradient(180deg, transparent 40%, var(--bg-deep) 140%);
}

img,
video,
canvas,
svg {
  max-width: 100%;
  display: block;
}

a {
  color: inherit;
}

button {
  font: inherit;
  color: inherit;
  cursor: pointer;
}

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

::selection {
  background: var(--accent);
  color: #fff;
}

h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 400;
  font-variation-settings: "SOFT" 28, "WONK" 1;
  letter-spacing: -0.015em;
  line-height: 1.06;
  color: var(--text-strong);
  margin: 0;
  text-wrap: balance;
}

/*
 * The one place Instrument Serif survives: the "City Optimise Society"
 * lockup on the homepage. Everything else display-weight is Fraunces.
 */
.wordmark {
  font-family: var(--font-wordmark);
  font-variation-settings: normal;
  letter-spacing: -0.01em;
}

/* The mono micro-label that appears above most blocks — a Chester-ism. */
.eyebrow {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-faint);
  margin: 0;
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 200;
  padding: 0.7rem 1.1rem;
  background: var(--surface-solid);
  border-radius: 0 0 12px 0;
}

.skip-link:focus {
  left: 0;
}

/* ── B. Metaball canvas ─────────────────────────────────────────── */

.page-frame {
  position: relative;
  z-index: 1;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

.bubbles-canvas {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.7s ease;
}

/*
 * The metaballs sit behind everything and the glass tiles blur them on
 * the way through — that refraction is what the whole liquid-glass look
 * is resting on, so don't give this a blend mode or raise it above the
 * content.
 */
.bubbles-canvas.is-active {
  opacity: 0.55;
}

/* Dark mode swallows them; give the metaballs a little more presence there. */
:root[data-theme="dark"] .bubbles-canvas.is-active {
  opacity: 0.8;
}

/*
 * Lift the in-flow children above the canvas. Listed explicitly rather
 * than as `> *:not(.bubbles-canvas)` — that blanket selector outranks
 * `.nav-dock` and would knock the navbar out of `position: fixed`.
 */
.page-frame > .shell,
.page-frame > .post-shell,
.page-frame > .site-footer {
  position: relative;
  z-index: 1;
}

/* ── C. iOS liquid-glass navbar ─────────────────────────────────── */

.nav-dock {
  position: fixed;
  z-index: 90;
  left: 50%;
  transform: translateX(-50%);
  bottom: max(1rem, env(safe-area-inset-bottom));
  display: flex;
  align-items: center;
  gap: 0.2rem;
  padding: 0.32rem;
  border-radius: var(--radius-pill);
  background: var(--glass-bg-nav);
  border: 1px solid var(--glass-border);
  box-shadow:
    0 1px 0 var(--glass-specular) inset,
    0 -1px 0 rgba(0, 0, 0, 0.04) inset,
    0 12px 40px -12px var(--glass-shadow),
    0 2px 8px -4px var(--glass-shadow);
  backdrop-filter: blur(var(--glass-blur-nav)) saturate(var(--glass-saturate));
  -webkit-backdrop-filter: blur(var(--glass-blur-nav)) saturate(var(--glass-saturate));
  max-width: calc(100vw - 1.5rem);
}

@media (min-width: 800px) {
  .nav-dock {
    bottom: auto;
    top: 1.1rem;
  }
}

/*
 * The sliding pill behind the active item. Positioned in JS (left/width
 * set from the active link's offsets) so it glides between items instead
 * of cross-fading — that glide is the iOS segmented-control tell.
 */
.nav-indicator {
  position: absolute;
  top: 0.32rem;
  bottom: 0.32rem;
  border-radius: var(--radius-pill);
  background: var(--surface-solid);
  box-shadow:
    0 1px 0 var(--glass-specular) inset,
    0 2px 8px -3px var(--glass-shadow);
  transition: left var(--dur-mid) var(--ease-ios),
              width var(--dur-mid) var(--ease-ios),
              opacity var(--dur-fast) ease;
  opacity: 0;
  pointer-events: none;
  z-index: 0;
}

.nav-indicator.is-ready {
  opacity: 1;
}

.nav-link {
  position: relative;
  z-index: 1;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.5rem 0.95rem;
  border-radius: var(--radius-pill);
  font-size: var(--step--1);
  font-weight: 500;
  letter-spacing: 0.01em;
  text-decoration: none;
  color: var(--text-soft);
  white-space: nowrap;
  transition: color var(--dur-fast) ease, transform var(--dur-fast) var(--ease-out);
}

.nav-link:hover {
  color: var(--text-strong);
}

.nav-link:active {
  transform: scale(0.94);
}

.nav-link.is-active {
  color: var(--text-strong);
  font-weight: 600;
}

.nav-link svg {
  width: 15px;
  height: 15px;
  flex: none;
  fill: currentColor;
}

.nav-link-label {
  display: none;
}

@media (min-width: 520px) {
  .nav-link-label {
    display: inline;
  }
}

.nav-sep {
  width: 1px;
  align-self: stretch;
  margin: 0.35rem 0.25rem;
  background: var(--border);
}

.theme-toggle {
  position: relative;
  z-index: 1;
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  border: 0;
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--text-soft);
  transition: color var(--dur-fast) ease, background var(--dur-fast) ease,
              transform var(--dur-fast) var(--ease-out);
}

.theme-toggle:hover {
  color: var(--text-strong);
  background: var(--hover);
}

.theme-toggle:active {
  transform: scale(0.9);
}

.theme-toggle svg {
  width: 16px;
  height: 16px;
  fill: currentColor;
}

/* Only one of the two glyphs shows, decided by the resolved theme. */
.theme-toggle .icon-moon { display: none; }
:root[data-theme="dark"] .theme-toggle .icon-sun { display: none; }
:root[data-theme="dark"] .theme-toggle .icon-moon { display: block; }

/*
 * The metaball toggle, sharing the theme button's shape. Solid bubbles while
 * the animation is running, struck-through outlines while it isn't — same
 * "filled = live, outline = muted" reading as the sun/moon pair. Driven off
 * data-animations, which the <head> bootstrap sets before first paint, so
 * the button never renders in the wrong state and then flips.
 */
.theme-toggle .icon-bubbles-off {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  stroke-linecap: round;
}

.theme-toggle .icon-bubbles { display: none; }
:root[data-animations="on"] .theme-toggle .icon-bubbles { display: block; }
:root[data-animations="on"] .theme-toggle .icon-bubbles-off { display: none; }

/* Off is a deliberate state, not a disabled one — keep it legible. */
.anim-toggle[aria-pressed="false"] {
  color: var(--text-faint);
}

/* ── D. Page shell & masthead ───────────────────────────────────── */

.shell {
  width: 100%;
  max-width: 1180px;
  margin: 0 auto;
  padding: 4.5rem 1.25rem 8rem;
  flex: 1;
}

@media (min-width: 800px) {
  .shell {
    padding-top: 7rem;
  }
}

.masthead {
  margin-bottom: 3.5rem;
  max-width: 46rem;
}

.masthead-mark {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  margin-bottom: 1.6rem;
  padding: 0.34rem 0.85rem 0.34rem 0.4rem;
  border-radius: var(--radius-pill);
  border: 1px solid var(--border);
  background: var(--glass-bg);
  backdrop-filter: blur(var(--glass-blur-subtle)) saturate(var(--glass-saturate));
  -webkit-backdrop-filter: blur(var(--glass-blur-subtle)) saturate(var(--glass-saturate));
  font-family: var(--font-mono);
  font-size: var(--step--1);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-soft);
  text-decoration: none;
}

.masthead-glyph {
  width: 24px;
  height: 24px;
  flex: none;
}

/* The full crest, sitting above the lockup on the homepage. */
.masthead-crest {
  width: clamp(92px, 13vw, 132px);
  height: auto;
  margin-bottom: 1.1rem;
}

.footer-crest {
  width: 26px;
  height: 26px;
  flex: none;
}

.masthead h1 {
  font-size: var(--step-4);
}

.masthead h1 em {
  font-style: italic;
  color: var(--accent);
}

.masthead-lede {
  margin: 1.4rem 0 0;
  font-size: var(--step-1);
  line-height: 1.55;
  color: var(--text-soft);
  max-width: 38rem;
  text-wrap: pretty;
}

.masthead-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.4rem;
  margin-top: 1.8rem;
  font-family: var(--font-mono);
  font-size: var(--step--1);
  color: var(--text-faint);
}

.masthead-meta strong {
  color: var(--text);
  font-weight: 500;
}

/* Section heading used above each grid / list. */
.section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  margin: 0 0 1.2rem;
  padding-bottom: 0.7rem;
  border-bottom: 1px solid var(--border);
}

.section-head h2 {
  font-size: var(--step-2);
}

.section-count {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  color: var(--text-faint);
  white-space: nowrap;
}

/* ── E. Bento tile grid ─────────────────────────────────────────── */

.bento {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.9rem;
}

@media (min-width: 720px) {
  .bento {
    grid-template-columns: repeat(6, minmax(0, 1fr));
    gap: 1rem;
  }
}

/*
 * Span classes let each tile claim its own footprint — the asymmetry is
 * the point, so they're set per-tile in the markup rather than derived.
 */
.tile {
  position: relative;
  grid-column: span 2;
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
  min-height: 168px;
  padding: 1.35rem;
  border: 1px solid var(--glass-border);
  border-radius: var(--radius-tile);
  /* Tint and blur are restated in section K, which thickens both while a
     metaball is behind the tile — edit the tokens, not these two lines. */
  background: var(--glass-bg);
  box-shadow: var(--shadow-tile);
  backdrop-filter: blur(var(--glass-blur-tile)) saturate(var(--glass-saturate));
  -webkit-backdrop-filter: blur(var(--glass-blur-tile)) saturate(var(--glass-saturate));
  text-align: left;
  text-decoration: none;
  color: inherit;
  overflow: hidden;
  isolation: isolate;
  transition: transform var(--dur-mid) var(--ease-out),
              box-shadow var(--dur-mid) var(--ease-out),
              border-color var(--dur-mid) ease;
}

/* Specular sheen across the top edge — the "liquid" in liquid glass. */
.tile::before {
  content: "";
  position: absolute;
  inset: 0 0 auto;
  height: 46%;
  z-index: -1;
  background: linear-gradient(180deg, var(--glass-specular), transparent);
  opacity: 0.5;
  pointer-events: none;
}

/* Accent bloom that lights up on hover, tinted per tile via --tile-hue. */
.tile::after {
  content: "";
  position: absolute;
  inset: -30% -10% auto -10%;
  height: 90%;
  z-index: -1;
  background: radial-gradient(60% 100% at 50% 0%, var(--tile-hue, var(--accent-soft)), transparent 70%);
  opacity: 0;
  transition: opacity var(--dur-mid) ease;
  pointer-events: none;
}

.tile:hover,
.tile:focus-visible {
  transform: translateY(-4px);
  box-shadow: var(--shadow-tile-hover);
  border-color: var(--accent);
}

.tile:hover::after,
.tile:focus-visible::after {
  opacity: 1;
}

.tile:active {
  transform: translateY(-1px) scale(0.988);
}

@media (min-width: 720px) {
  .tile-w2 { grid-column: span 2; }
  .tile-w3 { grid-column: span 3; }
  .tile-w4 { grid-column: span 4; }
  .tile-w6 { grid-column: span 6; }
  .tile-tall { min-height: 300px; }
  .tile-hero { min-height: 260px; }
}

.tile-icon {
  font-size: 1.5rem;
  line-height: 1;
}

.tile-eyebrow {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.tile h3 {
  font-size: var(--step-1);
  line-height: 1.15;
}

.tile-hero h3 {
  font-size: var(--step-3);
}

.tile p {
  margin: 0;
  font-size: var(--step--1);
  line-height: 1.6;
  color: var(--text-soft);
  text-wrap: pretty;
}

.tile-spacer {
  flex: 1;
  min-height: 0.4rem;
}

.tile-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  font-family: var(--font-mono);
  font-size: var(--step--1);
  color: var(--text-faint);
}

/* The little arrow chip in the corner, mirroring Marco's tiles. */
.tile-arrow {
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  flex: none;
  border-radius: var(--radius-pill);
  border: 1px solid var(--border);
  color: var(--text-soft);
  transition: transform var(--dur-mid) var(--ease-out),
              background var(--dur-mid) ease, color var(--dur-mid) ease,
              border-color var(--dur-mid) ease;
}

/* Drawn as a stroked path, not a filled one — the arrow is two strokes. */
.tile-arrow svg {
  width: 13px;
  height: 13px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2.2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.tile:hover .tile-arrow,
.tile:focus-visible .tile-arrow {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
  transform: translate(2px, -2px);
}

/* Big number used on the funding tile. */
.tile-figure {
  font-family: var(--font-display);
  font-size: var(--step-4);
  line-height: 0.92;
  letter-spacing: -0.03em;
  color: var(--text-strong);
}

.tile-figure small {
  display: block;
  margin-top: 0.5rem;
  font-family: var(--font-mono);
  font-size: var(--step--1);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-faint);
}

/* Checklist used inside the funding-use tile and sheets. */
.check-list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: 0.55rem;
}

.check-list li {
  display: grid;
  grid-template-columns: 1.1rem 1fr;
  gap: 0.6rem;
  font-size: var(--step--1);
  line-height: 1.55;
  color: var(--text-soft);
}

.check-list li::before {
  content: "✓";
  color: var(--accent);
  font-weight: 700;
  line-height: 1.5;
}

/* People rows on the credits tile. */
.person {
  display: flex;
  align-items: center;
  gap: 0.7rem;
}

.person-avatar {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  flex: none;
  border-radius: var(--radius-pill);
  background: var(--accent-soft);
  border: 1px solid var(--border);
  font-family: var(--font-mono);
  font-size: var(--step--1);
  color: var(--accent-ink);
}

.person-name {
  font-weight: 600;
  color: var(--text-strong);
  font-size: var(--step-0);
}

.person-role {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--step--1);
  color: var(--text-faint);
}

/* Tag chips, shared with the collection lists. */
.chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
}

.chip {
  display: inline-block;
  padding: 0.18rem 0.55rem;
  border-radius: var(--radius-pill);
  border: 1px solid var(--border);
  background: var(--accent-soft);
  font-family: var(--font-mono);
  font-size: 0.7rem;
  letter-spacing: 0.04em;
  color: var(--text-soft);
  white-space: nowrap;
}

/* ── F. Tile → sheet expansion ──────────────────────────────────── */
/*
 * Clicking a tile morphs it into a full sheet: JS clones the tile's
 * bounding rect onto the sheet, then releases it to its final size in
 * one spring. The original tile stays put but dims, so the page beneath
 * reads as "still there", the way an iOS app icon does while opening.
 */

.sheet-backdrop {
  position: fixed;
  inset: 0;
  z-index: 100;
  background: rgba(12, 10, 22, 0.42);
  backdrop-filter: blur(5px);
  -webkit-backdrop-filter: blur(5px);
  opacity: 0;
  transition: opacity var(--dur-mid) ease;
}

.sheet-backdrop.is-open {
  opacity: 1;
}

.sheet-backdrop[hidden] {
  display: none;
}

.sheet {
  position: fixed;
  z-index: 101;
  overflow: hidden;
  border: 1px solid var(--glass-border);
  border-radius: var(--radius-tile);
  background: var(--glass-bg-strong);
  box-shadow: 0 40px 90px -30px rgba(0, 0, 0, 0.5);
  backdrop-filter: blur(var(--glass-blur-sheet)) saturate(var(--glass-saturate));
  -webkit-backdrop-filter: blur(var(--glass-blur-sheet)) saturate(var(--glass-saturate));
  /* left/top/width/height are driven from JS; the transition here is what
     actually performs the morph from tile rect → sheet rect. */
  transition: left var(--dur-slow) var(--ease-ios),
              top var(--dur-slow) var(--ease-ios),
              width var(--dur-slow) var(--ease-ios),
              height var(--dur-slow) var(--ease-ios),
              border-radius var(--dur-slow) var(--ease-ios),
              opacity var(--dur-fast) ease;
}

.sheet[hidden] {
  display: none;
}

.sheet.is-open {
  border-radius: var(--radius-sheet);
}

.sheet-scroll {
  height: 100%;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}

.sheet-bar {
  position: sticky;
  top: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.85rem 1.1rem;
  background: var(--glass-bg-nav);
  border-bottom: 1px solid var(--border);
  backdrop-filter: blur(var(--glass-blur-nav)) saturate(var(--glass-saturate));
  -webkit-backdrop-filter: blur(var(--glass-blur-nav)) saturate(var(--glass-saturate));
}

.sheet-bar-title {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-soft);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.sheet-close {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  flex: none;
  padding: 0.35rem 0.8rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: var(--surface-solid);
  font-size: var(--step--1);
  font-weight: 500;
  transition: transform var(--dur-fast) var(--ease-out), background var(--dur-fast) ease;
}

.sheet-close:hover {
  background: var(--hover);
}

.sheet-close:active {
  transform: scale(0.93);
}

.sheet-body {
  padding: 2rem 1.5rem 3rem;
  max-width: var(--measure);
  margin: 0 auto;
  /* Contents fade up slightly after the morph starts, so the frame
     lands first and the text arrives into it. */
  opacity: 0;
  transform: translateY(14px);
  transition: opacity var(--dur-mid) ease var(--dur-fast),
              transform var(--dur-mid) var(--ease-out) var(--dur-fast);
}

.sheet.is-open .sheet-body {
  opacity: 1;
  transform: none;
}

@media (min-width: 720px) {
  .sheet-body {
    padding: 2.6rem 2.5rem 4rem;
  }
}

.sheet-body h2 {
  font-size: var(--step-3);
  margin-bottom: 0.9rem;
}

/* While a sheet is open the page behind is locked and pushed back. */
body.is-sheet-open {
  overflow: hidden;
}

.tile.is-source {
  opacity: 0.25;
  transform: scale(0.985);
}

/* ── G. Collection lists (blog, reading) ────────────────────────── */

.filter-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 1.5rem;
}

.filter-search {
  flex: 1 1 12rem;
  min-width: 0;
  padding: 0.5rem 0.9rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: var(--glass-bg);
  color: var(--text);
  font: inherit;
  font-size: var(--step--1);
  backdrop-filter: blur(var(--glass-blur-subtle));
  -webkit-backdrop-filter: blur(var(--glass-blur-subtle));
}

.filter-search::placeholder {
  color: var(--text-faint);
}

.filter-chip {
  padding: 0.4rem 0.8rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: transparent;
  font-family: var(--font-mono);
  font-size: var(--step--1);
  color: var(--text-soft);
  transition: background var(--dur-fast) ease, color var(--dur-fast) ease,
              border-color var(--dur-fast) ease, transform var(--dur-fast) var(--ease-out);
}

.filter-chip:hover {
  background: var(--hover);
  color: var(--text-strong);
}

.filter-chip:active {
  transform: scale(0.94);
}

.filter-chip.is-active {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}

/*
 * Chester's reading page reads as a ruled list, not cards — one row per
 * entry, metadata in mono on the right, hairline between rows.
 */
.entry-list {
  list-style: none;
  margin: 0;
  padding: 0;
  border-top: 1px solid var(--border);
}

.entry {
  border-bottom: 1px solid var(--border);
}

.entry-link {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0.35rem;
  padding: 1.15rem 0.6rem;
  text-decoration: none;
  color: inherit;
  border-radius: 14px;
  transition: background var(--dur-fast) ease, padding-left var(--dur-mid) var(--ease-out);
}

.entry-link:hover {
  background: var(--hover);
  padding-left: 1rem;
}

@media (min-width: 680px) {
  .entry-link {
    grid-template-columns: 1fr auto;
    align-items: baseline;
    column-gap: 1.5rem;
  }
}

.entry-title {
  font-family: var(--font-display);
  font-size: var(--step-1);
  line-height: 1.2;
  color: var(--text-strong);
}

.entry-link:hover .entry-title {
  color: var(--accent);
}

.entry-meta {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  color: var(--text-faint);
  white-space: nowrap;
}

.entry-summary {
  grid-column: 1 / -1;
  margin: 0;
  font-size: var(--step--1);
  line-height: 1.6;
  color: var(--text-soft);
  max-width: 60ch;
  text-wrap: pretty;
}

.entry-chips {
  grid-column: 1 / -1;
  margin-top: 0.2rem;
}

.entry-author {
  font-size: var(--step--1);
  color: var(--text-soft);
  font-style: italic;
}

.list-empty,
.list-loading {
  padding: 3rem 0;
  font-family: var(--font-mono);
  font-size: var(--step--1);
  color: var(--text-faint);
  text-align: center;
}

/* ── H. Post reader ────────────────────────────────────────────── */

.read-progress {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 95;
  height: 3px;
  width: 0;
  background: var(--accent);
  transition: width 80ms linear;
}

.post-shell {
  width: 100%;
  max-width: 1180px;
  margin: 0 auto;
  padding: 4.5rem 1.25rem 8rem;
  flex: 1;
}

@media (min-width: 800px) {
  .post-shell {
    padding-top: 7rem;
  }
}

.post-layout {
  display: grid;
  gap: 2.5rem;
}

@media (min-width: 1000px) {
  .post-layout {
    grid-template-columns: minmax(0, 1fr) 15rem;
    align-items: start;
  }
}

.post-article {
  min-width: 0;
  max-width: var(--measure);
}

.post-back {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  margin-bottom: 2rem;
  padding: 0.4rem 0.85rem 0.4rem 0.6rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: var(--glass-bg);
  font-family: var(--font-mono);
  font-size: var(--step--1);
  color: var(--text-soft);
  text-decoration: none;
  backdrop-filter: blur(var(--glass-blur-subtle));
  -webkit-backdrop-filter: blur(var(--glass-blur-subtle));
  transition: transform var(--dur-fast) var(--ease-out), color var(--dur-fast) ease;
}

.post-back:hover {
  color: var(--text-strong);
  transform: translateX(-3px);
}

.post-back svg {
  width: 13px;
  height: 13px;
  fill: currentColor;
}

.post-meta-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
  margin-bottom: 1rem;
}

.post-date {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  color: var(--text-faint);
}

.post-title {
  font-size: var(--step-3);
  margin-bottom: 0.9rem;
}

.post-summary {
  margin: 0 0 2rem;
  font-size: var(--step-1);
  line-height: 1.55;
  color: var(--text-soft);
  text-wrap: pretty;
}

.post-cover {
  width: 100%;
  border-radius: 18px;
  border: 1px solid var(--border);
  margin-bottom: 2.5rem;
}

.post-footer {
  margin-top: 4rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--border);
}

/* Sticky table of contents, desktop only. */
.post-toc {
  display: none;
}

@media (min-width: 1000px) {
  .post-toc {
    display: block;
    position: sticky;
    top: 6.5rem;
    max-height: calc(100vh - 9rem);
    overflow-y: auto;
    padding: 1.1rem;
    border: 1px solid var(--glass-border);
    border-radius: 20px;
    background: var(--glass-bg);
    backdrop-filter: blur(var(--glass-blur-tile)) saturate(var(--glass-saturate));
    -webkit-backdrop-filter: blur(var(--glass-blur-tile)) saturate(var(--glass-saturate));
  }
}

.toc-label {
  margin: 0 0 0.7rem;
  font-family: var(--font-mono);
  font-size: var(--step--1);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.toc-nav {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
}

.toc-link {
  padding: 0.3rem 0.5rem;
  padding-left: calc(0.5rem + var(--toc-indent, 0) * 0.75rem);
  border-radius: 8px;
  border-left: 2px solid transparent;
  font-size: var(--step--1);
  line-height: 1.4;
  color: var(--text-soft);
  text-decoration: none;
  transition: color var(--dur-fast) ease, border-color var(--dur-fast) ease,
              background var(--dur-fast) ease;
}

.toc-link:hover {
  color: var(--text-strong);
  background: var(--hover);
}

.toc-link.is-active {
  color: var(--accent);
  border-left-color: var(--accent);
  font-weight: 600;
}

.state-panel {
  padding: 5rem 0;
  text-align: center;
}

.state-panel[hidden] {
  display: none;
}

.spinner {
  width: 26px;
  height: 26px;
  margin: 0 auto 1rem;
  border: 2px solid var(--border);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

/* ── I. Footer ─────────────────────────────────────────────────── */

.site-footer {
  border-top: 1px solid var(--border);
  padding: 2.5rem 1.25rem 7rem;
  background: var(--glass-bg);
  backdrop-filter: blur(var(--glass-blur-subtle));
  -webkit-backdrop-filter: blur(var(--glass-blur-subtle));
}

@media (min-width: 800px) {
  .site-footer {
    padding-bottom: 3rem;
  }
}

.footer-inner {
  width: 100%;
  max-width: 1180px;
  margin: 0 auto;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  font-family: var(--font-mono);
  font-size: var(--step--1);
  color: var(--text-faint);
}

.footer-mark {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
}

.footer-links {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
}

.footer-links a {
  color: var(--text-soft);
  text-decoration: none;
  border-bottom: 1px solid transparent;
}

.footer-links a:hover {
  color: var(--accent);
  border-bottom-color: currentColor;
}

/* ── J. Motion gates ───────────────────────────────────────────── */

/*
 * Scroll reveal, gated on `data-reveal="armed"` — which nav.js sets only
 * once it has actually constructed the observer. Without that gate, a
 * `.reveal` element starts at opacity 0 and stays there forever if the
 * script fails to load or IntersectionObserver never fires, which loses
 * the content outright. Hiding is therefore opt-in by the code that can
 * guarantee the un-hiding.
 */
:root[data-reveal="armed"][data-animations="on"] .reveal {
  opacity: 0;
  transform: translateY(18px);
}

:root[data-reveal="armed"][data-animations="on"] .reveal.is-in {
  animation: reveal-in 0.7s var(--ease-out) both;
}

@keyframes reveal-in {
  from { opacity: 0; transform: translateY(18px); }
  to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}

/* ── K. Image lightbox (post reader) ───────────────────────────── */

.lightbox {
  max-width: 94vw;
  max-height: 94vh;
  padding: 0;
  border: 1px solid var(--glass-border);
  border-radius: 18px;
  background: var(--surface-solid);
  overflow: hidden;
}

.lightbox::backdrop {
  background: rgba(12, 10, 22, 0.72);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}

.lightbox img {
  max-width: 94vw;
  max-height: 94vh;
  width: auto;
  height: auto;
  cursor: zoom-out;
}

/* ── L. Standalone pages (/about/, /now/, /projects/ …) ─────────── */

.page-body {
  display: grid;
  gap: 2.5rem;
}

@media (min-width: 1060px) {
  .page-body {
    grid-template-columns: minmax(0, 1fr) 14rem;
    align-items: start;
  }
}

/*
 * Prose measure with full-bleed blocks: the article itself fills its grid
 * column, and each direct child is capped at the reading measure — except
 * the hydrated tile and list blocks, which are meant to span the column.
 */
.page-article {
  min-width: 0;
}

.page-article > * {
  max-width: var(--measure);
}

.page-article > .md-bento,
.page-article > .md-entry-list,
.page-article > table,
.page-article > .md-code-block {
  max-width: none;
}

.page-updated {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  color: var(--text-faint);
}

.page-updated[hidden] {
  display: none;
}

#page-toc {
  position: sticky;
  top: 6.5rem;
}

/*
 * Blocks hydrated by page.js out of ```tiles / ```links fences. They reuse
 * the home page's .tile and the collection pages' .entry-list wholesale;
 * these rules only re-set the spacing for sitting inside prose.
 */
.markdown-content .md-bento {
  margin: 2rem 0 2.5rem;
}

.markdown-content .md-entry-list {
  margin: 1.5rem 0 2.5rem;
}

/* Prose sets a max-width on p/li; inside a tile the tile is the measure. */
.markdown-content .md-bento p,
.markdown-content .md-bento li,
.markdown-content .md-entry-list p {
  max-width: none;
  margin: 0;
  font-size: var(--step--1);
}

.markdown-content .md-bento h3 {
  margin: 0;
  font-size: var(--step-1);
}

.markdown-content .md-entry-list .entry-title {
  font-size: var(--step-1);
}

/* A links row that isn't a link shouldn't pretend to be interactive. */
.markdown-content .md-entry-list div.entry-link {
  cursor: default;
}

.markdown-content .md-entry-list div.entry-link:hover {
  background: none;
  padding-left: 0.6rem;
}

/* A sheet is a summary; this is its hand-off to the full page. */
.sheet-more {
  margin-top: 2rem;
  padding-top: 1.2rem;
  border-top: 1px solid var(--border);
}

.sheet-more a {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  color: var(--accent);
  text-decoration: none;
  border-bottom: 1px solid transparent;
}

.sheet-more a:hover {
  border-bottom-color: currentColor;
}

.more-section {
  margin-top: 4rem;
}


/* ── K. Bubble-aware copy ───────────────────────────────────────── */
/*
 * Copy that sits straight on the page — the masthead, section heads, the
 * ruled entry lists — loses
 * contrast whenever a metaball drifts under it, and the glass tiles wash out
 * when a lit one comes through the blur. assets/js/bubble-legibility.js
 * samples the field about ten times a second (bubbles.js renders a tiny
 * offscreen copy and reads it back) and writes --bubble-cover, 0..1, onto
 * each element below: 0 = clear page behind it, 1 = sitting on a lit blob.
 *
 * Two things ride on that number — a scrim that fades in behind the type,
 * blurring and knocking back the blob, and a shift in the type's own colour
 * toward --bubble-ink — so a bubble passing behind the headline reads as a
 * deliberate cool-lit state rather than as a contrast accident. Nothing here
 * needs a fallback path: with no WebGL, no render-to-texture, or the
 * animation toggle off, --bubble-cover is never set and every rule collapses
 * to its 0 case, which is the design as it was before any of this.
 */

.masthead h1,
.masthead-lede,
.masthead-meta,
.section-head,
.entry-link,
[data-bubble-aware] {
  --bubble-cover: 0;
  /* How far the scrim spills past the copy it is shielding. Blocks that
     stand alone can afford a wide, soft halo; rows stacked against their
     neighbours keep it tight so the veils don't pile up where they meet. */
  --bubble-scrim-inset: -0.8em -1.5em;
  position: relative;
  transition: color var(--dur-mid) ease;
}

/* The head is treated whole rather than just its h2, so the count label on
   the far side of the row is shielded by the same veil. */
.section-head {
  --bubble-scrim-inset: -0.35em -1.1em;
}

.entry-link {
  --bubble-scrim-inset: 0 -0.9em;
}

/*
 * The scrim, mounted only while the animation layer is on — a backdrop-filter
 * costs its element a composited layer even at opacity 0, and with the
 * metaballs switched off there is nothing to shield the copy from.
 *
 * Masked to a soft ellipse: without it both the tint and the blur would show
 * a hard rectangular edge over the blob. It has to stay a plain positioned
 * pseudo-element (no `isolation`, no `filter` on any ancestor) or it would
 * become its own backdrop root and blur nothing but empty page.
 */
:root[data-animations="on"] .masthead h1::before,
:root[data-animations="on"] .masthead-lede::before,
:root[data-animations="on"] .masthead-meta::before,
:root[data-animations="on"] .section-head::before,
:root[data-animations="on"] .entry-link::before,
:root[data-animations="on"] [data-bubble-aware]::before {
  content: "";
  position: absolute;
  z-index: -1;
  inset: var(--bubble-scrim-inset);
  background: var(--bubble-scrim);
  backdrop-filter: var(--bubble-scrim-filter);
  -webkit-backdrop-filter: var(--bubble-scrim-filter);
  /*
   * Feathered well inside its own box — an ellipse that has faded out
   * before it reaches the corners, so what the eye reads is a soft patch of
   * calmer background, not a rounded rectangle sitting on the artwork.
   */
  -webkit-mask-image: radial-gradient(closest-side, #000 46%, rgba(0, 0, 0, 0.6) 74%, transparent 97%);
  mask-image: radial-gradient(closest-side, #000 46%, rgba(0, 0, 0, 0.6) 74%, transparent 97%);
  opacity: var(--bubble-cover);
  transition: opacity var(--dur-mid) ease;
  pointer-events: none;
}

/*
 * The colour shift. Each line keeps its plain colour first so browsers
 * without color-mix() drop only the shift and keep the scrim.
 */
.masthead h1 {
  color: var(--text-strong);
  color: color-mix(in oklab, var(--text-strong), var(--bubble-ink) calc(var(--bubble-cover) * 100%));
}

/* The accent italic gives up less of its hue — enough to cool with the rest
   of the lockup, not so much that "Society" stops being the accent word. */
.masthead h1 em {
  color: var(--accent);
  color: color-mix(in oklab, var(--accent), var(--bubble-ink) calc(var(--bubble-cover) * 78%));
}

.masthead-lede {
  color: var(--text-soft);
  color: color-mix(in oklab, var(--text-soft), var(--bubble-ink-soft) calc(var(--bubble-cover) * 100%));
}

.masthead-meta {
  color: var(--text-faint);
  color: color-mix(in oklab, var(--text-faint), var(--bubble-ink-faint) calc(var(--bubble-cover) * 100%));
}

.masthead-meta strong {
  color: var(--text);
  color: color-mix(in oklab, var(--text), var(--bubble-ink) calc(var(--bubble-cover) * 100%));
}

.section-head h2 {
  color: var(--text-strong);
  color: color-mix(in oklab, var(--text-strong), var(--bubble-ink) calc(var(--bubble-cover) * 100%));
}

.section-count {
  color: var(--text-faint);
  color: color-mix(in oklab, var(--text-faint), var(--bubble-ink-faint) calc(var(--bubble-cover) * 100%));
}

/*
 * Ruled lists — the homepage's "More" block, and the blog and reading
 * indexes, which are the same markup. Each row shields itself, so a blob
 * crossing the list lights up only the rows it is actually behind.
 */
.entry-title {
  color: var(--text-strong);
  color: color-mix(in oklab, var(--text-strong), var(--bubble-ink) calc(var(--bubble-cover) * 100%));
}

/* Keeps the hover accent, cooled by the same amount as the masthead's
   italic — without this the plain .entry-title mix below would be dropped
   by the earlier hover rule's higher specificity. */
.entry-link:hover .entry-title {
  color: var(--accent);
  color: color-mix(in oklab, var(--accent), var(--bubble-ink) calc(var(--bubble-cover) * 78%));
}

.entry-meta {
  color: var(--text-faint);
  color: color-mix(in oklab, var(--text-faint), var(--bubble-ink-faint) calc(var(--bubble-cover) * 100%));
}

.entry-summary,
.entry-author {
  color: var(--text-soft);
  color: color-mix(in oklab, var(--text-soft), var(--bubble-ink-soft) calc(var(--bubble-cover) * 100%));
}

/*
 * Tiles already blur what is behind them, so instead of a scrim they thicken
 * their own tint toward the solid panel colour and pull the blob's brightness
 * back through the glass — dark mode knocks a lit blob down, light mode
 * lifts a lilac one toward the paper.
 */
.tile {
  --bubble-cover: 0;
  background: var(--glass-bg);
  background: color-mix(in srgb, var(--glass-bg), var(--panel) calc(var(--bubble-cover) * 45%));
  backdrop-filter: blur(var(--glass-blur-tile)) saturate(var(--glass-saturate))
                   brightness(calc(1 + 0.14 * var(--bubble-cover)));
  -webkit-backdrop-filter: blur(var(--glass-blur-tile)) saturate(var(--glass-saturate))
                           brightness(calc(1 + 0.14 * var(--bubble-cover)));
  transition: transform var(--dur-mid) var(--ease-out),
              box-shadow var(--dur-mid) var(--ease-out),
              border-color var(--dur-mid) ease,
              background var(--dur-mid) ease;
}

:root[data-theme="dark"] .tile {
  backdrop-filter: blur(var(--glass-blur-tile)) saturate(var(--glass-saturate))
                   brightness(calc(1 - 0.3 * var(--bubble-cover)));
  -webkit-backdrop-filter: blur(var(--glass-blur-tile)) saturate(var(--glass-saturate))
                           brightness(calc(1 - 0.3 * var(--bubble-cover)));
}
