/*
 * Tondo — "the morning room"
 *
 * One room, one light. Warm linen paper, ink type, an old gold dark enough to
 * clear 4.5:1 on that paper. Display: Fraunces · Body: Newsreader.
 *
 * There used to be two skins here — a near-black gallery for the archive and
 * this linen page for the daily pick — and moving between them read as two
 * apps. Every screen now uses the components below. The only surface allowed
 * to go dark is the full-screen view, where the surround is the mount board.
 *
 * Style decisions and the reasons behind them: DESIGN.md.
 */

/*
 * The typefaces come from us — story 0009.
 *
 * These used to load from fonts.googleapis.com, which put two extra hosts on
 * the critical path, broke the product's face offline, and sent every reader's
 * IP to a third party on every cold load.
 *
 * The files are Google's own latin / latin-ext / vietnamese woff2 subsets,
 * byte-for-byte what the CDN was already serving. That is deliberate: this
 * story changes where the fonts come from and nothing else, and unmodified
 * bytes make a visual diff impossible by construction. They are variable
 * fonts and the product uses the axes — `opsz` at 60 and 100 explicitly,
 * and everywhere else via `font-optical-sizing: auto`, which is why no axis
 * is pinned and no static weight is substituted. Fraunces' `SOFT` and `WONK`
 * axes are already dropped in these files; the design uses neither.
 *
 * Three subsets, not one. `db/seeds/mia_paintings.json` carries ā č ō ū ž ȟ ŋ ƞ
 * in real artist names — all latin-ext. Shipping latin alone would drop those
 * names mid-word into Georgia, on exactly the non-Euro-canon curation the
 * Better bucket asks for. `unicode-range` means a reader only ever downloads
 * the subset their page actually needs.
 *
 * CJK (佛性成盛秀見 in the same file) is unchanged: neither family ships CJK
 * glyphs and the CDN never served any, so those titles fall back to a system
 * face exactly as they do today.
 *
 * Licence: SIL OFL 1.1, text committed alongside the files in this directory.
 */

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

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

@font-face {
  font-family: "Fraunces";
  font-style: italic;
  font-weight: 300 700;
  font-display: swap;
  src: url("/assets/fraunces-italic-vietnamese-6fd0a150.woff2") format("woff2");
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}

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

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

@font-face {
  font-family: "Fraunces";
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url("/assets/fraunces-roman-vietnamese-ff9facdc.woff2") format("woff2");
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}

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

