/* ============================================================
   هوش دات کام — design system
   Dark-only editorial. Built against impeccable's craft-floor:
   no gradients, no kicker/eyebrow above headings, no nested cards,
   radius <= 12px, shadows only with offset + blur, one accent.
   ============================================================ */

/* ---------- fonts ---------- */
/* One variable file spans weights 100-900; that range is what gives
   the page its typographic contrast without a second download. */
@font-face {
  font-family: 'Vazirmatn';
  src: url('/assets/vazirmatn-var.woff2') format('woff2-variations');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

/* ---------- tokens ---------- */
:root {
  color-scheme: dark;

  /* Surfaces: each step is a real elevation, not an alpha overlay.
     Chroma is 0.000 everywhere. These are pure greys, not blue-grey --
     a hint of blue is what makes a "dark theme" look cold instead of
     clean, and it is the single easiest way to make a minimal site look
     like a template. */
  --canvas:      oklch(0.155 0 0);
  --surface:     oklch(0.190 0 0);
  --surface-2:   oklch(0.225 0 0);
  --surface-3:   oklch(0.270 0 0);

  --fg:          oklch(0.960 0 0);
  --fg-muted:    oklch(0.745 0 0);
  --fg-subtle:   oklch(0.615 0 0);

  --line:        oklch(0.285 0 0);
  --line-strong: oklch(0.400 0 0);

  /* No accent. Emphasis is carried by weight and contrast, never hue --
     a link must be findable, but a page that is one colour is a page
     where nothing competes for the eye.

     Two levels, because one value cannot serve both: a link inside a
     sentence needs an underline to be findable at all, while a link in a
     nav or a list row is already identifiable by position and turning it
     white would shout for no reason. */
  --accent:      oklch(0.960 0 0);
  --accent-quiet:oklch(0.560 0 0);
  --on-accent:   oklch(0.110 0 0);
  /* Hover for chrome: not a colour change, a brightness one. */
  --fg-bright:   oklch(1 0 0);
  --fg-dim:      oklch(0.500 0 0);

  /* Status colours are the only hues left, and they are scoped to form
     validation where a hue is the fastest signal. Never decorative. */
  --ok:          oklch(0.770 0.115 155);
  --warn:        oklch(0.830 0.125 85);
  --danger:      oklch(0.690 0.165 22);

  /* Syntax hues, for code only. These are the second sanctioned colour on
     the site, and unlike the photos they are not decorative -- each one
     separates a different kind of token, so a reader can find the comment
     or the number without reading the whole line. Chosen for >=4.5:1 on
     --canvas, measured rather than assumed. */
  --code-comment: oklch(0.620 0.030 145);
  --code-key:     oklch(0.779 0.096 69.2);
  --code-num:     oklch(0.846 0.144 144.2);
  --code-fn:      oklch(0.727 0.091 299.7);
  --code-str:     oklch(0.846 0.144 144.2);

  /* Type scale — 1.25 modular, flattened at the small end. */
  --t-xs:   0.75rem;
  --t-sm:   0.8125rem;
  --t-base: 1rem;
  --t-md:   1.0625rem;
  --t-lg:   1.375rem;
  --t-xl:   1.75rem;
  --t-2xl:  2.25rem;
  --t-3xl:  2.875rem;

  /* Spacing — 4px base */
  --s-1: .25rem; --s-2: .5rem;  --s-3: .75rem; --s-4: 1rem;
  --s-5: 1.5rem; --s-6: 2rem;   --s-7: 3rem;    --s-8: 4rem;

  --radius:    8px;
  --radius-sm: 5px;
  --radius-lg: 12px;

  /* No elevation shadows. Depth comes from the surface steps and a border,
     which is the Swiss/editorial answer: a shadow on a flat grey surface is
     the only thing that makes it look like a floating card in a template. */

  /* Persian glyphs run wider than Latin, so the measure is set in rem. */
  --measure: 46rem;
  --ease: cubic-bezier(.16, 1, .3, 1);

  /* ---------- materials ----------
     The header is the only glass on the site. Everything else is a flat
     surface with a border, which is both the minimal look and the fast
     one: no backdrop-filter means no compositing layer, no repaint on
     scroll, and nothing to fall back to when the browser lacks support.

     The header still keeps an alpha, because a header scrolls over content
     and something must show through. */
  --glass-blur: 14px;
  --glass-sat: 100%;   /* no saturation boost: a boosted grey is still a hue */

  /* Flat surfaces, not translucent ones. */
  --glass:        var(--surface);
  --glass-line:   oklch(1 0 0 / .09);
  --glass-line-2: oklch(1 0 0 / .14);

  /* The header: the one material allowed to blur. */
  --glass-chrome:        oklch(0.130 0 0 / .78);
  --glass-chrome-line:   oklch(1 0 0 / .08);
  --glass-chrome-shadow: 0 1px 0 oklch(1 0 0 / .05);
  /* The header's own height. A sticky header covers whatever scrolls under it,
     so an anchor target needs this much scroll-margin to land clear of it. */
  --header-h:        5.5rem;
  /* A named stacking scale. Two arbitrary numbers in a stylesheet is how a
     header ends up behind a dropdown, and nothing in the CSS says why. */
  --z-header:        40;
  --z-overlay:       60;
}

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

/* ---------- base ---------- */
html {
  -webkit-text-size-adjust: 100%;
  scrollbar-color: var(--line-strong) var(--canvas);
  scrollbar-width: thin;
}

::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: var(--canvas); }
::-webkit-scrollbar-thumb {
  background: var(--line-strong);
  border: 3px solid var(--canvas);
  border-radius: 999px;
}
::-webkit-scrollbar-thumb:hover { background: var(--fg-subtle); }

body {
  margin: 0;
  background: var(--canvas);
  color: var(--fg);
  font-family: 'Vazirmatn', system-ui, sans-serif;
  font-size: var(--t-base);
  font-weight: 400;
  line-height: 1.75;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

/* Browser surfaces the design system would otherwise not own. */
::selection { background: var(--accent-quiet); color: var(--fg); }
:target { scroll-margin-block: 5rem; }

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; text-underline-offset: 3px; }

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

/* ---------- layout ---------- */
.container {
  width: 100%;
  max-width: 72rem;
  min-width: 0;
  margin-inline: auto;
  padding-inline: var(--s-5);
}
.page { padding-block: var(--s-7) var(--s-8); }

.skip-link {
  position: absolute; inset-inline-start: -9999px; top: var(--s-2);
  background: var(--accent); color: var(--on-accent);
  padding: var(--s-2) var(--s-4); border-radius: var(--radius-sm);
  font-weight: 600; z-index: var(--z-overlay);
}
.skip-link:focus { inset-inline-start: var(--s-4); }

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

/* ---------- header ---------- */
.site-header {
  position: sticky; top: 0; z-index: var(--z-header);
  background: var(--glass-chrome);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  box-shadow: var(--glass-chrome-shadow);
}
/* A bright top edge reads as light catching the material, which is what sells
   the glass. A 1px hairline does not. */
