/*
 * 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;
  --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.
 */

.masthead {
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-areas:
    "brand aside"
    "label aside"
    "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;
}

/* ---------- 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; }
  .masthead:has(.compass) ~ .page { 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 { 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;
    grid-template-areas:
      "brand   compass aside"
      "label   compass aside";
    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));
}

/* ---------- 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 {
  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);
}

/*
 * 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, and the scoping is the point. `.plate__img` is also every work in
 * `/feed`, which renders no rail — charging the term globally would shrink the
 * gallery's images on a 375x667 screen to pay for a control that screen does not
 * have. `:has()` is the same idiom `.masthead:has(.compass)` already uses, and a
 * modifier class would mean the page knowing about its own children twice.
 *
 * 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: 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;
  max-height: min(55vh, 55dvh, calc(100dvh - 19rem - var(--rail-reserve)));
}

.page: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;
}
.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);
}

/* Whose words these are. Only appears on a day the curator did not write, and
 * it is metadata about the prose above it, so it steps back like the rest of
 * the metadata does (rule 4).
 *
 * Scoped under `.label__body` on purpose: a bare `.label__source` loses to
 * `.label__note p` and `.label__text` on specificity and renders as full-size
 * ink prose, which is the opposite of stepping back. */
.label__body .label__source,
.label__source {
  margin: 0.9rem 0 0;
  font-size: 0.72rem;
  line-height: 1.4;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  color: var(--ink-dim);
}

/* ---------- 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` and `.rail__count` join 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 they need
 * 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,
.walk__step,
.walk__today,
.rail__act,
.rail__count,
.days__remove {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
}

/* 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. */
.days__remove,
.rail__act {
  padding: 0;
  font-family: var(--serif-body);
  color: var(--gold);
  background: none;
  border: 0;
  cursor: pointer;
}
.days__remove:hover,
.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. */

/* ---------- 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; }
}

/* A kept work whose day the curator removed. Same row, no link, and the only
 * row that carries a button — nesting one inside the row's own <a> would be an
 * accessibility defect, and this is the one row whose owner has no other way to
 * let it go. */
.days__link--gone { cursor: default; }
.days__date--gone { color: var(--ink-faint); }
/* Gold is what "this navigates" looks like everywhere in the product, and this
 * row does not. The title steps back to ink so the only gold left on it belongs
 * to the one thing that still acts: Remove. */
.days__link--gone .days__title { color: var(--ink); }

.days__remove-form { margin: 0 0 1.25rem; }

/* ---------- 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, then the keep frame, and that is structural rather than
 * emphasis. Keep and its count share one frame, a frame is one contiguous flex
 * item, and the count only exists after the fetch — so the frame GROWS and shoves
 * whatever is to its right about 60pt, on every open, for exactly the reader who
 * already has a collection. Last position means there is nothing to its right.
 *
 * 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; }

/* Height and the bare-button reset both 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 no words (`decisions/0010`). */
.rail__act {
  justify-content: center;
  min-width: var(--tap);
}
.rail__act 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; }

/* The only word in the rail. It is metadata about the action beside it, so it
 * steps back to dim like every other count in this product (rule 4) — but it
 * keeps the word `kept`, because with the labels gone it is the one thing on the
 * row telling a first-time reader what the glyph does. */
.rail__count {
  font-size: 0.8rem;
  color: var(--ink-dim);
  font-variant-numeric: tabular-nums;
}

/* ---------- 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__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; }
}