@font-face {
  font-family: "Newsreader";
  font-style: italic;
  font-weight: 300 600;
  font-display: swap;
  src: url("/assets/newsreader-italic-latinext-e778c051.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: "Newsreader";
  font-style: italic;
  font-weight: 300 600;
  font-display: swap;
  src: url("/assets/newsreader-italic-vietnamese-68e1f49e.woff2") format("woff2");
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}

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

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

@font-face {
  font-family: "Newsreader";
  font-style: normal;
  font-weight: 300 600;
  font-display: swap;
  src: url("/assets/newsreader-roman-vietnamese-d00bc857.woff2") format("woff2");
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}

:root {
  --bg: #f4efe6;
  --bg-lift: #ece5d8;
  --ink: #2a241c;       /* 13.4:1 on linen */
  --ink-dim: #635a4c;   /*  6.0:1 */
  --ink-faint: #6f6456; /*  5.1:1 — still AA at small sizes */
  --gold: #7d5f18;      /*  5.3:1 */
  --gold-deep: #5f4711;
  --warn: #a33f28;
  --warn-rgb: 163, 63, 40; /* channel triple of --warn, for the destructive chip's alpha steps (story 0034) */
  --hairline: rgba(125, 95, 24, 0.3);
  --frame-edge: rgba(42, 36, 28, 0.18);
  --mount-bg: #100e0b;  /* the one dark surface — see DESIGN.md, accepted exceptions */
  --serif-display: "Fraunces", "Iowan Old Style", Georgia, serif;
  --serif-body: "Newsreader", Georgia, serif;
  --measure: 42.5rem;

  /* The touch-target bar, DESIGN.md rule 9. A token because story 0014 gave the
     rail glyphs a width as well as a height and put the same number into
     `.plate__img`'s reserve — four sites, and a bar that half-lands is exactly
     the failure ISSUE-002 shipped once. In `px`, deliberately: a finger does not
     get bigger when a reader raises their text size. */
  --tap: 44px;
}

* { box-sizing: border-box; }

/* `hidden` is a promise; a later `display` rule must not be able to break it. */
[hidden] { display: none !important; }

html {
  background: var(--bg);
  scrollbar-color: #cdc3b2 var(--bg);
}

body {
  margin: 0;
  min-height: 100dvh;
  font-family: var(--serif-body);
  color: var(--ink);
  background:
    radial-gradient(120% 55% at 50% 0%, rgba(125, 95, 24, 0.05), transparent 62%),
    var(--bg);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* Paper tooth over everything, very quiet */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 3;
  opacity: 0.045;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

::selection { background: rgba(125, 95, 24, 0.25); color: var(--ink); }

a { color: var(--gold); text-decoration: none; }
a:hover { color: var(--gold-deep); }
:focus-visible { outline: 2px solid var(--gold); outline-offset: 3px; border-radius: 2px; }

/* Small-caps link — the one link style in the product. */
.caps-link {
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.2em;
}

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

/* ---------- Masthead ----------
 * One bar for every screen: what this is on the left, where you are on the
 * right. Never sticky — nothing is allowed to hover over a painting.
 */

/* Story 0017 added a third column for the corner, and `label` spanning the
 * first TWO is the whole trick. Give `you` its own column while `label` stays
 * in the `1fr` and that column drops 198.19 -> 134.19px at root 20: "Artwork of
 * the day" wraps to two lines and the bar goes 117.00 -> 137.00px, which is
 * +20px against a fold the front door clears by 9. The label had width to give
 * and the brand did not, so the label gives it and ends up wider than before.
 *
 * `aside` sits on row 1 only now instead of spanning both. It keeps
 * `align-self: center`, which centres it against the brand's line box rather
 * than against a two-row block — the same optical result, one less thing to
 * restate. */
.masthead {
  display: grid;
  grid-template-columns: 1fr auto auto;
  grid-template-areas:
    "brand   aside   you"
    "label   label   you"
    "compass compass compass";
  align-items: baseline;
  column-gap: 1rem;
  row-gap: 0.15rem;
  /* Story 0008. Under `viewport-fit=cover` the page owns the unsafe strips, so
     the brand would otherwise sit under the notch and, in landscape, under the
     rounded corner. The horizontal insets are 0 in portrait. */
  padding:
    calc(0.9rem + env(safe-area-inset-top))
    calc(clamp(1.15rem, 5vw, 2rem) + env(safe-area-inset-right))
    0.75rem
    calc(clamp(1.15rem, 5vw, 2rem) + env(safe-area-inset-left));
  border-bottom: 1px solid var(--hairline);
}

.masthead__brand {
  grid-area: brand;
  font-family: var(--serif-display);
  font-size: 1.02rem;
  font-weight: 560;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink);
  font-variation-settings: "opsz" 60;
}
.masthead__brand:hover { color: var(--ink); }
.masthead__brand::first-letter { color: var(--gold); }

.masthead__label {
  grid-area: label;
  margin: 0;
  font-style: italic;
  font-size: 0.86rem;
  color: var(--ink-faint);
}

/* The date on the daily page, the count in the archive. On a fallback day the
 * date is the pick's own, never today's. */
.masthead__aside {
  grid-area: aside;
  align-self: center;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  color: var(--ink-dim);
  white-space: nowrap;
}

/* The corner (story 0017) — the reader's own room, top right, on every screen.
 *
 * It spans the brand and label rows and takes its height from them, which is
 * the entire reason it is here rather than on the compass row: that band
 * already measures over a touch target at the accessibility cap, so a 44px box
 * across it adds nothing to a bar the front door has 9px of fold to spare on.
 *
 * Height comes from the 44px registry below, which this class joins. WIDTH does
 * not: that registry gets width for free from the words inside a caps-link, and
 * a glyph has none — so `.masthead__you` sits with `.rail__act` in the
 * wordless-control rule further down, which states `min-width` once for both.
 * `DESIGN.md` rule 9 is every direction it has.
 *
 * `justify-self: end` so the box hugs the right edge — without it the column is
 * `auto`-sized and the glyph floats in the middle of its own target. */
.masthead__you {
  grid-area: you;
  align-self: center;
  justify-self: end;
  color: var(--gold);
}

/* On `/you` itself the mark stays in the bar, the link comes away and the
 * colour steps back — the same treatment the compass gives the screen you are
 * standing on, which is why `.masthead__you--here` sits in that rule rather
 * than restating the token here. One idiom for "you are here", not two. */

/* ---------- Compass ----------
 * Where else you can go. Four destinations, always all four, same order on every
 * screen. The label above says which one you are on; this row says what the
 * others are, and marks the current one by taking its link away.
 *
 * It costs 44px per row, always, because `DESIGN.md` rule 9 applies to every one
 * of these and `.caps-link` sets size and tracking but never height. That is why
 * the labels are one word each: measured at 375px, four one-word labels fit on
 * one row and four two-word labels wrap to two, and the second row comes
 * straight out of the space above the artwork. The front door clears the
 * accessibility fold by 9px with this row in place and by 92px without it —
 * `test/system/dynamic_type_test.rb` is what stops the next thing that wants
 * space up here. Numbers in `specs/0012-getting-around/plan.md`.
 *
 * No separator glyph. The gap at 0.2em tracking already parts the words, and
 * `·` is spent twice over on `.label__artist-dates` and `Kept · Remove`.
 *
 * The gap started at 1.4rem and is now clamped, which is not fussiness — it is
 * the difference between one row and two. Measured at 375px with Dynamic Type
 * at its cap: the four labels are 270px wide and the page leaves 329px, so
 * three 1.4rem gaps (354px total) wrap and three 1rem gaps (330px) still wrap,
 * by one pixel. A wrapped row costs 44px straight out of the fold budget.
 *
 * The clamp is what stops the arms race. A pure `rem` gap grows with the text
 * at exactly the moment there is least room for it, so the middle term is `vw`:
 * the space between two words is about how wide the screen is, not how large
 * the reader set their type. Widen any of the three numbers and
 * `dynamic_type_test.rb` goes red, which is the rule working. */
.compass {
  display: flex;
  flex-wrap: wrap;
  column-gap: clamp(0.7rem, 3.5vw, 1rem);
}

.masthead .compass {
  grid-area: compass;
}

/* The compass row is 44px of touch target, and a touch target is mostly air.
 * Below the third-column breakpoint that air is the space under the label and
 * the space above the plate, so the masthead and the page stop adding their own
 * on top of it. Measured on the front door at 375x667: this is 22px of the 44
 * the row costs, and the front door needs all 22.
 *
 * Scoped to narrow widths on purpose. Above 46rem the compass moves into the
 * middle column and stops being a row at all, so both paddings go back to what
 * every other screen uses. */
@media (max-width: 45.999rem) {
  .masthead:has(.compass) { padding-bottom: 0; }
  /* `:not(.page--empty)` is ISSUE-001, found by /qa 2026-08-17, and it is the
   * exclusion rather than a louder second rule. Without it this selector — two
   * classes and a `:has()` — outranks `.page--empty`, so every empty screen
   * carrying a compass computed 6.4px of top padding instead of the
   * `clamp(4rem, 18vh, 8rem)` it declares. `/you`, `/collection` with nothing
   * kept and the 404 all rendered their ornament hard under the hairline.
   *
   * Stated here so the value keeps ONE home. Restating the clamp under a
   * heavier selector would have worked today and gone stale the first time
   * `.page--empty` changed — which is the shape of the bug being fixed.
   *
   * Older than the story that surfaced it: the collapse dates from story 0012
   * giving those screens a compass, and only became visible when `/you` shipped
   * as a page whose entire layout is `.page--empty`. */
  .masthead:has(.compass) ~ .page:not(.page--empty) { padding-top: 0.4rem; }
}

/* The screen you are on. Same type, same tracking, same 44px box as its
 * neighbours — only the colour steps back and only the <a> is missing. As a bare
 * <span> it drops out of small caps into body type and reads as a bug. */
.compass__here,
.masthead__you--here { color: var(--ink-dim); }

/* Wide enough for a third column: the compass rides beside the label instead of
 * under it, and the masthead costs 75px instead of 104px.
 *
 * Every area is named, including `aside`. Naming only brand/label/compass leaves
 * `.masthead__aside` — which sets `grid-area: aside` unconditionally — placed
 * implicitly, and implicit placement happening to look right at one width is not
 * a layout. */
@media (min-width: 46rem) {
  .masthead:has(.compass) {
    grid-template-columns: 1fr auto auto auto;
    grid-template-areas:
      "brand   compass aside you"
      "label   compass aside you";
    column-gap: 2rem;
  }

  .masthead:has(.compass) .compass {
    margin-top: 0;
    align-self: center;
  }
}

/* ---------- The rail ----------
 * `/feed` only, and the one narrowing of `DESIGN.md` rule 5.
 *
 * The gallery is the product's only unbounded screen: 110 works of infinite
 * scroll, a masthead that is never sticky, and a coda that renders on the last
 * page. Two flicks in there was nothing on screen to leave by. So on that one
 * screen the compass is not in the masthead at all — it is this, sitting under
 * the bar at the top of the page and staying behind when the bar scrolls away.
 *
 * Nothing changes shape. There is no cross-browser way for CSS to know an
 * element has become stuck, and inventing one costs this app its first
 * scroll-watching JavaScript to hide a wordmark. The masthead simply leaves and
 * the rail simply does not.
 *
 * `z-index: 1` is enough and is deliberately small: `.zoom` is 20 and must cover
 * this, and `body::after` — the paper tooth over the whole product — is 3 and
 * should keep passing over it like it passes over everything else.
 *
 * The safe-area terms match `.masthead`'s. Under `viewport-fit=cover` the page
 * owns the unsafe strips (story 0008), so once this is stuck at `top: 0` it is
 * the thing under the notch. */
.compass--rail {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--bg);
  border-bottom: 1px solid var(--hairline);
  padding:
    calc(0.45rem + env(safe-area-inset-top))
    calc(clamp(1.15rem, 5vw, 2rem) + env(safe-area-inset-right))
    0.35rem
    calc(clamp(1.15rem, 5vw, 2rem) + env(safe-area-inset-left));
}

/* ---------- The gallery index door ----------
 * Story 0027. `.compass--rail` only — appended after the four destinations
 * when the caller passes `door:`. `margin-left: auto` on a `flex-wrap: wrap`
 * row (`.compass` already sets it) is what lets this sit flush right when
 * there is room and drop to its own line when there is not — the row wraps
 * itself; nothing here has to detect the accessibility cap by hand.
 *
 * `--gold` in both states, never `--ink` — the keep glyph's rule (`.rail__act`
 * below): fill is the entire state change. The filled quadrant IS "you have
 * a filter active"; a colour step on top of that would be two idioms for one
 * fact. */
.compass__door {
  margin-left: auto;
  color: var(--gold);
}
.compass__door:hover { color: var(--gold-deep); }

/* ---------- The wing label ----------
 * Story 0027. `/feed/index` — the filter's own screen, replacing the three
 * in-flow facet rows this section used to hold (below `.compass--rail`,
 * one per lit facet). That row shape rated 3/11 on the owner's own Chesky
 * scale: 34 controls before the first work, 176 of 318 two-facet
 * combinations dead. This is the door instead.
 */

.wings__summary { margin: 0 0 1.6rem; }

.wings__actions {
  display: flex;
  column-gap: 1.4rem;
}

/* `.days__month` is the base class in the markup — this only adds the
 * heading's second job: the "Every X" unset link at the right end, when
 * that facet is active. Reusing `.days__month` outright (rather than a new
 * heading style) is deliberate — it is the product's one "a new section
 * starts here" idiom, and the index invents nothing next to it. */