.site-header::after {
  content: ""; position: absolute; inset-block-start: 0; inset-inline: 0;
  block-size: 1px; background: var(--glass-line-2); pointer-events: none;
}
.site-header-inner {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s-4); min-height: 4.25rem;
}

.brand { display: flex; align-items: center; gap: var(--s-3); color: var(--fg); min-width: 0; }
.brand:hover { text-decoration: none; }
/* The mark is the same file the tab shows, inlined. The old rule drew an
   accent-coloured tile with the letter as text; the SVG brings its own dark
   tile, so the background and the font sizing go and only the box survives. */
.brand-mark {
  display: grid; place-items: center; flex: none;
  inline-size: 2.25rem; block-size: 2.25rem;
  line-height: 0;
}
.brand-mark svg { inline-size: 100%; block-size: 100%; display: block; }
/* Fallback: if the icon file is missing, brandmark_svg() returns the bare
   letter, and this is what gives it back a tile. */
.brand-mark:not(:has(svg)) {
  background: var(--fg); color: var(--bg);
  border-radius: var(--radius-sm);
  font-weight: 800; font-size: 1.25rem; line-height: 1;
}
.brand-text { display: flex; flex-direction: column; gap: .1rem; min-width: 0; }
.brand-name { font-weight: 700; font-size: 1.0625rem; letter-spacing: -.01em; white-space: nowrap; }
.brand-tagline {
  font-size: var(--t-xs); color: var(--fg-subtle);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.site-nav { display: flex; align-items: center; gap: var(--s-1); }
.nav-link {
  position: relative;
  display: inline-flex; align-items: center; gap: var(--s-2);
  min-block-size: 2.5rem; padding-inline: var(--s-3);
  color: var(--fg-muted); font-size: var(--t-sm); font-weight: 500;
  border-radius: var(--radius-sm);
  transition: color .18s var(--ease), background-color .18s var(--ease);
}
.nav-link:hover { color: var(--fg); background: var(--surface-2); text-decoration: none; }
/* The active page, with no accent colour to mark it. A background fill alone
   is too weak against a surface-3 hover, so the state is carried by weight
   and a rule under the label as well -- two signals, neither of them hue. */
.nav-link.is-active {
  color: var(--fg);
  background: var(--surface-2);
  font-weight: 700;
}
.nav-link.is-active::after {
  content: "";
  position: absolute;
  inset-inline: var(--s-3);
  inset-block-end: .3rem;
  block-size: 2px;
  background: var(--fg);
  border-radius: 1px;
}
.nav-link svg { inline-size: 1.125rem; block-size: 1.125rem; flex: none; }

/* ---------- buttons ---------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s-2);
  min-block-size: 2.75rem; padding-inline: var(--s-4);
  background: var(--accent); color: var(--on-accent);
  border: 1px solid transparent; border-radius: var(--radius-sm);
  font: inherit; font-weight: 600; font-size: var(--t-sm);
  cursor: pointer; text-decoration: none; white-space: nowrap;
  transition: background-color .18s var(--ease), transform .12s var(--ease);
}
.btn:hover { background: color-mix(in oklch, var(--accent) 88%, var(--fg)); text-decoration: none; }
.btn:active { transform: scale(.98); }
.btn svg { inline-size: 1.125rem; block-size: 1.125rem; }
.btn-outline {
  background: transparent; color: var(--fg); border-color: var(--line);
}
.btn-outline:hover { background: var(--surface-2); border-color: var(--line-strong); }
.btn:disabled { opacity: .5; cursor: not-allowed; }

/* ---------- badges ---------- */
.badge {
  display: inline-flex; align-items: center; gap: var(--s-1);
  padding: .2rem .55rem;
  background: var(--surface-2); color: var(--fg-muted);
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  font-size: var(--t-xs); font-weight: 500; line-height: 1.6;
  white-space: nowrap;
}
.badge-accent { background: var(--accent-quiet); color: var(--fg); border-color: transparent; }
.badge-ok { color: var(--ok); border-color: color-mix(in oklch, var(--ok) 32%, var(--line)); }
.pulse {
  inline-size: 6px; block-size: 6px; border-radius: 50%;
  background: var(--ok); flex: none;
  animation: pulse 2.4s var(--ease) infinite;
}
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }

/* ---------- section headers ---------- */
.section { margin-block-start: var(--s-8); }
.section-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--s-4); margin-block-end: var(--s-5);
  padding-block-end: var(--s-3); border-block-end: 1px solid var(--line);
}
.section-title { margin: 0; font-size: var(--t-lg); font-weight: 700; letter-spacing: -.015em; }
.section-link { font-size: var(--t-sm); color: var(--fg-subtle); font-weight: 500; }
.section-link:hover { color: var(--fg-bright); }

/* ---------- hero ---------- */
.hero { padding-block: var(--s-8) var(--s-7); }
.hero p {
  margin: var(--s-4) 0 0; max-inline-size: 54ch;
  font-size: var(--t-md); color: var(--fg-muted); line-height: 1.85;
  text-wrap: pretty;
}

/* ---------- featured slider ---------- */
.featured { margin-block-start: 0; }
.slider {
  position: relative;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius-lg); overflow: hidden;
}
.slider-viewport { overflow: hidden; }
.slider-track { display: flex; transition: transform .55s var(--ease); will-change: transform; }
.slide {
  flex: 0 0 100%; min-inline-size: 0;
  padding: var(--s-6);
  display: flex; flex-direction: column; gap: var(--s-3);
}
.slide-meta { display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap; }
.slide h2 {
  margin: 0; font-size: var(--t-xl); font-weight: 700;
  line-height: 1.35; letter-spacing: -.02em; text-wrap: balance;
}
.slide h2 a { color: var(--fg); }
.slide h2 a:hover { color: var(--fg-bright); text-decoration: none; }
.slide p {
  margin: 0; max-inline-size: var(--measure);
  color: var(--fg-muted); line-height: 1.85; text-wrap: pretty;
}
/* The control row is a thin strip, not a panel: the counter sits on the start
   side, the two arrows group on the end side, and a hairline above separates
   them from the slide. Nothing is centred, because a centred row with a gap on
   each side reads as three unrelated things rather than one control. */
