/* aleksandra.tech
   One stylesheet, one token set.

   The rule this file is built on: each level in the hierarchy differs from its
   neighbour by at least two of {size, weight, colour}. Separating two levels by
   2px, or by weight alone, does not read as a level. */

/* ---------------------------------------------------------------- fonts --- */

@font-face {
  font-family: "Inter var";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("/assets/fonts/inter-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Inter var";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("/assets/fonts/inter-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* --------------------------------------------------------------- tokens --- */

:root {
  color-scheme: light;

  /* Colour. Every pair below meets WCAG AA against its background — the
     previous palette's label colour (#989898 on #fefefe) was 2.9:1, on the
     smallest text on the site. */
  --bg: #fefefe;          /* --text 18.7:1  --muted 6.3:1  --faint 4.6:1 */
  --surface: #f5f5f3;
  --text: #111;
  --muted: #5f5f5f;
  --faint: #757575;
  --rule: #e3e3e0;
  --accent: #355d41;      /* 7.5:1 — carries links */
  --accent-soft: #e8efe9;

  /* Type. Four weights, really loaded, actually distinct. */
  --font: "Inter var", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

  --t-label: 0.75rem;                          /* 12 */
  --t-sm: 0.875rem;                            /* 14 */
  --t-body: 1.0625rem;                         /* 17 */
  --t-lead: clamp(1.125rem, 0.95rem + 0.7vw, 1.25rem);   /* 18 → 20 */
  --t-title: 1.3125rem;                        /* 21 */
  --t-h3: 1.5rem;                              /* 24 */
  --t-h2: clamp(1.5rem, 1.1rem + 1.6vw, 1.875rem);       /* 24 → 30 */
  --t-h1: clamp(2.25rem, 1.5rem + 3vw, 3.125rem);        /* 36 → 50 */

  --w-normal: 400;
  --w-medium: 500;
  --w-semi: 600;
  --w-bold: 700;

  /* Space, 4px base. */
  --s1: 0.25rem;  --s2: 0.5rem;   --s3: 0.75rem;  --s4: 1rem;
  --s5: 1.5rem;   --s6: 2rem;     --s7: 3rem;     --s8: 4rem;   --s9: 6rem;

  --r-sm: 8px;
  --r-md: 14px;
  --r-icon: 22px;
  --r-pill: 999px;

  /* One container for the whole site. Topbar, content and footer all sit in it,
     so every page has the same left edge and the same side padding — the whole
     site widens or narrows from this single value. Reading measure is still
     enforced per-component, so prose stays legible inside it. */
  --frame: 1120px;
  --measure: 68ch;
}

@media (prefers-color-scheme: dark) {
  :root {
    color-scheme: dark;
    --bg: #111;           /* --text 16.7:1  --muted 7.2:1  --faint 5.5:1 */
    --surface: #191a18;
    --text: #f1f1ee;
    --muted: #a0a0a0;
    --faint: #8a8a8a;
    --rule: #2f2f2f;
    --accent: #a7c5ad;    /* 10.1:1 */
    --accent-soft: #1c241e;
  }
}

/* ----------------------------------------------------------------- base --- */

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

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: var(--t-body);
  line-height: 1.6;
  font-synthesis-weight: none;
  -webkit-font-smoothing: antialiased;
}

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

h1, h2, h3, h4 { margin: 0; text-wrap: balance; }

/* Not balanced. Balancing evens out line lengths, which on a three-line post
   title shortened every line and left a wide ragged gap down the right. */
h1 { font-size: var(--t-h1); font-weight: var(--w-bold); line-height: 1.08; letter-spacing: -0.03em; text-wrap: pretty; }
h2 { font-size: var(--t-h2); font-weight: var(--w-semi); line-height: 1.2;  letter-spacing: -0.02em; }
h3 { font-size: var(--t-h3); font-weight: var(--w-semi); line-height: 1.3;  letter-spacing: -0.01em; }

p, ul, ol { margin: 0; }

a { color: inherit; }

/* Links inside running text are the one place --accent earns its keep. */
a:focus-visible,
button:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 2px;
}

/* The single label style. Previously this was written seven times with
   near-identical values; every date, kicker and eyebrow now points here. */
.label {
  font-size: var(--t-label);
  font-weight: var(--w-medium);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--faint);
  line-height: 1.4;
}

.lead {
  font-size: var(--t-lead);
  color: var(--muted);
  line-height: 1.55;
  max-width: 62ch;
}
/* .lead is also used on a wrapper around markdown from a page's .md file, which
   arrives as one or more <p>. They inherit the size; they need their own gap. */
.lead > p + p { margin-top: var(--s4); }
.lead a { color: var(--accent); }

.frame {
  width: min(calc(100% - var(--s8)), var(--frame));
  margin-inline: auto;
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: var(--s2);
  padding: var(--s2) var(--s4);
  background: var(--text);
  color: var(--bg);
  border-radius: var(--r-sm);
  z-index: 10;
}
.skip-link:focus { left: var(--s4); }

/* --------------------------------------------------------------- topbar --- */