.wings__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  column-gap: 1rem;
}

/* The value a reader has selected — same treatment `.compass__here` gives
 * the screen you are standing on. */
.wings__here { color: var(--ink-dim); }

.wings__row {
  display: flex;
  flex-wrap: wrap;
  column-gap: 1.1rem;
  margin: 0 0 1.6rem;
  padding: 0;
  list-style: none;
}

.wings__note {
  margin: 0 0 0.6rem;
  font-size: 0.82rem;
  font-style: italic;
  color: var(--ink-faint);
  line-height: 1.45;
}

/* ---------- The plates ----------
 * A real work stands for every offered tradition or subject — the index
 * looks like the gallery it indexes. `auto-fill` runs 3 across at 320px,
 * 4 at 375-430, and wider at the desktop measure, off one rule.
 *
 * The edge hugs the picture, not a square of linen (design review outside
 * voice #10): `.plates__img` is a fixed-height, free-width row — the same
 * idiom `.days__thumb` already uses — so a landscape plate and a hanging
 * scroll both keep their own shape and the frame border sits on the image
 * itself, never on an empty margin around it. */
.plates {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
  column-gap: 10px;
  row-gap: 14px;
  margin: 0 0 1.6rem;
  padding: 0;
  list-style: none;
}

.plates__cell {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  color: inherit;
  row-gap: 0.3rem;
}


.plates__name {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  color: var(--gold);
}

.plates__cell--here .plates__name { color: var(--ink-dim); }

.plates__count {
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  color: var(--ink-faint);
}

/* ---------- Page column ---------- */

.page {
  max-width: var(--measure);
  margin: 0 auto;
  /* The bottom inset clears the home indicator. `.coda` is a footer inside this
     element on every screen that has one, so it inherits the clearance rather
     than adding its own — padding on both would double-count it. */
  padding:
    clamp(1rem, 3.5vw, 1.9rem)
    calc(clamp(1.15rem, 5vw, 1.5rem) + env(safe-area-inset-right))
    calc(3.5rem + env(safe-area-inset-bottom))
    calc(clamp(1.15rem, 5vw, 1.5rem) + env(safe-area-inset-left));
}

.page--empty {
  padding-top: clamp(4rem, 18vh, 8rem);
  text-align: center;
}

/* One item in the archive. The rule under the label is the only separator. */
.post { margin: 0 0 clamp(4.5rem, 10vw, 7rem); }

.post .label::after {
  content: "";
  display: block;
  margin: clamp(2.2rem, 5vw, 3.2rem) auto 0;
  width: min(11rem, 40%);
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--hairline) 25%, var(--hairline) 75%, transparent);
}

/* Scroll-in reveal (classes applied only when JS + motion allowed) */
.post.reveal-init {
  opacity: 0;
  transform: translateY(26px);
}
.post.reveal-init.in {
  opacity: 1;
  transform: none;
  transition: opacity 0.9s cubic-bezier(0.22, 1, 0.36, 1), transform 0.9s cubic-bezier(0.22, 1, 0.36, 1);
}

/* ---------- The artwork ---------- */

.plate { margin: 0 0 clamp(1.2rem, 4.5vw, 1.75rem); }

.plate__zoom {
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  background: none;
  cursor: zoom-in;
}

/*
 * Never cropped. `contain` plus a viewport-relative cap means a tall scroll
 * and a wide panorama both arrive whole, letterboxed against the paper — and
 * the cap leaves the opening of the note above the fold on a small phone.
 */
.plate__img,
.days__thumb img,
.plates__img {
  display: block;
  margin-inline: auto;
  max-width: 100%;
  max-height: min(55vh, 55dvh);
  width: auto;
  height: auto;
  object-fit: contain;
  background: var(--bg-lift);
  border: 1px solid var(--frame-edge);
}

/* Fixed height, free width — `.days__thumb`'s own idiom below, here on the
 * shared `.plate__img` selector (`/simplify`: `.plates__img` used to restate
 * `object-fit`/`background`/`border` instead of joining that rule above).
 * Placed AFTER the shared rule on purpose, same as `.days__thumb img`'s own
 * override two rules down — two selectors of equal specificity, so cascade
 * ORDER decides, and the shared rule's `max-height: min(55vh, 55dvh)` must
 * lose here: a plate grid cell has no viewport-relative fold to answer to. */
.plates__img {
  height: 80px;
  max-height: none;
  width: auto;
}

/* The resting idiom — a value whose face has no stored picture stays a real,
 * tappable wing; it just has no likeness to show for it yet. AFTER
 * `.plates__img` above, for the identical reason: this element carries
 * BOTH classes (`_plates.html.erb`), so `.plates__img--none` and
 * `.plates__img` are equal-specificity selectors on the same node, and
 * whichever is later in the file wins the `width` property. Caught by hand
 * (own re-check after the `/code-review` fix above) — a synthetic element
 * with both classes measured 1920px wide, the shared rule's `width: auto`
 * silently winning over this one, before this rule moved down here. */
.plates__img--none { width: 60px; }

/*
 * The third term, added by story 0012: the cap also yields to the text.
 *
 * 55vh was always in service of the sentence above it — "the cap leaves the
 * opening of the note above the fold." A flat 55vh only does that while
 * everything else above the fold stays the same size, and two things changed:
 * the compass took 22 net pixels off the top of the page, and Dynamic Type
 * scales every `rem` between the plate and the note by up to 1.25 (story 0008).
 * Measured on the front door at 375x667, the two together put the note's first
 * line 1px below the fold, and the plate was the only thing in the frame with
 * any give.
 *
 * `17rem` is the room the label needs: title, artist, medium, and one line of
 * the note, at whatever size the reader has chosen. Because it is `rem` and the
 * other two terms are `vh`, this only ever wins when the text is scaled up —
 * at the default root size `100dvh - 19rem` is 363px against a 367px 55vh, so
 * `min()` picks within 4px of 55vh and the artwork is the size it has always
 * been. It gives back real height only as the text grows, which is the trade
 * rule 2 does allow: a smaller picture, never a cropped one.
 *
 * 19rem and not 17: the number is set by the worst page this product publishes,
 * not the friendliest. `db/seeds/mia_paintings.json` holds 18 titles over 60
 * characters and one of 104, and a title that long over a two-line artist row
 * is what `dynamic_type_test.rb` measures. 17rem passed the short fixture and
 * missed that one by 10px.
 *
 * Only `.plate__img`. The archive's thumbnails are 112px in a fixed box and
 * have no relationship to the fold.
 */
/*
 * Story 0014's action rail sits between the plate and the label, so on the
 * screens that render one the room above the note grows and this term has to
 * grow with it. That is the ONLY thing that varies, so it is the only thing the
 * rule below overrides — the three invariant terms and the reasoning above them
 * stay written once.
 *
 * `19rem + one touch target`, and the split is the whole point: the plan said
 * `22rem` and `dynamic_type_test.rb` rejected it. The `19rem` is the label's
 * TEXT budget and belongs in `rem` because it scales with the reader's text
 * size. The rail does not — its height is the touch-target bar rule 9 fixes in
 * pixels, identical at every Dynamic Type setting. Charging a fixed cost in
 * `rem` billed the picture 60px it never spent, and at the accessibility cap the
 * plate gave up 28% of its height against the 25% this file bounds it to. The
 * rail's own margins stay in `rem` and still scale, which is right — those are
 * spacing around text.
 *
 * Scoped to the `.plate` itself, not to the `.page`. Story 0020 widened this
 * from one screen to three: `/`, `/days/:date`, and now `/feed` and
 * `/artists/:slug`, whose posts each carry their own rail. A page-level
 * selector would have charged EVERY plate on a page the moment ONE of them
 * had a rail — wrong on `/feed`, where the reserve has to follow each post's
 * own rail rather than blanket the whole scroll. `.plate:has(+ .rail)` reads
 * per-plate instead: the sibling combinator inside `:has()` is the same idiom
 * `.masthead:has(.compass)` already uses, scoped one level narrower.
 *
 * It only ever bites where this term is smaller than 55vh. At 402x874 that is
 * 526px against 481px, so `min()` still picks 55vh and the rail costs the
 * artwork nothing there at all. At 375x667 it is 319px against 367px, and the
 * plate loses 44px of its 363. Accepted 2026-08-14 for `/` and `/days/:date`,
 * and again 2026-08-19 (`/plan-design-review`) for `/feed` and
 * `/artists/:slug`: rule 2 allows a smaller picture, never a cropped one, and
 * only works tall enough to be height-capped move at all.
 */