.slider-controls {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s-3);
  min-block-size: 3.25rem;
  padding: var(--s-2) var(--s-4) var(--s-2) var(--s-5);
  border-block-start: 1px solid var(--line);
  background: var(--surface-2);
}
.slider-btn {
  display: grid; place-items: center; flex: none;
  inline-size: 2.5rem; block-size: 2.5rem;
  background: transparent; color: var(--fg-muted);
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  cursor: pointer;
  transition: color .18s var(--ease), border-color .18s var(--ease), background-color .18s var(--ease);
}
.slider-btn:hover { color: var(--fg); border-color: var(--line-strong); background: var(--surface-2); }
.slider-btn svg { inline-size: 1.125rem; block-size: 1.125rem; }
.slider-count {
  font-size: var(--t-sm);
  color: var(--fg-subtle);
  font-variant-numeric: tabular-nums;
  letter-spacing: .01em;
}
.slider-count strong {
  color: var(--fg);
  font-weight: 650;
  font-size: var(--t-base);
  padding-inline-end: .35em;
}
.slider-arrows { display: flex; align-items: center; gap: var(--s-2); }
.slider-more, .slide-more {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  min-block-size: 2.75rem;
  margin-block-start: var(--s-1);
  color: var(--fg-muted);
  font-size: var(--t-sm);
  text-decoration: none;
  border-block-end: 1px solid var(--line-strong);
  transition: color .18s var(--ease), border-color .18s var(--ease);
}
.slide-more:hover { color: var(--fg-bright); border-color: var(--fg-bright); }

/* ---------- category nav ---------- */
.cat-nav { display: flex; flex-wrap: wrap; gap: var(--s-2); }
.cat-link {
  padding: var(--s-2) var(--s-3); min-block-size: 2.25rem;
  display: inline-flex; align-items: center;
  background: var(--surface); color: var(--fg-muted);
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  font-size: var(--t-sm); font-weight: 500;
  transition: color .18s var(--ease), border-color .18s var(--ease);
}
.cat-link:hover { color: var(--fg); border-color: var(--line-strong); text-decoration: none; }
.cat-link.is-active { background: var(--fg); color: var(--canvas); border-color: var(--fg); }

/* ---------- post list ---------- */
.post-list { display: flex; flex-direction: column; }
.post-item {
  display: grid; gap: var(--s-2);
  padding-block: var(--s-5); border-block-end: 1px solid var(--line);
  color: inherit; transition: opacity .18s var(--ease);
}
.post-item:first-child { padding-block-start: 0; }
.post-item:hover { text-decoration: none; }
.post-item:hover .post-item-title { color: var(--fg-bright); }
.post-item-meta { display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap; }
.post-item-date { font-size: var(--t-xs); color: var(--fg-subtle); font-variant-numeric: tabular-nums; }
.post-item-title {
  margin: 0; font-size: var(--t-lg); font-weight: 700;
  line-height: 1.45; letter-spacing: -.015em; text-wrap: balance;
  transition: color .18s var(--ease);
}
.post-item-excerpt {
  margin: 0; max-inline-size: var(--measure);
  color: var(--fg-muted); line-height: 1.85; text-wrap: pretty;
}

/* ---------- list + sidebar ---------- */
.with-side {
  display: grid; gap: var(--s-7);
  grid-template-columns: minmax(0, 1fr) 18rem;
  align-items: start;
}
.side {
  position: sticky; top: 5.5rem;
  display: flex; flex-direction: column; gap: var(--s-6);
}
.side-block { border-block-start: 2px solid var(--fg); padding-block-start: var(--s-3); }
.side-title { margin: 0 0 var(--s-3); font-size: var(--t-sm); font-weight: 700; letter-spacing: .02em; }
.side-list { list-style: none; margin: 0; padding: 0; }
.side-item { padding-block: var(--s-3); border-block-end: 1px solid var(--line); }
.side-item:last-child { border-block-end: none; }
.side-item a { color: var(--fg); font-size: var(--t-sm); font-weight: 500; line-height: 1.6; }
.side-item a:hover { color: var(--fg-bright); text-decoration: none; }
.side-item .post-item-date { display: block; margin-block-start: .2rem; }
.side-note { margin: 0; font-size: var(--t-sm); color: var(--fg-subtle); line-height: 1.8; }

/* ---------- article ---------- */
.article-head { margin-block-end: var(--s-6); }
.article-head h1 {
  margin: 0; max-inline-size: 26ch;
  font-size: clamp(var(--t-xl), 4.4vw, var(--t-2xl));
  font-weight: 800; line-height: 1.28; letter-spacing: -.028em;
  text-wrap: balance;
}
.article-meta {
  display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap;
  margin-block-start: var(--s-4); padding-block-end: var(--s-4);
  border-block-end: 1px solid var(--line);
}
.article-body { max-inline-size: var(--measure); }
.article-body > * + * { margin-block-start: var(--s-4); }

.lede {
  margin: 0 0 var(--s-5);
  font-size: var(--t-md); line-height: 1.9;
  color: var(--fg-muted);
  text-align: justify;
}
.article-body h2 {
  margin: var(--s-7) 0 0; font-size: var(--t-lg);
  font-weight: 700; letter-spacing: -.015em; line-height: 1.4;
  scroll-margin-block-start: 6rem;
}
.article-body h3 {
  margin: var(--s-6) 0 0; font-size: var(--t-md);
  font-weight: 700; line-height: 1.5; scroll-margin-block-start: 6rem;
}
.article-body h4 { margin: var(--s-5) 0 0; font-size: var(--t-base); font-weight: 700; line-height: 1.6; }
.article-body p {
  margin: 0;
  text-align: justify;
  /* A hair off every space: less slack for the justifier to hand out, so the
     worst stretched space drops. Measured effect on this site's posts is
     small but in the right direction -- see justify-check.js. */
  word-spacing: -0.01em;
}
.article-body ul, .article-body ol { margin: 0; padding-inline-start: 1.35rem; }
.article-body li { margin-block-start: var(--s-2); }
.article-body li::marker { color: var(--fg-subtle); }
/* With no accent colour a link inside a sentence is found by its underline
   and nothing else, so the underline has to do real work: a solid grey that
   is clearly visible, thick enough to survive on a phone, and set below the
   baseline so it does not cut through Persian descenders. */
.article-body a {
  text-decoration: underline;
  text-decoration-color: var(--fg-subtle);
  text-decoration-thickness: 1px;
  text-underline-offset: .25em;
}
.article-body a:hover {
  color: var(--fg-bright);
  text-decoration-color: var(--fg-bright);
  text-decoration-thickness: 2px;
}
/* Justification is for running prose only.
 *
 * A justified list looks broken rather than tidy: the marker sits at the start
 * edge and the text after it is stretched across the full measure, so a one
 * word item ends up with a line of huge gaps. Code must never be stretched --
 * a space in a command is a space, and widening it changes what the reader
 * copies. A table cell is one or two words, so justify can only add gaps there
 * too. Headings are display type, set short, and a stretched heading is the
 * most obviously wrong thing on the page.
 */
.article-body ul,
.article-body ol,
.article-body li,
.article-body table,
.article-body th,
.article-body td,
.article-body pre,
.article-body code,
.article-body blockquote,
.article-body figcaption,
.article-body h2,
.article-body h3,
.article-body h4 { text-align: start; }
/* ---------- callout ----------
   A white box with black text, in a site that is otherwise black. The point is
   the polarity flip: an emphasis that is still inside the palette, so it reads
   as "this is the line to keep" and not as a different skin.

   No border, no shadow, no gradient. The site's rules forbid all three, and a
   light box that also carried decoration would stop reading as emphasis.

   `overflow-wrap: anywhere` on the code inside: the box narrows the column, so
   an unbreakable Latin token that was fine in the full-width body can now
   overflow it. `word-break: break-all` would split Persian words; `anywhere`
   only breaks when the line has nowhere else to go. */
