/* ============================================================
 * Deque Lab — article shell
 * Everything a blog post needs on top of assets/styles.css: the
 * hero, the left rail and its contents, the prose scale, and the
 * in-body components (callouts, checklists, tables, cards, FAQ).
 *
 * Split out of the first post's inline <style> when the second
 * post arrived — two copies of 1,100 lines would have drifted
 * within a week. One file, one cache entry, every article.
 * ============================================================ */

html { scroll-padding-top: 90px; }

:root {
  /* An all-system serif alternative, kept as an escape hatch: Charter is
     what Medium licenses and ships on macOS/iOS, Georgia covers Windows,
     Noto Serif covers Android. Zero bytes and no swap. */
  --f-serif: Charter, 'Bitstream Charter', 'Sitka Text', Georgia, 'Noto Serif', serif;
  /* The shared palette carries no red — only the accent green and the
     amber used for "no" in tables. --red is the negative counterpart to
     --accent; --red-2 is the muted tone for a cross that should read as
     "absent" rather than as a warning. */
  --red: #cf3b34;
  --red-2: #d98079;

  /* Reading face for the article body. Lato now comes from the shared
     stylesheet as --f-sans, so this only exists as the one switch that
     returns the prose to Charter without touching anything else. */
  --f-read: var(--f-sans);
}

/* ============ Hero ============ */
.post-hero { padding: 64px 0 40px; }
.post-hero-grid {
  display: grid; grid-template-columns: 1.05fr 1fr;
  gap: 56px;
  /* stretch, not center: the art takes its height from the title column so
     the two sides stay flush even if the headline rewraps. */
  align-items: stretch;
}
@media (max-width: 900px) { .post-hero-grid { grid-template-columns: 1fr; gap: 36px; } }

.post-hero h1 {
  font-size: clamp(32px, 4.4vw, 54px); line-height: 1.08;
  letter-spacing: -0.035em; font-weight: 500; margin: 18px 0 0;
}

.post-meta {
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px 26px;
  margin-top: 26px;
  font-family: var(--f-mono); font-size: 12.5px; color: var(--muted);
}
.post-meta .m { display: inline-flex; align-items: center; gap: 8px; }
.post-meta svg { width: 15px; height: 15px; color: var(--accent); flex: none; }

.post-tags { display: flex; flex-wrap: wrap; gap: 8px; margin: 22px 0 0; padding: 0; list-style: none; }

.crumbs {
  margin-top: 22px; font-size: 13px; color: var(--muted); line-height: 1.6;
}
.crumbs a { color: var(--accent); }
.crumbs a:hover { text-decoration: underline; }
.crumbs span[aria-hidden] { color: var(--line-2); margin: 0 6px; }

/* ============================================================
 * Hero art — a 3D scene built entirely from CSS transforms.
 * No library, no image request: the <h1> stays the LCP element and
 * the whole thing costs zero bytes over the wire. Only transform and
 * opacity animate, so it runs on the compositor — no layout, no paint,
 * no CLS. Everything is sized in cqw so the scene scales like an SVG.
 * ============================================================ */
/* No frame, no border, no card shadow — the scene sits directly on the
   section. The only background is an edgeless accent haze. */
.hero-art {
  position: relative;
  container-type: inline-size;
  background: radial-gradient(58% 62% at 62% 45%, color-mix(in oklch, var(--accent) 11%, transparent), transparent 72%);
}
/* Absolute fill: the height comes from the title column via align-items:stretch,
   so the scene is laid out against whatever that resolves to. */
.scene {
  position: absolute; inset: 0;
  display: grid;
  grid-template-columns: minmax(0, 1.2fr) minmax(0, 0.8fr);
  gap: 2.2cqw; align-items: center;
  perspective: 90cqw;
  font-family: var(--f-sans);
}
/* Flex/grid rather than absolute percentages: the frame height is driven by
   the headline, so a percentage-positioned composition would collide the
   moment the title rewrapped. This adapts instead. */
.scene .stage { display: flex; flex-direction: column; gap: 2.4cqw; min-width: 0; }
.scene .feats { display: flex; flex-direction: column; align-items: flex-end; gap: 1.9cqw; min-width: 0; }

/* ---- Panel: flat and rectangular, facing the reader ---- */
.scene .panel {
  background: #fff; border-radius: 2.2cqw;
  /* Stacked hard shadows build the extruded slab edge; the last is the cast shadow. */
  box-shadow:
    0 0.8cqw 0 var(--line),
    0 1.6cqw 0 var(--line-2),
    0 2.8cqw 4.5cqw rgba(14,18,16,0.15);
  animation: drift 9s ease-in-out infinite;
}
.scene .panel-bar {
  display: flex; align-items: center; gap: 1cqw;
  padding: 1.8cqw 2.4cqw;
  background: var(--accent); border-radius: 2.2cqw 2.2cqw 0 0;
}
.scene .panel-bar i { width: 1.5cqw; height: 1.5cqw; border-radius: 50%; background: rgba(255,255,255,0.55); }
.scene .brandpill {
  margin-left: auto; display: inline-flex; align-items: center;
  background: #fff; border-radius: 999px; padding: 0.7cqw 1.7cqw;
}
.scene .brandpill img { height: 2.1cqw; width: auto; display: block; }
.scene .panel-body { padding: 2.4cqw 2.6cqw 2.6cqw; }
.scene .panel-title {
  display: flex; align-items: center; justify-content: space-between; gap: 1.5cqw;
  font-size: 2.4cqw; font-weight: 600; color: var(--ink); letter-spacing: -0.01em;
  padding-bottom: 1.8cqw; border-bottom: 1px solid var(--line);
}
.scene .panel-title em {
  font-style: normal; font-family: var(--f-mono); font-size: 1.7cqw; letter-spacing: 0.05em;
  color: #a45300; background: #fff4e5; border: 1px solid #f4d9b0;
  border-radius: 999px; padding: 0.5cqw 1.2cqw;
}
.scene .rows { list-style: none; margin: 0; padding: 0; }
.scene .rows li {
  display: grid; grid-template-columns: 1fr auto auto; align-items: center; gap: 1.4cqw;
  padding: 1.7cqw 0; border-bottom: 1px dashed var(--line);
  font-size: 2.1cqw;
}
.scene .rows li:last-child { border-bottom: 0; padding-bottom: 0; }
.scene .rows .nm { color: var(--ink-2); font-weight: 500; }
.scene .rows .qt {
  font-family: var(--f-mono); font-size: 1.6cqw; color: var(--muted);
  background: var(--paper-2); border: 1px solid var(--line);
  border-radius: 999px; padding: 0.4cqw 1.1cqw;
}
.scene .rows .pr { font-family: var(--f-mono); font-weight: 500; color: var(--accent); }
.scene .rows .pr.was { color: var(--muted-2); text-decoration: line-through; }