.plate__img {
  --rail-reserve: 0px;
  --pin-reserve: 0px;
  max-height: min(55vh, 55dvh, calc(100dvh - 19rem - var(--rail-reserve) - var(--pin-reserve)));
}

/* The front door's pin row (story 0033) is one more fixed touch target
 * between the label's meta line and the first written line — same shape as
 * the rail's own reserve, and for the same reason: `--tap` is pixels, not
 * text, and a `rem` budget must not bill the picture for something that
 * does not scale with Dynamic Type.
 *
 * 15px, not the full 44px `--tap` the row actually measures. A full tap's
 * worth doubled the rail's own reserve and `dynamic_type_test.rb`'s OWN
 * calibrated bound caught it: at the accessibility cap the plate gave up
 * more than the quarter of its height that test polices (28% measured
 * against a 25% ceiling). 15px is the largest reserve that still clears
 * that ceiling — solved from the same inequality the test enforces, not
 * guessed — and it closes the fold-budget gap the pin row's own margins
 * cannot close alone (`.label__meta:has(+ .conv)` below carries the rest).
 * `.page--thread` is a server-rendered class (`daily/_day`), not
 * `.cmt__pin[open]` — the reserve must not depend on the pin's own
 * open/closed state, or opening the note would resize the artwork under
 * the reader mid-read. */
.page--thread .plate__img { --pin-reserve: 15px; }

.plate:has(+ .rail) .plate__img { --rail-reserve: var(--tap); }

/* The rail is the plate's own footer, not a separate block, so it takes over the
 * gap rather than adding to it. Scoped the same way: a plate with no rail after
 * it keeps the margin it has always had. */
.plate:has(+ .rail) { margin-bottom: 0.5rem; }

.plate__resting {
  margin: 0;
  padding: 2.5rem 1.5rem;
  border: 1px solid var(--frame-edge);
  background: var(--bg-lift);
  text-align: center;
  font-style: italic;
  color: var(--ink-dim);
}

/* ---------- Wall label ----------
 * Same label under every picture. Prose is ink; everything that is metadata
 * about the prose — dates, medium, credit — steps back to dim.
 */

.label__title {
  margin: 0 0 0.4rem;
  font-family: var(--serif-display);
  font-size: clamp(1.5rem, 5vw, 2rem);
  font-weight: 480;
  line-height: 1.16;
  font-variation-settings: "opsz" 100;
  text-wrap: balance;
}
/* Long museum titles step down rather than get cut — never truncate a title. */
.label__title--long { font-size: clamp(1.2rem, 3.8vw, 1.45rem); }

/* Artist plus life dates wraps to two lines on plenty of works, and at the
 * default line-height those lines collide with each other and with the meta. */
.label__artist { margin: 0 0 0.55rem; font-size: 1.02rem; line-height: 1.4; }
.label__artist-name {
  color: var(--gold);
  letter-spacing: 0.02em;
}
/* Story 0018. The one place a wall label wraps a control. Padding on an
 * INLINE element (not `inline-block`) extends the tap target without adding
 * to the line box, so `.label__artist`'s wrap behaviour and the fold-budget
 * tests that measure it are unaffected — the `inline-block` + negative-margin
 * draft was rejected for exactly that risk (eng review D10, outside voice
 * X10).
 *
 * The base is NOT `.label__artist`'s line-height (1.02rem × 1.4 = 22.85px).
 * Measured directly (`getBoundingClientRect`, not read off the cascade): an
 * inline non-replaced element's own box for hit-testing tracks the font's
 * content-area metrics, not the paragraph's line-height — line-height on the
 * element itself changes nothing about it. At a 1.02rem font-size that base
 * measures ~16px, close to but under 1em, so the formula targets 46px rather
 * than 44 to clear the bar with margin instead of landing on it by luck:
 * 45.66px measured at the default root, 45.41-45.59px at the Dynamic Type
 * range this file's other rules are tested against
 * (`test/system/dynamic_type_test.rb`). */
a.label__artist-name {
  padding-block: calc((46px - 1.02rem) / 2);
}
/* The unlinkable artist — blank, culture fallback, or deny-listed — and every
 * artist name on `/`, where the artist page is walled but the front door is
 * not (design review D11, eng review X11). Gold means tappable; this keeps
 * that literally true everywhere it appears. */
.label__artist-name--dim {
  color: var(--ink-dim);
}
.label__artist-dates {
  color: var(--ink-faint);
  font-style: italic;
}
.label__artist-dates::before { content: " · "; font-style: normal; }

.label__meta {
  margin: 0 0 1.25rem;
  font-size: 0.76rem;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  color: var(--ink-dim);
}

/* Body copy: the hand-written note on the daily page, the museum's own
 * description in the archive. One type treatment for both. */
.label__note p,
.label__text {
  margin: 0 0 0.9rem;
  font-size: 1.06rem;
  font-weight: 400;
  line-height: 1.66;
  color: var(--ink);
}
.label__note p:last-child { margin-bottom: 0; }

/* Only the archive clamps: museum catalogue copy runs to paragraphs and the
 * point of the archive is the pictures. The daily note is never clamped. */