.callout {
  margin: var(--s-6) 0;
  padding: var(--s-5);
  background: #fff;
  color: #0a0a0a;
  border-radius: var(--radius);
  text-align: start;
}
.callout-text {
  margin: 0;
  color: inherit;
  text-align: justify;
  hyphens: none;
}
/* Two classes beat `.article-body code`, which comes later in the file with the
   same specificity and would otherwise win -- and it sets color: var(--code-str),
   a cyan that is unreadable on white. Inside the box the code is plain black on
   a light grey chip; the syntax colours belong to `pre`, not to a word in a
   sentence. */
.callout .callout-text code {
  background: #f0f0f0;
  color: #1a1a1a;
  overflow-wrap: anywhere;
  padding-inline: .15em;
  border-radius: 3px;
}


/* Justify needs a measure wide enough to have slack to distribute.
 *
 * Measured on this site's own posts with justify-check.js: the worst stretched
 * word space is 1.1-1.9x normal on a desktop column and 2.2-3.2x at 390px. A
 * river becomes visible somewhere around 2x, so the phone column is over the
 * line and the desktop column is not.
 *
 * The levers that could fix the phone case were all tried and measured:
 * narrowing the measure changes nothing (the viewport already caps it),
 * `hyphens: auto` changes nothing (Persian has no hyphenation dictionary in
 * any browser), and `word-spacing` moves 3.17x down to about 2.9x, which is
 * still over. So the phone column falls back to `start`. That is a measured
 * boundary, not a guess: raise it if the measure ever changes.
 */
@media (max-width: 48rem) {
  /* Justify on a phone costs font size, and that is the trade the reader chose.
   *
   * Measured on this site's own eight posts at 390px, the worst inter-word gap
   * is 9.33x at the desktop's 17px, 6.13x at 16px, and 2.49x at 14.5px with
   * `word-break: break-all` on inline code. A river becomes visible somewhere
   * around 2x, so 14.5px is the size at which the phone column stops looking
   * broken.
   *
   * Two levers did the work and both are load-bearing:
   *   - the smaller size, which puts more words on a line and gives the
   *     justifier slack to distribute
   *   - `break-all` on inline code, because 38 of 133 paragraphs carry three or
   *     more code chips, and a line of five unbreakable Latin tokens is what
   *     produced the 9.33x in the first place
   *
   * Tried and rejected, each measured: `hyphens: auto` (Persian has no
   * hyphenation dictionary in any browser), `text-wrap: pretty` (no effect at
   * all, and it is an opponent of justify), `text-justify: kashida` (Chromium
   * ignores it, computed value stays `auto`), `text-justify:
   * inter-character` (moves the slack into the joined letters and breaks the
   * word's shape), narrowing the column (the viewport already caps it), and
   * `display: inline-block` on the code chip (no better than break-all alone).
   */
  .article-body p,
  .article-body .lede {
    text-align: justify;
    font-size: 14.5px;
  }
  /* Inline code is a break opportunity. `overflow-wrap: anywhere` does nothing
   * here: an inline <code> is an LTR island inside RTL and the layout engine
   * sees no break opportunity in it. `word-break: break-all` gives it one. */
  .article-body code { word-break: break-all; }
}

.article-body strong { font-weight: 700; }
/* ---------- code ----------
   Syntax colour is the second place colour is allowed, and the reason is
   different from the photos: these hues carry meaning. A comment is not
   decoration, it is an instruction, and a reader scanning for what a snippet
   does should not have to read it. Comments are in Persian, so the rules
   have to work RTL prose inside an LTR block -- hence the explicit bidi
   isolation on the comment class rather than relying on the parent.

   Every hue here is desaturated enough to stay under 4.5:1 on --canvas. */
.article-body code {
  font-family: ui-monospace, 'SF Mono', Menlo, monospace;
  font-size: .875em; background: var(--surface-2);
  padding: .15em .4em; border-radius: var(--radius-sm);
  direction: ltr; unicode-bidi: embed;
  color: var(--code-str);
}
.article-body pre {
  margin: var(--s-5) 0; padding: var(--s-5) var(--s-5);
  background: oklch(0.239 0 0); border: 1px solid oklch(0.270 0 0);
  border-radius: 20px; overflow-x: auto;
  box-shadow: 0 8px 24px rgb(0 0 0 / 0.35);
  direction: ltr; text-align: left; line-height: 1.6; font-size: var(--t-sm);
}
.article-body pre code { background: none; padding: 0; }
/* A comment in an LTR code block holding RTL Persian needs its own embedding
   or the punctuation jumps to the wrong end of the line. */
.code-comment {
  color: var(--code-comment);
  unicode-bidi: isolate;
  font-style: normal;
}
.code-key { color: var(--code-key); }
.code-num { color: var(--code-num); }
.code-fn  { color: var(--code-fn); }
.code-str { color: var(--code-str); }
.article-body blockquote {
  margin: var(--s-5) 0;
  padding-block: var(--s-1); padding-inline-start: var(--s-4);
  border-inline-start: 2px solid var(--line-strong);
  color: var(--fg-muted); font-size: var(--t-md);
}
.article-body figure { margin: var(--s-6) 0; }
.article-body figcaption {
  margin-block-start: var(--s-2); font-size: var(--t-sm);
  color: var(--fg-subtle); text-align: center;
}
.article-body hr { margin: var(--s-7) 0; border: 0; border-block-start: 1px solid var(--line); }

/* Tables: contained, headers wrap, numerals align. */
.table-scroll {
  overflow-x: auto; -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain; max-width: 100%;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface);
}
.table-scroll > table {
  display: table; inline-size: 100%; min-inline-size: 0;
  border-collapse: collapse; margin: 0; font-size: var(--t-sm);
  table-layout: auto;
}
.table-scroll th, .table-scroll td {
  padding: var(--s-3) var(--s-4); text-align: start;
  border-block-end: 1px solid var(--line); vertical-align: top;
  overflow-wrap: anywhere;
}
.table-scroll thead th {
  background: var(--surface-2); color: var(--fg);
  font-weight: 700; white-space: normal;
}
.table-scroll tbody tr:last-child td { border-block-end: none; }
.table-scroll tbody tr:hover { background: var(--surface-2); }
.table-scroll td { color: var(--fg-muted); }
.table-scroll td.num, .table-scroll th.num {
  direction: ltr; text-align: center;
  font-variant-numeric: tabular-nums; white-space: nowrap;
}