/* ---- Floating feature chips ---- */
.scene .chip {
  display: inline-flex; align-items: center; gap: 1.3cqw;
  padding: 1.5cqw 2.1cqw;
  background: #fff; border-radius: 2.2cqw;
  font-size: 2.1cqw; font-weight: 600; color: var(--ink);
  letter-spacing: -0.01em; white-space: nowrap;
  box-shadow:
    0 0.7cqw 0 var(--line-2),
    0 1.6cqw 3cqw rgba(14,18,16,0.16);
}
.scene .chip svg { width: 2.6cqw; height: 2.6cqw; flex: none; }
.scene .chip .ic {
  width: 4.4cqw; height: 4.4cqw; flex: none;
  display: grid; place-items: center; border-radius: 1.3cqw;
  background: color-mix(in oklch, var(--accent) 14%, white); color: var(--accent);
}
.scene .chip.amber .ic { background: #fff4e5; color: #a45300; }

/* Chips sit in a flex column so they can never collide with the panel.
   --dx staggers them horizontally for a scattered, hand-placed look; the
   float keyframes fold that offset in so the two transforms don't fight. */
/* All offsets are <= 0 so a chip can never push past the right edge. */
.scene .c1 { --dx: -2.5cqw; animation: float 6.5s ease-in-out infinite; }
.scene .c2 { --dx: 0;       animation: float 7.5s ease-in-out infinite 0.6s; }
.scene .c3 { --dx: -4cqw;   animation: float 8.2s ease-in-out infinite 1.2s; }
.scene .c4 { --dx: -1cqw;   animation: float 7s   ease-in-out infinite 0.3s; }

/* ---- "B2B" blocks, real 3D cubes ---- */
.scene .blocks {
  display: flex; gap: 1cqw;
  padding-left: 1.5cqw;
  transform-style: preserve-3d;
}
.scene .cube {
  position: relative; width: 8cqw; height: 8cqw;
  transform-style: preserve-3d;
  transform: rotateX(-16deg) rotateY(-22deg);
}
.scene .cube.flip { animation: flip 9s ease-in-out infinite; }
.scene .cube b {
  position: absolute; inset: 0;
  display: grid; place-items: center;
  font-family: var(--f-mono); font-size: 3.4cqw; font-weight: 500;
  color: var(--ink-2); border-radius: 0.8cqw;
  backface-visibility: hidden;
}
.scene .cube .fr { transform: translateZ(4cqw); background: #f1eee3; }
.scene .cube .tp { transform: rotateX(90deg) translateZ(4cqw); background: #faf8f2; }
.scene .cube .rt { transform: rotateY(90deg) translateZ(4cqw); background: #ded8c6; }

@keyframes float {
  0%, 100% { transform: translate(var(--dx, 0), 0); }
  50%      { transform: translate(var(--dx, 0), -1.7cqw); }
}
@keyframes drift {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-1.2cqw); }
}
/* Slow quarter-turn: the front face reads B, the right face C. */
@keyframes flip {
  0%, 38%   { transform: rotateX(-16deg) rotateY(-22deg); }
  50%, 88%  { transform: rotateX(-16deg) rotateY(-112deg); }
  100%      { transform: rotateX(-16deg) rotateY(-22deg); }
}

/* The scene is decorative — hold it still for anyone who asks. */
@media (prefers-reduced-motion: reduce) {
  .scene .panel, .scene .chip, .scene .cube { animation: none; }
}
/* Stacked layout: cap the width rather than changing the aspect ratio.
   The chip positions are tuned against one geometry — altering the ratio
   shifts the panel relative to them and they start covering the prices. */
@media (max-width: 900px) {
  /* Stacked: there is no title column to take height from any more, so the
     frame needs an explicit ratio. width:100% is required, not redundant —
     this is a grid item with container-type:inline-size, so auto margins
     alone collapse it to zero because containment stops the contents from
     establishing a width. */
  .hero-art {
    width: 100%; max-width: 560px; margin-inline: auto;
    aspect-ratio: 100 / 56;
  }
}

/* ============ Body layout ============ */
.post-layout {
  display: grid; grid-template-columns: 300px 1fr;
  gap: 64px; align-items: start;
  padding: 8px 0 88px;
}
/* Grid tracks default to min-width:auto, so the widest table would otherwise
   inflate the whole column and push the page sideways. Let the track shrink
   and leave horizontal scrolling to .stable-wrap, where it belongs. */
.post-layout > * { min-width: 0; }
@media (max-width: 1000px) { .post-layout { grid-template-columns: 1fr; gap: 36px; } }

/* ---- Left rail ---- */
.post-rail {
  position: sticky; top: 90px;
  /* On short viewports the card is taller than the space available and the
     share buttons get clipped — let it scroll internally instead. */
  max-height: calc(100vh - 106px);
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  scrollbar-color: var(--line-2) transparent;
}
@media (max-width: 1000px) {
  .post-rail { position: static; max-height: none; overflow: visible; }
}
.rail-card {
  background: #fff; border: 1px solid var(--line);
  border-radius: var(--r-lg); padding: 26px 24px;
}

.rail-author { display: flex; align-items: center; gap: 14px; }
.rail-author img {
  width: 52px; height: 52px; flex: none; border-radius: 50%;
  object-fit: cover; border: 2px solid #fff;
  box-shadow: 0 4px 14px rgba(14,18,16,0.14), 0 0 0 1px var(--line);
}
.rail-author .n { font-weight: 600; font-size: 16px; letter-spacing: -0.015em; }
.rail-author .r { color: var(--muted); font-size: 13px; margin-top: 2px; }

.rail-sep { height: 1px; background: var(--line); margin: 22px 0; border: 0; }

.rail-label {
  font-family: var(--f-mono); font-size: 11px; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--muted); margin: 0 0 14px; font-weight: 500;
}

/* ---- Reading progress ---- */
.toc-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.toc-head .rail-label { margin: 0; }
.toc-pct {
  font-family: var(--f-mono); font-size: 12px; font-weight: 500;
  color: var(--accent); font-variant-numeric: tabular-nums;
}
.toc-track {
  height: 3px; margin: 10px 0 6px;
  background: var(--paper-3); border-radius: 999px; overflow: hidden;
}
.toc-track > span {
  display: block; height: 100%; width: 0;
  background: var(--accent); border-radius: 999px;
  transition: width 160ms linear;
}

/* ---- Grouped, self-collapsing contents ---- */
.toc { list-style: none; margin: 0; padding: 0; }
.toc-group + .toc-group { border-top: 1px solid var(--line); }

.toc-group-head {
  width: 100%; display: grid;
  grid-template-columns: auto 1fr auto auto; align-items: center; gap: 10px;
  padding: 12px 0; background: none; border: 0; text-align: left;
  font-family: inherit; color: var(--ink-2); cursor: pointer;
}
.toc-group-head:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 8px; }