.label__text {
  display: -webkit-box;
  -webkit-line-clamp: 4;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.label__body.expanded .label__text {
  display: block;
  -webkit-line-clamp: none;
  overflow: visible;
}

.label__more {
  margin-top: 0.4rem;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  font-family: var(--serif-body);
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.2em;
  color: var(--gold);
}
.label__more:hover { color: var(--gold-deep); }

.label__credit {
  margin: 1.3rem 0 0;
  font-size: 0.82rem;
  font-style: italic;
  color: var(--ink-faint);
}

/* ---------- Artist page ----------
 * The name, the works, the ending — nothing else earns the page (story 0018).
 * The heading reuses `.label__title` / `.label__title--long` verbatim: it is
 * the same display type doing the same job, one level up the page.
 */
.artist-page__heading { margin-bottom: 1.8rem; }

/* Not `.label__artist-dates` — that class's `::before` prints a leading " · "
 * meant to follow a name inline, and standing alone that reads as a stray
 * bullet before the dates. */
.artist-page__dates {
  margin: 0.2rem 0 0;
  font-style: italic;
  color: var(--ink-faint);
}

/* ---------- Closing beat ----------
 * Ends the daily ritual, ends the archive, and carries the empty state.
 */

.coda {
  margin-top: clamp(2.5rem, 8vw, 3.5rem);
  text-align: center;
}

.ornament {
  color: var(--gold);
  font-size: 0.9rem;
  letter-spacing: 0.5em;
  margin-bottom: 1.6rem;
}
.ornament::before, .ornament::after {
  content: "———";
  color: var(--hairline);
  font-size: 0.7rem;
  vertical-align: 0.15em;
  margin: 0 0.9rem;
}

.coda__line {
  margin: 0 0 1.4rem;
  font-family: var(--serif-display);
  font-style: italic;
  font-size: 1.15rem;
  color: var(--ink-dim);
}

.coda__note {
  max-width: 34rem;
  margin: 0 auto 2rem;
  font-size: 0.95rem;
  line-height: 1.7;
  color: var(--ink-faint);
}

/* DESIGN.md rule 9 for everything built on `.caps-link`. That class sets size
 * and tracking, never height, so each tappable thing built on it states the 44px
 * bar here — assuming `.caps-link` carried it is how ISSUE-002 shipped 15px
 * targets.
 *
 * Listed explicitly rather than nested under `.coda`: the walk happens to render
 * inside a coda today, and a target size should not depend on where a partial
 * gets called from.
 *
 * NOT the only place in the file. `.zoom__close` is the one tappable control
 * that is not a caps-link — a fixed-position button on the dark overlay with its
 * own border, background and `min-width` — so it states its own bar next to the
 * rest of its chrome. Two homes, on purpose; adding a third for a caps-link is
 * the mistake.
 *
 * `.rail__act` joins this list rather than restating 44 a third time. The
 * rail's buttons are not caps-links — they are bare glyphs — but the HEIGHT
 * is the same fact for every control in the product, and one registry is
 * what stops a future change to the bar from half-landing. What it needs
 * that nothing else here does is a WIDTH, because this rule has always got
 * width for free from the words inside a caps-link and a glyph has none.
 * That part, being genuinely new, is stated on its own below — and it reads
 * `--tap` too, so the bar is one number rather than one number and three
 * copies of it. */
.coda .caps-link,
.compass .caps-link,
.wings .caps-link,
.legal .caps-link,
.masthead__you,
.compass__door,
.walk__step,
.walk__today,
.rail__act {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
}

/* `.legal .caps-link` joined that list in story 0017, and it should have been
 * there since 0016. Privacy and Support are the one nav App Review guideline
 * 5.1.1(i) requires to exist, and they were shipping at the ~15px a caps-link
 * gets from its type alone — ISSUE-002's exact failure mode, on the two links
 * least able to afford it. Found by the design review, not by a person tapping
 * them, which is the argument for keeping the list in one place. */

/* A bare <button> in gold: same type, none of the button chrome. `.rail__act`
 * belongs here for the same reason `.keep__toggle` used to — it is a button
 * wearing no button, and the reset is the same five declarations. Only the
 * `font-family` differs in effect, and it is inert on a glyph-only control. */
.rail__act {
  padding: 0;
  font-family: var(--serif-body);
  color: var(--gold);
  background: none;
  border: 0;
  cursor: pointer;
}
.rail__act:not(.rail__act--waiting):hover { color: var(--gold-deep); }

/* There was a `.coda > .caps-link + .caps-link` rule here until story 0012,
 * spacing the two ways out that the archive and the collection used to end with.
 * The compass carries those doors now and no coda has two adjacent caps-links
 * left, so the rule went. The gap between compass items is `column-gap` on the
 * flex row, which is the same job done by the layout that owns it. */

/* ---------- Long-form document (story 0016) ----------
 * The privacy policy and the support page. The longest continuous prose in the
 * product by a wide margin — longer than any editorial note — and the only
 * screen whose job is reading rather than looking.
 *
 * No new tokens, no new type step, no new ornament. Body is the same size and
 * measure as a wall label because it is the same act of reading; headings reuse
 * the small-caps treatment `.days__month` already established for "a new section
 * starts here"; sections are separated by the `✦` that DESIGN.md rule 6 already
 * permits. A `<h2>` scale of its own would have been a fourth type step for two
 * pages, and cards or boxes would have broken rule 6 outright.
 *
 * `.page` already caps width at `--measure`, so nothing here restates it.
 */

.doc__stamp {
  margin: 0 0 1.6rem;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.2em;
  color: var(--ink-faint);
}

/* One size up, italic display: the same voice `.coda__line` uses to open and
 * close a screen, here opening a document. */
.doc__lead {
  margin: 0 0 0.4rem;
  font-family: var(--serif-display);
  font-size: 1.2rem;
  line-height: 1.55;
  color: var(--ink);
}

.doc__head {
  margin: 0 0 1.2rem;
  font-family: var(--serif-body);
  font-size: 0.72rem;
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: 0.2em;
  color: var(--ink-dim);
}

/* Matches `.label__note p` exactly — same size, same leading, same ink. Two
 * declarations rather than one shared selector because the wall label clamps
 * and this never does; joining them would put a `-webkit-line-clamp` one
 * refactor away from a truncated privacy policy. */
.doc p {
  margin: 0 0 0.9rem;
  font-size: 1.06rem;
  line-height: 1.66;
  color: var(--ink);
}

.doc p:last-child { margin-bottom: 0; }

.doc strong { font-weight: 560; }

.doc__link {
  color: var(--gold);
  text-decoration: underline;
  text-underline-offset: 0.15em;
}
.doc__link:hover { color: var(--gold-deep); }

/* The divider between sections carries the space, so headings do not also need
 * a large top margin — that would double the gap and float the heading between
 * its section and the one before it. */
.doc .ornament { margin: 2.2rem 0 1.6rem; }

/* Privacy and Support in the coda of the collection and archive screens
 * (guideline 5.1.1(i): the policy must be reachable inside the app, not only
 * from the App Store listing).
 *
 * Story 0012 deleted the old `.coda > .caps-link + .caps-link` rule when the
 * compass took over the two ways out and no coda had two adjacent caps-links
 * left. This is a row again, so the spacing comes back — as a flex row that
 * owns its own gap, rather than an adjacent-sibling margin that only works for
 * exactly two. */
.legal {
  display: flex;
  justify-content: center;
  column-gap: 2rem;
}

/* ---------- The days behind you ----------
 * A contents list, not a gallery — /feed is the gallery. Rows are separated by a
 * hairline and whitespace: no card, no fill, no radius, no shadow, on any state.
 *
 * The title carries --gold here and the artist steps back, inverted from the wall
 * label. In a list the title is the target; on the day page it is the subject.
 */

.days { list-style: none; margin: 0; padding: 0; }

.days__month {
  margin: 2.6rem 0 0.4rem;
  padding-bottom: 0.5rem;
  font-family: var(--serif-body);
  font-size: 0.72rem;
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: 0.2em;
  color: var(--ink-dim);
  border-bottom: 1px solid var(--hairline);
}
.days__month:first-of-type { margin-top: 0.6rem; }

.days__day { border-bottom: 1px solid var(--frame-edge); }

.days__link {
  display: grid;
  grid-template-columns: 1fr 4.5rem;
  column-gap: 1.25rem;
  align-items: start;
  padding: 1.15rem 0 1.25rem;
  color: inherit;
}

.days__text { display: block; min-width: 0; }

.days__date {
  display: block;
  margin-bottom: 0.35rem;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  color: var(--ink-dim);
}
.days__date--now { color: var(--gold); }

/* Never clamped, never truncated: museum titles run past 100 characters and the
 * row grows to hold them (rule 3). Past 60 the type steps down instead. */
.days__title {
  display: block;
  margin-bottom: 0.25rem;
  font-family: var(--serif-display);
  font-size: 1.22rem;
  font-weight: 480;
  line-height: 1.2;
  font-variation-settings: "opsz" 100;
  color: var(--gold);
  text-wrap: balance;
}
.days__title--long { font-size: 0.92rem; }

.days__artist { display: block; font-size: 0.95rem; line-height: 1.4; color: var(--ink-dim); }

.days__link:hover .days__title { color: var(--gold-deep); }

/* Fixed height, free width: a hanging scroll stays tall and narrow, a folding
 * screen stays wide and short, and every text column still starts at the same x. */
.days__thumb {
  display: flex;
  align-items: flex-start;
  justify-content: center;
  height: 4.5rem;
}
/* Frame, fill and never-crop come from `.plate__img` above — the same treatment
 * every artwork in the product gets. Only the cap differs: this one fills its
 * row box rather than a share of the viewport. */
.days__thumb img { max-height: 100%; }

@media (min-width: 641px) {
  .days__link { grid-template-columns: 1fr 7rem; }
  .days__thumb { height: 7rem; }
}

/* ---------- The action rail ----------
 *
 * Under the plate, above the wall label. Story 0014,
 * `decisions/0010-actions-become-a-rail.md`.
 *
 * This used to be `.keep`, the last row of the wall label. That put the
 * product's only habit mechanic 40-60pt below the fold on a fallback day and
 * several hundred below on a hand-written one — rule 8 shows that note whole, so
 * the page a curator writes is the taller one and the good case was the day
 * nobody wrote. Position next to the artwork does not move with note length,
 * which is the entire fix.
 *
 * Left-grouped, never space-between. Spread across the 680px measure put the two
 * items 466px apart, which stops reading as a pair and starts reading as the
 * `.walk` nav row further down the same page. Instagram spreads; it can, at
 * 402pt, and this cannot, at 680.
 *
 * Order is Zoom, Share (shell-only), the comment bubble, then Keep last.
 * Keep no longer grows on fetch — the count left the frame with the "N kept"
 * text (decisions/0023) — so nothing here shoves sideways any more; the
 * order is a stable convention now, not a load-bearing constraint.
 *
 * The gap is small because the 44px targets are mostly air already; it only
 * keeps them from touching. */
.rail {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0 0 0.75rem;
}

/* The keep frame. It carries no reserved box of its own: the frame always ships
 * default content (the un-kept glyph — see `favorites/_control.html.erb`), and
 * that span is a `.rail__act`, so it is already `--tap` in both directions
 * before the fetch resolves. Reserving the same box here a second time would be
 * scaffolding for a default that is tested to exist. */
.rail__slot {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
}

/* `button_to` wraps its button in a <form>, so without this the FORM is the flex
 * item and the button is only as wide as its glyph. Every geometry assertion
 * would then measure the form, pass at 44px, and ship a 23px target — which is
 * the same shape of assumption that shipped 15px targets in ISSUE-002. */
.rail__form { display: contents; }

/* The wordless controls. Height and the bare-button reset come from the blocks
 * above; width is stated here, because rule 9 has always got width for free
 * from the words inside a caps-link and these have none (`decisions/0010`).
 * Once, for the rail's glyphs, the masthead's corner, and the gallery index
 * door (story 0027) — the same fact a fourth control does not get to restate. */
.masthead__you,
.rail__act,
.compass__door {
  justify-content: center;
  min-width: var(--tap);
}
.rail__act svg,
.masthead__you svg,
.compass__door svg { display: block; width: 23px; height: 23px; }

/* The mark holding its own place until the private frame lands. Same box, same
 * colour, no cursor and no hover — it is a drawing, not a control, and the real
 * button replaces it. Only on screen for the handful of frames the fetch takes,
 * which is exactly how long the alternative would have shown a hole. */
.rail__act--waiting { cursor: default; }

/* Share (story 0031). Hidden for every reader by default — browser and shell
 * alike get the identical markup, which is what keeps `/` byte-for-byte
 * public-cacheable — and revealed only once `layouts/_head.html.erb`'s inline
 * script has stamped `data-shell-share` on `<html>`, which it does before
 * this element (or anything else in `<body>`) ever paints. Two classes on the
 * selector, not source order, beat `.rail__act`'s own `display: inline-flex`
 * — specificity that does not depend on which rule happens to load last. */
.rail__act.rail__share { display: none; }
html[data-shell-share] .rail__act.rail__share { display: inline-flex; }

/* ---------- The walk between days ---------- */

.walk {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
  margin-bottom: 0.4rem;
}

/* Target size comes from the rule-9 block above, which names these two. */

/* Holds the right edge when there is no previous day to sit opposite; with both
 * links present `.walk`'s own space-between already does it. This now applies in
 * both cases — the coda's sibling-spacing rule is scoped to direct children, so
 * it no longer reaches inside the walk and outranks this. */
.walk__step--next { margin-inline-start: auto; }

/* ---------- Lazy-load sentinel ---------- */

.sentinel {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
  padding: 3.5rem 0 5rem;
}

.sentinel__ring {
  width: 1.75rem;
  height: 1.75rem;
  border-radius: 50%;
  border: 1px solid var(--hairline);
  border-top-color: var(--gold);
  animation: spin 1.1s linear infinite;
}

.sentinel__label {
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.22em;
  color: var(--ink-faint);
}

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

/* ---------- Full-screen view ----------
 * The one dark surface: at full screen the picture is the room, and a linen
 * surround would bounce light into it. Mount board, not wall.
 */

html.zoom-open,
html.zoom-open body { overflow: hidden; }

.zoom {
  position: fixed;
  inset: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--mount-bg);
  cursor: zoom-out;
  animation: zoom-fade 0.18s ease-out;
}