/* Sources: numbered with hanging indent. */
.sources { list-style: none; margin: 0; padding: 0; counter-reset: src; }
.sources li {
  position: relative; padding-inline-start: 2rem;
  margin-block-start: var(--s-3);
  font-size: var(--t-sm); line-height: 1.8; color: var(--fg-muted);
}
.sources li::before {
  counter-increment: src; content: counter(src);
  position: absolute; inset-inline-start: 0; top: .1rem;
  inline-size: 1.4rem; text-align: center;
  color: var(--fg-subtle); font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.sources a { direction: ltr; unicode-bidi: embed; display: inline-block; max-width: 100%; overflow-wrap: anywhere; }

/* ---------- figures ----------
   Generated WebP charts. Fixed aspect ratio and an explicit size so the
   box is reserved before the image arrives (no layout shift). */
.figure {
  margin: var(--s-6) 0;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  overflow: hidden;
}
.figure img {
  inline-size: 100%;
  block-size: auto;
  aspect-ratio: 16 / 9;
  object-fit: cover;
}
.figure figcaption {
  padding: var(--s-3) var(--s-4);
  border-block-start: 1px solid var(--line);
  font-size: var(--t-sm);
  color: var(--fg-subtle);
  text-align: start;
  line-height: 1.7;
}

/* ---------- glass materials ----------
   Applied per Apple's material-weight rule: the chrome above is the heaviest
   (blur 20, deep shadow), these structural regions are medium, and the
   interactive chips below are light. Nothing light is ever stacked on
   something light.

   The inset highlight is what actually sells glass: a one-pixel light edge
   along the top reads as light catching the surface. A flat border does not. */
.slider, /* The comment block was the one card on the page with no padding: it was
   created before --s-4 existed and never picked it up, so the heading, the
   count and the form fields all sat directly on the border line. It is also
   still on --glass while every other surface is flat, which is why it reads as
   a different material from the page around it. */
.comments {
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: none;
  padding: var(--s-5) var(--s-4);
}
.side-block {
  background: var(--glass);
  border: 1px solid var(--glass-line);
  border-radius: var(--radius-lg);
  box-shadow: none;
  padding: var(--s-4);
}
/* Flat surfaces. A border alone separates them; the frosted edge highlight
   only reads on a translucent surface, and on a flat card it is a 1px bright
   line that looks like a rendering artefact. */
.badge, .cat-link {
  background: var(--surface-2);
  border: 1px solid var(--line);
  box-shadow: none;
}

/* ---------- search ----------
   The header carries one icon, not a field. On a phone a field in the header
   left the icon row too narrow to line up, and the field itself was unusable at
   97px; the icon opens a sheet instead, where the field gets the whole width.

   All three header controls are the same size and share one baseline, so they
   line up by construction rather than by a per-item offset. `align-items:
   center` on a flex row plus an identical block-size is what does it.
   ------------------------------------------------------------------------ */
.icon-btn {
  display: grid;
  place-items: center;
  flex: none;
  inline-size: 2.75rem;
  block-size: 2.75rem;
  padding: 0;
  color: var(--fg-muted);
  background: transparent;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: color .18s var(--ease), border-color .18s var(--ease),
              background-color .18s var(--ease);
}
.icon-btn:hover { color: var(--fg); border-color: var(--line-strong); background: var(--surface-2); }
.icon-btn:focus-visible { outline: 2px solid var(--fg-bright); outline-offset: 2px; }
.icon-btn svg { inline-size: 1.125rem; block-size: 1.125rem; }

/* The sheet: a panel that slides up from the bottom on a phone and sits
   centred as a small dialog on a wide screen. `hidden` is what keeps it out
   of the layout and out of the tab order before the icon is pressed. */
.search-sheet { position: fixed; inset: 0; z-index: 60; }
.search-sheet[hidden] { display: none; }
.search-sheet-backdrop {
  position: absolute;
  inset: 0;
  background: rgb(0 0 0 / .6);
  animation: sheetFade .18s var(--ease);
}
.search-sheet-panel {
  position: absolute;
  inset-block-end: 0;
  inset-inline: 0;
  margin-inline: auto;
  max-inline-size: 34rem;
  padding: var(--s-4);
  padding-block-end: max(var(--s-5), env(safe-area-inset-bottom));
  background: var(--surface);
  border: 1px solid var(--line);
  border-block-end: none;
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  animation: sheetUp .22s var(--ease);
}
/* A grab handle, because a panel that slides up with no visible affordance
   reads as a card that happened to land at the bottom. It is the one piece of
   chrome here: a 36x4 bar in --line-strong, and it is aria-hidden because it
   carries no information a screen reader should read. The decorative top
   border the panel used to have is gone -- a 1px line across the full width
   read as a divider with nothing above it. */
.search-sheet-grip {
  inline-size: 36px; block-size: 4px;
  margin: calc(var(--s-2) * -1) auto var(--s-4);
  background: var(--fg-subtle);
  border-radius: 2px;
}
@media (min-width: 48rem) { .search-sheet-grip { display: none; } }
@media (min-width: 48rem) {
  .search-sheet-panel {
    inset-block-start: 50%;
    inset-block-end: auto;
    inset-inline-start: 50%;
    inset-inline-end: auto;
    transform: translate(50%, -50%);
    border-block-end: 1px solid var(--line);
    border-radius: var(--radius-lg);
    animation: sheetPop .2s var(--ease);
  }
}
@keyframes sheetFade { from { opacity: 0; } }
@keyframes sheetUp { from { transform: translateY(14px); opacity: .4; } }
@keyframes sheetPop { from { transform: translate(50%, -46%); opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
  .search-sheet-backdrop, .search-sheet-panel { animation: none; }
}

/* The title row exists only to hold the close button, and that is a waste of a
   whole row on a phone: the reader opened a field to type in, and a heading
   about searching pushes the field down. So the title is hidden on a phone --
   the field is self-evident and the dialog already carries an aria-label. The
   button keeps its own accessible name, so nothing is lost to a screen reader
   or to search. */
.search-sheet-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-3);
  margin-block-end: var(--s-3);
}
@media (max-width: 47.99rem) {
  /* The button floats to the top corner of the panel and the form takes the row
     below it, so the field needs no inline padding for it -- a measured check
     said the two boxes did not overlap, and a guess here would have pushed the
     field off-centre for nothing. */
  /* The close button is the one control that must not sit under the submit
     button. Measured, not guessed: with the button in the inline-end corner
     the two boxes overlapped by 44x28px -- in RTL the submit button lands on
     the same side, so "the far corner" is not a corner. The fix is a row of
     its own for the button, above the field, where nothing else can reach it.
     `position: absolute` is what put it there in the first place. */
  .search-sheet-head {
    position: static;
    justify-content: flex-start;
    margin-block-end: var(--s-2);
  }
  /* The row is only the button on a phone, so it is collapsed to the button's
     own height and the panel's grip absorbs the visual weight. 3rem is the
     48px hit area; the icon inside stays 1.125rem. */
  /* The X is the only control that closes the sheet on a phone besides the
     backdrop, and a visual review found it too faint at the inherited header
     weight -- so it is styled here rather than shared with the header row. */
  .search-sheet-head .icon-btn {
    color: var(--fg);
    background: var(--surface-2);
    border: 1px solid var(--fg-subtle);
  }
  .search-sheet-head .icon-btn svg { inline-size: 1.25rem; block-size: 1.25rem; }
  .search-sheet-head .icon-btn:active { background: var(--surface-3); }
  .search-sheet-title { display: none; }
  /* A larger hit area than the 44px the icon button already provides: this is
     the only way out on a phone besides the backdrop, so it gets a 48px box
     with a negative inset that does not move the layout. */
  .search-sheet-head .icon-btn {
    inline-size: 3rem; block-size: 3rem;
    border-radius: 50%;
  }
}
.search-sheet-title { font-weight: 650; color: var(--fg); }