/* The rule is what makes the header a band rather than a few links floating in
   space. It matters most on the home page, where there is no brand block: with
   nothing to the left of the nav, the gap above the h1 read as a hole. A closed
   band turns the same gap into the page's top margin. */
.topbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3) var(--s6);
  padding: var(--s4) 0;
  border-bottom: 1px solid var(--rule);
}

.brand {
  display: inline-flex;
  align-items: baseline;
  gap: var(--s2);
  text-decoration: none;
}

/* Deliberately below the page h1 and h2. The old brand block was 21px/550,
   which outranked most headings on the page beneath it. */
.brand-name {
  font-size: var(--t-body);
  font-weight: var(--w-semi);
  letter-spacing: -0.02em;
  line-height: 1.15;
}
.brand-role { font-size: var(--t-sm); color: var(--muted); line-height: 1.3; }

/* margin-left: auto, not justify-content on the parent: the brand is absent on
   the home page, and the nav has to stay in the same place either way. */
.nav {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s5);
  margin-left: auto;
  font-size: var(--t-sm);
  font-weight: var(--w-medium);
}

.nav a {
  text-decoration: none;
  color: var(--muted);
  padding: var(--s1) 0;
  border-bottom: 2px solid transparent;
}
.nav a:hover { color: var(--text); }
.nav a[aria-current="page"] { color: var(--text); border-bottom-color: var(--accent); }

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

.footer {
  margin-top: var(--s9);
  padding: var(--s5) 0 var(--s7);
  border-top: 1px solid var(--rule);
  display: flex;
  flex-wrap: wrap;
  gap: var(--s4) var(--s6);
  justify-content: space-between;
  align-items: baseline;
  font-size: var(--t-sm);
  color: var(--muted);
}

.footer-links { display: flex; flex-wrap: wrap; gap: var(--s4); }
.footer-links a { text-decoration: none; color: var(--muted); }
.footer-links a:hover { color: var(--text); text-decoration: underline; }

/* ------------------------------------------------------------ page head --- */

.page-head { padding: var(--s7) 0 var(--s6); }
.page-head h1 { margin-bottom: var(--s4); }
/* ----------------------------------------------------------------- home --- */

/* Same top padding as every other page's head now that the topbar closes with a
   rule — the space reads as the page's top margin instead of a gap beside the
   nav. */
.hero { padding: var(--s7) 0 var(--s6); }
.hero h1 { margin-bottom: var(--s4); }
.hero .lead { max-width: 60ch; }

.hero-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);
  margin-top: var(--s5);
}

/* The "Now" line. One rule-bounded row rather than a card: it earns a mention,
   not a fifth of the first screen. Same markup on the landing page and inside
   the About prose, so there is one treatment to maintain. */
/* Surface, not rules. Bounded by its own top and bottom rule it collided with
   the section rules around it — inside the About prose that made three
   horizontal lines within 100px, which read as a broken table. */
.now-line {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2) var(--s4);
  padding: var(--s3) var(--s4);
  background: var(--surface);
  border-radius: var(--r-md);
  margin-bottom: var(--s7);
  text-decoration: none;
}
.now-line:hover { background: var(--accent-soft); }
.now-line .label { display: flex; align-items: center; gap: var(--s2); flex: none; }
/* Title at list-row size and the arrow pushed to the far edge, so the row uses
   the width it occupies. At body size with the arrow tucked behind the title,
   the band ran the full frame with its content crowded into the first third and
   two thirds of it empty — which is what looked odd under a hero whose text
   stops well short of the frame. Bracketed left and right it reads like the
   rows on the Writing index and the tiles below it. */
.now-line-text {
  display: flex;
  flex: 1;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s4);
  font-size: var(--t-title);
  font-weight: var(--w-medium);
  letter-spacing: -0.01em;
}
.now-line-arrow { color: var(--accent); flex: none; transition: transform 0.18s ease; }
.now-line:hover .now-line-arrow { transform: translateX(4px); }

@media (prefers-reduced-motion: reduce) {
  .now-line-arrow { transition: none; }
  .now-line:hover .now-line-arrow { transform: none; }
}

/* Wrapped, the label sits on its own line and the title should start at the
   left edge again rather than stretching across a row of its own. */
@media (max-width: 620px) {
  .now-line-text { flex-basis: 100%; font-size: var(--t-body); }
}

/* The entry's own page. Its lead paragraph is the page's opening, so it reads at
   lead size rather than as the first paragraph of a body. */
/* No width cap here, on talk pages, on posts or on the 404 — every page is the
   width of the frame, and the text inside is what gets held to a reading
   measure. Capping the page instead meant /shipaton/ rendered the same tiles as
   the home page at a different size, and the 404 and talk pages each stopped at
   their own arbitrary width. */