.zoom__img {
  max-width: 100vw;
  max-height: 100dvh;
  width: auto;
  height: auto;
  object-fit: contain;
}

.zoom__close {
  position: fixed;
  /* `calc`, not `max`. Under `viewport-fit=cover` the inset is the height of the
     unsafe strip, so `max()` lands the button flush against the status bar with
     no breathing room; the gap has to be added to the inset, not compared with it. */
  top: calc(0.9rem + env(safe-area-inset-top));
  right: calc(0.9rem + env(safe-area-inset-right));
  min-width: var(--tap);
  min-height: var(--tap);
  padding: 0 1rem;
  border: 1px solid rgba(236, 229, 216, 0.3);
  border-radius: 2px;
  background: rgba(15, 13, 11, 0.72);
  color: #ece5d8;
  font-family: var(--serif-body);
  font-size: 0.74rem;
  text-transform: uppercase;
  letter-spacing: 0.2em;
  cursor: pointer;
}
.zoom__close:hover { border-color: rgba(236, 229, 216, 0.6); }

@keyframes zoom-fade { from { opacity: 0; } to { opacity: 1; } }

/* ---------- The curator's desk ---------- */

.adm {
  max-width: 46rem;
  margin: 0 auto;
  padding: 2rem clamp(1.15rem, 5vw, 2rem) 4rem;
}

.adm h1 {
  font-family: var(--serif-display);
  font-size: 1.5rem;
  font-weight: 480;
  margin: 0 0 1.5rem;
}

.adm__flash {
  margin: 0 0 1.5rem;
  padding: 0.7rem 0.9rem;
  border-left: 2px solid var(--gold);
  background: var(--bg-lift);
  font-size: 0.92rem;
}

.adm__errors {
  margin: 0 0 1.5rem;
  padding: 0.8rem 0.9rem;
  border-left: 2px solid var(--warn);
  background: var(--bg-lift);
  color: var(--ink);
  font-size: 0.92rem;
}
.adm__errors ul { margin: 0.4rem 0 0; padding-left: 1.1rem; }

.adm__field { margin: 0 0 1.4rem; }
.adm__field label {
  display: block;
  margin-bottom: 0.35rem;
  font-size: 0.74rem;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  color: var(--ink-dim);
}
.adm__field input,
.adm__field select,
.adm__field textarea {
  width: 100%;
  padding: 0.6rem 0.7rem;
  border: 1px solid var(--hairline);
  border-radius: 2px;
  background: var(--bg-lift);
  color: var(--ink);
  font-family: var(--serif-body);
  font-size: 1rem;
}
.adm__field textarea { min-height: 11rem; line-height: 1.6; resize: vertical; }

.adm__hint { margin: 0.35rem 0 0; font-size: 0.8rem; color: var(--ink-faint); }
.adm__hint.is-off-target { color: var(--warn); }
.adm__muted.is-off-target { color: var(--warn); font-style: normal; }

.adm__thumb {
  margin-top: 0.6rem;
  max-height: 8rem;
  border: 1px solid var(--frame-edge);
}

.adm__actions { display: flex; align-items: center; gap: 1.25rem; margin-top: 1.75rem; }
.adm__button {
  padding: 0.65rem 1.4rem;
  border: 1px solid var(--gold);
  border-radius: 2px;
  background: none;
  color: var(--gold);
  font-family: var(--serif-body);
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.2em;
  cursor: pointer;
}
.adm__button:hover { color: var(--gold-deep); border-color: var(--gold-deep); }

/* The queue is wide and the curator opens it on a phone: scroll the table, not
 * the whole page. */
.adm__queue-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.adm__queue { width: 100%; border-collapse: collapse; font-size: 0.95rem; }
.adm__queue th {
  text-align: left;
  padding: 0.5rem 0.6rem;
  border-bottom: 1px solid var(--hairline);
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  color: var(--ink-dim);
  font-weight: 400;
}
.adm__queue td { padding: 0.7rem 0.6rem; border-bottom: 1px solid var(--frame-edge); vertical-align: top; }
.adm__queue tr.is-upcoming td { color: var(--ink-dim); }
.adm__queue .adm__row-links { display: flex; flex-wrap: wrap; gap: 0.5rem 0.8rem; font-size: 0.78rem; }
.adm__muted { color: var(--ink-faint); font-style: italic; }