/* The field is the point of the dialog, so it gets the row to itself: 3.25rem is
   52px, comfortably above the 44px touch minimum, and the submit button sits
   beside it at the same height. The two were previously 2.75rem inside a form
   that also carried a heading, which left the tappable field the smallest
   thing on screen. */
.search-sheet-form { display: flex; gap: var(--s-2); }
/* The field carries a magnifier inside it, so the input is nested: the wrapper
   owns the box, the icon and the padding, and the input fills what is left. A
   review of a screenshot noted the field gave no sign it was a search box. */
.search-sheet-field {
  position: relative;
  flex: 1 1 auto;
  min-inline-size: 0;
  display: flex;
  align-items: center;
}
.search-sheet-field-icon {
  position: absolute;
  inset-inline-start: var(--s-4);
  display: flex;
  color: var(--fg-subtle);
  pointer-events: none;
}
.search-sheet-field-icon svg {
  inline-size: 1.125rem;
  block-size: 1.125rem;
}
.search-sheet-input {
  flex: 1 1 auto;
  min-inline-size: 0;
  block-size: 3.25rem;
  padding-inline: calc(var(--s-4) + 1.5rem) var(--s-4);
  font: inherit;
  font-size: 1rem;
  color: var(--fg);
  /* --canvas is oklch(0.155) and the sheet is oklch(0.19), a measured 1.06
     ratio: the field rendered DARKER than the panel containing it, which
     inverts the depth hierarchy a dark interface depends on. A control must sit
     above its surface, so the field takes the next step up from the panel
     rather than --canvas. */
  background: var(--surface-3);
  /* --line is oklch(0.285) and disappears against the sheet's near-black
     canvas, so the edge of the field could not be seen. A screenshot review
     caught it; no gate did. --fg-subtle is the lightest neutral that still
     reads as a border on this background. */
  border: 1px solid var(--fg-subtle);
  border-radius: var(--radius);
  caret-color: var(--fg);
}
.search-sheet-input::placeholder { color: var(--fg-subtle); }
/* The native clear control a type="search" field carries is nearly invisible on
   this skin, and it is the obvious way to start a second query -- so it has to
   be legible, or the sheet's only X (which closes the whole sheet) reads as the
   only way to erase text. */
.search-sheet-input::-webkit-search-cancel-button {
  -webkit-appearance: none;
  appearance: none;
  inline-size: 1.25rem;
  block-size: 1.25rem;
  cursor: pointer;
  background: var(--fg-subtle);
  /* The clear control sits at the inline-end, opposite the magnifier. */
  margin-inline-start: var(--s-2);
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M6 6l12 12M18 6L6 18' stroke='%23000' stroke-width='2.5' stroke-linecap='round'/%3E%3C/svg%3E") center / contain no-repeat;
}
.search-sheet-input::placeholder { color: var(--fg-subtle); }
.search-sheet-input:focus-visible { outline: 2px solid var(--fg-bright); outline-offset: 1px; }
.search-sheet-go {
  flex: none;
  block-size: 3.25rem;
  padding-inline: var(--s-5);
}

/* the page's own search form and its results */
.search-page-form { display: flex; gap: var(--s-2); margin-block-end: var(--s-4); }
.search-page-form .site-search-input { inline-size: 100%; block-size: 3.2rem; font-size: 1rem; }
.search-page-form .site-search-btn { inline-size: 3.2rem; block-size: 3.2rem; }
.search-page-count { color: var(--fg-muted); font-size: 0.9rem; margin-block-end: var(--s-4); }
.search-page-count:empty { display: none; }
#search-results .post-item { padding-block: var(--s-4); }
#search-results .post-item-title { color: var(--fg); }

/* A plain page title, for the pages that are not the home hero. The old .hero
   h1 rule carried a 24ch cap written for a two-line slogan; reusing .hero for
   a one-line title broke the word onto its own line, so these pages get their
   own element instead. */