.toc-group-n {
  width: 24px; height: 24px; flex: none;
  display: grid; place-items: center; border-radius: 8px;
  background: var(--paper-2); border: 1px solid var(--line);
  font-family: var(--f-mono); font-size: 10px; color: var(--muted-2);
  transition: background 180ms ease, color 180ms ease, border-color 180ms ease;
}
.toc-group-t {
  font-size: 13.5px; font-weight: 500; letter-spacing: -0.01em; line-height: 1.3;
  transition: color 180ms ease;
}
.toc-group-meta {
  font-family: var(--f-mono); font-size: 10.5px; color: var(--muted-2);
  font-variant-numeric: tabular-nums;
}
.toc-chev { width: 14px; height: 14px; color: var(--muted-2); transform: rotate(180deg); transition: transform 240ms ease; }

/* Group holding the section on screen. */
.toc-group.current .toc-group-n { background: var(--accent); color: var(--accent-ink); border-color: transparent; }
.toc-group.current .toc-group-t { color: var(--accent); }
/* Groups fully scrolled past. */
.toc-group.done .toc-group-n {
  background: color-mix(in oklch, var(--accent) 14%, white);
  border-color: color-mix(in oklch, var(--accent) 30%, white);
  color: var(--accent);
}
.toc-group.collapsed .toc-chev { transform: rotate(0deg); }

/* Groups render expanded and JS collapses them, so with no JS the sidebar
   degrades to a plain nested list instead of hiding 11 of the 13 links.
   0fr -> 1fr animates to the panel's natural height with no magic max-height. */
.toc-panel {
  display: grid; grid-template-rows: 1fr;
  transition: grid-template-rows 260ms ease;
}
.toc-group.collapsed .toc-panel { grid-template-rows: 0fr; }
.toc-items { overflow: hidden; list-style: none; margin: 0; padding: 0; }
/* Keeps collapsed links out of the tab order and away from screen readers. */
.toc-group.collapsed .toc-items { visibility: hidden; transition: visibility 0s linear 260ms; }

.toc-items li:last-child a { margin-bottom: 12px; }
.toc-items a {
  display: block; margin-left: 11px; padding: 6px 0 6px 15px;
  border-left: 2px solid var(--line);
  font-size: 13px; line-height: 1.45; color: var(--muted);
  transition: color 140ms ease, border-color 140ms ease;
}
.toc-items a:hover { color: var(--ink); border-left-color: var(--line-2); }
.toc-items a.read { color: var(--muted-2); border-left-color: color-mix(in oklch, var(--accent) 30%, var(--line)); }
.toc-items a.active {
  color: var(--accent); font-weight: 500;
  border-left-color: var(--accent);
  /* Second cue so the active row is not signalled by colour alone. */
  background: linear-gradient(90deg, color-mix(in oklch, var(--accent) 8%, transparent), transparent 70%);
}

.share { display: flex; gap: 10px; }
.share button, .share a {
  width: 40px; height: 40px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: #fff; border: 1px solid var(--line); color: var(--muted);
  transition: border-color 150ms ease, color 150ms ease, transform 150ms ease;
}
.share button:hover, .share a:hover,
.share button:focus-visible, .share a:focus-visible {
  border-color: var(--accent); color: var(--accent); transform: translateY(-1px);
}
.share svg { width: 17px; height: 17px; }
.share .copied { color: var(--accent); border-color: var(--accent); }

/* ---- Prose: serif, full column width ----
   No max-width by request: prose runs the full width of the grid column. */
.post-body {
  font-family: var(--f-read);
  font-size: 18px; line-height: 1.7; color: var(--ink-2);
}
/* Opt out of the global `section` padding — prose sets its own rhythm. */
.post-body section { padding: 0; }
.post-body section + section { margin-top: 8px; }
.post-body p { margin: 0 0 24px; }
.post-body .intro { font-size: 21px; line-height: 1.65; color: var(--ink-2); }

/* Anchor offset comes from html{scroll-padding-top} above — adding
   scroll-margin-top here too would double it and land jumps 180px down. */