/* ---------- Motion ---------- */

@media (prefers-reduced-motion: reduce) {
  .sentinel__ring { animation: none; }
  .zoom { animation: none; }
  /* The pinned comment's unfold goes instant (story 0033). The chevron flip
   * and the bubble's fill-for-state need no override — neither was ever a
   * transition (rule 7's other half: nothing rotates either). */
  .cmt__pin[open] .cmt__pinbody { animation: none; }
}

/* ---------------------------------------------------------------------------
 * The sign-in doors and the account row (story 0015, design review D3-D5;
 * rehomed by 0017).
 *
 * These were the landing page's per-visitor fragment, lazy-loaded into a frame
 * whose id was also the wall's bounce anchor. Story 0017 deleted the fragment
 * and moved the doors to `/you`, so what survives here is the door itself and
 * the quiet controls beside it. `.signin`, `.signin__note` and `.signin--in`
 * went with the markup that carried them — the corner uses `.coda__note` and
 * `.corner__doors` for the same jobs.
 *
 * House buttons, official marks (D4): the field, border, radius and type all
 * come from the token table; the providers are recognisable by their logos
 * alone. Google's G is the only non-token colour in the product — DESIGN.md
 * records the exception.
 */

.signin__doors {
  display: flex;
  justify-content: center;
  gap: 0.9rem;
  /* Stacked when narrow — two 44px doors side-by-side need more than a phone
   * gives them once Dynamic Type grows the labels. */
  flex-wrap: wrap;
}

/* DESIGN.md rule 9: min-height stated here, not assumed from type classes. */
.signin__door {
  display: inline-flex;
  align-items: center;
  gap: 0.6em;
  min-height: 44px;
  padding: 0 1.1rem;
  border: 1px solid var(--hairline);
  border-radius: 2px;
  background: var(--bg-lift);
  color: var(--ink);
  font-family: var(--serif-body);
  font-size: 0.95rem;
  cursor: pointer;
}
.signin__door:hover { border-color: var(--gold-deep); }

.signin__mark { flex: none; }

/* Chip anatomy borrowed from .signin__door (decisions/0024): a safe variant
 * (gold on hairline, same border/background as .signin__door — padding is
 * its own 1.15rem rather than .signin__door's 1.1rem, kept separate on
 * purpose rather than merging selector lists across two conceptually
 * distinct control families) and a destructive one (--warn tint).
 * `.push__enable` joins the styled group here for the first
 * time — it's an <a>, not a <button>, so it used to get color free from the
 * global `a` rule and nothing else: no box at all, the same gap as the two
 * buttons this reverses. */
.signin__out,
.account__delete,
.push__enable,
.push__disable {
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  padding: 0 1.15rem;
  border-radius: 2px;
  cursor: pointer;
}
.signin__out-form,
.account__form,
.push__form { display: inline-flex; }

.signin__out,
.push__enable,
.push__disable {
  border: 1px solid var(--hairline);
  background: var(--bg-lift);
  color: var(--gold);
}
.signin__out:hover,
.push__enable:hover,
.push__disable:hover { border-color: var(--gold-deep); color: var(--gold-deep); }
.signin__out:active,
.push__enable:active,
.push__disable:active {
  background: rgba(125, 95, 24, 0.14);
  border-color: var(--gold-deep);
  color: var(--gold-deep);
}

/* Delete this device's data (`DevicesController#destroy`) shares this class
 * with Delete account on purpose — same irreversible-action weight, same
 * chip. */
.account__delete {
  border: 1px solid rgba(var(--warn-rgb), 0.4);
  background: rgba(var(--warn-rgb), 0.055);
  color: var(--warn);
}
.account__delete:hover { border-color: var(--warn); background: rgba(var(--warn-rgb), 0.09); }
/* 0.10, not 0.14: the darker fill computed to 4.54:1 against --warn text —
 * technically AA (4.5:1) but with no real margin, on the state a reader is
 * looking at while deciding whether to release a tap on an irreversible
 * action. 0.10 restores a 4.82:1 margin (/code-review, computed WCAG). */
.account__delete:active { background: rgba(var(--warn-rgb), 0.1); border-color: var(--warn); }

/* ---------------------------------------------------------------------------
 * The corner (story 0017) — `/you`, the reader's own room.
 *
 * Two classes, and both earn it. Everything else on that page is borrowed:
 * `.ornament`, `.coda__line`, `.coda__note`, `.signin__doors`, `.signin__door`,
 * `.signin__out`, `.account__delete` and their forms all come from screens the
 * reader has already seen, which is what keeps a new surface from reading as a
 * new product.
 */

/* `.corner` names `<main>` here alone — not a third borrowed-vs-owned class,
 * a scope. `.page--empty` and `.ornament` are shared by five other screens
 * (the empty archive/collection/gallery, the 404), and a straight edit to
 * either would have moved all of them. Reviewed and approved against `/you`
 * alone, on a design-canvas mock, 2026-09-03 (`decisions/0025`, mock:
 * https://claude.ai/code/artifact/7f7d1116-e13e-45b4-bb72-31bef553e15d):
 * a tight top instead of the shared class's centering pad, and no flanking
 * dashes on the ornament — `letter-spacing` goes with the dashes it was
 * spacing, or the lone ✦ sits off-centre by half its trailing letter-space. */
.corner.page--empty { padding-top: clamp(1.5rem, 5vw, 2.5rem); }
.corner .ornament { letter-spacing: normal; }
.corner .ornament::before, .corner .ornament::after { content: none; }

/* The sentence the page is built around. Three of the four states are a reader
 * standing in front of a decision, and this is the decision said plainly.
 *
 * A MODIFIER, not a second class. It is `.coda__line`'s voice — Fraunces
 * italic — and the first version restated all five of that rule's declarations
 * to change three of them, including a font-size of 1.18rem against 1.15rem.
 * That is 0.5px at the default root: below anything a reader can see, and below
 * what the measurement discipline in this file would defend anywhere else. The
 * type step went; what stays is what actually differs — full `--ink` because
 * this line is asking for an answer rather than closing a page, a measure so it
 * breaks where a question should, and the leading that needs. */
.coda__line--ask {
  max-width: 26rem;
  margin-inline: auto;
  line-height: 1.45;
  color: var(--ink);
}

/* The quiet controls under it. `gap` rather than a margin between siblings,
 * for the reason `.legal` records: an adjacent-sibling rule only works for
 * exactly two, and the account state already has two while the device state
 * has one. Both axes, not just `column-gap` — two doors wrap to stacked rows
 * under Dynamic Type or a narrow phone, and a column-only gap left the
 * stacked pair touching border-to-border (found against this same screen). */
.coda__doors,
.corner__doors {
  display: flex;
  justify-content: center;
  align-items: center;
  flex-wrap: wrap;
  gap: 1.4rem;
}

/* ISSUE-002, found by /qa 2026-08-17. The bottom margin is the fix and it is
 * not decoration.
 *
 * On the device state with nothing kept, `Delete this device's data` is the
 * most prominent thing on the screen, and `.legal`'s Privacy and Support sat
 * about 25px under it in the identical caps-link gold. A destructive control
 * and two informational links, same type, same colour, same size, stacked —
 * which reads as a menu of three navigation choices rather than one door you
 * cannot walk back through.
 *
 * The gold caps-link treatment for a destructive control WAS house style
 * (story 0015 chose it) — reversed by story 0034 / `decisions/0024`:
 * `.account__delete` is now a `--warn`-tinted bordered chip, not flat text.
 * What was new HERE, at the time, was the adjacency; the margin below still
 * does its job regardless of which treatment the button itself carries. */
.corner__doors { margin: 2rem 0 3.5rem; }

/* The account row at the foot of the collection — the reader's own room is
 * where rare controls live. */
.account {
  margin-top: 2.5rem;
  text-align: center;
}