.now-entry .article-head .label { display: flex; align-items: center; gap: var(--s2); }
.label-source { color: inherit; text-decoration: none; }
.label-source:hover { color: var(--accent); }
/* The intro gets the h1's width, not the reading measure. It sits directly under
   the title, and at --measure it stopped 170px short of where the title wrapped,
   which read as the paragraph being cropped rather than as a measure. The width
   has to come off this element, not the paragraph: .prose caps the parent, and a
   child cannot be wider than the box it is in. Everything after the intro is
   ordinary prose and goes back to --measure.
   Written as .prose.now-entry-body because .prose sets the measure and is
   declared later in this file, so a single class here would lose to it. */
.now-entry-body { margin-bottom: var(--s7); }
.prose.now-entry-body { max-width: 900px; }
.now-entry-body > * + * { max-width: var(--measure); }
.now-entry-body > p:first-child {
  font-size: var(--t-lead);
  color: var(--muted);
  line-height: 1.55;
}

/* Breathing, not blinking: opacity and scale only, slow enough to read as alive
   rather than as an alert. */
.dot {
  width: 7px;
  height: 7px;
  border-radius: var(--r-pill);
  background: var(--accent);
  flex: none;
  animation: dot-pulse 3s ease-in-out infinite;
}

@keyframes dot-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: 0.4; transform: scale(0.7); }
}

@media (prefers-reduced-motion: reduce) {
  .dot { animation: none; }
}

.section { margin-bottom: var(--s8); }

.section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s4);
  margin-bottom: var(--s5);
  padding-bottom: var(--s3);
  border-bottom: 1px solid var(--rule);
}
.section-head a {
  font-size: var(--t-sm);
  font-weight: var(--w-medium);
  color: var(--accent);
  text-decoration: none;
  white-space: nowrap;
}
.section-head a:hover { text-decoration: underline; }

/* ------------------------------------------------------------- app tiles --- */

/* Named class, not a bare `span`: a bare selector also matched the wrapper, so
   every app name silently inherited the muted description colour. */
.app-name {
  display: block;
  font-size: var(--t-title);
  font-weight: var(--w-semi);
  letter-spacing: -0.015em;
  color: var(--text);
  line-height: 1.25;
}

/* Large icons, room for the description, and nothing hidden behind hover —
   which would put it out of reach on a phone. */
.app-showcase {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s5);
}

.app-tile {
  display: grid;
  grid-template-columns: 112px minmax(0, 1fr);
  gap: var(--s5);
  padding: var(--s6);
  border: 1px solid var(--rule);
  border-radius: var(--r-md);
  text-decoration: none;
}
.app-tile:hover { border-color: var(--accent); background: var(--surface); }
.app-tile picture { display: block; align-self: start; }
.app-tile img { width: 112px; height: 112px; border-radius: 26px; }

/* Not a link, so it must not offer to be one: no hover, and a dashed icon well
   in place of an icon it doesn't have yet. */
.app-tile-quiet { border-style: dashed; }
.app-tile-quiet:hover { border-color: var(--rule); background: none; }
.app-tile-unknown {
  display: grid;
  place-items: center;
  width: 112px;
  height: 112px;
  align-self: start;
  border: 2px dashed var(--rule);
  border-radius: 26px;
  font-size: 44px;
  font-weight: var(--w-bold);
  line-height: 1;
  color: var(--faint);
}
.app-tile-quiet .app-name { color: var(--muted); }

/* Column, not a grid of auto rows, so the call to action can be pushed to the
   bottom. Cards in a row stretch to a shared height, and without this the CTA
   floated at whatever height its own description happened to end at — two
   cards side by side never lined up. */
.app-tile-body { display: flex; flex-direction: column; gap: var(--s2); }
.app-tile .app-name { font-size: var(--t-h3); }
.app-tile-tagline { font-size: var(--t-body); color: var(--muted); line-height: 1.55; }
/* Arrow to the trailing edge: on its own at the left it read as a stray glyph
   under the text, and every other pairing on the site (row title/date, talk
   event/recording) puts the secondary mark on the right. */
.app-tile-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: var(--s3);
  margin-top: auto;
  padding-top: var(--s4);
}
/* An arrow with no label, so it has to carry the affordance on its own: bigger
   than body text, accent-coloured, and it moves on hover. */
/* Pushed to the left of the row by its own margin rather than by
   space-between on the parent, so a tile with no mark still keeps its arrow
   at the right edge. */
.shipaton-mark {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  margin-right: auto;
  font-size: var(--t-label);
  font-weight: var(--w-medium);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--faint);
}
.app-tile .shipaton-mark img { width: 28px; height: 26px; border-radius: 0; }

.app-tile-cta {
  font-size: var(--t-title);
  line-height: 1;
  color: var(--accent);
  transition: transform 0.18s ease;
}
.app-tile:hover .app-tile-cta { transform: translateX(4px); }

@media (prefers-reduced-motion: reduce) {
  .app-tile-cta { transition: none; }
  .app-tile:hover .app-tile-cta { transform: none; }
}

/* ------------------------------------------ directory list (rows to scan) -- */

.row-list { border-top: 1px solid var(--rule); }

/* The hover state is a padded, rounded panel rather than a bleed-to-edge band,
   so it reads as a target rather than a selection. The divider is a pseudo
   element instead of a border so the panel can have rounded corners, and it
   steps aside while the row is hovered. */