/* Headings stay in Geist so structure reads as distinct from the prose. */
.post-body h2 {
  font-family: var(--f-sans);
  font-size: clamp(26px, 2.6vw, 32px); line-height: 1.2;
  letter-spacing: -0.025em; font-weight: 600; color: var(--ink);
  margin: 52px 0 18px;
}
.post-body h3 {
  font-family: var(--f-sans);
  font-size: 21px; line-height: 1.35; font-weight: 600;
  letter-spacing: -0.015em; color: var(--ink); margin: 34px 0 12px;
}
.post-body a { color: var(--accent); text-underline-offset: 3px; }
.post-body a:hover { text-decoration-thickness: 2px; }

/* Inline brand link: logo + label read as a single word.
   nowrap keeps the mark from being orphaned at the end of a line, and the
   underline lives on the inner span so it runs under the text only. */
.post-body a.brand-link { white-space: nowrap; text-decoration: none; }
.post-body a.brand-link span { text-decoration: underline; text-underline-offset: 3px; }
.post-body a.brand-link:hover span { text-decoration-thickness: 2px; }
.post-body a.brand-link img {
  /* Overrides the site-wide `img { display: block }` in styles.css, which
     would otherwise push the mark onto a line of its own. */
  display: inline-block;
  /* Sized in em so the mark tracks the prose, not a fixed pixel value.
     1em of a 19px serif overshoots optically — 0.95 matches the cap height.
     The -0.12em nudge drops it off the baseline onto the x-height centre. */
  width: 0.95em; height: 0.95em;
  margin-right: 0.28em;
  vertical-align: -0.12em;
}
/* 700, not 600: Charter and Georgia ship only 400 and 700, so an in-between
   weight makes the browser synthesise a smeared fake bold. */
.post-body strong { color: var(--ink); font-weight: 700; }

.post-body code {
  font-family: var(--f-mono); font-size: 0.86em;
  background: var(--paper-2); border: 1px solid var(--line);
  border-radius: 5px; padding: 1px 5px;
}

.post-body ul, .post-body ol { margin: 0 0 24px; padding-left: 22px; display: grid; gap: 10px; }
.post-body li { font-size: 18px; line-height: 1.68; }
.post-body li::marker { color: var(--accent); }

/* ============================================================
 * Numbered timeline — styling only, applied to any <ul class="steps">.
 * Numbers come from a CSS counter, so the markup stays a plain list,
 * items can wrap to any height, and each list restarts at 01.
 *
 * Everything below is derived from the four knobs — change --tl-size
 * alone and the disc, the number, the dotted line and the text
 * alignment all rescale together.
 * ============================================================ */
.steps {
  --tl-size: 25px;    /* disc diameter                              */
  --tl-num: 12px;     /* number size inside the disc                */
  --tl-gap: 18px;     /* vertical space between items               */
  --tl-space: 14px;   /* disc → text gap                            */
  --tl-line: 30px;    /* first text line box (18px × 1.68 ≈ 30)     */
  /* Row height the disc and the first text line share — whichever is
     taller. Both are centred on it, so they always sit on one axis. */
  --tl-row: max(var(--tl-size), var(--tl-line));

  list-style: none;
  counter-reset: tl;
  padding-left: 0!important;
  margin: 28px 0;
  gap: var(--tl-gap);
}

/* Deliberately NOT a grid: an item may contain a <strong> lead-in plus
   loose text, and every child of a grid — including each bare text run —
   becomes its own grid item, which drops the text into the narrow disc
   column. Normal flow with an out-of-flow disc keeps any inline markup
   inside an item flowing as one paragraph. */
.steps > li {
  position: relative;
  padding-left: calc(var(--tl-size) + var(--tl-space));
  /* Text sinks by half the difference when the disc is the taller of
     the two; zero while the disc is smaller than a line of text. */
  padding-top: calc((var(--tl-row) - var(--tl-line)) / 2);
  min-height: var(--tl-size);
}

/* Numbered <h3>: the same disc on a heading rather than a list item, for
   runs of steps that carry their own sub-headings. The counter resets per
   section, so each section starts again at 01, and the number lives in CSS
   instead of being typed into the heading text. */
.post-body section { counter-reset: sh; }
.post-body h3.step-h {
  --tl-size: 28px;    /* a heading carries a slightly larger disc */
  --tl-num: 13px;
  --tl-space: 14px;
  --tl-line: 28px;    /* first text line box (21px × 1.35 ≈ 28) */
  --tl-row: max(var(--tl-size), var(--tl-line));

  position: relative;
  counter-increment: sh;
  padding-left: calc(var(--tl-size) + var(--tl-space));
  padding-top: calc((var(--tl-row) - var(--tl-line)) / 2);
  min-height: var(--tl-size);
}
.post-body h3.step-h::before { content: counter(sh, decimal-leading-zero); }

/* The disc: counter value, zero-padded (01, 02, …). */
.steps > li::before {
  counter-increment: tl;
  content: counter(tl, decimal-leading-zero);
}
.steps > li::before,
.post-body h3.step-h::before {
  position: absolute;
  z-index: 1;
  left: 0;
  top: calc((var(--tl-row) - var(--tl-size)) / 2);

  width: var(--tl-size);
  height: var(--tl-size);
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 600;
  line-height: 1;
  letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums;
  border-bottom: 2px solid var(--accent);
}

@media (max-width: 640px) {
  .steps {
    --tl-size: 22px;
    --tl-num: 11px;
    --tl-gap: 14px;
    --tl-space: 12px;
    --tl-line: 29px;
  }
}

/* ---- Screenshots ----
   A pair of admin captures shown side by side. They arrive at slightly
   different ratios, so each is letterboxed into one shared box on the
   admin's own canvas grey (#f1f0f1, sampled from the captures) — the bands
   are invisible and both frames end up the same height. Shrunk this far the
   UI text is small, so each frame links out to the full-size file. */
.shots {
  /* Per-pair geometry: every capture in a pair shares one ratio and one
     canvas colour, so the two frames always end up the same height. */
  --shot-ratio: 1316 / 784;
  --shot-bg: #f1f0f1;

  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
  margin: 18px 0 4px;
}
/* The quantity pair is white-backed and near-identical in ratio
   (2.586 vs 2.581), so the two frames match without any padding. */