/* ---------------------------------------------------------------------------
 * The conversation (story 0033, replacing the sit gate). A comment thread in
 * the product's own materials, not Instagram's: the museum/curator voice is
 * the pinned comment, the reader's own line is a comment beside it, and the
 * whole section sits behind one rail glyph. `decisions/0023`.
 *
 * Front door only carries the bubble and the toggle; the archive and preview
 * render the same markup permanently open, no JS (0032 D7's browsing
 * posture, carried).
 */

/* The bubble joins the rail's own idiom: --gold in both states (DESIGN.md's
 * rail constant), a dim glyph in a gold row reads disabled. State is FILL,
 * never colour — the grammar Keep already taught, so the rail keeps one
 * state language across both its toggles. No CSS rule for it: `sit_controller`
 * sets the `fill` attribute directly, the same mechanism Keep's own glyph
 * uses server-side — one idiom, not a CSS rule shadowing a Ruby one. */

.conv { margin-top: 0.5rem; }
.conv[hidden] { display: none; }

/* `.label__meta`'s own 1.25rem bottom margin is a block-margin collapse
 * away from winning this gap outright (20px against `.conv`'s 8px) — fine
 * everywhere `.label__meta` is the LAST thing in the label (a wordless day,
 * where it collapses with `.label__credit` instead), wrong here: the pin
 * row's own 44px is new weight the 0032 gate never carried, and
 * `dynamic_type_test.rb`'s fold-budget bound is calibrated against the
 * whole stack, not `.conv` alone. Scoped to the one structural condition
 * that matters (a `.conv` follows), not to `.cmt__pin[open]` or anything
 * else that changes on interaction — the gap must not move while the
 * reader is looking at it. */
.label__meta:has(+ .conv) { margin-bottom: 0.5rem; }

.cmt { display: flex; gap: 0.7rem; align-items: flex-start; }

/* The reader's slot is a <turbo-frame>, not a bare `.cmt` — a custom
 * element with no UA stylesheet defaults to `display: inline`, so without
 * this it neither participates in the block flow the way `.cmt`'s margins
 * expect nor sits any distance from the pin row above it. Zero extra gap,
 * deliberately: the two avatars already read as separate comments, and the
 * fold-budget math (`--pin-reserve` above) has no more room to spend on
 * one that only helps polish. Revisit at the first design pass if the
 * thread reads as cramped in practice. */
.cmt-frame { display: block; }

/* The identity medallion — drawn, never filled gold (rule 6 amendment: a
 * gilt disc on the calmest page collides with the 0017 amendment's own
 * tripwire). `--bg-lift` is already the house's second surface (image
 * placeholders, form fields); a hairline edge is the same frame every
 * artwork wears. Decorative — the summary/name beside it carries the
 * accessible name. */
.cmt__avatar {
  flex: none;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg-lift);
  border: 1px solid var(--hairline);
  font-family: var(--serif-display);
  font-size: 0.85rem;
  font-weight: 500;
  color: var(--gold-deep);
}

.cmt__col { min-width: 0; flex: 1; }

/* The pinned comment: a native <details>, so it opens with no JS (0032's
 * floor, relocated). No box, no fill, no radius on the details itself —
 * rule 6 — the fold is the only chrome. */
.cmt__pin { margin: 0; }

/* States its own bar (rule 9, ISSUE-002 precedent) rather than inheriting
 * one — the summary is the whole reveal control. Glyphs inside are
 * aria-hidden; the words carry the announcement, and native <details>
 * supplies "collapsed"/"expanded" without a redundant aria-expanded here. */
.cmt__fold {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  min-height: var(--tap);
  list-style: none;
  cursor: pointer;
  font-family: var(--serif-body);
  font-size: 0.68rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  color: var(--ink-dim);
  /* Explicit, not `normal`: a flex container's computed line-height stays
   * the literal keyword `normal` in Chrome rather than resolving to a
   * pixel value the way a block element's does, which `dynamic_type_test`'s
   * fold-budget math reads with `parseFloat` and needs a real number from. */
  line-height: 1.5;
}
.cmt__fold:hover { color: var(--ink); }
.cmt__fold::marker { content: none; }
.cmt__fold::-webkit-details-marker { display: none; }
.cmt__fold svg { flex: none; }

/* Two static orientations, swapped instantly on the `open` attribute — no
 * transition. Rule 7 is "nothing rotates" as much as "nothing slides"; the
 * chevron flip is a state change, not a motion. */
.cmt__pin[open] .cmt__fold .chev { transform: rotate(180deg); }

/* The body fades in on open (rule 7), instant under reduced motion. The
 * note text itself reuses `.label__note` / `.label__body` verbatim — no
 * third prose treatment for the same words. */
.cmt__pinbody { margin-top: 0.5rem; }
.cmt__pin[open] .cmt__pinbody { animation: cmt-fade 0.45s ease-out; }
@keyframes cmt-fade { from { opacity: 0; } to { opacity: 1; } }

/* Credit, relocated inside the pin as its meta row (wordless days keep a
 * standalone credit line elsewhere — see `daily/_note.html.erb`). Same
 * treatment `.label__credit` already carries. */
.cmt__meta {
  margin: 0.7rem 0 0;
  font-size: 0.82rem;
  font-style: italic;
  color: var(--ink-faint);
}
.cmt__pinbody .label__note,
.cmt__pinbody .label__body { margin: 0; }

/* The reader's own comment: run-in name, never bold — DESIGN.md's weight
 * range stays 380-560, and a bare <b> renders 700 (rule violation). Roman
 * name against the reader-voice italic answer carries the contrast
 * instead. */
.cmt__who { font-weight: 500; letter-spacing: 0.02em; }
.cmt__body {
  margin: 0;
  font-size: 1.02rem;
  line-height: 1.6;
  color: var(--ink);
}
.cmt__body--mine {
  font-style: italic;
  color: var(--ink-dim);
  font-family: var(--serif-display);
}
.cmt__body--mine .cmt__who { font-style: normal; color: var(--ink); }

/* Tap-to-edit (today's pick only): the rendered comment is a real button
 * wearing no button chrome — `.rail__act`'s own reset, restated because
 * this one keeps its text-heavy sizing rather than a glyph's fixed box. */
.cmt__edit {
  display: block;
  width: 100%;
  padding: 0;
  margin: 0;
  border: 0;
  background: none;
  text-align: left;
  font-family: inherit;
  cursor: pointer;
}

/* A line to write on: hairline underline on the linen, no box, no chrome.
 * The prompt above is the label (aria-labelledby). */
.sit__prompt {
  font-family: var(--serif-display, Fraunces), Georgia, serif;
  font-style: italic;
  font-weight: 460;
  font-size: 1rem;
  line-height: 1.45;
  color: var(--ink-dim);
  margin: 0 0 0.3rem;
}

.sit__form { margin: 0; }
.sit__input {
  display: block;
  width: 100%;
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--hairline);
  border-radius: 0;
  color: var(--ink);
  font: inherit;
  font-style: italic;
  padding: 0.45rem 2px 0.55rem 2px;
  min-height: var(--tap);
  box-sizing: border-box;
}
.sit__input:focus { outline: none; border-bottom-color: var(--gold); }
.sit__input::placeholder { color: var(--ink-faint); font-style: italic; opacity: 1; }

/* The autosave whisper — the only acknowledgment the page ever gives. */
.sit__saved {
  min-height: 20px;
  margin: 0.25rem 0 0 0;
  text-align: right;
  font-size: 0.66rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* The signed-out line: plain text, no link, no door — the corner glyph is
 * the door, and a nag has no place on the calm page. Hidden while the pin
 * is open: a hint under an open note is the nag this rule already refused
 * once, just with a new mechanism now that there is no reveal event to key
 * it off. Pin-first DOM order is what makes this selector reach the hint at
 * all. */
.sit__hint {
  font-style: italic;
  font-size: 0.9rem;
  color: var(--ink-faint);
  margin: 0.5rem 0 0.2rem 0;
}
.conv:has(.cmt__pin[open]) .sit__hint { display: none; }