.row {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--s2) var(--s5);
  align-items: baseline;
  padding: var(--s4);
  margin-inline: calc(var(--s4) * -1);
  border-radius: var(--r-md);
  text-decoration: none;
}
.row::after {
  content: "";
  position: absolute;
  left: var(--s4);
  right: var(--s4);
  bottom: 0;
  border-bottom: 1px solid var(--rule);
}
.row:hover { background: var(--surface); }
.row:hover::after { border-color: transparent; }

/* The listing is the canonical home of this content, so its titles are the
   same size here as in any teaser that points at it — not smaller. */
/* Title and description share one measure so they read as a single block with
   the date pushed to the far right, instead of a full-width title sitting over
   a stub of description. */
/* Accent-coloured, so a row reads as a link before you hover it. A hover-only
   affordance told you nothing on first glance and nothing at all on a phone. */
.row-title {
  font-size: var(--t-title);
  font-weight: var(--w-semi);
  letter-spacing: -0.015em;
  line-height: 1.3;
  max-width: 46ch;
  color: var(--accent);
}
.row:hover .row-title { text-decoration: underline; text-underline-offset: 0.18em; }
.row-desc {
  grid-column: 1;
  font-size: var(--t-sm);
  color: var(--muted);
  line-height: 1.5;
  max-width: 72ch;
}
.row .label { grid-row: 1; grid-column: 2; white-space: nowrap; }

.year-head {
  margin: var(--s7) 0 var(--s3);
  color: var(--faint);
  font-size: var(--t-sm);
  font-weight: var(--w-semi);
  letter-spacing: 0.02em;
}
.year-head:first-child { margin-top: 0; }

/* ----------------------------------------------------------------- talks --- */
/* Four identical cards, each entirely a link to its own talk page. The slide
   fills the top of the card at its own 16:9 ratio, so nothing is cropped and
   the four cards are the same shape. Accent title plus arrow, so the card
   reads as a link before you hover it. */

.talk-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s5);
}

.talk-card {
  display: grid;
  align-content: start;
  gap: var(--s4);
  padding: var(--s4);
  border: 1px solid var(--rule);
  border-radius: var(--r-md);
  text-decoration: none;
}
.talk-card:hover { border-color: var(--accent); background: var(--surface); }

.talk-card-shot { position: relative; display: block; }
.talk-card-slide {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border-radius: var(--r-sm);
  background: var(--surface);
}

/* Fixed white-on-dark, not tokens: it sits on a slide image, not on the page, so
   it has to hold up over whatever that image happens to be in either theme. */
.talk-card-play {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  display: grid;
  place-items: center;
  width: 54px;
  height: 54px;
  border-radius: var(--r-pill);
  background: rgba(17, 17, 17, 0.55);
  border: 2px solid rgba(255, 255, 255, 0.92);
  backdrop-filter: blur(2px);
  transition: background 0.18s ease, transform 0.18s ease;
}
/* A triangle from borders rather than a glyph, so it can't be affected by the
   font stack and lands on the same pixels every time. */
.talk-card-play::after {
  content: "";
  width: 0;
  height: 0;
  margin-left: 4px;
  border-left: 15px solid #fff;
  border-top: 9px solid transparent;
  border-bottom: 9px solid transparent;
}
.talk-card:hover .talk-card-play {
  background: rgba(17, 17, 17, 0.78);
  transform: translate(-50%, -50%) scale(1.06);
}

@media (prefers-reduced-motion: reduce) {
  .talk-card-play { transition: none; }
  .talk-card:hover .talk-card-play { transform: translate(-50%, -50%); }
}

.talk-card-body { display: grid; gap: var(--s2); }

/* Event on the left, recording state on the right, both at label size so the
   card title stays the only thing competing for the eye. */
.talk-card-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s1) var(--s4);
}
.talk-card-media {
  font-size: var(--t-label);
  font-weight: var(--w-medium);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--accent);
}
.talk-card-media-none { color: var(--faint); }
.talk-card-title {
  font-size: var(--t-h3);
  font-weight: var(--w-semi);
  letter-spacing: -0.015em;
  line-height: 1.25;
  color: var(--accent);
}
.talk-card:hover .talk-card-title { text-decoration: underline; text-underline-offset: 0.18em; }


/* --------------------------------------------------------------- badges --- */

.status {
  display: inline-block;
  font-size: var(--t-label);
  font-weight: var(--w-medium);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
  background: var(--surface);
  border-radius: var(--r-pill);
  padding: 3px var(--s3);
}

/* Shippy sits next to "Coming soon" on the app's own page. Rocket-flame orange
   (#FF8100, the Shipaton brand colour) would fail contrast against the page
   background at label size, so the mark carries the colour and the words stay in
   the site's own muted grey. The border warms on hover instead. */
/* Reserves the height of the App Store button even on the pages that only have
   a "Coming soon" pill, so all four app heroes are the same shape and one icon
   size lines up on every one of them. */