.page-head { padding-block: var(--s-7) var(--s-5); }
.page-head h1 {
  margin: 0;
  font-size: clamp(var(--t-xl), 4vw, var(--t-2xl));
  font-weight: 750;
  line-height: 1.2;
  letter-spacing: -.02em;
  color: var(--fg-bright);
}
.page-head p {
  margin: var(--s-3) 0 0;
  color: var(--fg-muted);
  line-height: 1.8;
}
.site-search-label { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); }
.site-search-input {
  min-inline-size: 0;
  padding-inline: var(--s-3);
  font: inherit;
  color: var(--fg);
  background: var(--canvas);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
}
.site-search-input::placeholder { color: var(--fg-subtle); }
.site-search-input:focus-visible { outline: 2px solid var(--fg-bright); outline-offset: 1px; }
.site-search-btn {
  display: grid; place-items: center; flex: none;
  inline-size: 2.75rem; block-size: 2.75rem; padding: 0;
  color: var(--fg-muted);
  background: transparent;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.site-search-btn:hover { color: var(--fg); background: var(--surface-2); }
.site-search-btn svg { inline-size: 1.125rem; block-size: 1.125rem; }

/* ---------- photos ----------
   One rule serves every thumbnail: the file is always 16:9, so
   aspect-ratio reserves the box and the page never shifts on load.

   The photos are the one place colour is allowed. The chrome around them
   is pure grey, so a saturated photo has something neutral to sit against
   -- that contrast is the point, and desaturating it would throw away the
   only warmth on the page. Nothing to bake into the file: the og:image a
   crawler fetches is already the same picture. */
.thumb {
  display: block;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  background: var(--canvas);
  border-radius: var(--radius);
}

/* Post rows: photo on the inline-start side, text takes the rest. Below the
   narrowest breakpoint the photo goes full width above the text. */
.post-item-pic {
  display: block;
  overflow: hidden;
  border-radius: var(--radius);
  inline-size: 100%;
  aspect-ratio: 16 / 9;
}
.post-item-pic .thumb { inline-size: 100%; block-size: 100%; }
.post-item-text { display: grid; gap: var(--s-2); min-inline-size: 0; }
.post-item .post-item-title { display: block; }
.post-item .post-item-excerpt { display: block; }

@media (min-width: 40.01rem) {
  .post-item { grid-template-columns: 12rem minmax(0, 1fr); align-items: start; }
}

/* Slider: photo beside the text on wide screens, stacked when narrow. */
.slide-pic {
  overflow: hidden;
  border-radius: var(--radius);
  aspect-ratio: 16 / 9;
  inline-size: 100%;
}
.slide-pic .thumb { inline-size: 100%; block-size: 100%; }
.slide-body { display: grid; gap: var(--s-3); align-content: start; min-inline-size: 0; }
@media (min-width: 56rem) {
  .slide { flex-direction: row; align-items: center; gap: var(--s-6); }
  .slide-pic { flex: 0 0 34rem; }
  .slide-body { flex: 1 1 auto; }
}

/* Sidebar: rank number, small square-ish photo, then the text. */
.side-item { display: flex; align-items: flex-start; gap: var(--s-3); }
.side-rank {
  flex: none;
  inline-size: 1.5rem;
  color: var(--fg-subtle);
  font-size: var(--t-sm);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  line-height: 1.6;
  text-align: center;
}
.side-thumb {
  flex: none;
  inline-size: 5.5rem;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border-radius: var(--radius-sm, 4px);
}
.side-thumb .thumb { inline-size: 100%; block-size: 100%; }
.side-text { min-inline-size: 0; flex: 1 1 auto; }
@media (max-width: 64rem) {
  /* The sidebar collapses under the article on narrow screens; the tiny
     thumbnails stop earning their space there. The rank number stays,
     because it still communicates "ordered by popularity". */
  .side-thumb { display: none; }
}

/* ---------- comments ---------- */
.comments { margin-block-start: var(--s-8); }
.comment-form { display: flex; flex-direction: column; gap: var(--s-3); margin-block-start: var(--s-4); }
.field { display: flex; flex-direction: column; gap: var(--s-2); }
.field label { font-size: var(--t-sm); font-weight: 600; color: var(--fg-muted); }
.field input, .field textarea {
  inline-size: 100%; padding: var(--s-3);
  background: var(--canvas); color: var(--fg);
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  font: inherit; font-size: var(--t-sm);
  transition: border-color .18s var(--ease);
}
.field textarea { min-block-size: 7rem; resize: vertical; line-height: 1.8; }
.field input:focus, .field textarea:focus { border-color: var(--accent-quiet); }
.field input::placeholder, .field textarea::placeholder { color: var(--fg-subtle); }
.form-note { font-size: var(--t-xs); color: var(--fg-subtle); }
.comment-list { list-style: none; margin: var(--s-5) 0 0; padding: 0; }
.comment { padding-block: var(--s-4); border-block-start: 1px solid var(--line); }
.comment-head { display: flex; align-items: baseline; gap: var(--s-2); margin-block-end: var(--s-2); }
.comment-author { font-weight: 600; font-size: var(--t-sm); }
.comment-date { font-size: var(--t-xs); color: var(--fg-subtle); font-variant-numeric: tabular-nums; }
.comment p { margin: 0; font-size: var(--t-sm); line-height: 1.85; color: var(--fg-muted); white-space: pre-wrap; }
.comment-actions { display: flex; gap: var(--s-4); margin-block-start: var(--s-2); }
.comment-actions button {
  background: none; border: none; padding: 0; cursor: pointer;
  color: var(--fg-subtle); font: inherit; font-size: var(--t-xs); font-weight: 500;
}
.comment-actions button:hover { color: var(--fg-bright); }

/* ---------- footer ---------- */
.site-footer {
  margin-block-start: var(--s-8); padding-block: var(--s-6) var(--s-7);
  border-block-start: 1px solid var(--line);
  color: var(--fg-subtle); font-size: var(--t-sm);
  overflow-x: hidden;
}
.site-footer-inner {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s-4); flex-wrap: wrap; max-width: 100%;
}
.site-footer p { margin: 0; }
.footer-links { display: flex; gap: var(--s-4); flex-wrap: wrap; }
.footer-link { color: var(--fg-subtle); }
.footer-link:hover { color: var(--fg-bright); }

.visually-hidden {
  position: absolute; inline-size: 1px; block-size: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}

/* ---------- responsive ---------- */
@media (max-width: 60rem) {
  .with-side { grid-template-columns: minmax(0, 1fr); }
  .side { position: static; }
}
@media (max-width: 48rem) {
  .container { padding-inline: var(--s-4); }
  .page { padding-block: var(--s-6) var(--s-7); }
  .hero { padding-block: var(--s-7) var(--s-6); }
  /* On a phone the control row was the worst part of the page: two arrows, a
     row of seven 44px dots and a counter could not share 360px, so the dots
     wrapped and the counter was pushed off the edge. With the dots gone the
     row is a number and two arrows, and it fits with room to spare. */
  .slider-controls { padding: var(--s-2) var(--s-3) var(--s-2) var(--s-4); }
  .slider-btn { inline-size: 2.75rem; block-size: 2.75rem; }
  .slider-count { font-size: var(--t-base); }
  .slider-count strong { font-size: var(--t-lg); }
  .slide { padding: var(--s-5) var(--s-4); gap: var(--s-2); }
  .slide h2 { font-size: var(--t-lg); }
  .slide p { font-size: var(--t-sm); line-height: 1.8; }
  .brand-tagline { display: none; }
  .nav-link span { display: none; }
  /* With the labels hidden, the three header controls are icons on their own.
     Two of them wore a box and one did not, so the row read as two buttons and
     a loose glyph: geometrically aligned, visually not a set. On a phone all
     three take the same 44px box, and the active state moves from a filled
     background to weight plus a rule, because a fill would be the only cue
     left and it is too weak to read at a glance. */
  .nav-link {
    padding-inline: 0;
    inline-size: 2.75rem;
    block-size: 2.75rem;
    justify-content: center;
    border: 1px solid var(--line);
  }
  .nav-link svg { inline-size: 1.125rem; block-size: 1.125rem; }
}
@media (max-width: 30rem) {
  .slider-btn { inline-size: 2.25rem; block-size: 2.25rem; }
  .slide h2 { font-size: var(--t-lg); }
}

/* ---------- motion ---------- */
/* ---------- forms: the full state set ----------
   A form element is the one place a "click and it does nothing" bug is
   invisible to a sighted mouse user and fatal to everyone else. Every control
   gets its states explicitly, and the note is a live region so validation
   reaches a screen reader instead of only changing pixels. */
.comment-form input,
.comment-form textarea {
  inline-size: 100%;
  background: var(--surface-2);
  color: var(--fg);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: var(--s-3);
  font: inherit;
  line-height: 1.7;
  transition: border-color var(--dur, 120ms) var(--ease, ease-out),
              background-color var(--dur, 120ms) var(--ease, ease-out);
}
.comment-form textarea { min-block-size: 7rem; resize: vertical; }
.comment-form input::placeholder,
.comment-form textarea::placeholder { color: var(--fg-subtle); }

.comment-form input:hover,
.comment-form textarea:hover { border-color: var(--line-strong); }

/* The focus ring itself must never animate in -- keyboard users need an
   indicator on the first frame. Only the border colour responds here. */
.comment-form input:focus-visible,
.comment-form textarea:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-color: var(--accent);
}

.comment-form input:active,
.comment-form textarea:active { background: var(--surface-3); }