.shots-quantity {
  --shot-ratio: 1306 / 506;
  --shot-bg: #fff;
}
/* Lead illustration under the intro. Not a .shot: those wrap a link to the
   full-size capture, and there is nothing to zoom into here. Width and
   height are on the tag so the 3:2 box is reserved before the bytes land. */
.lead-art { margin: 34px 0 0; }
.lead-art img {
  width: 100%; height: auto; display: block;
  border: 1px solid var(--line); border-radius: var(--r-lg);
}

/* Worked example: a rule stated once, then the cases that pass and fail.
   Chips rather than a table — five short values do not need column headers,
   and they wrap down the page on a phone without any extra rules.
   Colour follows the convention plan_cell() already set for the plan table:
   --accent for yes, --red for no, and the glyph is decorative because the
   sr-only word beside it is what actually gets announced.

   Selectors carry .post-body deliberately. The prose default is
   `.post-body ul { display: grid; padding-left: 22px }` at (0,1,1), so a
   bare `.qty-ex` at (0,1,0) loses and the chips render as full-width blocks.
   Matching the prefix beats it on specificity instead of on source order. */
.post-body .qty-ex {
  margin: 14px 0 0;
  padding-left: 0;
  list-style: none;
}
.post-body .qty-ex li {
  padding: 6px 0px;
  border-radius: 999px;
  font-size: 14px;
  line-height: 1.5;
  border: 1px solid transparent;
  white-space: nowrap;
}
.post-body .qty-ex li::before { font-size: 13px; line-height: 1; }
.post-body .qty-ex .ok {
  color: color-mix(in oklch, var(--accent) 80%, black);
}
.post-body .qty-ex .ok::before {
  content: "\2713";
  color: var(--accent);
  margin-right: 10px;
}
.post-body .qty-ex .no { color: #8a2c2c; }
.post-body .qty-ex .no::before {
  content: "\2717";
  color: var(--red-2);
  margin-right: 10px;
}
.post-body .qty-ex b { font-weight: 600; }

.shot { margin: 0; }
/* The frame lives on the link, not the image: the radius can then clip
   whatever sits inside it, including a trimmed capture. */
.shot a {
  display: block;
  overflow: hidden;
  text-decoration: none;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--shot-bg);
  transition: transform 0.18s ease, box-shadow 0.18s ease;
}

.shot img {
  width: 100%;
  height: auto;
  aspect-ratio: var(--shot-ratio);
  object-fit: contain;
  display: block;
}

/* quantity.png was captured with the dimmed page behind the modal: a
   #787878 ring measuring 10px left, 12px right, 11px top, 9px bottom, with
   the modal's own rounded corners inside it. Zoom past the ring and the
   frame's radius does the rounding, with the file left untouched.
   The axes scale separately because the capture is 2.6x wider than tall, so
   one percentage cannot clear both: 1.02 lifts 12.4px off each side (clears
   12), while the same figure would lift only 4.8px off the top (needs 11).
   1.06 vertically clears it with 13.8px, at the cost of stretching the
   image ~4% taller than life — invisible at this size, and the full-res
   original is one click away. */
.shot-trim img { transform: scale(var(--trim-x, 1.02), var(--trim-y, 1.06)); }

/* volume-pricing.png carries a much thinner ring (4/6/5/5px), so a single
   uniform figure clears every side and nothing gets stretched. */
.shots-volume {
  --shot-ratio: 1250 / 782;
  --shot-bg: #fff;
  --trim-x: 1.02;
  --trim-y: 1.02;
}

/* ---- Pros / cons lines inside a .steps item ----
   Two columns, not an inline chip: the labels line up as a column of their
   own and the verdicts all start at the same x, so a reader can run down
   the list comparing like with like. Solid fill because these are the
   judgement in the section — they should outrank the body text. */
.sr-only {
  position: absolute; width: 1px; height: 1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}

/* ---- Plan-features table ----
   A plain white grid: rules between every row and column, no column tint.
   The only colour is the accent — the tier glyphs, the ticks and the one
   value ("Unlimited") that is an exception rather than a yes or no. */
.stable-wrap {
  overflow-x: auto;
  margin: 0 0 10px;
  background: #fff;
  border: 1px solid var(--line);
  border-radius: 12px;
}
.stable {
  width: 100%;
  min-width: var(--stable-min, 540px);
  border-collapse: collapse;
  font-family: var(--f-sans);
  font-size: 15px;
  text-align: left;
  color: var(--ink-2);
}
/* Column rules come from the cells, so they stop at the group bands, which
   span the full width and read as dividers in their own right. */
.stable thead th + th,
.stable tbody td { border-left: 1px solid var(--line); }
.stable thead th { border-bottom: 1px solid var(--line); }
.stable tbody th, .stable tbody td { border-top: 1px solid var(--line); }
.stable tbody:first-of-type tr:first-child th { border-top: 0; }

.stable thead th {
  padding: 16px 20px;
  font-weight: 500;
  color: var(--ink);
  white-space: nowrap;
  background: color-mix(in oklch, var(--accent) 10%, white);
}
.pl-ico {
  display: block;
  width: 19px; height: 19px;
  margin: 0 auto 7px;
  fill: var(--accent);
  color: var(--accent);
}
.pl-name { font-size: 15px; }

.stable tbody th, .stable tbody td { padding: 14px 20px; line-height: 1.45; vertical-align: top; }
.stable tbody th[scope="row"] { font-weight: 400; }
/* Prose tables read left; only the mark matrix centres its value columns,
   where a tick has nothing to align to but the column itself. */
.stable-marks { --stable-min: 660px; }
.stable-marks thead th + th,
.stable-marks tbody td { text-align: center; vertical-align: middle; }
.ft { display: block; color: var(--ink); }
.fs { display: block; margin-top: 2px; font-size: 13.5px; color: var(--muted); }