.app-hero-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s3) var(--s4);
  min-height: 52px;
}

.shipaton-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  padding: 3px var(--s3) 3px 5px;
  border: 1px solid var(--rule);
  border-radius: var(--r-pill);
  font-size: var(--t-label);
  font-weight: var(--w-medium);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
  text-decoration: none;
  transition: border-color 0.18s ease, color 0.18s ease;
}
/* Scoped past .app-hero img, which sets the 140px icon and would otherwise win
   on source order and blow Shippy up to the size of the app icon. */
.app-hero .shipaton-badge img { width: 34px; height: 31px; border-radius: 0; flex: none; }
.shipaton-badge:hover { border-color: #ff8100; color: var(--text); }

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s2);
  padding: var(--s3) var(--s5);
  border-radius: var(--r-sm);
  border: 1px solid var(--text);
  background: var(--text);
  color: var(--bg);
  font-size: var(--t-sm);
  font-weight: var(--w-semi);
  text-decoration: none;
}
.btn:hover { opacity: 0.85; }
.btn svg { width: 16px; height: 16px; fill: currentColor; }

/* The App Store button, sized like a store badge rather than a text link: the
   label at body size, the glyph scaled to the cap height around it, and lifted a
   hair because the Apple mark's optical centre sits below its bounding box (the
   leaf is at the top). */
.btn-store {
  gap: var(--s3);
  padding: var(--s3) var(--s6);
  font-size: var(--t-body);
  letter-spacing: -0.01em;
}
.btn-store svg { width: 21px; height: 21px; transform: translateY(-1.5px); }

.btn-secondary { background: transparent; color: var(--text); border-color: var(--rule); }
.btn-secondary:hover { border-color: var(--text); opacity: 1; }

/* ------------------------------------------------------------ app pages --- */

/* Top-aligned, not centred. Centring is what "not aligned with the icon" was:
   the text column is taller than the icon, so vertical centring left the icon
   floating between the h1 and the button, level with neither. Aligned to the top
   it sits on the same line as the app name. */
/* The icon is sized to the text column beside it, so the two end on the same
   line. At 140px it stopped between the tagline and the button, which is what
   "not aligned with the icon" was: aligned at the top, level with nothing at the
   bottom. 164px is the height of h1 + tagline + the footer row below. */
.app-hero {
  display: grid;
  grid-template-columns: 164px minmax(0, 1fr);
  gap: var(--s5);
  align-items: start;
  padding: var(--s7) 0 var(--s6);
}
.app-hero picture { display: block; }
.app-hero img { width: 164px; height: 164px; border-radius: 36px; }
.app-hero h1 { font-size: var(--t-h1); margin-bottom: var(--s2); }
.app-hero .tagline { color: var(--muted); font-size: var(--t-lead); margin-bottom: var(--s4); }

/* Every screenshot fits and every screenshot is the same height, even though
   App Store exports arrive in more than one aspect ratio.
   Flex shrinks each item in proportion to its own width, so one shared factor
   scales the whole row; since the optimiser writes every screenshot at the same
   pixel height, the scaled heights stay equal too. A grid of equal columns did
   the opposite — it fixed the widths, which made the wider shots come out
   shorter than the rest. min-width: 0 is what lets a replaced element shrink
   below its intrinsic width at all. */
.shots {
  display: flex;
  gap: var(--s3);
  margin-bottom: var(--s7);
}
.shots img {
  flex: 0 1 auto;
  min-width: 0;
  width: auto;
  height: auto;
  border-radius: var(--r-md);
  border: 1px solid var(--rule);
}

/* Prose left at a readable measure, facts and cross-navigation in a right rail,
   so the page fills the frame instead of trailing off into empty space. */
.app-body {
  display: grid;
  grid-template-columns: minmax(0, var(--measure)) minmax(280px, 400px);
  justify-content: space-between;
  gap: var(--s8);
  align-items: start;
}

/* A panel rather than loose small print in the margin: at 14px on the bare
   background this read as a footnote to the page instead of part of it. */
.app-aside {
  display: grid;
  gap: var(--s5);
  padding: var(--s5);
  background: var(--surface);
  border-radius: var(--r-md);
  font-size: var(--t-body);
}
.app-aside .label { margin-bottom: var(--s2); }
.app-aside p { color: var(--muted); }
.app-aside a { color: var(--accent); }

.app-aside-apps { display: grid; gap: var(--s4); }
.app-aside-apps a {
  display: grid;
  grid-template-columns: 44px minmax(0, 1fr);
  gap: var(--s3);
  align-items: center;
  text-decoration: none;
  color: var(--text);
}
.app-aside-apps a:hover { color: var(--accent); }
.app-aside-apps img { width: 44px; height: 44px; border-radius: 12px; }

/* Shippy on the corner of the icon, the way a badge sits on a Home Screen app.
   Absolute, so the row stays two columns and the mark can overhang the icon's
   edge; scoped past the 44px icon rule above, which would otherwise size it. */