.comment-form input:disabled,
.comment-form textarea:disabled,
.comment-form button:disabled {
  opacity: .55;
  cursor: not-allowed;
}

/* Invalid state, driven by the field's own validity rather than a class, so
   it cannot drift out of sync with what the browser actually rejects. */
.comment-form input:user-invalid,
.comment-form textarea:user-invalid { border-color: var(--danger); }

.form-note { margin: var(--s-3) 0 0; font-size: var(--t-sm); color: var(--fg-muted); line-height: 1.7; }
.form-note[data-state="error"] { color: var(--danger); }
.form-note[data-state="success"] { color: var(--ok); }
/* Reserved space: a message appearing must not shove the layout down. */
.form-note:empty { margin-block-start: 0; }

/* ---------- anchor targets clear the sticky header ----------
   A heading that is the target of a link scrolls to the very top of the
   viewport, which is exactly where a sticky header sits, so the reader lands
   on a heading they cannot see. scroll-margin is the fix and it costs nothing. */
:where(h2, h3)[id] { scroll-margin-top: calc(var(--header-h) + 1.5rem); }

/* ---------- table of contents ----------
   A TOC earns its space only when the post is long enough to scan. It sits
   after the hero figure, not before it, because the figure is the lede's
   evidence and belongs directly under the claim it supports. */
.toc {
  margin-block: 2.5rem 1.5rem;
  padding: 1.25rem 1.5rem;
  background: var(--glass);
  border: 1px solid var(--glass-line);
  border-radius: var(--radius);
  box-shadow: none;
}
.toc-title {
  margin-block-end: .75rem;
  font-size: .8125rem;
  font-weight: 600;
  color: var(--fg-muted);
  letter-spacing: .01em;
}
.toc ol {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: .5rem;
}
/* RTL: the marker has to sit on the right, and a plain counter needs the
   marker detached or it renders on the wrong side. */
.toc li { counter-increment: none; }
.toc li a {
  display: flex;
  gap: .625rem;
  align-items: baseline;
  color: var(--fg);
  text-decoration: none;
  font-size: .9375rem;
  line-height: 1.5;
}
.toc li a::before {
  content: counter(toc, persian);
  counter-increment: toc;
  flex: none;
  min-inline-size: 1.5rem;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
  font-size: .8125rem;
}
.toc ol { counter-reset: toc; }
.toc li a:hover { color: var(--fg-bright); }

@media (prefers-contrast: more) { .toc { -webkit-backdrop-filter: none; backdrop-filter: none; background: var(--surface); } }

/* ---------- touch targets ----------
   Measured, not assumed: nav links were 40px tall and category chips 36px,
   four and eight pixels under the 44px minimum. On a phone that is the
   difference between a tap and a miss. Only on coarse pointers, because a
   mouse has no target-size requirement and the extra padding just makes the
   desktop nav look loose. */
@media (pointer: coarse) {
  .nav-link,
  .cat-link,
  .footer-link,
  .badge,
  .brand,
  .slider-btn,
  .slider-dot,
  .toc li a,
  .btn {
    min-block-size: 2.75rem;
  }
  /* A chip is inline-flex, so min-block-size alone leaves it hugging its
     text; the inline padding has to grow with it. */
  .badge { padding-block: .5rem; }
  .cat-link { padding-block: var(--s-2); }
  /* The dot indicators are a separate control: they need a real box, not a
     bigger dot, or they overlap each other. */
    /* Sidebar rows are a genuine tap target on a phone, not inline prose: the
     whole row is one link to a post. */
  .side-text a { min-block-size: 2.75rem; display: flex; align-items: center; }
  .side-item { padding-block: .375rem; }
  /* The nav collapses to icons on a phone, and the icon alone is 34px wide --
     tall enough but still a narrow target. min-inline-size fixes the other
     axis without needing the label back. Same for the slider arrows and the
     footer links, which are all icon-sized or text-only on a phone. */
  .nav-link,
  .slider-btn,
  .footer-link { min-inline-size: 2.75rem; justify-content: center; }
  /* A footer link is text, not an icon, so centring it looks wrong. */
  .footer-link { justify-content: flex-start; }
}

/* ---------- touch behaviour ----------
   Without touch-action the browser waits ~300ms to decide whether a tap is a
   scroll or a click, which is the "this site feels slow" feeling on mobile.
   The blue flash on tap is a separate, uglier problem: it is the browser's
   default highlight, not our design. */
:where(a, button, summary, label, [role="button"]) {
  touch-action: manipulation;
  -webkit-tap-highlight-color: oklch(1 0 0 / .14);
}

/* ---------- accessibility fallbacks ----------
   Glass only works where the layer above it has something to refract. Three
   OS-level signals can make it unusable, and each needs its own response —
   reduced transparency is not reduced motion. */
@media (prefers-reduced-transparency: reduce) {
  .site-header, .slider, .comments, .side-block {
    background: var(--surface);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
  .badge, .cat-link {
    background: var(--surface-2);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
}

@media (prefers-contrast: more) {
  /* Near-solid backgrounds with a defined, contrasting border. */
  .site-header, .slider, .comments, .side-block {
    background: var(--canvas);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    border-color: var(--line-strong);
  }
  :root {
    --glass-line:   oklch(1 0 0 / .30);
    --glass-line-2: oklch(1 0 0 / .40);
  }
}

/* No backdrop-filter support at all: the translucent background alone would be
   washed out and unreadable over the canvas. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .site-header { background: var(--surface); }
  .slider, .comments, .side-block { background: var(--surface); }
}

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

/* ---------- print ----------
   Ink is expensive and a dark page prints as a solid black rectangle, so
   print is the one place the site inverts to light. */
@media print {
  .site-header, .slider-controls, .comments, .site-footer { display: none; }
  body { background: #fff; color: #000; }
  .article-body a { text-decoration-color: #000; }
}

/* The /about/ topic table. Nothing in the site was a <table> before, so this had
   no styling at all and rendered with the UA default sheet: no rule between
   rows, no alignment, and in RTL the count column drifted off the edge. Kept to
   the site's monochrome language -- a hairline rule, no fill, no accent. */
.about-table {
  width: 100%;
  border-collapse: collapse;
  margin: var(--s-6) 0;
  font-size: 0.95rem;
  text-align: start;
}
.about-table th,
.about-table td {
  padding: var(--s-3) var(--s-2);
  border-block-end: 1px solid var(--line);
  font-weight: 400;
}
.about-table thead th {
  border-block-end: 1px solid var(--line-strong);
  color: var(--fg-subtle);
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.02em;
}
.about-table tbody th {
  font-weight: 500;
}
.about-table td {
  /* the count is a number: LTR digits in an RTL cell drift to the wrong edge */
  text-align: end;
  font-variant-numeric: tabular-nums;
  color: var(--fg-subtle);
  inline-size: 5.5rem;
}
.about-table tbody tr:last-child th,
.about-table tbody tr:last-child td {
  border-block-end: 0;
}