/* App-store icon beside a listing name. The icon is now taller than one
   line, so the app column gets a min-width: without it the icon eats the
   horizontal room and the longest name breaks onto a third line. */
/* ---- Install path ----
   The route to a feature, drawn as one line: app-store mark, the app's own
   icon, then the admin breadcrumb. Centred, and it wraps to as many lines as
   the width needs rather than scrolling sideways.

   Size is one number. Change --ap-ico on the element (inline style or a
   rule) and the icons, their radius and the separators scale with it.

   .post-body prefix on purpose: the prose defaults for p and a are
   (0,1,1), so an unprefixed class would lose the cascade. */
.post-body .app-path {
  --ap-ico: 25px;
  display: flex; flex-wrap: wrap; align-items: center; justify-content: left;
  gap: 4px 8px;
  margin: 16px 0 0;
  font-family: var(--f-sans); font-size: 15px; line-height: 1.5;
}
/* One link around both icons, not two: same destination twice would be two
   tab stops and two identical announcements. */
.post-body .app-path .ap-node {
  display: inline-flex; align-items: center; gap: 7px;
  text-decoration: none; color: var(--ink); font-weight: 700;
  transition: background 140ms ease, border-color 140ms ease;
}
.post-body .app-path .ap-node:hover {
  outline: 0.5px solid var(--accent); outline-offset: 2px;
}
.post-body .app-path .ap-node:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px;
}
.post-body .app-path .ap-ico {
  width: var(--ap-ico); height: var(--ap-ico);
  flex: none; display: block;
  border-radius: calc(var(--ap-ico) * 0.22);
}
/* The store mark is a glyph, not a tile — no plate behind it. */
.post-body .app-path .ap-ico.ap-store {
  border: 0; border-radius: 0; color: var(--accent);
  width: calc(var(--ap-ico) * 0.82); height: calc(var(--ap-ico) * 0.82);
}
.post-body .app-path img.ap-ico { border: 1px solid var(--line-2); object-fit: cover; }
/* Sized against the text, not --ap-ico: these mostly sit between word steps,
   so scaling them with the icons made them heavy at larger settings. */
.post-body .app-path .ap-sep {
  color: var(--muted-2); font-size: 1.15em; line-height: 1;
  flex: none; user-select: none;
}
.post-body .app-path .ap-step { color: var(--ink-2); white-space: nowrap; }

.ft-app { display: flex; align-items: center; gap: 10px; }
.app-ico {
  flex: 0 0 auto;
  width: 34px; height: 34px;
  border-radius: 8px;
  border: 1px solid var(--line);
  object-fit: cover;
  display: block;
}
.stable-apps tbody th[scope="row"] { min-width: 250px; }

.stable .grp th {
  background: color-mix(in oklch, var(--accent) 10%, white);
  padding: 12px 20px;
  font-weight: 700;
  font-size: 15px;
  color: var(--ink);
  text-align: left;
}
.stable .mk { font-size: 17px; line-height: 1; }
.stable .mk.yes { color: var(--accent); }
.stable .mk.no { color: var(--red-2); }
.stable .mk-txt { color: var(--accent); font-weight: 600; }
.post-body .src { font-size: 14px; color: var(--muted); margin: 0 0 24px; }

/* ---- Theme comparison cards ----
   One row per theme: identity, who it suits, pros, cons. Each card carries
   its own hue in --c, which drives the rail, the number ring, the name and
   the chip; pros/cons keep a fixed green and red so the verdict reads the
   same way down every card. */