.app-aside-icon { position: relative; display: block; width: 44px; }
.app-aside-apps .shipaton-dot {
  position: absolute;
  right: -8px;
  bottom: -6px;
  width: 26px;
  height: 24px;
  border-radius: 0;
}

.app-meta {
  margin-top: var(--s7);
  padding-top: var(--s4);
  border-top: 1px solid var(--rule);
  font-size: var(--t-sm);
  color: var(--muted);
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2) var(--s3);
}
.app-meta a { color: var(--muted); }
.app-meta .sep { color: var(--rule); }

/* ----------------------------------------------------------------- prose --- */

.prose {
  max-width: var(--measure);
  font-size: var(--t-body);
  line-height: 1.7;
  /* Long API names and file paths are normal in these posts and will otherwise
     push the whole page sideways on a phone — a 68-character path in one draft
     overflowed a 375px viewport by 84px. */
  overflow-wrap: break-word;
}

.prose > * + * { margin-top: var(--s4); }

/* 24px/600 against 17px/400 body. Previously both were 17px, differing only
   in weight, so a subhead did not read as one. */
/* A section break, not just a bigger line: on a long post the type step alone
   left every section running into the one above it. */
.prose h2 {
  margin-top: var(--s8);
  padding-top: var(--s5);
  border-top: 1px solid var(--rule);
  scroll-margin-top: var(--s4);
}
/* first-of-type, not first-child: the first section heading loses its rule even
   when a callout sits above it. */
.prose > h2:first-of-type { padding-top: 0; border-top: 0; }
.prose > h2:first-child { margin-top: 0; }
.prose h3 { margin-top: var(--s7); font-size: var(--t-h3); }
.prose h2 + *, .prose h3 + * { margin-top: var(--s4); }

.prose ul, .prose ol { padding-left: var(--s5); }
.prose li + li { margin-top: var(--s2); }
.prose li::marker { color: var(--faint); }

.prose a { color: var(--accent); text-decoration: underline; text-underline-offset: 0.18em; text-decoration-thickness: 1px; }
.prose a:hover { text-decoration-thickness: 2px; }

.prose strong { font-weight: var(--w-semi); }

.prose blockquote {
  margin-left: 0;
  margin-right: 0;
  padding-left: var(--s5);
  border-left: 3px solid var(--accent);
  color: var(--muted);
}

.prose code {
  font-family: var(--mono);
  font-size: 0.9em;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 4px;
  padding: 0.1em 0.35em;
}

/* Code blocks could not previously exist without breaking the layout: there
   was no `pre` rule at all, so no padding and no horizontal escape. */
.prose pre {
  overflow-x: auto;
  padding: var(--s4);
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--r-sm);
  font-size: var(--t-sm);
  line-height: 1.55;
  -webkit-overflow-scrolling: touch;
}
.prose pre code { background: none; border: 0; padding: 0; font-size: inherit; }

.prose img { border-radius: var(--r-sm); }
.prose figure { margin: var(--s6) 0; }
.prose figcaption { margin-top: var(--s2); font-size: var(--t-sm); color: var(--muted); }

.prose hr { border: 0; border-top: 1px solid var(--rule); margin: var(--s7) 0; }

.table-scroll { overflow-x: auto; margin: var(--s5) 0; }
.prose table { width: 100%; border-collapse: collapse; font-size: var(--t-sm); }
.prose th, .prose td { padding: var(--s3); border: 1px solid var(--rule); text-align: left; vertical-align: top; }
.prose th { background: var(--surface); font-weight: var(--w-semi); }

.callout {
  background: var(--surface);
  border-radius: var(--r-sm);
  padding: var(--s4);
  font-size: var(--t-sm);
  color: var(--muted);
}

/* Inside an article a callout is something you are meant to read, not fine
   print in the margin: body size, full contrast, and an accent edge. */
.prose > .callout {
  padding: var(--s4) var(--s5);
  border-left: 3px solid var(--accent);
  border-radius: 0 var(--r-sm) var(--r-sm) 0;
  font-size: var(--t-body);
  color: var(--text);
  line-height: 1.65;
}

/* --------------------------------------------------------------- article --- */

/* The title gets more width than the text under it. At 50px, the reading
   measure holds barely five words per line, so a long title broke into three
   short lines with a wide gap down the right. It still stops short of the
   contents rail. */
.article-head { padding: var(--s7) 0 var(--s6); }
.article-head .label { margin-bottom: var(--s3); }
.article-head h1 { margin-bottom: var(--s4); max-width: 900px; }
.article-head .label,
.article-head .lead { max-width: var(--measure); }

/* Prose plus a sticky "On this page" rail, which also puts the horizontal
   space to work on long posts instead of leaving an empty right margin. */
/* No align-items: start here. It shrinks the rail's grid area to its own
   content height, which leaves position: sticky nothing to travel inside — the
   contents list scrolled away with the article instead of following it. */
.article-layout {
  display: grid;
  grid-template-columns: minmax(0, var(--measure)) minmax(0, 1fr);
  gap: var(--s8);
}

/* The rule runs down the list, not the whole rail, so the current entry can
   claim a segment of it — the position indicator you expect from a contents
   list, rather than a colour change you have to hunt for. */