.tcards { display: grid; gap: 16px; margin: 26px 0 28px; }
.tcard {
  display: grid;
  /* The article column is narrower than the mockup's canvas, so the tracks
     are weighted towards the two verdict columns, which carry the most
     text; identity keeps just enough for the name and its subtitle. */
  grid-template-columns: 1.12fr 0.95fr 1.05fr 1.15fr;
  border: 1px solid var(--line);
  border-left: 5px solid var(--c);
  border-radius: 16px;
  background: linear-gradient(100deg, color-mix(in oklch, var(--c) 8%, #fff), #fff 52%);
  overflow: hidden;
}
.tcard > * { padding: 20px 16px; min-width: 0; }
.tcard > * + * { border-left: 1px solid var(--line); }

.tc-id {
  display: grid;
  grid-template-columns: 1fr;
  column-gap: 14px;
  align-content: center;
}
.post-body .tc-name {
  margin: 0;
  font-family: var(--f-sans);
  font-size: 22px;
  line-height: 1.2;
  letter-spacing: -0.01em;
  color: var(--c);
  scroll-margin-top: 0;
}
.post-body .tc-name a { color: inherit; text-decoration: none; }
.post-body .tc-name a:hover { text-decoration: underline; text-underline-offset: 3px; }
.post-body .tc-sub {
  margin: 5px 0 0;
  font-size: 14px;
  line-height: 1.45;
  color: var(--muted);
}

/* The three cell labels are one component. They were not: "Best for" is a
   <span> and Pros/Cons are <p>, so `.post-body .tc-cell p` at (0,2,1) beat
   `.post-body .tc-h` at (0,2,0) and pushed 11px of top margin onto those two
   while the span sat flush. Naming the cell in the selector wins that on
   specificity — which is what the !important patches were standing in for. */
.post-body .tc-cell .tc-chip,
.post-body .tc-cell .tc-h {
  display: block;
  margin: 0;
  font-size: 12.5px;
  font-weight: 600;
  line-height: 1.5;
}
/* Colour is the only pro/con signal left now the tick and cross icons are
   out of the markup, so it stays while everything else matches. */
.post-body .tc-cell .tc-h.is-pro { color: #2f8f4e; }
.post-body .tc-cell .tc-h.is-con { color: #cf3b34; }

.post-body .tc-cell p { margin: 11px 0 0; font-size: 12.5px; line-height: 1.5; }

.tc-line {
  display: grid;
}

/* Four columns need room. Below that, identity and "best for" share a row
   and pros/cons share the next; narrower still, everything stacks and the
   dividers move from the side to the top. */
@media (max-width: 900px) {
  .tcard { grid-template-columns: 1fr 1fr; }
  .tcard > *:nth-child(3) { border-left: 0; }
  .tcard > *:nth-child(n + 3) { border-top: 1px solid var(--line); }
}
@media (max-width: 560px) {
  .tcard { grid-template-columns: 1fr; }
  .tcard > * + * { border-left: 0; border-top: 1px solid var(--line); }
  .tcard > * { padding: 16px; }
}

/* A lone capture takes the whole column rather than half of it: this one is
   a seven-row order table, and half the width would leave the rows
   unreadable with an empty half beside them. No ring to trim. */
.shots-single {
  grid-template-columns: 1fr;
  --shot-ratio: 2326 / 1246;
  --shot-bg: #fff;
}

/* The signup modal sits on a near-black backdrop with rounded corners, and
   1.05 is the smallest uniform zoom that clears both the 16-20px ring and
   the corner arcs — 1.06 leaves a margin, with no stretching. The ratio is
   the signup's, so the slightly wider admin capture letterboxes by ~3px a
   side into #f1f0f1, which is exactly its own canvas colour. */
/* catalog-3.png is a wide, short banner capture on Shopify's own grey
   page background — no trim, the ratio just has to match the file. */
.shots-catalog {
  --shot-ratio: 1992 / 300;
  --shot-bg: #f1f0f1;
  margin: 18px 0 24px!important;
}

.shots-request {
  --shot-ratio: 1156 / 966;
  --shot-bg: #f1f0f1;
  --trim-x: 1.06;
  --trim-y: 1.06;
}

/* ---- Live volume-pricing block ----
   The storefront side is rebuilt in markup rather than screenshotted: the
   prices stay searchable and selectable, and it renders sharp at any zoom.
   Sizing is in cqw against the figure, so it scales exactly like the
   capture next to it and both frames stay the same height. */
.shot-live { container-type: inline-size; }
.vp {
  /* Side by side the row already has a height, and matching it exactly
     beats the ratio — the capture's own 1px frame makes it a hair taller.
     Stacked, there is no row to match and the ratio takes over. */
  height: 100%;
  aspect-ratio: var(--shot-ratio);
  display: flex;
  flex-direction: column;
  gap: 2.4cqw;
  font-family: var(--f-sans);
  color: #3d3d3d;
}
.vp-card {
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: #fff;
  overflow: hidden;          /* lets the radius clip the striped rows */
}
.vp-tiers-card { flex: 1; display: flex; }
.vp-tiers {
  width: 100%;
  height: 100%;              /* rows then split the frame evenly */
  border-collapse: collapse;
  table-layout: fixed;
  font-size: clamp(10px, 3.3cqw, 15px);
}
.vp-tiers caption {
  position: absolute; width: 1px; height: 1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
.vp-tiers th, .vp-tiers td {
  padding: 0 4.4cqw;
  font-weight: 400;
  line-height: 1.4;
  white-space: nowrap;
}
.vp-tiers th { text-align: left; }
.vp-tiers td { text-align: right; }
/* Zebra from the first row down, matching the storefront block. */
.vp-tiers tr:nth-child(odd) { background: #f7f7f7; }
.vp-min {
  padding: 3.4cqw 4.4cqw;
  font-size: clamp(10px, 3.3cqw, 15px);
  line-height: 1.4;
}
/* Half of an already-narrow column is unreadable — stack on small screens. */
@media (max-width: 700px) {
  .shots { grid-template-columns: 1fr; gap: 16px; margin: 14px 0 2px; }
}

.post-body blockquote {
  margin: 28px 0; padding: 20px 24px;
  background: var(--paper-2); border-left: 3px solid var(--accent);
  border-radius: 0 12px 12px 0; font-size: 18px; color: var(--ink-2);
}
.post-body blockquote p:last-child { margin-bottom: 0; }

/* Badge and verdict helpers, shared by every table on the page. */
.stable .plus {
  font-family: var(--f-mono); font-size: 10.5px; letter-spacing: 0.06em;
  background: var(--ink); color: var(--paper); border-radius: 999px; padding: 3px 8px; white-space: nowrap;
}
.stable .yes { color: var(--accent); font-weight: 600; }
.stable .no { color: var(--red); font-weight: 600; }
.stable td strong { color: var(--ink); }

.callout {
  margin: 28px 0; padding: 22px 24px;
  background: color-mix(in oklch, var(--accent) 7%, white);
  border: 1px solid color-mix(in oklch, var(--accent) 28%, white);
  border-radius: 14px; font-size: 18px;
}
.callout p:last-child { margin-bottom: 0; }
.callout .k {
  display: block; font-family: var(--f-mono); font-size: 11px; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--accent); margin-bottom: 8px;
}

/* ---- Tip callout ----
   Outline box with the bulb straddling the top-left corner; the bulb
   itself is emitted by tip_bulb(). Overrides .callout's tinted fill, so
   the two must be used together: class="callout callout-tip". */
.callout-tip {
  position: relative;
  margin: 75px 0 30px;
  padding: 26px 30px 26px 34px;
  background: transparent;
  border: 1px solid color-mix(in oklch, var(--accent) 72%, white);
  border-radius: 10px;
}
.tip-bulb {
  position: absolute;
  top: -32px;
  left: -28px;
  width: 62px;
  height: 62px;
}

.mt-6 {
  margin-top: 30px;
}

.tip-k { font-weight: 700; color: var(--ink); }
/* Tucked inside the box on small screens — hanging off the corner would
   push past the viewport edge. */
@media (max-width: 560px) {
  .callout-tip { margin-top: 40px; padding: 24px 20px; }
  .tip-bulb { top: -30px; left: 6px; width: 54px; height: 54px; }
}

/* ---- In-content app promo ----
   A card, not a banner. Constrained width and a left-aligned layout so it
   reads as a product being shown rather than the page shouting: at full
   bleed the same content looked like an interstitial ad and got skipped.
   Everything sizes from --apr-logo, so one number moves the block. */
.post-body .app-promo {
  --apr-logo: 56px;
  display: flex; align-items: flex-start; gap: 18px;
  max-width: 560px;
  margin: 30px 0;
  padding: 20px 22px;
  background: #fff;
  border: 1px solid var(--line);
  border-left: 4px solid var(--accent);
  border-radius: 14px;
  box-shadow: 0 1px 2px rgba(14, 18, 16, 0.04);
  margin-left: auto;
  margin-right: auto;
}
.post-body .apr-logo {
  width: var(--apr-logo); height: var(--apr-logo);
  flex: none; display: block; margin: 0;
  border-radius: calc(var(--apr-logo) * 0.22);
  border: 1px solid var(--line);
}
.post-body .apr-body { min-width: 0; }
.post-body .apr-eyebrow {
  display: block;
  font-family: var(--f-mono); font-size: 10.5px; letter-spacing: 0.13em;
  text-transform: uppercase; color: var(--muted);
}
.post-body .apr-name {
  margin: 5px 0 0;
  font-family: var(--f-sans); font-size: 17px; font-weight: 700;
  line-height: 1.3; letter-spacing: -0.01em; color: var(--ink);
}
.post-body .apr-desc {
  margin: 8px 0 0;
  font-size: 14.5px; line-height: 1.55; color: var(--muted);
}
.post-body .apr-desc strong { color: var(--ink-2); font-weight: 700; }
.post-body .apr-foot {
  display: flex; align-items: center; flex-wrap: wrap; gap: 10px 14px;
  margin-top: 15px;
}
.post-body .apr-btn {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 8px 18px; border-radius: 999px;
  background: var(--accent); color: var(--accent-ink);
  font-size: 14px; font-weight: 700; text-decoration: none;
  transition: background 140ms ease;
}
.post-body .apr-btn::after { content: "\2192"; }
.post-body .apr-btn:hover { background: var(--accent-mid); }
.post-body .apr-btn:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.post-body .apr-price {
  font-family: var(--f-mono); font-size: 12.5px; color: var(--muted);
}
@media (max-width: 560px) {
  .post-body .app-promo { flex-direction: column; gap: 14px; }
}

/* Comparison table in "What is a B2B store" — two columns that are read
   against each other, so the header row carries the contrast, not colour
   inside the cells. */
.vs-table td:first-child + td { color: var(--muted); }

/* Marker is positioned, not a grid column. As a grid the box only worked
   for items made of one text node: a leading <strong> became grid item one
   and the sentence after it was placed in the 22px marker column, which
   broke it to one word per line. Absolute positioning lets an item hold
   any inline markup. */
/* ---- Nested week plan ----
   Weeks are the checklist items; their steps are a plain disc list inside.
   Sizes come from --wk-size so one number moves the whole block, and both
   values are the article's own defaults rather than a foreign stylesheet.

   Selectors name .post-body because the prose defaults (`.post-body ul` at
   (0,1,1) sets display:grid and padding-left:22px, `.post-body li` sets the
   type) would otherwise outrank a bare class and the discs would sit in the
   wrong place. display is reset to block: a grid <ul> blockifies its items
   and the markers stop rendering. */
.post-body .week-plan { --wk-size: 18px; }
.post-body .week-plan .wk {
  display: block;
  font-size: var(--wk-size);
  font-weight: 700;
  line-height: 1.5;
  color: var(--ink);
}
.post-body .week-plan > li > ul {
  display: block;
  list-style: disc;
  margin: 8px 0 0;
  padding-left: 22px;
}
.post-body .week-plan > li > ul > li {
  font-size: var(--wk-size);
  line-height: 1.5;
  color: var(--ink-2);
}
.post-body .week-plan > li > ul > li + li { margin-top: 7px; }
.post-body .week-plan > li > ul > li::marker { color: var(--accent); }

.checklist { list-style: none; padding-left: 0; }
/* Direct children only. A nested list inside an item — the pack-size
   example — would otherwise inherit a checkbox per line. */
.checklist > li {
  position: relative; padding-left: 32px;
  font-size: 18px; line-height: 1.6;
}
.checklist > li + li { margin-top: 14px; }
.checklist > li::before {
  content: "→";
  position: absolute;
  left: 0;
}

/* ---- FAQ ---- */
.faq { display: grid; gap: 10px; margin: 24px 0 0; }
.faq details {
  background: #fff; border: 1px solid var(--line); border-radius: 14px;
  transition: border-color 150ms ease;
}
.faq details[open], .faq details:hover { border-color: var(--line-2); }
/* The question is a heading, so it stays in Geist; the answer is prose and
   inherits the serif. */
.faq summary {
  cursor: pointer; list-style: none; padding: 18px 22px;
  font-family: var(--f-sans);
  font-size: 18px; font-weight: 600; color: var(--ink); letter-spacing: -0.015em;
  display: flex; align-items: flex-start; justify-content: space-between; gap: 16px;
  line-height: 1.45;
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after {
  content: "+"; font-family: var(--f-mono); font-size: 20px; color: var(--accent);
  line-height: 1; flex: none; transition: transform 180ms ease;
}
.faq details[open] summary::after { transform: rotate(45deg); }
.faq .a { padding: 0 22px 20px; color: var(--muted); font-size: 17.5px; line-height: 1.66; }

/* ---- Related ---- */
/* .blog/.post/.cover-art now live in assets/styles.css: /blogs, the
   homepage row and these cards all render the same component. */

.sec-head { display: grid; grid-template-columns: 1fr auto; gap: 24px; align-items: end; margin-bottom: 32px; }
@media (max-width: 820px) { .sec-head { grid-template-columns: 1fr; } }

@media (prefers-reduced-motion: reduce) {
  .toc-items a, .toc-group-n, .toc-group-t, .toc-chev, .toc-panel, .toc-track > span,
  .share button, .share a, .faq summary::after { transition: none; }
}

.mb-1 {
  margin-bottom: 0.5rem!important;
}

.mt-3 {
  margin-top: 24px!important;
}