/* A short note has no contents rail, so nothing needs the second column. */
.post-solo .article-layout { display: block; }

.toc {
  position: sticky;
  top: var(--s5);
  max-height: calc(100vh - var(--s8));
  overflow-y: auto;
}
.toc .label { margin-bottom: var(--s3); padding-left: var(--s4); }
.toc ol {
  list-style: none;
  padding: 0;
  margin: 0;
  font-size: var(--t-sm);
  border-left: 1px solid var(--rule);
}
.toc li { margin: 0; line-height: 1.4; }
.toc a {
  display: block;
  padding: var(--s2) 0 var(--s2) var(--s4);
  margin-left: -1px;
  border-left: 2px solid transparent;
  color: var(--muted);
  text-decoration: none;
}
.toc a:hover { color: var(--text); }
.toc a.is-current {
  color: var(--accent);
  font-weight: var(--w-medium);
  border-left-color: var(--accent);
}
.toc .toc-sub a { padding-left: var(--s6); }

.post-nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s4);
  justify-content: space-between;
  margin-top: var(--s7);
  padding-top: var(--s5);
  border-top: 1px solid var(--rule);
}
.post-nav-item {
  display: grid;
  gap: var(--s1);
  max-width: 42ch;
  text-decoration: none;
  font-size: var(--t-body);
  font-weight: var(--w-medium);
}
.post-nav-item:hover { color: var(--accent); }
.post-nav-next { text-align: right; margin-left: auto; }

/* ------------------------------------------------------------ talk pages --- */

/* Laid out exactly like a post: pinned to the left edge of the frame, the h1
   allowed to run wider than its text, everything else at the reading measure. */
.talk-page .talk-embed { max-width: var(--measure); }

/* The abstract above is muted and larger; this is Aleksandra's own note about
   the day. Without a distinct treatment the two read as the same voice. */
.talk-note {
  max-width: var(--measure);
  margin: var(--s6) 0;
  padding-left: var(--s5);
  border-left: 3px solid var(--accent);
}
.talk-note .label { margin-bottom: var(--s2); }
.talk-note p { font-size: var(--t-body); line-height: 1.7; }

/* Stands in for the missing player, so it is framed like one: a bordered panel
   the width of the video, not another accent-edged aside stacked under the note
   above it — two of those in a row read as one confused block. */
.talk-alt {
  display: grid;
  gap: var(--s2);
  max-width: var(--measure);
  margin: var(--s6) 0;
  padding: var(--s5);
  border: 1px solid var(--rule);
  border-radius: var(--r-md);
  text-decoration: none;
}
.talk-alt:hover { border-color: var(--accent); background: var(--surface); }
.talk-alt-title {
  font-size: var(--t-title);
  font-weight: var(--w-semi);
  letter-spacing: -0.015em;
  line-height: 1.3;
  color: var(--accent);
}
.talk-alt:hover .talk-alt-title { text-decoration: underline; text-underline-offset: 0.18em; }

.talk-embed { margin: var(--s6) 0; max-width: 900px; }
.talk-embed iframe {
  width: 100%;
  aspect-ratio: 16 / 9;
  height: auto;
  border: 0;
  border-radius: var(--r-md);
  background: var(--surface);
}

.back {
  display: inline-block;
  margin: var(--s5) 0 0;
  font-size: var(--t-sm);
  color: var(--muted);
  text-decoration: none;
}
.back:hover { color: var(--text); }

/* ----------------------------------------------------------- about page --- */

.about-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(200px, 260px);
  gap: var(--s8);
  align-items: start;
}
.about-portrait { width: 100%; border-radius: var(--r-md); }

/* The Now line sits inside .prose, so it has to opt out of prose link styling
   or the whole row renders underlined. */
.prose .now-line,
.prose .now-line:hover { text-decoration: none; margin: var(--s5) 0; }

.about-aside { display: grid; gap: var(--s5); font-size: var(--t-sm); }
.about-aside .label { margin-bottom: var(--s2); }
.about-aside p { color: var(--muted); }
.about-aside a { color: var(--accent); }

/* ------------------------------------------------------------- now page --- */

/* Padding and gap sized for the full-frame card. At --s4 both, with the card
   now the width of the page, the text sat tight against the artwork on one side
   and the card edge on the other. */
.event-card {
  display: grid;
  grid-template-columns: minmax(0, 200px) minmax(0, 1fr);
  gap: var(--s6);
  align-items: center;
  padding: var(--s6);
  background: var(--surface);
  border-radius: var(--r-md);
  margin: var(--s6) 0;
}
.event-card img { border-radius: var(--r-sm); }
.event-card h2 { font-size: var(--t-h3); margin-bottom: var(--s3); }
.event-card .label { margin-bottom: var(--s2); }
/* Body size, not small print: this is a paragraph someone is meant to read and
   act on, and at --t-sm it was the smallest text in the card. */
.event-card p {
  color: var(--muted);
  font-size: var(--t-body);
  line-height: 1.65;
  max-width: 60ch;
  margin-bottom: var(--s5);
}

/* ------------------------------------------------------------------- 404 --- */

/* Three elements and nothing else on the page, so they need room between them.
   At --s4 under a 50px h1 the whole thing read as one squashed block. */
.notfound { padding: var(--s9) 0; }
.notfound h1 { margin-bottom: var(--s5); }
.notfound .hero-actions { margin-top: var(--s7); }

/* ---------------------------------------------------------- syntax theme --- */

.prose pre[class*="language-"] { color: var(--text); }
.token.comment, .token.prolog, .token.doctype, .token.cdata { color: var(--faint); }
.token.punctuation { color: var(--muted); }
.token.property, .token.tag, .token.constant, .token.symbol, .token.deleted { color: #b1542a; }
.token.boolean, .token.number { color: #7a4bb5; }
.token.selector, .token.attr-name, .token.string, .token.char, .token.builtin, .token.inserted { color: #2f6f4f; }
.token.operator, .token.entity, .token.url { color: var(--muted); }
.token.atrule, .token.attr-value, .token.keyword { color: #1f6aa8; }
.token.function, .token.class-name { color: #a13b6f; }
.token.regex, .token.important, .token.variable { color: #b1542a; }

@media (prefers-color-scheme: dark) {
  .token.property, .token.tag, .token.constant, .token.symbol, .token.deleted { color: #e5a07a; }
  .token.boolean, .token.number { color: #c9a2ee; }
  .token.selector, .token.attr-name, .token.string, .token.char, .token.builtin, .token.inserted { color: #92d3ab; }
  .token.atrule, .token.attr-value, .token.keyword { color: #8ec2ef; }
  .token.function, .token.class-name { color: #f0a0c8; }
  .token.regex, .token.important, .token.variable { color: #e5a07a; }
}

/* ---------------------------------------------------------- breakpoints --- */

@media (max-width: 860px) {
  :root { --s9: 4rem; --s8: 3rem; }

  .talk-grid { grid-template-columns: 1fr; }

  /* Fixed height and wrap instead of shrink-to-one-row: below this width a
     single row would render every screenshot too small to read. */
  .shots { flex-wrap: wrap; justify-content: center; }
  .shots img { flex: 0 0 auto; height: 260px; }

  .app-body { grid-template-columns: 1fr; gap: var(--s6); }
  .app-showcase { grid-template-columns: 1fr; }

  /* Stacked under the prose, the rail has the full width to spread across
     rather than becoming a long thin column of labels. */
  .app-aside { grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: var(--s5); }
  .app-aside .aside-wide { grid-column: 1 / -1; }
  .app-aside-apps { display: flex; flex-wrap: wrap; gap: var(--s4) var(--s6); }
  .app-aside-apps a { display: flex; align-items: center; gap: var(--s3); }

  /* No room for a sticky rail; the list moves inline above the prose. */
  .article-layout { grid-template-columns: 1fr; gap: var(--s5); }
  .toc {
    position: static;
    order: -1;
    max-height: none;
    padding: var(--s4) 0;
    border-top: 1px solid var(--rule);
    border-bottom: 1px solid var(--rule);
  }
  .toc .label { padding-left: 0; }
  .toc ol { columns: 2; column-gap: var(--s6); border-left: 0; }
  .toc li { break-inside: avoid; }
  .toc a { padding-left: 0; margin-left: 0; border-left: 0; }
  .toc .toc-sub a { padding-left: var(--s4); }

  .about-grid { grid-template-columns: 1fr; gap: var(--s6); }
  .about-portrait { max-width: 200px; }
  .event-card { grid-template-columns: 1fr; }
}

@media (max-width: 620px) {
  .frame { width: calc(100% - var(--s5)); }

  .topbar { padding: var(--s4) 0; gap: var(--s3); }
  .nav { gap: var(--s4); width: 100%; }


  .app-hero { grid-template-columns: 1fr; gap: var(--s4); }
  .app-hero img { width: 96px; height: 96px; border-radius: 22px; }

  .shots img { height: 200px; }

  .app-tile { grid-template-columns: 80px minmax(0, 1fr); gap: var(--s4); padding: var(--s4); }
  .app-tile img { width: 80px; height: 80px; border-radius: 18px; }

  .toc ol { columns: 1; }
  .post-nav-next { text-align: left; margin-left: 0; }

  /* Date moves under the title rather than fighting it for the same line.
     The hover panel's outdent also shrinks to match the frame gutter at this
     width, or it pushes the page 4px wider than the viewport. */
  .row {
    grid-template-columns: 1fr;
    gap: var(--s1);
    margin-inline: calc(var(--s3) * -1);
    padding-inline: var(--s3);
  }
  .row::after { left: var(--s3); right: var(--s3); }
  .row .label { grid-row: auto; grid-column: 1; }

  .toc ol { columns: 1; }

  .footer { gap: var(--s4); }
}

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

@media print {
  .topbar, .footer, .back, .toc { display: none; }
  body { font-size: 11pt; }
}
