/* =====================================================================
 * HOW THIS FILE IS ORGANISED  — read this before adding anything.
 *
 * mw.css loads LAST (main.css -> $extraCss -> mw.css), so it wins every
 * cascade tie. That makes it powerful and makes it dangerous.
 *
 * IT IS ORGANISED CHRONOLOGICALLY, NOT BY COMPONENT, AND AS OF AUG 2026 THAT IS
 * A DECISION, NOT NEGLECT. A by-component regrouping was built, verified at zero
 * computed-style differences across 39 rendered pages x 5 viewports, shipped,
 * and then REVERTED. Read the next paragraph before attempting it again.
 *
 * WHY THE REGROUPING FAILED. Roughly a third of the classes this file targets
 * never appear in any page the harness can render — the reader's JS-built
 * overlay panels (.ov-*, .ie-*, .mv-*, .mc-*, .et-*, .sh-*, .evm-*, .lp-*,
 * .eg-*, .rs-*, .mm-*) are created at runtime by reader.js and cannot be
 * rendered by render_public.py or render_reader.py. The style oracle therefore
 * verified about two thirds of the file and reported zero differences, which was
 * true and useless. Moving blocks produced 62 EQUAL-SPECIFICITY ORDER
 * INVERSIONS: pairs such as `.page-media-block .page-media` against
 * `.page-media.page-image`, both (0,2,0), where whichever comes last wins. One
 * of them removed the decorative gold frame from every image inside a book, and
 * it was found by a human on a phone, not by any tool in this repository.
 *
 * WHAT WOULD HAVE TO BE TRUE TO TRY AGAIN
 *   1. A fixture that mounts every reader overlay panel so the oracle can see
 *      them. Until that exists, moving anything here is unverifiable.
 *   2. tools/css_inversions.py must report 0. It detects exactly the failure
 *      above: equal specificity, co-matchable subjects, a shared property, and
 *      relative order changed.
 *   3. Both, after every step — not one, and not only at the end.
 *
 * WHAT TO DO INSTEAD. Navigate by the section banners below. They are historical
 * ("Revision pass", "Round B", "(#13)") and say WHEN a rule arrived, not what it
 * styles, so use the index and grep. Do not move blocks to tidy them.
 *
 * THE RULES FOR ADDING TO THIS FILE
 *   1. Put a fix in the file that OWNS the component if one exists. mw.css is
 *      for cross-cutting overrides, not for new components.
 *   2. Appending at the END is the safe place for a genuinely new rule: it
 *      cannot invert an existing pair.
 *   3. After ANY edit, check comment structure. A comment closed early silently
 *      deletes every rule after it and check.sh does not catch it. Strip all
 *      comments with a non-greedy regex and assert neither delimiter survives.
 *      (Described in prose on purpose: pasting the delimiters into a comment
 *      closes the comment, which is the bug being guarded.)
 *   4. Verify with tools/style_oracle.py AND tools/css_inversions.py. Neither
 *      alone is sufficient.
 *
 * SECTION INDEX (line numbers shift as the file grows; regenerate with a grep
 * for the banner rule rather than trusting these if they look wrong)
 *   line     1  MemorableWorld — additive styles on top of the OFE baseline (m
 *   line    69  Revision pass: provided assets + polish (no redesign)
 *   line   211  Reader revision: toolbar panels, cover edit, full-bleed media 
 *   line   569  Refinement pass (restored block)
 *   line   727  Subscription plans — inside the book, no overlap, no duplicate
 *   line   811  Account flow screens: OTP verification, Payment, Profile
 *   line  1064  Button contrast fix: `.sb-body a { color: var(--page-accent) }
 *   line  1092  Scroll / overscroll guards
 *   line  1123  Personal Diary — workflow UI (added this round)
 *   line  1476  (7) Image editor — the canvas IS the crop frame
 *   line  1488  EDITOR LAYOUT — three zones: tools rail | workspace | guidance
 *   line  1666  Refinement round: backdrop books, media page fit, editor heigh
 *   line  1767  Couple Diary — Round B (room, creation book, password gate)
 *   line  1960  In Memory Of — reuses the room, shelf, tiles and editor stylin
 *   line  2193  Shelf presentation — real shelf, real book silhouettes
 *   line  2276  (#9) GLOBAL DECORATIVE MEDIA FRAME
 *   line  2448  Media: fill the page, and no black letterbox around video
 *   line  2478  Blurred backdrop: books must look like the real shelf
 *   line  2494  ONE SHARED MEDIA FRAME COMPONENT  (used by every module)
 *   line  2550  SHELF: books rest ON the board, and stand together
 *   line  2576  SHELF + COVER — SINGLE SOURCE OF TRUTH
 *   line  2628  PANEL DENSITY STANDARD  (Add Entry, Edit Entry, Search, Change
 *   line  2692  THE END page
 *   line  2719  #14 BACKDROP SHELF = HOMEPAGE SHELF
 *   line  2742  #13 PARTICIPANTS PAGE
 *   line  2785  (#7) GUEST STATIC BOOK — Private Library preview for non-subsc
 *   line  4043  ICON ALIGNMENT — public books (Poems, Forty Poems, Quotes, Dai
 *   line  4092  ICON ALIGNMENT — date-based books, ONE reusable pattern
 *   line  4163  POEM TITLE — clearance for the favourite heart.
 *   line  4583  BOTTOM NAVIGATION — see includes/bottom-nav.php for how to rem
 * ===================================================================== */

/* =====================================================================
 * MemorableWorld — additive styles on top of the OFE baseline (main.css).
 * Only the MW-specific pieces live here: the two library headings, the
 * profile plaque, the daily Quote/Poem skin to match the live cream frames,
 * and the header profile/subscription items. Nothing in main.css is removed.
 * ===================================================================== */

/* ---- Daily frames: match the live cream gilt panels (dark text on cream) ---- */
.page-home .frame-inner {
  background: linear-gradient(#fbf5e9, #f3ead6);
  border: 1px solid rgba(180, 150, 80, .5);
  color: var(--ink);
}
.page-home .daily-section h3 {
  color: var(--maroon);
  text-transform: uppercase;
  letter-spacing: 1.2px;
}
/* ============ ONE TYPOGRAPHIC SCALE FOR THIS CARD ============
   MEASURED, at five widths, before being changed. The poem excerpt has always
   been responsive — main.css gives it `clamp(12px, .95vw, 14.5px)` — but the
   quote and this title were hard pixel values set HERE, and mw.css loads last,
   so they overrode the scale and then stayed put at every viewport:

       width    quote    poem excerpt    quote vs poem
        390     17px        12px            +42%
        768     17px        12px            +42%
       1440     17px       13.68px          +24%
       1920     17px       14.5px           +17%

   That is the reported "some quotes and poems are suddenly much larger than
   expected", and it is worst on a phone — the one place there is least room
   for it. Nothing in the recent rounds caused it; the fixed values and the
   clamped one have simply never been in the same system.

   The DESKTOP APPEARANCE IS DELIBERATELY UNCHANGED: both clamps are tuned so
   the maximum is the current value and it is reached at the same widths the
   design was approved at. Only the small end moves, and it moves onto the
   scale the poem already uses. The two-line clamp below is expressed in `em`,
   so it follows the type instead of fighting it — no layout figure changes.

   A quote stays LARGER than a poem excerpt on purpose: it is display italic
   and there is one of it. It is now larger by a controlled ratio rather than
   by whatever the viewport happens to be. */
.page-home .daily-title {
  color: #4a3826; font-weight: 600;
  font-size: clamp(12.5px, 1.04vw, 15px);
}
/* ================= TWO LINES, ALWAYS TWO LINES =================
   Quote of the Day and Poem of the Week are previews on a fixed card, and the
   card is one of two stacked in a column of fixed height. Nothing clamped the
   excerpt, so the length of whatever the Admin happened to schedule decided the
   homepage's layout: a long quote pushed the author and Read More down, and on
   a short viewport pushed them off the card entirely.

   Clamped to exactly two lines with the browser's own ellipsis. `min-height`
   reserves both lines whether or not the text needs them, so the author and the
   Read More button sit at the SAME height every day — a card that changes shape
   with the content is a card that cannot be designed around.

   -webkit-line-clamp needs -webkit-box; the pair is already used for the cover
   title (line 321) and is the only cross-browser way to clamp by LINE rather
   than by height. Where it is unsupported the text simply flows and the
   max-height below still stops the card growing. */
.page-home .daily-excerpt {
  color: #5b4a37;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  /* 2 lines exactly, at this element's own line-height. */
  min-height: calc(2em * 1.55 / 1);
  max-height: calc(2em * 1.55 / 1);
}
/* The quote sets its own line-height (1.4), so its two-line box is a different
   height from the poem's (1.55). Stated per element rather than shared, because
   a single figure would clip one of them. */
.page-home .daily-quote {
  min-height: calc(2em * 1.4);
  max-height: calc(2em * 1.4);
}
.page-home .daily-quote {
  font-family: var(--font-display);
  font-style: italic;
  font-size: clamp(13.5px, 1.18vw, 17px);
  line-height: 1.4;
  color: #3c2a1e;
}
.page-home .daily-author {
  margin: 0 0 12px;
  font-weight: 700;
  font-size: 12.5px;
  letter-spacing: .4px;
  color: var(--maroon);
}
.page-home .flourish-fallback { color: var(--frame-gold); }

/* ---- Header: profile avatar + Subscription pill ---- */
.nav-profile { padding: 0 !important; display: inline-flex; align-items: center; }
.nav-profile img {
  width: 40px; height: 40px; border-radius: 50%;
  border: 2px solid var(--maroon); object-fit: cover; display: block;
}
.nav-profile img.img-missing { display: none; }
.nav-profile img.img-missing + .profile-ic-fallback {
  display: inline-block; width: 40px; height: 40px; border-radius: 50%;
  background: #d9cfc0 radial-gradient(circle at 50% 38%, #b7ab98 0 34%, transparent 35%);
  border: 2px solid var(--maroon);
}
.btn-subscription { background: var(--maroon); color: #fff; }
.btn-subscription:hover { background: var(--maroon-dark); }

/* ---- Reader title page: MemorableWorld subtitle support ---- */
.tp-sub {
  font-family: var(--font-display);
  font-style: italic;
  font-size: 17px;
  color: #6f5a44;
  margin: 6px 0 0;
  text-align: center;
}

/* ---- Reader title page: what the book is for ----
   Sits under the subtitle in the same voice, one step quieter and narrower so
   it reads as a description rather than a second title. Measured against the
   existing page rather than given a new scale: same display face and colour
   family as .tp-sub, with a max-width so a long lede wraps to a readable
   column instead of spanning the whole leaf. */
.tp-summary {
  font-family: var(--font-display);
  font-size: 15px;
  line-height: 1.65;
  color: #7b6750;
  margin: 14px auto 0;
  max-width: 34ch;
  text-align: center;
}

/* ---- Diary entry opener (dated running head, no "Summary:" label) ---- */
.blk-entry-open { text-align: center; padding: 2px 0 4px; }
.blk-entry-open .entry-rule {
  display: inline-block; width: 46px; height: 2px; border-radius: 2px;
  background: var(--frame-gold);
}

/* =====================================================================
 * Revision pass: provided assets + polish (no redesign)
 * ===================================================================== */

/* ---- Left column: framed nature pictures (forest / river) ---- */
.picture-frame .picture-inner {
  padding: 8px;
  background: linear-gradient(#fbf5e9, #f0e6d2);
  position: relative;
}

/* Two more openings in the same wall — see .mw-scene-view. */
.picture-inner .mw-scene-view { aspect-ratio: 4 / 3; border-radius: 2px; }
.page-home .daily-panel { gap: 30px; justify-content: flex-start; }


/* ============ ONE LANDSCAPE, SEEN THROUGH SEVERAL WINDOWS ============
   Every opening in the room paints the SAME file. Which PART of it each one
   shows is set by window-scene.js from the measured geometry of the wall —
   background-size and background-position are written per element, so nothing
   here may set either or it would fight the script.

   THE SHAPE IS STILL DECIDED IN CSS. aspect-ratio reserves the box before the
   bytes arrive, which is what the removed width/height attributes used to do;
   without it the openings collapse and the room reflows on load.

   The colour underneath is the fallback. It replaces the onerror handler the
   <img> elements carried: a scene that never arrives leaves a muted green
   opening rather than a broken-image icon. */
.mw-scene-view {
  display: block;
  width: 100%;
  background-color: #55704f;
  background-repeat: no-repeat;
  /* covers the opening until the script has measured; the script then
     replaces both of these with the wall-relative values */
  background-size: cover;
  background-position: 50% 50%;
}
/* Painted in only once the wall has been measured, so the first frame cannot
   show five identical centred crops before the offsets land. */
.room-body:not([data-scene-ready]) .mw-scene-view { background-image: none !important; }

/* ---- Centre: two small panes on top, large forest window below ---- */
.room-windows { gap: 14px; }

/* shared window joinery: painted timber frame + sill, like the live site */
.room-windows .window-frame {
  background: linear-gradient(#f3ece0, #e6dccb);
  border: 10px solid #f2ebdd;
  border-radius: 4px;
  box-shadow:
    0 10px 26px rgba(60, 40, 20, .26),
    inset 0 0 0 1px rgba(140, 112, 66, .45),
    inset 0 2px 5px rgba(255, 255, 255, .7);
  position: relative;
}

/* top row: two small panes side by side */
.window-top-row { display: flex; gap: 10px; padding: 8px; }
.win-pane {
  position: relative; flex: 1 1 0; overflow: hidden;
  border-radius: 3px;
  border: 1px solid rgba(140, 112, 66, .5);
  box-shadow: inset 0 0 0 3px #fbf7ee, inset 0 3px 10px rgba(0,0,0,.22);
}
/* height: auto IS LOAD-BEARING, not tidiness.
   The <img> width/height ATTRIBUTES added for layout stability are
   presentational hints: they set the `height` property when no author rule
   does. This rule set width and aspect-ratio but not height, so `height="413"`
   won, both dimensions became definite, aspect-ratio was ignored, and the two
   window panes rendered 413px tall instead of width x 3/4 — which is the
   "centrepiece images suddenly became larger" regression.
   With height:auto the attributes still reserve the box before the bytes
   arrive (their whole purpose) and aspect-ratio still decides the shape.
   That defect belonged to the <img> elements these openings replaced; it is
   recorded because the fix — letting the CSS shape win over presentational
   attributes — is why .mw-scene-view carries aspect-ratio and no height. */
/* The pane is an OPENING now, not a picture. The <img> and its fallback span
   are gone with it — see .mw-scene-view for what replaced the onerror
   handler. The 4/3 shape is unchanged, so the joinery and the room's rhythm
   are exactly as they were. */
.win-pane .mw-scene-view { aspect-ratio: 4 / 3; }
/* glass: soft diagonal sheen so it reads as being behind glazing */
.win-pane .pane-glass {
  position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(118deg,
    rgba(255,255,255,.30) 0%, rgba(255,255,255,.10) 22%,
    rgba(255,255,255,0) 46%, rgba(255,255,255,.12) 74%, rgba(255,255,255,0) 100%);
}

/* bottom: the large forest window, four panes divided by white mullions */
.window-scene { position: relative; overflow: hidden; padding: 0; }
.window-scene .mw-scene-view { aspect-ratio: 16 / 11; }
/* mullions: one horizontal + three vertical bars, with a soft shadow so the
   glazing bars sit in front of the view rather than looking painted on */
.window-scene .window-mullions {
  position: absolute; inset: 0; pointer-events: none;
  background:
    linear-gradient(#fdfaf3, #efe7d8) 50% 0 / 7px 100% no-repeat,
    linear-gradient(#fdfaf3, #efe7d8) 25% 0 / 7px 100% no-repeat,
    linear-gradient(#fdfaf3, #efe7d8) 75% 0 / 7px 100% no-repeat,
    linear-gradient(#fdfaf3, #efe7d8) 0 46% / 100% 7px no-repeat;
  filter: drop-shadow(1px 2px 3px rgba(30, 20, 8, .45));
}
/* glazing sheen across the whole window */
.window-scene .window-glass {
  position: absolute; inset: 0; pointer-events: none;
  background:
    linear-gradient(116deg,
      rgba(255,255,255,.28) 0%, rgba(255,255,255,.08) 20%,
      rgba(255,255,255,0) 44%, rgba(255,255,255,.14) 68%, rgba(255,255,255,0) 100%),
    radial-gradient(120% 70% at 50% 0%, rgba(255,255,255,.16), transparent 60%);
}
/* inner reveal shadow: the wall casting into the opening */
.window-scene::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  box-shadow: inset 0 0 26px rgba(40, 28, 12, .34);
}

.page-home .library-group { margin-bottom: 16px; }         /* remove excessive lighting */
.book .cover-face,
.book { box-shadow: none; }                          /* remove heavy book shadows */
.shelf-board {
  box-shadow: 0 4px 8px rgba(60,40,20,.18);          /* one soft shelf shadow only */
}

/* ---- Books: uniform sizing, fully visible, natural seating ---- */
/* The shelf wall must not clip books or their hover labels. */
.page-home .shelf-wall,
.shelf-wall { overflow: visible !important; }

.shelf-books {
  display: flex; flex-wrap: wrap; justify-content: center;
  padding-top: 26px;          /* headroom so hover lift + label are never cut */
}
.shelf-books .book {
  width: 96px; margin: 0; overflow: visible;
}
/* Fluid below the cap, fixed at it. The book item above can flex-shrink
   (see the nowrap+shrink rule for .page-home rows); a hard 96px image
   here would overflow the narrowed item and overlap its neighbours, so
   the image follows the item's width and keeps its aspect ratio, topping
   out at the same 96x150 box as before. contain (not cover) => whole
   cover visible, never cropped. */
.shelf-books .book .cover-face {
  width: 100%; max-width: 96px;
  height: auto; max-height: 150px;
  object-fit: contain;
  border-radius: 2px; background: transparent;
}

/* ---- Hover label: never clipped ---- */
.page-home .book-tooltip,
.book-tooltip { display: none; }
.page-home .book:hover .book-tooltip,
.book:hover .book-tooltip { display: block; }
.book-tooltip {
  font-size: 12px;
  max-width: none;
}

/* =====================================================================
 * Reader revision: toolbar panels, cover edit, full-bleed media pages
 * ===================================================================== */

/* ---- Closed cover: single-line gold title, artwork inside the maroon frame ----
   The board area is inset from the left by the spine band, so both the title
   and the artwork are positioned against that inner area, not the whole face. */
/* Do NOT change .cover-front's positioning: reader.css sets
   `position:absolute; inset:0` so the face fills the cover. Overriding it with
   `position:relative` collapsed the face to ~55px and squashed the artwork. */
.cover-front { overflow: hidden; }

.cover-front .cover-book-title {
  position: absolute; z-index: 5;
  top: 4.5%;                                 /* moved higher still */
  left: 15%; right: 8%;                     /* clear of the spine band */
  margin: 0; padding: 0;
  text-align: center;
  font-family: var(--font-display, 'Cormorant Garamond', serif);
  font-size: clamp(15px, 2.3vw, 27px);
  font-weight: 800;
  line-height: 1.14;
  letter-spacing: .3px;
  color: #f5cd62;
  /* long titles wrap to a second line instead of being clipped; the clamp
     above keeps them from growing past the board */
  white-space: normal;
  overflow-wrap: break-word;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
  text-shadow: 0 1px 0 rgba(90, 55, 0, .55), 0 3px 9px rgba(0, 0, 0, .55);
}

/* Artwork plate: strictly inside the maroon board, never overflowing. */
.cover-front .cover-art {
  position: absolute; z-index: 2;
  /* reader.css sets width/height:100% on .cover-art. Left alone that pushes the
     plate past the maroon board; setting height:auto instead collapsed it to
     ~37px so the artwork rendered as a squashed strip. Neutralising both and
     anchoring all four offsets lets the board define the box correctly. */
  width: unset !important;
  height: unset !important;
  /* TOP INSET SETS THE GAP UNDER THE TITLE. 15% left a visible band of empty
     board between the title and the artwork; 11% closes it without the two
     touching. This is the DEFAULT, so it governs every book that prints a
     title — Personal Diary, Couple Diary, In Memory Of, Special Events, My
     Life in Pictures, My Favorites, and anything added later.
     Poem books are unaffected: they override this with their own inset and
     print no title at all. */
  inset: 11% 5.5% 6% 12.5%;
  overflow: hidden;                          /* clips any oversized upload */
  border-radius: 2px;
  background: #241611;
  box-shadow: 0 4px 14px rgba(0, 0, 0, .38);
}
.cover-front .cover-art img {
  /* absolute fill: works regardless of how the plate's height resolves, so the
     artwork can never collapse or stretch out of proportion */
  position: absolute; inset: 0;
  width: 100% !important; height: 100% !important;
  /* ============ `cover`, NOT `contain` ============
     This was `contain`, with the comment "whole cover visible, no crop". The
     cost of that was bands of the plate's own near-black fill (#241611)
     wherever the image and the plate disagreed on shape — and they always
     disagree, because the plate is NOT the export ratio. Measured: the plate
     is 0.7904 wide-to-tall on desktop and 0.6323 on a phone, against a 0.80
     export. On a phone that letterboxed a correctly fitted cover by about a
     fifth of its height, and an un-normalised portrait upload showed the
     reported black edges down the left and right.

     `cover` fills the plate and crops the overflow, which is what the
     poem-book, full-cover and favourite-book plates below already do for
     exactly this reason. Nothing is stretched: the aspect is preserved and the
     overflow is clipped by the plate's `overflow: hidden`.

     There is very little left to crop. Every upload path now normalises to
     900x1125 through OFEReaderCover.fitToCover() before it is previewed or
     sent — the reader's cover panels always did, and the five create pages now
     do too — so what `cover` trims is the ~1% ratio difference on desktop
     rather than an arbitrary photo. */
  object-fit: cover;
  object-position: center;
  display: block;
}

/* ---- Toolbar ---- */
.reader-toolbar .tool-add { background: var(--maroon, #7b3335); }
.reader-toolbar .tool-add svg { fill: #fff; }
.reader-toolbar .tool-add:hover { background: var(--maroon-dark, #5f2628); }
.reader-toolbar .tool-upload svg { fill: var(--maroon, #7b3335); }

/* While the book is CLOSED only Upload + Return Book are offered.
   .book-closed is toggled on the reader room by reader.js. */
/* CLOSED: only Change Cover / Title (+ Return Book, which is not a .tool). */
.reader-room.book-closed .reader-toolbar .tool:not(.tool-upload) { display: none !important; }
.reader-room:has(.flipbook.closed) .reader-toolbar .tool:not(.tool-upload) { display: none !important; }

/* OPEN: all reader controls. Change Cover stays available because it now
   opens as a normal page spread (it opens the book first if closed). */

/* (Change Cover now opens on a normal page spread — no cover-mounted panel.) */

/* ---- In-book panels: Search / Add Entry / Edit Entry / Change Cover ----
   All four share one professional layout: generous vertical rhythm, full-width
   fields, clear labels, one primary action. */
.ov-panel {
  display: flex; flex-direction: column;
  padding: clamp(14px, 2vw, 24px) clamp(14px, 2.2vw, 26px) clamp(12px, 1.6vw, 20px);
}
.ov-panel .ov-title {
  margin: 0 0 8px;
  font-family: var(--font-display, 'Cormorant Garamond', serif);
  font-size: clamp(20px, 2.3vw, 27px);
  font-weight: 800; text-align: center; color: var(--maroon, #7b3335);
  letter-spacing: .2px;
}
.ov-panel .overlay-rule {
  height: 2px; width: 54px; border-radius: 2px;
  background: linear-gradient(90deg, transparent, #c9a24e 25%, #c9a24e 75%, transparent);
}

.ov-form {
  display: flex; flex-direction: column;
  gap: clamp(11px, 1.5vw, 17px);
  /* ============ THE BOTTOM GAP LIVES OR DIES HERE ============
     This was `flex: 1 1 auto`, and flex-SHRINK is what broke the breathing
     space at the bottom of every panel. .ov-panel is the scroll container and
     carries a real padding-bottom (20px desktop, 12px phone), but a shrink of
     1 with min-height: 0 let this form be squeezed BELOW its own content the
     moment the content grew. The children then overflowed the form's box and
     painted straight down through the panel's padding, because the padding
     sits under the form's (shrunken) box, not under the content that escaped
     it. Measured on Change Cover the instant an image preview appeared: form
     456.4px tall against 563px of content, and the gap under Save went from
     92.4px to 0.2px. Android was already at -0.8px before the upload.

     A previous attempt added padding and an ::after spacer to every scrolling
     container. It did not work, for the reason above — no amount of padding on
     a box helps when the content has escaped that box — and the spacer broke
     the static guest book, whose artwork is height:100%. Both were reverted;
     see the note further down this file.

     `flex: 1 0 auto` keeps the GROW, so a short panel still hands its spare
     height to the textarea (that is what min-height: 0 was written for), and
     removes the SHRINK, so a tall panel's real height enters the flex layout,
     the panel scrolls as a scroll container should, and its padding-bottom is
     honoured at the end of the scroll. One declaration, every .ov-panel in the
     reader, no per-panel rules. Verified by tools/panel_bottom_gap.py. */
  flex: 1 0 auto; min-height: 0;
}
.ov-field { display: flex; flex-direction: column; gap: 6px; }
.ov-label {
  font: 700 11.5px/1.2 var(--font-ui, Poppins, sans-serif);
  letter-spacing: .5px; text-transform: uppercase; color: #7a6446;
}

.ov-input, .ov-textarea {
  width: 100%; box-sizing: border-box;
  padding: 11px 13px;
  border: 1px solid #d3c6ae; border-radius: 9px;
  background: #fffdf8; color: #3c2a1e;
  font: 400 14px/1.4 var(--font-ui, Poppins, sans-serif);
  transition: border-color .15s, box-shadow .15s;
}
.ov-input:focus, .ov-textarea:focus {
  outline: none; border-color: var(--maroon, #7b3335);
  box-shadow: 0 0 0 3px rgba(123, 51, 53, .12);
}
.ov-textarea { resize: vertical; min-height: 96px; line-height: 1.55; }

/* upload areas */
.ov-drop {
  display: flex; align-items: center; justify-content: center; gap: 9px;
  padding: 15px 12px; cursor: pointer;
  border: 1.5px dashed #c6ab78; border-radius: 10px;
  background: #fdf8ee; color: var(--maroon, #7b3335);
  font: 600 13px/1 var(--font-ui, Poppins, sans-serif);
  transition: background .15s, border-color .15s;
}
.ov-drop:hover { background: #f8f0dd; border-color: var(--maroon, #7b3335); }
.ov-drop-ic {
  width: 26px; height: 26px; border-radius: 50%;
  display: grid; place-items: center;
  background: rgba(123, 51, 53, .1); font-size: 14px;
}

/* actions */
.ov-primary {
  width: 100%; margin-top: 2px;
  padding: 13px 18px; border: none; border-radius: 10px; cursor: pointer;
  background: var(--maroon, #7b3335); color: #fff;
  font: 700 14px/1 var(--font-ui, Poppins, sans-serif); letter-spacing: .3px;
  box-shadow: 0 3px 10px rgba(123, 51, 53, .28);
  transition: background .15s, transform .12s;
}
.ov-primary:hover { background: var(--maroon-dark, #5f2628); transform: translateY(-1px); }
.ov-secondary {
  flex: 1 1 0; padding: 11px 12px; cursor: pointer;
  border: 1px solid #d3c6ae; border-radius: 9px;
  background: #fffdf8; color: var(--maroon, #7b3335);
  font: 600 13px/1 var(--font-ui, Poppins, sans-serif);
  transition: background .15s, border-color .15s;
}
.ov-secondary:hover { background: #f8f0dd; border-color: var(--maroon, #7b3335); }
.ov-jump { display: flex; gap: 10px; }
.ov-divider { height: 1px; margin: 16px 0; background: rgba(150,120,70,.28); }
.ov-note {
  margin: 9px 0 0; text-align: center;
  font: italic 400 11px/1.3 var(--font-ui, Poppins, sans-serif); color: #9a866a;
}

/* the panel may scroll internally if a page is short, but the book never does */
.ov-panel { overflow-y: auto; overflow-x: hidden; }

/* ---- FORM FOOTER SPACING: REMOVED --------------------------------------
   A padding-bottom + ::after spacer used to be applied globally here, to
   .ov-panel and .page-overlay, and again further down to .sb-scroll /
   .sb-panel. All of it is gone.

   It was reported as not actually fixing the problem it was written for —
   some panels still ended flush against the bottom edge — while the ::after
   height it added to EVERY scrolling container had a side effect elsewhere:
   the static guest book's artwork page is a .sb-scroll whose image is
   height:100%, so ~50px of spacer pushed it past its container and the
   picture became scrollable.

   Removing it restores each container's own bottom padding: .ov-panel back to
   its clamp(12px, 1.6vw, 20px), .page-overlay back to reader.css's 30px,
   .sb-scroll back to none and .sb-panel back to its own 16px.

   Do NOT reintroduce this as a blanket rule. When the footer spacing is
   solved properly it needs to target the panels that actually scroll and
   actually have a footer control, not every scrolling box on the platform. */

/* ---- Reader must fit: no scrolling ---- */
.page-reader { overflow: hidden; }
.page-reader .reader-room { overflow: hidden; }
.page-reader .page-body { overflow: hidden; }

.page-media { margin: 0; width: 100%; display: flex; flex-direction: column; }
.page-media img,
.page-media video {
  width: 100%; height: 100%; object-fit: cover; display: block; border-radius: 2px;
  max-height: 100%;
}
.page-media figcaption {
  position: absolute; left: 0; right: 0; bottom: 0; padding: 10px 14px;
  background: linear-gradient(transparent, rgba(20,12,6,.72));
  color: #f4ecd8; font-size: 13px; font-style: italic; text-align: center;
}
.page-media { position: relative; height: 100%; }
.page-image .media-fallback { display: none; }
.page-image img.img-missing { display: none; }
.page-image img.img-missing + .media-fallback {
  display: block; width: 100%; height: 100%;
  background: repeating-linear-gradient(45deg, #e7ddca, #e7ddca 16px, #ded2ba 16px, #ded2ba 32px);
}
.page-video video { background: #1c120b; }

/* The table flexes: rows shrink evenly so even Diamond (11 lines) fits its
   page without overlapping the totals or the Subscribe button. */
.plan-table {
  justify-content: flex-start;
}

/* Media pages keep their date visible above the media */
.page-full .page-head { display: flex !important; }
/* ---- THE SIDE PADDING ON A MEDIA PAGE IS DECIDED HERE, AND ONLY HERE ----
   This said `padding: 0 !important`, and further down the file a later rule
   asked for the breathing room the design actually wants:

       .page.page-full .page-body { padding-left: 10px; padding-right: 10px; }

   That rule is (0,3,0) and this one is (0,2,0) — but `!important` outranks
   specificity, so the later, more specific, deliberately-written rule never
   applied and every media frame sat hard against the edge of the page body.
   The declaration a reader of this file would believe was in force was not the
   one the browser used, which is why the frames looked wider than intended on
   both images and video, on every book and at every width.

   The vertical zero is what matters to the layout (the media column must own
   the full height between the date and the page number); the sides were only
   ever collateral. Both values are stated here now, so there is one answer. */
.page-full .page-body { padding: 0 10px !important; }

/* ---- Video page: fully visible, clear centred play indicator ---- */
/* Footage is never cropped — trimming it cuts off faces and action — so a clip
   whose shape differs from the page leaves bars. They were solid #150d07 on
   desktop and parchment below 430px, so the two platforms disagreed and neither
   read as deliberate. The bars now carry the same treatment the photographs
   get: an over-scaled blurred copy of the clip's own first frame, painted into
   a canvas by reader-video-backdrop.js. CSS cannot blur a video's content, and
   a second <video> would decode the same file twice — a real cost on a phone. */
.page-media.page-video {
  background: #150d07;
  display: grid; place-items: center;
  position: relative; overflow: hidden;
}
.page-media.page-video .page-media-vblur {
  position: absolute; top: 0; left: 0; width: 100%; height: 100%;
  object-fit: cover;
  transform: scale(1.18);
  filter: blur(22px) saturate(.85) brightness(.55);
  z-index: 0; pointer-events: none;
  opacity: 0; transition: opacity .25s;
}
/* Shown only once a frame has actually been painted; if the canvas is tainted
   or the codec unsupported it stays hidden and the dark fallback shows. */
.page-media.page-video .page-media-vblur.is-painted { opacity: 1; }
.page-media.page-video video {
  position: relative; z-index: 1;
  width: 100%; height: 100%;
  object-fit: contain;              /* never crop the video */
  background: transparent;
}
/* central play badge, shown until the video starts */
.page-video .video-play {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 68px; height: 68px; border-radius: 50%;
  background: rgba(255,255,255,.92);
  display: grid; place-items: center;
  box-shadow: 0 6px 20px rgba(0,0,0,.5);
  pointer-events: none; transition: opacity .2s;
}
.page-video .video-play::after {
  content: ''; display: block;
  border-style: solid; border-width: 15px 0 15px 25px;
  border-color: transparent transparent transparent var(--maroon, #7b3335);
  margin-left: 6px;
}
.page-video.is-playing .video-play { opacity: 0; }

/* Image page: the COMPLETE photo, on a frame that is still filled edge to edge.
   Two earlier attempts each solved half of this. `contain` kept the whole
   picture but let the #efe7d8 mount show as pale bars inside the gold border.
   `cover` filled the frame but CROPPED — a landscape photo lost its sides, a
   portrait one its top and bottom, and the subject could be cut in half.
   Neither is right for a memory-preservation product, where the picture is the
   thing being preserved.
   Both copies are the same file, so this costs one fetch. */
.page-media.page-image {
  display: grid; place-items: center; background: #efe7d8;
  position: relative; overflow: hidden;
}
/* The backdrop: over-scaled and blurred so it fills whatever the sharp copy
   leaves. `scale(1.18)` pushes the blur's own soft edge past the frame — a
   blurred image drawn exactly to size leaves a pale rim, the same lesson the
   editor's paintBackdrop() records. Dimmed and slightly desaturated so it
   reads as a surface rather than competing with the photograph on top. */
.page-media.page-image .page-media-blur {
  position: absolute; top: 0; left: 0; width: 100%; height: 100%;
  object-fit: cover;
  transform: scale(1.18);
  filter: blur(22px) saturate(.85) brightness(.78);
  z-index: 0;
  pointer-events: none;
}
/* The subject: complete, uncropped, its own aspect ratio, as large as the
   frame allows. */
.page-media.page-image .page-media-sharp {
  position: relative; z-index: 1;
  object-fit: contain;
  width: 100%; height: 100%;
}

/* =====================================================================
 * READER LIGHTBOX — enlarging a photograph on a page of the book
 * Separate from the Image Editor's own enlargement, which only exists while
 * the editor is open. This is what a READER gets.
 * ===================================================================== */
.rlb {
  position: fixed; inset: 0; z-index: 5000;
  background: rgba(12, 9, 6, .93);
  display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 10px;
  padding: 60px 16px 20px;
}
.rlb-shell {
  flex: 1 1 auto; min-height: 0; width: 100%;
  display: flex; align-items: center; justify-content: center;
}
/* The complete picture, never cropped to fill the screen — the same rule the
   book itself follows. */
.rlb-img {
  max-width: 100%; max-height: 100%;
  width: auto; height: auto;
  object-fit: contain; border-radius: 3px;
  box-shadow: 0 24px 60px -20px rgba(0, 0, 0, .9);
}
.rlb-close {
  position: absolute; top: 14px; right: 14px;
  width: 44px; height: 44px; border-radius: 12px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  background: rgba(255, 246, 228, .12); border: 1px solid rgba(255, 246, 228, .3);
  color: #f6ecd8; cursor: pointer;
}
.rlb-close svg { width: 18px; height: 18px; fill: currentColor; }
.rlb-close:hover { background: #a83a3a; border-color: #a83a3a; color: #fff; }
.rlb-cap {
  flex: 0 0 auto; margin: 0; max-width: 60ch; text-align: center;
  font: 500 12.5px/1.4 var(--font-ui, Poppins, sans-serif); color: #d6c8ae;
}
/* The affordance: a photograph that can be enlarged says so on a pointer.
   Not applied on touch, where there is no cursor to show. */
.has-reader-lightbox .page-media.page-image .page-media-sharp { cursor: zoom-in; }

/* ---- Entry edit affordance ---- */
.entry-edit {
  width: 30px; place-items: center;
}

/* ---- Registration form: grouped rows, prominent plan badge ---- */
.reg-form { display: flex; flex-direction: column; gap: 13px; }

/* compact selected-plan summary, sitting just above Create Account */
.reg-plan-line {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin: 4px 0 2px; padding: 9px 13px;
  border: 1px solid rgba(150, 120, 70, .45); border-radius: 9px;
  background: #fdf8ee;
  font: 500 12.5px/1 var(--font-ui, Poppins, sans-serif); color: #5b4a37;
}
.reg-plan-line .rpl-label {
  font-weight: 700; font-size: 10.5px; letter-spacing: .8px;
  text-transform: uppercase; color: #9a866a;
}
.reg-plan-line .rpl-name { font-weight: 700; color: var(--maroon, #7b3335); }
.reg-plan-line .rpl-dot { color: #c2ad8a; }
.reg-plan-line .rpl-price { font-weight: 700; color: #3c2a1e; }
.reg-plan-line .rpl-save {
  padding: 3px 8px; border-radius: 20px;
  background: rgba(123, 51, 53, .1); color: var(--maroon, #7b3335);
  font-weight: 700; font-size: 10.5px;
}
.reg-plan-line .rpl-change {
  margin-left: auto; color: var(--maroon, #7b3335);
  font-weight: 600; font-size: 12px; text-decoration: underline;
}

/* #1 Registration top row: first fields LEFT, small photo control RIGHT,
   both directly under the plan box. SINGLE OWNER of this layout — the
   parallel float-based rules that used to live in auth.css (and a
   .page-register float at the end of this file) are gone: mw.css loads
   last, so its old centred 96px rules always beat them, and float cannot
   work here anyway (.sb-form is a flex column per reader.css, and float
   has no effect on flex items). */
.reg-top { display: flex; align-items: flex-start; gap: 14px; }
.reg-top-fields {
  flex: 1 1 auto; min-width: 0;
  display: flex; flex-direction: column;
  gap: clamp(7px, .8vw, 11px);   /* same rhythm as .sb-form (auth.css) */
}
.reg-avatar-block { flex: 0 0 auto; display: flex; justify-content: center; margin: 2px 0 0; }
.reg-avatar { display: flex; flex-direction: column; align-items: center; gap: 5px; cursor: pointer; }
.reg-avatar-ring {
  position: relative; width: 60px; height: 60px; border-radius: 50%;
  display: grid; place-items: center;
  background: #f6efe1;
  border: 2px dashed #c6ab78;
  overflow: hidden;
  transition: border-color .15s, background .15s;
}
.reg-avatar:hover .reg-avatar-ring { border-color: var(--maroon, #7b3335); background: #f8f0dd; }
.reg-avatar-img { width: 100%; height: 100%; object-fit: cover; display: none; }
.reg-avatar-img.has-src { display: block; }
.reg-avatar-img.has-src ~ .reg-avatar-ic { display: none; }
.reg-avatar-ic {
  font-size: 22px; font-weight: 300; color: #b9a179; line-height: 1;
}
.reg-avatar-text {
  /* TYPOGRAPHY FLOOR: this is the instruction the user reads to upload an
     avatar. max-width raised with it so the two words still fit on two lines
     rather than three, keeping the control the same height. */
  font: 600 12px/1.25 var(--font-ui, Poppins, sans-serif); color: var(--maroon, #7b3335);
  text-align: center; max-width: 92px;
}

/* THE IDENTITY BLOCK STACKS ON A PHONE — THE SAME WAY PROFILE ALREADY DOES.

   `.reg-top` is a desktop arrangement (fields on the left, avatar column on the
   right) and it had NO mobile rule of any kind. Measured on a 371px book page:
   the avatar column takes 76px plus a 14px gap, so First Name and Last Name were
   squeezed to ~108px each and the Email field stopped ~90px short of the right
   edge — which is exactly the crowding reported, including the "Upload Profile
   Image" label wrapping onto a second line.

   Profile already solved this shape: avatar centred at the top, caption beneath,
   fields full width below (.pf-avatar-block). Registration now reads the same
   way rather than inventing a second arrangement — `order: -1` lifts the avatar
   above the fields without touching the markup, so the DOM order that suits
   desktop is unchanged and no PHP is edited.

   Sizes are Profile's, not new numbers: 64px ring, 11px caption. */
@media (max-width: 860px) {
  .reg-top { flex-direction: column; align-items: center; gap: 8px; }
  .reg-top-fields { width: 100%; }
  .reg-avatar-block { order: -1; margin: 0; }
  .reg-avatar-ring { width: 64px; height: 64px; }
  .reg-avatar-text { font-size: 11px; max-width: none; }
}

/* two-up rows */
/* .reg-row removed — see the REGISTRATION note in auth.css. */

.page-register .sb-form input,
.page-register .sb-form select { width: 100%; box-sizing: border-box; }

/* =====================================================================
 * Refinement pass (restored block)
 * ===================================================================== */

/* ---- Reader over the room (real-URL architecture) ----
   The reader is a real page that renders the blurred library room BEHIND
   the book (.reader-room-behind below), replacing the old transparent
   iframe over a live shelf page. The page and the reader room are made
   transparent so that backdrop shows through everywhere the book itself
   doesn't cover — the same view the iframe used to give, but on ONE
   document, on every path: opened from a shelf, direct URL, or deep link.

   The old .reader-embed selectors in main.css/reader.css are dead (no
   body carries that class any more); they are left in place only because
   those files have paired .min siblings and are deliberately untouched. */
.page-reader.reader-over-room,
.page-reader.reader-over-room .reader-room {
  background: transparent !important;
}

/* STACKING — the dimming scrim must never fall on the book itself.
   .reader-overlay is z-index 90 (main.css). The reader's own content has to
   sit ABOVE it, exactly as the old iframe did at z-index 96, so only the
   backdrop behind is dimmed and the book keeps its full brightness.

   This rule was accidentally deleted while rewriting the backdrop block in a
   previous round, which is precisely why the book started looking darker and
   duller: with no z-index, the book fell below the scrim and got painted
   over by it. Do not remove: without it the book is dimmed. */
.page-reader.reader-over-room .reader-room {
  position: relative;
  z-index: 96;
}
/* The toolbar lives inside .reader-room, so it inherits that stacking
   context — but the read-progress bar and toasts are separate; keep them
   above the scrim too. */
.page-reader.reader-over-room .reader-toolbar,
.page-reader.reader-over-room .read-progress { z-index: 97; }

/* The originating page, sitting behind the open book.
   Clips at the viewport, but the page inside keeps its NATURAL height — an
   earlier version constrained the room to viewport height, which compressed
   its internal layout and produced the merged books / squashed shelves.

   Deliberately NO filter here: blur and dimming come solely from the
   existing `body.book-open-mode .library-room` rule (blur 6px,
   brightness .82) that the iframe version also used. Adding a filter here
   stacked a second blur and a second brightness reduction on top of it,
   which is what made the reader look darker than the page behind it. */
.reader-room-behind {
  position: fixed;
  inset: var(--site-header-h) 0 0 0;
  overflow: hidden;
  z-index: 0;
  pointer-events: none;
  /* NO BACKGROUND OF ITS OWN. This briefly carried a painted gradient
     environment while the rendered room was removed; the room is back, because
     the point of this layer is that it IS the shelf the book came off, and a
     generic backdrop breaks that. The blinking it was meant to cure came from
     `decoding="async"` on the covers, which is fixed at source — see the note
     in reader.php. */
}

.reader-room-behind > .page-home {
  /* THE ALIGNMENT FIX, corrected.
     The real page's chain is `body { min-height: 100vh; display: flex;
     flex-direction: column }` with `.library-room { flex: 1 }` — the room
     fills the viewport when content is short but GROWS PAST it when the
     shelves are taller, and the page scrolls. `inset: 0` here pinned the
     bottom edge too, clamping the room to exactly viewport height: content
     still overflowed and drew at natural positions, but the room's own box
     (its wall gradient, and the floor strip that sits at its flex end) ended
     mid-content — the floor band painted ACROSS the lower shelves. Pinning
     only the top and giving min-height:100% reproduces the real chain;
     .reader-room-behind's overflow:hidden clips the excess at the viewport,
     which is exactly what the real page shows at scroll position 0. */
  position: absolute;
  inset: 0 0 auto 0;
  min-height: 100%;
  display: flex;
  flex-direction: column;
}
/* No min-height:0 — the real page's room never shrinks below its content,
   so the backdrop's must not either. */
.reader-room-behind > .page-home > .library-room { flex: 1 1 auto; }

/* Cross-document view transitions: where supported the browser morphs the
   originating page into this one (and back), so the handoff is continuous.
   Unsupported browsers ignore it and simply navigate — no JS, no fallback. */
@view-transition { navigation: auto; }

.reader-backdrop { display: none; }   /* older decorative CSS room, retired */

/* ---- Date: smaller still, edit control beside it ---- */
.page-head {
  font-weight: 700;
  color: #6b5335;
  gap: 8px;
}

/* ---- Edit entry control ---- */
.entry-edit,
.fp-icon {
  position: absolute; top: 4px; right: 10px; z-index: 8;
  display: inline-flex; align-items: center; gap: 5px;
  height: 24px; padding: 0 10px 0 8px;
  border: 1px solid rgba(150, 120, 70, .55);
  border-radius: 13px; cursor: pointer;
  background: rgba(255, 252, 245, .95);
  color: var(--maroon, #7b3335);
  font: 600 11px/1 var(--font-ui, Poppins, sans-serif);
  box-shadow: 0 1px 4px rgba(60, 40, 20, .2);
  transition: background .15s, border-color .15s, transform .12s;
}
.entry-edit svg,
.fp-icon svg { width: 13px; height: 13px; fill: currentColor; }
.entry-edit .ee-label { line-height: 1; }
.entry-edit:hover,
.fp-icon:hover { background: #fff; border-color: var(--maroon, #7b3335); transform: translateY(-1px); }

/* The two controls sit in a POSITIONED CONTAINER rather than being placed
   individually. Sibling selectors would have had to know whether Edit was
   present — a collection poem shows Remove alone — and would have depended on
   the order they happen to be appended in. A flex row is order-independent and
   collapses correctly when there is only one. */
.fp-actions {
  position: absolute; top: 4px; right: 10px; z-index: 8;
  display: inline-flex; align-items: center; gap: 6px;
}
.fp-actions .fp-icon { position: static; padding: 0 9px; }
.fp-del-btn:hover { background: var(--maroon, #7b3335); border-color: var(--maroon, #7b3335); }
.fp-del-btn:hover svg { fill: #fff; }
.page-full .entry-edit { background: rgba(255, 252, 245, .94); top: 6px; right: 8px; }

/* ---- Hover on ANY shelf renders above the shelves below ---- */
.shelf { overflow: visible; }
.shelf:hover { z-index: 50; }
.shelf-books { z-index: 4; overflow: visible; }
.shelf-board { z-index: 2 !important; }
.library-group { position: relative; overflow: visible; z-index: 1; }
.library-group:hover,
.library-group:focus-within { z-index: 200 !important; }
.shelf-books .book { position: relative; z-index: 1; }
.shelf-books .book:hover { z-index: 60 !important; }
.book-tooltip { z-index: 70 !important; }

/* ---- Library headings: stronger hierarchy ---- */
.library-group { margin-bottom: 20px; }
.library-heading {
  display: block; text-align: center;
  font-family: var(--font-display, 'Cormorant Garamond', serif);
  font-size: clamp(28px, 3vw, 40px);
  font-weight: 900;
  letter-spacing: .4px; line-height: 1.1;
  color: #241a12;
  margin: 0 auto 14px; padding-bottom: 8px;
  position: relative;
  text-shadow: 0 1px 0 rgba(255,255,255,.6);
}
.library-heading::after {
  content: ''; position: absolute; left: 50%; bottom: 0; transform: translateX(-50%);
  width: 58px; height: 3px; border-radius: 2px;
  background: linear-gradient(90deg, transparent, #c9a24e 25%, #c9a24e 75%, transparent);
}

/* =====================================================================
 * Subscription plans — inside the book, no overlap, no duplicate heading
 * ===================================================================== */
.page-plans-book { overflow: hidden; }
/* `.plans-room .reader-toolbar { justify-content: center }` was removed here.
   It existed only to undo the mobile toolbar's `flex-start` for this one page;
   now that the shared rule is `safe center`, the base declaration already
   centres this toolbar at every width and the override was a page-specific
   patch for a bug that no longer exists. */
.plan-page {
  height: 100%; display: flex; flex-direction: column;
  padding: 4px 6px 2px; overflow: hidden; box-sizing: border-box;
}
.plan-table { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
.plan-row {
  display: flex; justify-content: space-between; align-items: center; gap: 8px;
  /* NO SHRINKING. This was `flex: 0 1 auto`, and flex-shrink: 1 let every row
     be compressed below its own content height whenever the module list was
     taller than the table — which is most plans on a phone. Paired with the
     `overflow: hidden` below, the row then SLICED its own text: measured at
     390px, a row needing 32px was given 25.8px, and at 320px it was given 18px.
     That is the reported "module information overlapping/clipping on mobile" —
     descenders cut off and each row's text running into the next row's card.
     It was not a spacing problem and no amount of padding could fix it.

     .plan-table already carries `overflow-y: auto`, so the excess has somewhere
     to go. Scrolling a long module list is the trade the client accepted
     explicitly (see the phone block further down); unreadable rows were never
     part of it. */
  flex: 0 0 auto; min-height: 0;
  padding: clamp(2px, .5vh, 6px) 3px;
  border-bottom: 1px solid rgba(150,120,70,.24);
  font-size: clamp(10px, .95vw, 13px); line-height: 1.2; color: #4a3826;
  overflow: hidden;
}
.plan-row .pl-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.plan-row .pl-price { font-weight: 700; flex: 0 0 auto; }
.plan-subscribe {
  flex: 0 0 auto; display: block; margin: 8px auto 2px;
  width: max-content; min-width: 140px; text-align: center;
  padding: 8px 24px; border-radius: 20px;
  background: var(--maroon, #7b3335); color: #fff;
  font-weight: 700; font-size: 12.5px; text-decoration: none; transition: background .15s;
}
.plan-subscribe:hover { background: var(--maroon-dark, #5f2628); }
.plans-flipbook .page-head {
  font-family: var(--font-display, 'Cormorant Garamond', serif);
  font-size: clamp(19px, 2.1vw, 26px) !important;
  font-weight: 800; color: var(--maroon, #7b3335);
}
.plans-flipbook .page-body { overflow: hidden; }

/* ---- Second/lower-shelf hover: label must clear the plank of the shelf above ----
   Sibling .shelf elements each create a stacking context. Raising only the
   hovered shelf is not enough because the plank above sits in a LATER-painted
   sibling; so the shelf-wall gets an explicit isolation and the hovered shelf
   is promoted above every sibling, while non-hovered planks drop back. */
.shelf-wall { isolation: isolate; }
.shelf { z-index: 1; }
.shelf:hover,
.shelf:focus-within { z-index: 100 !important; }
.shelf:hover .shelf-books,
.shelf:focus-within .shelf-books { z-index: 101 !important; }
.shelf:hover .book:hover,
.shelf:focus-within .book:hover { z-index: 102 !important; }
.shelf:hover .book:hover .book-tooltip,
.shelf:focus-within .book:hover .book-tooltip { z-index: 103 !important; }
/* the plank never needs to sit above a neighbouring shelf's hovered label */
.shelf .shelf-board { z-index: 0 !important; position: relative; }
.shelf .board-fallback { box-shadow: 0 5px 9px rgba(52, 32, 12, .38); }

/* ---- Book hover label: render BELOW the book ----
   Sibling .shelf elements are separate stacking contexts, so a label drawn
   ABOVE a book is painted over by the heading/plank that follows it in the
   DOM. Flipping the label under the book keeps it clear of both, on every
   shelf, without touching the shelf layout. */
.book-tooltip {
  bottom: auto !important;
  top: calc(100% + 8px) !important;
  transform: translateX(-50%) !important;
}
/* the little arrow now points up toward the book */
.book-tooltip::after {
  top: auto !important;
  bottom: 100% !important;
  border-top-color: transparent !important;
  border-bottom-color: var(--maroon-dark, #5f2628) !important;
}
/* the shelf row needs room underneath for the label */
.shelf-books { padding-bottom: 34px; }

/* ---- Paired fields (Upload Image | Upload Video) ----
   ONE COLUMN ON A NARROW SCREEN, and this is the root of the "cramped media
   controls" report rather than anything in the chip itself.

   The pair split the panel into two equal columns at EVERY width. At 320px
   that gives each media field about 120px, which has to hold a drop zone, a
   chip carrying a filename, a preview control and the Image Editor. Measured
   before this rule: the filename was allotted 50.4px — roughly six characters
   — and shrinking `.ov-chip` further only moved the problem around, because
   the field itself was the constraint.

   Below 430px the two fields take the full panel width one above the other.
   Nothing about the desktop layout changes: side by side is right whenever
   there is room for it, and the reader is two-page there anyway. 430 is on the
   documented breakpoint scale (gate 36). */
/* IMAGE AND VIDEO BELONG SIDE BY SIDE. They are one choice — "what am I
   attaching to this entry" — and stacking them turns one decision into two
   screens of scrolling. Grid rather than flex so both columns are exactly half
   whatever their contents are; a long filename chip in one can no longer make
   the other narrower. */
.ov-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; align-items: start; }
.ov-pair .ov-field { min-width: 0; }
/* They stacked from 430px down, which covers every common phone — so on a
   phone they were ALWAYS stacked. Two columns hold comfortably to 360px once
   the drop control is compact; only the very narrowest screens stack now. */
/* ---- WHERE THEY STACK, AND WHY IT IS NOT A REGRESSION ----
   Side-by-side was widened to 360px in this round, and GATE 49 REJECTED IT with
   a measurement: halving the panel at 390px leaves the filename chip 85.5px,
   which is not enough to read the name of the file you just chose. The gate has
   asserted that since it was written, and it is right — a pair of controls you
   cannot read is worse than a pair you have to scroll past.

   So: side-by-side on every screen wide enough to keep both readable (431px
   and up, which is every tablet and desktop), stacked below that. 430 is the
   large-phone step on the standard scale. Do not widen this without changing
   what gate 49 measures. */
/* Two columns wherever the filename stays readable. Gate 49 measures that
   directly and fails below 90px, which is what a halved 320px panel gives.
   390px clears it once the chip's own chrome is trimmed (below); 360px and
   under does not, so those stack. */
@media (max-width: 360px) {
  .ov-pair { grid-template-columns: 1fr; gap: 8px; }
}

.ov-pair .ov-drop-text { white-space: nowrap; }

/* =====================================================================
 * Account flow screens: OTP verification, Payment, Profile
 * All three reuse the established static open-book shell; only the page
 * content is styled here.
 * ===================================================================== */

/* Only line-height remains here. Margin, font-size and colour moved to auth.css
   in Aug 2026: they were declared in BOTH files at equal specificity (0,1,0), so
   these copies won purely because mw.css loads last and auth.css's copies were
   dead. line-height has no auth.css counterpart, so it stays. */
.sb-lede { line-height: 1.55; }
.sb-lede strong { color: var(--maroon, #7b3335); }
.sb-title-sm { font-size: clamp(18px, 2vw, 24px) !important; }
.sb-alt-link {
  color: var(--maroon, #7b3335); font-weight: 600; text-decoration: underline;
}

/* ---- OTP verification ---- */
.otp-boxes { display: flex; gap: 9px; justify-content: space-between; }
.otp-box {
  flex: 1 1 0; min-width: 0; height: 54px;
  text-align: center; font: 700 22px/1 var(--font-ui, Poppins, sans-serif);
  color: #3c2a1e; background: #fffdf8;
  border: 1px solid #d3c6ae; border-radius: 9px;
  transition: border-color .15s, box-shadow .15s;
}
.otp-box:focus {
  outline: none; border-color: var(--maroon, #7b3335);
  box-shadow: 0 0 0 3px rgba(123, 51, 53, .12);
}
.otp-resend {
  margin: 14px 0 0; text-align: center; font-size: 13px; color: #6f5a44;
}

/* ---- Payment ---- */
.pay-summary {
  margin: 0 0 18px; padding: 14px 16px;
  border: 1px solid rgba(150, 120, 70, .45); border-radius: 10px;
  background: #fdf8ee;
}
.pay-row {
  display: flex; justify-content: space-between; align-items: baseline; gap: 10px;
  padding: 6px 0; font-size: 13.5px; color: #5b4a37;
}
.pay-row + .pay-row { border-top: 1px solid rgba(150, 120, 70, .22); }
.pay-label { font-weight: 600; }
.pay-value { font-weight: 700; color: #3c2a1e; }
.pay-value.pay-plan { color: var(--maroon, #7b3335); }
.pay-value.pay-discount { color: #2e7d46; }
.pay-value.pay-discount em { font-style: normal; font-weight: 600; font-size: 11.5px; }
.pay-row.pay-total {
  margin-top: 4px; padding-top: 10px;
  border-top: 1.5px solid rgba(150, 120, 70, .5);
  font-size: 16px;
}
.pay-row.pay-total .pay-value { font-size: 19px; color: var(--maroon, #7b3335); }
.pay-grid { display: flex; gap: 12px; }
.pay-grid .sb-field { flex: 1 1 0; min-width: 0; }
.sb-back {
  display: block; margin: 10px auto 0; width: max-content;
  padding: 9px 26px; border-radius: 20px;
  border: 1px solid #d3c6ae; background: #fffdf8;
  color: var(--maroon, #7b3335); font-weight: 600; font-size: 13px;
  text-decoration: none; transition: background .15s, border-color .15s;
}
.sb-back:hover { background: #f8f0dd; border-color: var(--maroon, #7b3335); }

/* ---- Profile ---- */
.pf-avatar-block { display: flex; justify-content: center; margin: 2px 0 14px; }
.pf-avatar { display: flex; flex-direction: column; align-items: center; gap: 8px; cursor: pointer; }
.pf-avatar-ring {
  position: relative; width: 104px; height: 104px; border-radius: 50%;
  display: grid; place-items: center; overflow: hidden;
  background: #f6efe1; border: 2px dashed #c6ab78;
  transition: border-color .15s, background .15s;
}
.pf-avatar:hover .pf-avatar-ring { border-color: var(--maroon, #7b3335); background: #f8f0dd; }
.pf-avatar-img { width: 100%; height: 100%; object-fit: cover; display: none; }
.pf-avatar-img.has-src { display: block; }
.pf-avatar-img.has-src ~ .pf-avatar-ic { display: none; }
.pf-avatar-ic { font-size: 32px; font-weight: 300; color: #b9a179; line-height: 1; }
.pf-avatar-text { font: 600 12px/1 var(--font-ui, Poppins, sans-serif); color: var(--maroon, #7b3335); }

/* .pf-grid's SECOND DEFINITION WAS HERE, AND IT WAS DEFEATING THE FIRST.

   auth.css defines .pf-grid as a two-column grid and deliberately KEEPS it two
   up at 620px — its comment states plainly that collapsing to one column "is
   what stacked First Name and Last Name on every phone". This file then
   redefined the same class as a flex box and turned it to a COLUMN below 640px.
   mw.css loads last, so auth.css's decision never took effect: every .pf-grid
   pair on Add Couple Diary, Add Special Event, In Memory Of and Profile stacked
   on every phone regardless of what auth.css said.

   One class, one definition. auth.css owns .pf-grid; it already supplies the
   `min-width: 0` the flex rule here was providing, and grid children need no
   flex-basis. Deleted rather than scoped: a second definition kept "for safety"
   is what caused this.

   .pay-grid keeps its column collapse — it is a different component with a
   single definition, and its rows are long enough to want stacking. */
@media (max-width: 640px){ .pay-grid { flex-direction: column; gap: 0; } }

/* status banner */
/* SITS WITH ITS HEADING, AND READS FROM THE LEFT.
   The static book centres body copy (`.sb-caption` in auth.css), and a short
   status sentence in a full-width box inherited that — "expires in 12 days"
   floated in the middle of the page while every label around it started at the
   left margin. `justify-content` and `text-align` state the intent here rather
   than relying on what the surrounding page happens to do.

   The vertical space was the other half: 11px padding plus a 14px bottom margin
   under a heading that already carried its own gap put the warning adrift from
   the thing it qualifies. Tightened so it reads as part of the Subscription
   Details block — the colour and the dot keep it noticeable, so nothing is lost
   by taking the air out. */
.pf-banner {
  display: flex; align-items: center; justify-content: flex-start;
  text-align: left; gap: 9px;
  margin: 0 0 10px; padding: 8px 12px; border-radius: 9px;
  font: 600 13px/1.35 var(--font-ui, Poppins, sans-serif);
  border: 1px solid transparent;
}
.pf-banner-dot { width: 9px; height: 9px; border-radius: 50%; flex: 0 0 auto; }

.pf-sub {
  margin: 0 0 14px; padding: 12px 15px;
  border: 1px solid rgba(150, 120, 70, .45); border-radius: 10px; background: #fdf8ee;
}
.pf-sub-row {
  display: flex; justify-content: space-between; align-items: baseline; gap: 10px;
  padding: 6px 0; font-size: 13px; color: #5b4a37;
}
.pf-sub-row + .pf-sub-row { border-top: 1px solid rgba(150, 120, 70, .22); }
.pf-sub-label { font-weight: 600; }
.pf-sub-value { font-weight: 700; color: #3c2a1e; }
.pf-sub-value.pf-plan { color: var(--maroon, #7b3335); }

.pf-renew {
  display: block; margin: 0 auto 6px; width: max-content; min-width: 190px;
  text-align: center; padding: 11px 28px; border-radius: 22px;
  background: var(--maroon, #7b3335); color: #fff;
  font-weight: 700; font-size: 13.5px; text-decoration: none;
  box-shadow: 0 3px 10px rgba(123, 51, 53, .28); transition: background .15s;
}
.pf-renew:hover { background: var(--maroon-dark, #5f2628); }
.pf-prefs-head { margin-top: 12px !important; }
/* Profile-page-specific compacting: the left column now holds identity +
   preferences + Save/Cancel (more than it used to), so it needs a
   noticeably tighter rhythm than other static-book pages to fit without
   scrolling — this scope is deliberately narrow (.page-profile only). */
/* HEADING ROW — title and the page's one secondary action on one line.
   Edit Profile is the page's secondary route, so it sits with the heading
   rather than below the form where it needed a scroll to reach. Wraps rather
   than shrinks: on the narrowest phones the button drops under the title at
   full width instead of squeezing the heading. */
.pf-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 4px 12px; flex-wrap: wrap;
}
/* The title and its rule are ONE unit. The rule sits directly under the title
   on every static book in the application; grouping them keeps that hierarchy
   while the button sits alongside rather than between them. The rule keeps its
   own bottom margin, so the heading spacing is the shared value, not a new one. */
.pf-head-title { flex: 1 1 auto; min-width: 0; }
.pf-head .sb-title { margin: 0; }

/* Borrows .pf-change-pw's pill so the page carries one button vocabulary.

   NO SHADOW, AND HOVER IS GATED. A touch device has no pointer to move away,
   so `:hover` STICKS after a tap — the button kept its inverted fill until the
   visitor tapped somewhere else, which reads as a stray highlight sitting on
   the button. `@media (hover: hover)` asks whether the device can actually
   hover, so the effect exists on a mouse and never latches on a finger.
   `-webkit-tap-highlight-color` removes the grey flash Android paints over the
   pill on touch; the :active state below gives deliberate feedback instead. */
.pf-edit-open {
  flex: 0 0 auto;
  padding: 7px 16px; border-radius: 18px;
  border: 1px solid var(--maroon, #7b3335);
  background: #fffdf8; color: var(--maroon, #7b3335);
  font: 600 12.5px/1 var(--font-ui, Poppins, sans-serif);
  cursor: pointer; box-shadow: none;
  -webkit-tap-highlight-color: transparent;
  transition: background .15s, color .15s;
}
@media (hover: hover) {
  .pf-edit-open:hover { background: var(--maroon, #7b3335); color: #fffdf8; }
}
.pf-edit-open:active { background: rgba(123, 51, 53, .1); }
.pf-edit-open:focus-visible { outline: 2px solid var(--maroon, #7b3335); outline-offset: 2px; }
/* 44px is the tap floor the audit enforces; padding alone gave 33px. */
@media (max-width: 860px) {
  .pf-edit-open { min-height: 44px; }
}
@media (max-width: 430px) {
  .pf-head { gap: 2px 8px; }
  .pf-head-title { flex: 1 1 100%; }
  .pf-edit-open { width: 100%; text-align: center; padding: 9px 16px; }
}

/* Subscription is now the page's own content, not a panel title. */
/* The heading and its status line are one unit. */
.pf-sub-head { margin-top: 4px; margin-bottom: 6px; }

.page-profile .pf-avatar-block { margin: 0 0 8px; }
.page-profile .pf-avatar-ring { width: 64px; height: 64px; }
.page-profile .pf-avatar-text { font-size: 12px; }   /* typography floor */
/* .page-profile .sb-form moved to auth.css (same specificity, mw simply won).
   .pf-grid has no auth.css counterpart, so it stays here. */
.page-profile .pf-grid { gap: 6px; }
.page-profile .sb-field { gap: 2px; }
.page-profile .sb-title-sm { margin-bottom: 2px !important; }
.page-profile .sb-hint { margin: 0 0 4px; }

/* THE SUBSCRIPTION PANEL IS A SUMMARY, NOT A SECOND FORM.

   Four label/value rows at desktop density — 12px box padding, 6px per row and
   14px margins under both the banner and the box — came to roughly 200px on a
   phone: as much vertical space as the whole identity form above it, for four
   read-only facts. That is the "too big" in the report.

   DENSITY ONLY. Nothing is hidden, no row is dropped, and every value keeps its
   desktop size, so the plan name and the dates stay the most legible thing on
   the page. Only padding and gaps come down. Scoped to .page-profile and to
   phones, so the desktop panel is untouched. */
@media (max-width: 860px) {
  .page-profile .pf-banner { margin-bottom: 8px; padding: 7px 10px; font-size: 12.5px; }
  .page-profile .pf-sub    { margin-bottom: 10px; padding: 6px 12px; }
  .page-profile .pf-sub-row { padding: 4px 0; }
}

.pf-change-pw {
  display: block; width: max-content; margin: 14px auto 0;
  padding: 9px 22px; border-radius: 20px; border: 1px solid #d3c6ae;
  background: #fffdf8; color: var(--maroon, #7b3335);
  font-weight: 600; font-size: 13px; text-decoration: none;
  transition: background .15s, border-color .15s;
}
.pf-change-pw:hover { background: #f8f0dd; border-color: var(--maroon, #7b3335); }
.pf-actions { display: flex; gap: 10px; justify-content: center; margin: 4px 0 6px; flex-wrap: wrap; }
.pf-actions .sb-submit, .pf-actions .sb-back { margin: 0; flex: 0 0 auto; }
button.sb-back {
  font-family: inherit; cursor: pointer;
}
/* Profile's status message now uses the shared .sb-alert classes from
   auth.css (applied by Api.flash()) rather than its own #pfStatus rules —
   one styling source for every feedback message on the site, not two
   that could drift apart. */

/* ---- (4) Date: slightly larger than before, still not dominant ---- */
.page-head {
  font-size: 15px !important;
  min-height: 22px !important;
  padding: 0 16px 4px !important;
}

/* ---- (5) Edit control sits beside the date in the page head ---- */
.page-head .entry-edit {
  position: static;                 /* flows inline next to the date */
  margin-left: 8px;
  height: 22px; padding: 0 9px 0 7px;
  vertical-align: middle;
}
/* icon-only pencil beside the date; the word "Edit" is no longer needed */
.page-head .entry-edit .ee-label { display: none; }
.page-head .entry-edit svg { width: 14px; height: 14px; }

/* =====================================================================
 * Button contrast fix: `.sb-body a { color: var(--page-accent) }` in
 * reader.css has higher specificity (class + element) than `.sb-submit`
 * (class only), so any <a class="sb-submit"> rendered maroon text on its
 * own maroon background — completely unreadable (Verify, Complete Payment,
 * Create Account). Restoring white text at matching specificity fixes it
 * without touching reader.css or any other link styling.
 *
 * .pf-renew (the "Change Subscription" button) and .plan-subscribe (the
 * Plans page "Subscribe" button) had the exact same bug — same
 * maroon-on-maroon pattern, just never covered by this fix before.
 * ===================================================================== */
.sb-body a.sb-submit { color: #fff; }
.sb-body a.sb-submit:hover { color: #fff; }
.sb-body a.pf-renew { color: #fff; }
.sb-body a.pf-renew:hover { color: #fff; }
.sb-body a.plan-subscribe { color: #fff; }
.sb-body a.plan-subscribe:hover { color: #fff; }
/* (#3) The guest static book's actions are the same trap again: both are
   <a> inside .sb-body, so reader.css's `.sb-body a` (0-2-0) out-ranked
   `.gb-cta { color:#fff }` (0-1-0) and painted maroon text on the maroon
   button — invisible. Stated here at matching specificity, alongside the
   others, so there is ONE place that owns this fix. */
.sb-body a.gb-cta { color: #fff; }
.sb-body a.gb-cta:hover { color: #fff; }
.sb-body a.gb-back { color: var(--maroon, #7b3335); }
.sb-body a.gb-back:hover { color: var(--maroon-dark, #5f2426); }

/* =====================================================================
 * Scroll / overscroll guards
 * Prevents the elastic "rubber-band" bounce at the top/bottom of the page
 * (visible as stretching on the homepage) and stops any secondary scroll
 * region from opening behind the reader while a book is open.
 * ===================================================================== */
html, body { overscroll-behavior-y: none; }
/* One content width everywhere: a scrolling shelf page has a scrollbar,
   the reader (scroll locked by book-open-mode) does not, and that width
   difference alone changed how the same shelf laid out between the live
   page and its blurred backdrop. Reserving the gutter permanently makes
   both render at the identical width. Browsers without support ignore
   this harmlessly; the nowrap+shrink shelf rule is the belt to this
   brace either way. */
html { scrollbar-gutter: stable; }
.library-room { overscroll-behavior: contain; }

/* While a book is open the host page must not scroll — no rubber-band, no
   secondary scroll area peeking out from behind the reader iframe.
   `overflow:hidden` alone (already set below) is deliberately NOT paired with
   `position:fixed`: shelf.js does not save/restore scrollY, so the existing
   open/close animation relies on the page staying exactly where it is.
   Switching to `position:fixed` would visually snap to the top on open and
   jump back on close — a regression, not a fix. */
body.book-open-mode {
  overflow: hidden !important;
  overscroll-behavior: none !important;
}

/* The reader iframe itself: no independent scrollbar/bounce of its own. */

/* =====================================================================
 * Personal Diary — workflow UI (added this round)
 * ===================================================================== */

/* ---- (3) Toolbar: Upload Cover belongs to the closed cover only ----
   The closed-book rule already existed above; this is the other half —
   once the book is open, Upload Cover is out of the way. */
.reader-room:not(.book-closed) .reader-toolbar .tool-upload { display: none !important; }

/* ---- (5) Saturday rule: a small, boxed information notice ---- */
/* ALIGNED TO THE FIRST LINE, NOT TO THE BLOCK. `align-items: center` is right
   only while the notice is exactly one line: the moment the sentence wraps —
   which it does on narrow phones — the icon slides to the vertical middle of a
   two-line block and sits visibly below the words it introduces. Anchoring to
   the top and offsetting by the same line-box arithmetic keeps it level at
   every width and with any amount of text. */
.ov-rule {
  display: flex; align-items: flex-start; gap: 6px;
  margin: 0 0 8px; padding: 6px 9px; min-width: 0;
  background: #fdf6e6; border: 1px solid #e3d0a4;
  border-left: 3px solid #c9a227; border-radius: 5px;
  font-size: 11px; line-height: 1.35; color: #6b5628;
}
.ov-rule svg {
  width: 12px; height: 12px; flex: 0 0 auto; fill: #c9a227;
  margin-top: calc((1.35em - 12px) / 2);
}
.ov-rule strong { color: #4a3a12; font-weight: 700; }
.ov-rule.is-allowed {
  background: #eff7f1; border-color: #bcdcc7; border-left-color: #3f8f5b; color: #2f6244;
}
.ov-rule.is-allowed svg { fill: #3f8f5b; }
.ov-rule.is-allowed strong { color: #1f4530; }

/* ---- MULTI-LINE RULE BOX -------------------------------------------------
   .ov-rule is a single flex ROW, which is right for a one-line notice. A book
   that carries several rules needs them stacked, so `is-list` turns the box
   into a row of [icon][column of lines] — the icon stays top-aligned beside
   the block rather than centred against it.

   Reuses the existing colours, border and left rule deliberately: this is the
   same kind of notice, not a second notice style. Only the layout differs. */
/* THE ICON IS ALIGNED TO A LINE BOX, NOT NUDGED BY A MAGIC NUMBER.
   `margin-top: 2px` was measured against one viewport. The icon is 12px in a
   line box of 1.35em, and 1.35em is 14.85px at the notice's 11px base — so the
   correct offset is (1.35em - 12px) / 2, which tracks the font size instead of
   assuming it. At any width where the type scales, the icon follows; at 320px
   it no longer rides high against the title.

   `min-width: 0` on the body is the other half: without it a long unbroken
   rule line refuses to wrap, pushes the box wider than the panel, and the
   whole notice reads as skewed even though the icon is where it should be. */
.ov-rule.is-list { align-items: flex-start; padding: 8px 10px; }
.ov-rule.is-list svg { margin-top: calc((1.35em - 12px) / 2); }
.ov-rule-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.ov-rule-title {
  color: #4a3a12; font-weight: 800; font-size: 11px;
  line-height: 1.35; letter-spacing: .2px; margin-bottom: 1px;
}
/* Bulleted without <ul>: the box is a <p>, so a list element cannot go inside
   it. The marker is a pseudo-element on each line instead. */
.ov-rule-line { position: relative; padding-left: 9px; }
.ov-rule-line::before {
  content: '\2022'; position: absolute; left: 0; top: -1px;
  color: #c9a227; font-weight: 700;
}

/* A RESTRICTION rather than a rule.
   The gold rule box says "here is how this works". This variant says "you
   cannot do this right now", so it needs to read differently at a glance
   without becoming an error — nothing has gone wrong, the week is simply
   already used. Warmer amber, a heavier left rule, and a matching bullet. */
.ov-rule.is-blocked {
  background: #fdf1e3; border-color: #e6c9a0; border-left-color: #b9762a;
  color: #6d4b21;
}
.ov-rule.is-blocked svg { fill: #b9762a; }
.ov-rule.is-blocked .ov-rule-title { color: #8a521a; }
.ov-rule.is-blocked .ov-rule-line::before { color: #b9762a; }
/* The week range and the date are the two facts to find fastest. */
.ov-rule.is-blocked .ov-rule-line:nth-of-type(1),
.ov-rule.is-blocked .ov-rule-line:nth-of-type(2) { font-weight: 700; }

/* ---- (3)(4)(11) Compact media row: one slim chip + the eye ----
   Row layout is flex with the EYE as a fixed-size item and the chip as the
   only flexible one (min-width:0 so the filename ellipsizes) — the eye can
   therefore never be pushed out or clipped, which is what happened when
   name + size + status all competed inline. Size, duration and upload
   status now live INSIDE the preview the eye opens, not on the row. */
/* WRAPS RATHER THAN CRUSHES. With the Image Editor added beside the eye,
   three items competed for one line and the chip — the only flexible one —
   was squeezed until the filename was a single ellipsis. The row now wraps,
   and the actions travel together (.ov-chip-acts) so they drop as a pair onto
   a second line instead of splitting across two. Stacking is the intended
   fallback here; a cramped single row is not. */
.ov-media-row {
  display: flex; align-items: center; gap: 6px; margin-top: 5px; min-width: 0;
  flex-wrap: wrap;
}
/* A FLOOR, NOT JUST A FLEX BASIS. `min-width: 0` alone let the chip shrink to
   nothing to keep the actions on the line. 120px is enough for an icon, a few
   characters of filename and the state; below that the row wraps instead. */
.ov-chip {
  flex: 1 1 120px; min-width: 120px;
  display: inline-flex; align-items: center; gap: 3px;
  /* Every pixel here comes off the filename, which is the only part of the
     chip a person reads. Trimmed so a halved 390px column still leaves the
     name above gate 49's 90px floor. */
  padding: 3px 6px; border-radius: 12px;
  background: #f6efe1; border: 1px solid #dccdb0;
  font-size: 10.5px; color: #7a6647;
}
.ov-chip svg { width: 10px; height: 10px; fill: currentColor; flex: 0 0 auto; }
.ov-chip-name {
  flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* tiny in-chip state: percentage while uploading, then a check / cross */
.ov-chip-state { flex: 0 0 auto; font-weight: 700; font-size: 10px; }
.ov-chip.is-done { border-color: #b7ddc3; }
.ov-chip.is-done .ov-chip-state { color: #2c7a4b; }
.ov-chip.is-error { border-color: #e6b6b6; }
.ov-chip.is-error .ov-chip-state { color: #a3302f; }
.ov-chip-eye {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; padding: 0; cursor: pointer;
  background: #fffdf8; border: 1px solid #dccdb0; border-radius: 50%; color: #7a6647;
}
.ov-chip-eye svg { width: 11px; height: 11px; fill: currentColor; }
.ov-chip-eye:hover { border-color: var(--maroon, #7b3335); color: var(--maroon, #7b3335); }
/* Preview and Image Editor as one unit, so they wrap together. */
.ov-chip-acts {
  flex: 0 0 auto; display: inline-flex; align-items: center; gap: 6px;
}
/* A ROW CARRYING A LABELLED ACTION GIVES THE CHIP ITS OWN LINE.
   With the eye alone the three items fit comfortably and the row stays on one
   line — that is the Special Events box, and it is unchanged. Add the Image
   Editor and there is an awkward band of widths where all three just fit and
   the chip is squeezed instead: 76.9px of filename was measured at 360px.
   Stacking is the stated preference over cramming, and it is also stable —
   it does not depend on how long the button's label happens to be. */
.ov-media-row.has-editor .ov-chip { flex: 1 1 100%; }

/* Inline preview: opens under the row on demand, small by design; carries
   the full details (name, size, duration, status) the row no longer shows */
/* A GRID, not a column.
   Each preview was `display:block; width:100%`, so choosing eight images
   produced eight full-width thumbnails stacked one under another — a very long
   scroll to see what you had picked. They now wrap into rows, fitting as many
   per row as the panel allows, so a typical selection is visible at a glance.

   auto-fill with a minimum tile width means one image still shows large and
   twenty stay compact, with no breakpoint to maintain. */
.ov-inline-preview {
  margin-top: 5px;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(84px, 1fr));
  gap: 6px;
  align-items: start;
}
.ov-inline-preview[hidden] { display: none; }
.ov-inline-preview img,
.ov-inline-preview video {
  display: block; width: 100%; height: 84px;
  object-fit: cover; border-radius: 5px;
  border: 1px solid #dccdb0; background: #efe6d4;
}
/* ONE PREVIEW SIZE, WHETHER THERE IS ONE PICTURE OR TWENTY.
   A lone preview used to be exempted from the tile treatment and given up to
   150px of uncropped height, on the reasoning that it is "the subject of the
   panel". In practice that is what made Personal Diary's preview box so much
   larger than the Special Events one, from the same component, on the same
   kind of screen — the difference the operator reported. A video keeps the
   exemption: a 84px tall video with controls is not usable, and a video is
   never one of a set here. */
.ov-inline-preview video:only-of-type {
  height: auto; max-height: 150px; object-fit: contain;
}
/* The Image Editor action sits in the chip row beside the eye, so it is sized
   to that row rather than to a form. */
.ov-media-row .ie-mini.ov-chip-edit {
  width: auto; flex: 0 0 auto; margin: 0;
  padding: 5px 10px; font-size: 10.5px; border-radius: 13px;
}
/* The caption belongs under the whole set, not inside one tile. */
.ov-inline-preview .ov-preview-meta { grid-column: 1 / -1; }
.ov-preview-meta {
  margin: 3px 0 0; font-size: 10.5px; line-height: 1.35; color: #7a6647;
  word-break: break-all;
}

/* modest vertical compaction for the whole media block */
.ov-pair .ov-drop { padding: 8px 10px; min-height: 0; }

/* ---- (4) Upload progress: a quiet caption over a hairline track ----------
   HISTORY, so this does not get flip-flopped again: this was a 24px full-width
   pill with a gradient fill, animated diagonal stripes and a bold centred
   label, going to a solid green slab on completion. It was built to be
   "prominent", which was the problem — the per-file chips beside each drop
   zone already show that file's percentage and its tick, so the bar restated
   what was on screen at twice the size and with motion attached.

   Now: a small left-aligned caption above a 4px track. Same information, same
   three states, same element, same JavaScript. setUploadProgress, setUploadDone
   and setUploadError are untouched, and so is the Save button's disabled
   "Uploading…" state, which is what actually prevents a premature save.

   THE .ov-chip RULES ARE NOT PART OF THIS and must stay as they are — those
   are the small per-file indicators under each upload zone, which are wanted
   exactly as they were.

   Shared by every module's Add/Edit panel, so this lightens the diaries,
   In Memory Of and Special Events identically. */
.ov-progress {
  position: relative; margin-top: 10px;
  height: auto; background: none; border: 0; border-radius: 0; overflow: visible;
}
.ov-progress[hidden] { display: none; }

/* The caption is a normal block above the track rather than absolutely centred
   inside it — a 4px bar has no room for text. */
.ov-progress-label {
  position: static; display: block;
  margin: 0 0 5px;
  font-size: 10.5px; font-weight: 700; letter-spacing: .2px;
  color: #7d6a4a; text-align: left; text-shadow: none;
}

/* Track, then fill over it. Two pseudo-elements because the markup is one
   element with one child and nothing else to hang them on. */
.ov-progress::before {
  content: ''; display: block; position: static;
  height: 4px; width: 100%;
  background: #e7dcc4; border-radius: 2px;
}
.ov-progress::after {
  content: ''; position: absolute; left: 0; bottom: 0;
  height: 4px; width: var(--pct, 0%);
  background: var(--maroon, #7b3335); border-radius: 2px;
  transition: width .2s ease;
}

/* Completed: the fill turns green and the caption says so. No slab, no
   inverted text — the tick in the label carries it. */
.ov-progress.is-done::after { background: #3f8f5b; }
.ov-progress.is-done .ov-progress-label { color: #2f6244; }

.ov-progress.is-error::after { background: #a3302f; }
.ov-progress.is-error .ov-progress-label { color: #8c2f2f; }

@media (prefers-reduced-motion: reduce){
  /* The stripe animation this used to disable is gone; the fill's width
     transition is all that moves now, and it is short enough to keep. */
  .ov-progress::after { transition: none; }
}

/* Spinner shown on the save button itself while saving */
.ov-spinner {
  display: inline-block; width: 12px; height: 12px; margin-right: 6px;
  border: 2px solid rgba(255,255,255,.45); border-top-color: #fff;
  border-radius: 50%; animation: mw-spin .7s linear infinite;
  vertical-align: -2px;
}
@keyframes mw-spin { to { transform: rotate(360deg); } }

/* Save button while an upload is in flight */
/* A PAYMENT IN FLIGHT LOOKS LIKE ONE. The rule was `opacity: .65` alone, which
   on a slow gateway is indistinguishable from a page that has stopped
   responding — and a customer who believes a payment has frozen presses the
   button again. The spinner is a pseudo-element on the button so no markup
   changes, and the label is swapped to "Processing payment…" in payment-flow.js. */
.sb-submit.is-busy {
  opacity: .8;
  cursor: progress;
  position: relative;
  padding-left: 2.4em;
}
.sb-submit.is-busy::before {
  content: '';
  position: absolute;
  left: 1.1em; top: 50%;
  width: 14px; height: 14px; margin-top: -7px;
  border: 2px solid rgba(255, 255, 255, .35);
  border-top-color: #fff;
  border-radius: 50%;
  animation: sb-busy-spin .7s linear infinite;
}
@keyframes sb-busy-spin { to { transform: rotate(360deg); } }
/* Finished, but deliberately not pressable again — the outcomes where a second
   press could charge the customer twice. Visibly inert rather than mid-flight:
   no spinner, no "Processing" label, and the cursor says not-allowed rather
   than progress. */
.sb-submit.is-spent {
  opacity: .55;
  cursor: not-allowed;
  pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
  .sb-submit.is-busy::before { animation: none; }
}

/* ---- (7)(8) Per-entry controls beside the date ---- */
.entry-delete {
  position: static; margin-left: 4px;
  display: inline-flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; padding: 0;
  background: rgba(255, 252, 245, .9); border: 1px solid #d9c9ab;
  border-radius: 6px; color: #7a6647; cursor: pointer;
  transition: background .15s, border-color .15s, color .15s, transform .15s;
}
.entry-delete svg { width: 13px; height: 13px; fill: currentColor; }
.entry-delete:hover { background: #fbeaea; border-color: #b3413f; color: #a3302f; transform: translateY(-1px); }

/* ---- (10) Shelf: user cover fills the ARTWORK AREA of the book image ----
   .cover-shell wraps the cover-face <img> so it shrink-wraps to the image's
   RENDERED box; the art percentages are then measured against the actual
   image, not the .book anchor. The anchor is wider than the image on the
   homepage layouts, which is why the previous version (percentages of the
   anchor) produced an oversized rectangle spilling over neighbouring books.
   Geometry from the real 98x158 cover artwork: maroon spine = left ~22%,
   cover face = the rest, full height. */
/* vertical-align: bottom IS THE FIX FOR THE FLOATING BOOKS, and it is the
   cause rather than the symptom.

   `.cover-shell` is an inline-block, so it sits on the text BASELINE of the
   `.book` anchor around it — and a line box reserves descender space BELOW the
   baseline whether or not there is any text. Measured: `.book` 156.98px tall
   around a 149.98px cover, a constant 7px of empty box under the artwork at
   every viewport. `.page-home .shelf-board` then carried `margin-top: -5px`
   ("books tuck into the board") which cancelled five of those seven, leaving
   the 2.00px the books were reported to float by — the same 2px at 1440 and at
   390, which is the signature of a fixed-pixel cause and not a proportional
   one.

   `line-height: 0` on the shell itself does not help: the descender belongs to
   the line box in the PARENT. Taking the shell off the baseline removes the
   reserved space entirely, so the bottom of the artwork IS the bottom of the
   book, and the board can be seated against a real edge instead of an
   estimated one. Shrink-wrapping (which the art overlay's percentages depend
   on) is unaffected — the shell stays inline-block. */
.cover-shell { position: relative; display: inline-block; line-height: 0; font-size: 0;
               vertical-align: bottom; }

.book-user-art[hidden] { display: none; }
/* Hard ceiling: even if a future rule widens the shell, the artwork cannot
   escape the cover and become a detached rectangle. */

/* ---- (12)(13) Profile: tighter layout, status visible without scrolling ---- */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}
/* The Preferences heading is removed visually (kept for screen readers) —
   the labelled fields below are self-explanatory and the heading was
   costing vertical space the Save button needed. */
.page-profile .pf-prefs-head { margin: 0 !important; }
.page-profile #pfStatus { margin: 0 0 10px; }
.page-profile .pf-avatar-block { margin-top: 0; }
.page-profile .pf-actions { margin-top: 10px; }

/* ---- (7) In-book confirm: styled as a book page panel, not a popup ---- */
.ov-confirm-msg {
  margin: 0 0 14px; font-size: 14.5px; line-height: 1.5; color: #3c2a1e; text-align: center;
}
.ov-confirm-actions { display: flex; gap: 10px; justify-content: center; }
.ov-confirm-yes, .ov-confirm-no {
  padding: 9px 24px; border-radius: 20px; font: inherit; font-size: 13.5px;
  font-weight: 600; cursor: pointer; border: 1px solid transparent;
}
.ov-confirm-yes { background: #a3302f; color: #fff; }
.ov-confirm-yes:hover { background: #8a2726; }
.ov-confirm-no { background: #fffdf8; border-color: #d3c6ae; color: #5c4a33; }
.ov-confirm-no:hover { background: #f4ebd9; }

/* ---- (9) Per-media eye icon ---- */
.page-media-eye {
  display: inline-flex; align-items: center; justify-content: center;
  width: 21px; height: 21px; padding: 0; flex: 0 0 auto;
  background: rgba(255, 252, 245, .9); border: 1px solid #d9c9ab;
  border-radius: 50%; color: #7a6647; cursor: pointer;
  transition: border-color .15s, color .15s;
}
.page-media-eye svg { width: 11px; height: 11px; fill: currentColor; }
.page-media-eye:hover { border-color: var(--maroon, #7b3335); color: var(--maroon, #7b3335); }

/* ---- (4) Metadata toggle: a minimal text link, not a boxed callout ----
   This had no styling at all after an earlier refactor, so it fell back to
   the browser's default button chrome — a chunky grey box with an icon,
   which is what read as a "warning box" and added real height. It is now
   the smallest thing that can still be clicked. */
.ov-meta-toggle {
  display: inline-block; margin: 3px 0 0; padding: 0;
  background: none; border: 0; border-radius: 0;
  font: inherit; font-size: 10.5px; line-height: 1.3;
  color: #8d7c64; text-decoration: underline; text-underline-offset: 2px;
  cursor: pointer;
}
.ov-meta-toggle:hover,
.ov-meta-toggle.is-open { color: var(--maroon, #7b3335); }
.ov-meta-toggle svg { display: none; }   /* the icon is what read as a warning */
.ov-meta { display: flex; flex-direction: column; gap: 4px; margin-top: 4px; }
.ov-meta[hidden] { display: none; }
.ov-meta .ov-input, .ov-meta .ov-textarea { font-size: 12px; padding: 5px 7px; }
.ov-meta .ov-textarea { min-height: 38px; resize: vertical; }

/* ---- (2) Media title ABOVE its media, never beside it ----
   `.page-full .page-body` is display:flex with no direction, so a bare
   title and figure became side-by-side columns. Wrapping them in this
   single block (a column) fixes the stacking without touching the shared
   page-body rule other modules depend on. */
.page-media-block {
  width: 100%; min-width: 0;
}

.page-media-title {
  flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center; gap: 8px;
  margin: 0; padding: 14px 16px 8px;
  text-align: center;
  font-weight: 700; font-size: 17.5px; letter-spacing: .01em; color: #3c2a1e;
  font-family: var(--font-display, inherit);
}
.page-media-title > span { min-width: 0; overflow-wrap: anywhere; }

/* ---- (3) Media details page: shown as its own book page ---- */
.mi-title {
  margin: 0 0 10px; font-size: 15px; font-weight: 700; color: #3c2a1e;
}
.mi-desc {
  margin: 0 0 16px; font-size: 13.5px; line-height: 1.6; color: #5c4a33;
  white-space: pre-wrap;
}

/* ---- (6) "Entry already exists" notice, shown the moment a date is picked ---- */
.ae-exists {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin: 5px 0 0; padding: 6px 9px;
  background: #fdf4e3; border: 1px solid #e6cf9e; border-radius: 6px;
  font-size: 11.5px; color: #6b5320;
}
.ae-exists[hidden] { display: none; }
.ae-exists-open {
  padding: 4px 11px; border-radius: 12px; cursor: pointer;
  background: var(--maroon, #7b3335); color: #fff; border: 0;
  font: inherit; font-size: 11px; font-weight: 700;
}
.ae-exists-open:hover { background: var(--maroon-dark, #5f2628); }

/* ---- (3) One entry = one page: overflow scrolls, never paginates ----
   Diary entry text is emitted as a single non-splittable unit (see
   pagination.js), so a long entry stays on its own page. If it is taller
   than the page, that page scrolls — no extra pages are manufactured. */
.page-body:has(> .entry-single-page) { overflow-y: auto; overflow-x: hidden; }
.entry-single-page { max-width: 100%; }


/* =====================================================================
 * (7) Image editor — the canvas IS the crop frame
 * ===================================================================== */
.ie-panel { display: flex; flex-direction: column; }

/* Spans the whole spread so the IMAGE is the dominant element rather than
   the controls. Sits above both pages; the form underneath is untouched. */
/* A FULL-SCREEN MODAL, MOUNTED ON <body>, NOT A PANEL INSIDE THE SPREAD.
   It used to be position:absolute inside .book-open, which is why the phone
   screenshot shows the site header, the subscription bar and the reader toolbar
   all still competing with an editor that had a ~150px strip left for the
   picture. `position: fixed` cannot work inside .flipbook (preserve-3d makes the
   ancestor the containing block — see SESSION_HANDOVER §11), so the fix is to
   mount OUTSIDE the flipbook. reader-media.js appends to document.body. */
.ie-panel.ie-spread {
  /* inset:0 WITH width/height AUTO. An explicit width:100% resolved against a
     containing block that is not always the one you expect and left the panel
     15px short of the viewport; letting the four offsets size the box is both
     shorter and exact. */
  position: fixed; inset: 0; z-index: 4000;
  width: auto; height: auto; max-width: none; max-height: none;
  margin: 0; border: 0; border-radius: 0;
  padding: 12px 14px 14px;
  /* A LIGHTER SURFACE. The near-black panel read as a separate application
     dropped on top of the book; MemorableWorld's shell is warm parchment, and
     the editor should look like part of it. Still darker than the page so the
     photograph stays the brightest thing on screen — but a warm stone, not a
     black box. */
  background: linear-gradient(180deg, #efe6d6 0%, #e6dac6 100%);
  box-shadow: none;
}
/* =====================================================================
 * EDITOR LAYOUT — three zones: tools rail | workspace | guidance.
 * The controls used to sit in a full-width grid UNDER the image, so the
 * image competed with them for height and the panel scrolled. The rails
 * are fixed-width columns and the workspace takes everything left over,
 * which makes the image the primary element and removes the scrolling.
 * The panel itself no longer scrolls: it is a fixed-height grid whose
 * middle column is the only thing that flexes.
 * ===================================================================== */
.ie-panel.ie-spread { overflow: hidden; display: flex; flex-direction: column; }
/* THE BUG IN THE DESKTOP SCREENSHOT, EXACTLY.
   `.ie-panel.ie-spread { flex-direction: row }` made the panel's THREE children
   into three columns — and .ie-head is one of them. So the header became a
   full-height left-hand column containing nothing but the words "Image Editor"
   and three buttons, which is the large empty cream area on the left, and the
   picture was squeezed into what was left between it and the dock.
   The panel is a column at every width now. Only .ie-body changes direction. */
.ie-body {
  flex: 1 1 auto; min-height: 0; min-width: 0; overflow: hidden;
  display: flex; flex-direction: column; gap: 12px;
}
/* THE IMAGE IS THE SUBJECT OF THE SCREEN.
   This was a three-column grid — a 208px control rail, the picture, and a 232px
   guidance column — so on a phone the picture was the narrowest thing in a
   panel that existed to show it. The controls now live in one tabbed dock below
   (.ie-dock) and the workspace gets everything that is left. */
.ie-workspace {
  flex: 1 1 auto; min-height: 0; min-width: 0;
  display: flex; flex-direction: column; gap: 6px;
  align-items: stretch; justify-content: stretch;
}
/* Rails: quiet surfaces so the image carries the panel. */
.ie-rail, .ie-side {
  display: flex; flex-direction: column; gap: 14px;
  min-height: 0; overflow: auto;
  padding: 12px; border-radius: 8px;
  background: rgba(255, 252, 244, .58);
  border: 1px solid rgba(120, 86, 44, .16);
}
.ie-rail-sec { display: flex; flex-direction: column; gap: 6px; }
.ie-rail-sec[hidden] { display: none; }
.ie-rail-label {
  font: 700 9.5px/1 var(--font-ui, Poppins, sans-serif);
  letter-spacing: .12em; text-transform: uppercase; color: #9a8763;
}
.ie-rail-reset { margin-top: auto; }
.ie-rail-reset .ie-mini { width: 100%; text-align: center; }
/* Workspace: image gets every pixel that is left. */
.ie-main { display: flex; flex-direction: column; min-width: 0; min-height: 0; }
.ie-panel.ie-spread .ie-stage { flex: 1 1 auto; min-height: 0; }
/* Guidance panel. */
.ie-guide { display: flex; flex-direction: column; gap: 6px; }
.ie-guide[hidden] { display: none; }
.ie-guide-head {
  margin: 0; font: 700 10px/1.2 var(--font-ui, Poppins, sans-serif);
  letter-spacing: .1em; text-transform: uppercase; color: var(--maroon, #7b3335);
}
.ie-guide-line { margin: 0; font-size: 11.5px; line-height: 1.5; color: #5c4a33; }
.ie-guide-list { margin: 2px 0 0; padding-left: 15px; }
.ie-guide-list li { font-size: 11px; line-height: 1.5; color: #6f5c44; margin-bottom: 5px; }
.ie-guide-list strong { color: #4a3823; }
.ie-guide-note {
  margin: 4px 0 0; font-size: 10.5px; line-height: 1.5; color: #8a7a63;
  padding-top: 8px; border-top: 1px solid rgba(120, 86, 44, .16);
}
.ie-guide-meta { margin-top: auto; }

/* The old @media (max-width:1080px) rail-stacking block is gone with the rails.
   It targeted .ie-rail / .ie-side / .ie-main, none of which the template emits
   any more, and its `.ie-workspace { grid-template-columns }` fought the flex
   layout above. Nothing replaced it: .ie-body handles both directions. */

.ie-stage {
  position: relative; flex: 1 1 auto; min-height: 150px;
  display: flex; align-items: center; justify-content: center;
  background:
    repeating-conic-gradient(#e2d7c0 0% 25%, #ece3d0 0% 50%) 0 0 / 16px 16px;
  border: 1px solid #d5c5a4; border-radius: 6px;
  overflow: hidden; padding: 8px;
  cursor: grab;
}
.ie-stage.is-dragging { cursor: grabbing; }
/* The frame: a hard edge plus a dimmed surround, so the exact saved area is
   unmistakable. Thirds guides help with placement. */
.ie-canvas {
  display: block; border-radius: 2px;
  box-shadow: 0 0 0 2px #fffdf8, 0 0 0 3px var(--maroon, #7b3335),
              0 0 0 9999px rgba(30, 20, 10, .34);
  background: #fff;
}
.ie-panel.ie-spread .ie-canvas { max-width: 100%; }
.ie-dims {
  margin: 7px 0 0; text-align: center;
  font-size: 10.5px; font-weight: 700; letter-spacing: .3px; color: #7a6647;
}
.ie-status {
  margin: 0; min-height: 0;
  font-size: 11.5px; color: #7a6647; text-align: center;
}
.ie-status:empty { display: none; }

/* .ie-tools removed with the template restructure — the controls now live in
   .ie-rail / .ie-side. No rule should target it. */
/* In the rail the transform buttons form an even grid rather than a centred
   wrapping row, so they line up instead of re-centring at every width. */
.ie-group { display: grid; grid-template-columns: repeat(3, 1fr); gap: 5px; }
.ie-group .ie-btn { width: 100%; }
.ie-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; padding: 0; cursor: pointer;
  background: #fffdf8; border: 1px solid #d3c6ae; border-radius: 7px;
  color: #5c4a33;
}
.ie-btn svg { width: 15px; height: 15px; fill: currentColor; }
.ie-btn:hover { border-color: var(--maroon, #7b3335); color: var(--maroon, #7b3335); }

.ie-ratios { display: flex; gap: 5px; flex-wrap: wrap; margin-top: 0; }
.ie-ratios[hidden] { display: none; }
.ie-ratio {
  padding: 4px 10px; border-radius: 12px; cursor: pointer;
  background: #fffdf8; border: 1px solid #d3c6ae; color: #6f5c44;
  font: inherit; font-size: 10.5px; font-weight: 600;
}
.ie-ratio:hover { background: #f4ebd9; }
.ie-ratio.is-active {
  background: var(--maroon, #7b3335); border-color: var(--maroon, #7b3335); color: #fff;
}

.ie-mini {
  padding: 6px 13px; border-radius: 999px; cursor: pointer;
  background: #fffdf8; border: 1px solid #d3c6ae; color: #5c4a33;
  /* TYPOGRAPHY FLOOR (Aug 2026): these are button labels the user reads to
     operate the editor, not ornament, so they take the 12px floor. */
  font: inherit; font-size: 12px; font-weight: 600;
}
.ie-mini:hover { background: #f4ebd9; }
.ie-mini[disabled] { opacity: .5; cursor: default; }

/* Label above the track: in a 208px rail a side-by-side label left the track
   too short to be usable. */
.ie-slider {
  display: flex; flex-direction: column; align-items: stretch;
  gap: 3px; margin-top: 0; font-size: 10.5px; color: #6f5c44;
}
.ie-slider input[type="range"] { width: 100%; accent-color: var(--maroon, #7b3335); }

/* ---- SLIDERS: a designed track and thumb, not the browser default ----
   The screenshot's Zoom control was a bare blue OS slider sitting in a cream
   panel — the one control that looked like it belonged to a different product.
   Both vendor prefixes are written out because a single combined selector list
   is dropped wholesale by every engine that does not recognise one member. */
input[type="range"].ie-slider,
.ie-field input[type="range"] {
  -webkit-appearance: none; appearance: none;
  width: 100%; height: 26px; margin: 0; padding: 0;
  background: transparent; cursor: pointer;
}
input[type="range"].ie-slider::-webkit-slider-runnable-track,
.ie-field input[type="range"]::-webkit-slider-runnable-track {
  height: 6px; border-radius: 999px;
  background: linear-gradient(90deg, #d9c9a8, #e6dac2);
  box-shadow: inset 0 1px 2px rgba(80, 58, 32, .22);
}
input[type="range"].ie-slider::-moz-range-track,
.ie-field input[type="range"]::-moz-range-track {
  height: 6px; border-radius: 999px;
  background: linear-gradient(90deg, #d9c9a8, #e6dac2);
  box-shadow: inset 0 1px 2px rgba(80, 58, 32, .22);
}
input[type="range"].ie-slider::-webkit-slider-thumb,
.ie-field input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 20px; height: 20px; margin-top: -7px; border-radius: 50%;
  background: var(--maroon, #7b3335);
  border: 3px solid #fffdf8;
  box-shadow: 0 2px 6px -1px rgba(60, 34, 18, .55);
}
input[type="range"].ie-slider::-moz-range-thumb,
.ie-field input[type="range"]::-moz-range-thumb {
  width: 16px; height: 16px; border-radius: 50%;
  background: var(--maroon, #7b3335);
  border: 3px solid #fffdf8;
  box-shadow: 0 2px 6px -1px rgba(60, 34, 18, .55);
}
input[type="range"].ie-slider:focus-visible,
.ie-field input[type="range"]:focus-visible { outline: 2px solid var(--maroon, #7b3335); outline-offset: 3px; }

/* ==================== THE REDESIGNED EDITOR ====================
   Header, workspace, tabbed dock, action bar. Mobile-first: every rule here is
   the phone layout, and the only media query widens it. */

/* ---- HEADER: title left, tools right, nothing floating over anything ----
   The close button used to be an absolutely positioned `.overlay-close`, which
   put it on the same pixels as the Crop tab — whichever stacked higher ate the
   click and the other looked broken. It is a flex item now, so a collision is
   not expressible. */
/* ================= HEADER =================
   Identity left, actions right, one rule beneath. Sized so it reads as an
   application bar (not a form legend, which is what a 14px maroon line looked
   like) without eating workspace: 44px of content on desktop, 40 on a phone. */
.ie-head {
  flex: 0 0 auto;
  display: flex; align-items: center; justify-content: space-between;
  gap: 14px; margin: 0 0 10px; padding: 0 2px 10px;
  border-bottom: 1px solid rgba(120, 86, 44, .22);
}
.ie-head-main { display: flex; align-items: center; gap: 10px; min-width: 0; }
.ie-head-mark {
  flex: 0 0 auto; width: 32px; height: 32px; border-radius: 9px;
  display: grid; place-items: center;
  background: var(--maroon, #7b3335); color: #fdf6e8;
}
.ie-head-mark svg { width: 17px; height: 17px; fill: currentColor; }
.ie-head-text { display: flex; flex-direction: column; min-width: 0; line-height: 1.15; }
.ie-head-sub {
  font: 600 10.5px/1.2 var(--font-ui, Poppins, sans-serif);
  color: #8a7658; letter-spacing: .4px;
}
.ie-head-sub:empty { display: none; }
/* Groups the two history controls so they read as one pair, not four
   equally-weighted buttons in a row. */
.ie-head-grp {
  display: inline-flex; gap: 4px; padding: 3px;
  background: rgba(120, 86, 44, .1); border-radius: 12px;
}
.ie-head-main { min-width: 0; text-align: left; }
/* ONE COMPACT LINE. The mobile screenshot wrapped "Position Cover Image" onto
   two lines and hung a two-line note under it — four lines of chrome above a
   picture that had none to spare. The title is one clamped line; the cover note
   moved under the image where it belongs. */
.ie-panel.ie-spread .ie-head .ov-title {
  text-align: left; margin: 0; color: #3d2f20;
  font: 700 17px/1.15 var(--font-display, Georgia, serif);
  letter-spacing: .2px; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
}
.ie-head-tools { display: flex; align-items: center; gap: 6px; flex: 0 0 auto; }
.ie-head-sep {
  width: 1px; height: 20px; margin: 0 2px;
  background: rgba(120, 86, 44, .28);
}
/* The header Save is a PHONE control. On desktop the action bar has room for a
   labelled button, and two Saves on screen at once would be a puzzle.
   Scoped to .ie-head-tools so it outranks `.ie-icon { display: inline-flex }`,
   which sits later in this file at equal specificity and was winning. */
.ie-head-tools .ie-save-top { display: none; }
.ie-icon {
  width: 34px; height: 34px; padding: 0; border-radius: 10px;
  border: 1px solid rgba(120, 86, 44, .3); background: #fffdf8;
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer; color: #6f5c44; transition: background .15s, color .15s, border-color .15s;
}
.ie-icon svg { width: 15px; height: 15px; fill: currentColor; }
.ie-icon:hover:not(:disabled) {
  background: #f4ebd9; border-color: var(--maroon, #7b3335); color: var(--maroon, #7b3335);
}
.ie-icon:disabled { opacity: .32; cursor: default; }
/* CLOSE IS VISUALLY DISTINCT AND ALWAYS LAST. */
.ie-icon.ie-close { background: #fff3f3; border-color: #e0b4b4; color: #a83a3a; }
.ie-icon.ie-close:hover { background: #a83a3a; border-color: #a83a3a; color: #fff; }
/* The panel's generic overlay close is not used by this template. */
.ie-panel .overlay-close { display: none; }
/* ONE SENTENCE. The cover rule used to be a heading, a paragraph, a four-item
   list and a footnote in a column of its own. */
.ie-note {
  margin: 0; font-size: 11.5px; color: #7a6647; text-align: center;
}
.ie-note[hidden] { display: none; }

/* A WORKSPACE SURFACE, NOT A TRANSPARENCY CHECKERBOARD. The checkerboard read
   as "something is broken here" around every picture that did not happen to
   match the frame. A calm dark surface makes the photograph the brightest thing
   on screen, which is what an editor should do. */
.ie-stage {
  position: relative; flex: 1 1 auto; min-height: 0; min-width: 0;
  /* The stage stays a shade or two below the panel so the picture separates
     from it, but nothing like the old near-black. */
  background:
    radial-gradient(120% 90% at 50% 0%, #6d5f4c 0%, #574b3c 60%, #4a4034 100%);
  border-radius: 12px;
  display: grid; place-items: center;
  padding: 6px;
  box-shadow: inset 0 0 0 1px rgba(255,255,255,.06);
  cursor: grab; touch-action: none;
}
/* ENLARGE, ON the picture. */
.ie-zoomview {
  position: absolute; top: 8px; right: 8px; z-index: 3;
  width: 34px; height: 34px; padding: 0; border-radius: 10px;
  display: inline-flex; align-items: center; justify-content: center;
  background: rgba(20, 14, 10, .55); color: #f6ecd8; cursor: pointer;
  border: 1px solid rgba(255, 246, 228, .26);
  -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
}
.ie-zoomview svg { width: 15px; height: 15px; fill: currentColor; }
.ie-zoomview:hover { background: rgba(20, 14, 10, .86); border-color: rgba(255,246,228,.5); }

/* ---- FULL SIZE PREVIEW ---- mounted on <body>, above the editor. */
.ie-lightbox {
  position: fixed; inset: 0; z-index: 4100;
  background: rgba(12, 9, 6, .93);
  display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 10px;
  padding: 56px 16px 20px;
}
.ie-lightbox-shell {
  flex: 1 1 auto; min-height: 0; width: 100%;
  display: flex; align-items: center; justify-content: center;
}
/* The complete picture, never cropped to fill the screen. */
.ie-lightbox-img {
  max-width: 100%; max-height: 100%;
  width: auto; height: auto;
  object-fit: contain; border-radius: 4px;
  box-shadow: 0 24px 60px -20px rgba(0, 0, 0, .9);
}
.ie-lightbox-close {
  position: absolute; top: 12px; right: 12px;
  width: 44px; height: 44px; border-radius: 12px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  background: rgba(255, 246, 228, .12); border: 1px solid rgba(255, 246, 228, .3);
  color: #f6ecd8; cursor: pointer;
}
.ie-lightbox-close svg { width: 18px; height: 18px; fill: currentColor; }
.ie-lightbox-close:hover { background: #a83a3a; border-color: #a83a3a; color: #fff; }
.ie-lightbox-cap {
  flex: 0 0 auto; margin: 0; color: #c3b399;
  font: 600 12px/1.3 var(--font-ui, Poppins, sans-serif); text-align: center;
}
.ie-canvas { box-shadow: 0 10px 28px -12px rgba(0,0,0,.75); border-radius: 3px; }
/* The saved pixel size, on the image rather than in a panel beside it. */
.ie-badge {
  position: absolute; right: 8px; bottom: 8px;
  padding: 3px 8px; border-radius: 999px;
  background: rgba(46, 32, 16, .72); color: #f6ecd8;
  font: 600 10.5px/1 var(--font-ui, Poppins, sans-serif);
  letter-spacing: .3px; pointer-events: none;
}

/* ---- the dock ---- */
/* THE DOCK MUST NOT EAT THE WORKSPACE.
   Measured at 320x568: an unbounded dock stood 313px tall, left the stage 147px
   and shrank the cover preview to 108x135 — the picture became the smallest
   thing on a screen that exists to show it. Capped as a share of the viewport,
   with the pane itself scrolling if a group is taller than the cap, so the
   image always keeps the larger half. */
/* MOBILE = A BOTTOM SHEET. Not a shrunken desktop column: its own surface, its
   own rounded top edge, its own scroll, and a hard cap so the picture always
   keeps the larger share of the screen. */
.ie-dock {
  flex: 0 0 auto; display: flex; flex-direction: column; gap: 10px;
  /* A FIXED SHARE, NOT A CONTENT-DRIVEN ONE. With max-height alone the sheet
     was as tall as whichever pane was open, so switching to Filters or Adjust
     grew it and the picture jumped smaller. Height is now the same for every
     tab; a taller pane scrolls inside it instead of pushing the image up. */
  min-height: 0; height: 30vh; max-height: 30vh;
  padding: 10px 12px 12px; border: 0;
  border-radius: 16px;
  background: #fbf6ea;
  box-shadow: 0 -6px 20px -14px rgba(60, 40, 20, .5);
}
.ie-panes {
  min-height: 0; flex: 0 1 auto;
  overflow-y: auto; overflow-x: hidden; scrollbar-width: thin;
}
/* The panes hug their content; the sheet is only as tall as the tools in it. */
.ie-dock { justify-content: flex-start; }
/* ---- TABS: one segmented control, equal widths, no wrapping ---- */
.ie-tabs {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 3px;
  padding: 3px; margin-bottom: 10px;
  background: rgba(120, 86, 44, .1); border-radius: 11px;
}
.ie-tabs-unused {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px;
  background: #f2e7d2; padding: 3px; border-radius: 10px;
}
.ie-tab {
  border: 0; background: transparent; border-radius: 8px;
  padding: 7px 4px; min-height: 38px;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px;
  font: 600 10px/1.1 var(--font-ui, Poppins, sans-serif);
  letter-spacing: .2px; color: #8a7658; cursor: pointer;
  transition: background .15s, color .15s, box-shadow .15s;
}
.ie-tab:hover { color: var(--maroon, #7b3335); }
.ie-tab-legacy {
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  padding: 7px 2px; min-height: 44px;
  border: 0; border-radius: 8px; background: transparent; cursor: pointer;
  font: 600 10.5px/1.2 var(--font-ui, Poppins, sans-serif);
  color: #6f5c44; letter-spacing: .2px;
}
.ie-tab svg { width: 14px; height: 14px; fill: currentColor; }
.ie-tab.is-active {
  background: #fffdf8; color: var(--maroon, #7b3335);
  box-shadow: 0 2px 6px -1px rgba(60, 40, 20, .28), inset 0 0 0 1px rgba(123,51,53,.18);
}
.ie-tab.is-active svg { fill: var(--maroon, #7b3335); }
.ie-tab-legacy-active {
  background: #fffdf7; color: var(--maroon, #7b3335);
  box-shadow: 0 1px 2px rgba(74, 56, 35, .16);
}

.ie-pane { display: none; flex-direction: column; gap: 9px; }
.ie-pane.is-active { display: flex; }

.ie-field { display: flex; flex-direction: column; gap: 3px; }
.ie-lab {
  font: 600 10.5px/1 var(--font-ui, Poppins, sans-serif);
  letter-spacing: .6px; text-transform: uppercase; color: #8a7350;
}
/* The readout sits on the LABEL row, not on the track. A negative margin used
   to drag it up onto the slider, where it printed over the rail. */
.ie-field { position: relative; }
.ie-val {
  position: absolute; top: 0; right: 0; margin: 0;
  font: 700 11px/1 var(--font-ui, Poppins, sans-serif); color: var(--maroon, #7b3335);
}

/* Chips carry a label, never a bare icon — nobody should have to guess. */
.ie-filters, .ie-ratios, .ie-fit-row {
  display: flex; gap: 6px; flex-wrap: wrap;
}
.ie-chip {
  padding: 7px 12px; min-height: 36px;
  border: 1px solid rgba(120, 86, 44, .35); border-radius: 999px;
  background: #fffdf7; cursor: pointer;
  font: 600 11.5px/1 var(--font-ui, Poppins, sans-serif); color: #5c4a33;
}
.ie-chip:hover { background: #f4ebd9; border-color: rgba(120, 86, 44, .55); }
.ie-chip.is-active {
  background: var(--maroon, #7b3335); border-color: var(--maroon, #7b3335); color: #fff;
  box-shadow: 0 2px 10px -3px rgba(123, 51, 53, .6);
}
/* Fill cover / Show whole image is ONE choice, so it reads as one control. */
.ie-fit-row { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
.ie-fit-row .ie-chip { width: 100%; justify-content: center; text-align: center; }
.ie-cover-fixed { margin: 0; font-size: 11px; color: #6f5c44; }
.ie-cover-fixed[hidden] { display: none; }

/* Transform actions: icon AND word, two per row on a phone. */
/* Four compact controls in one row from 360px up; two rows only on the very
   narrowest screens, where the labels would otherwise clip. */
.ie-pane .ie-actions {
  display: grid; grid-template-columns: repeat(2, 1fr); gap: 6px;
  margin-top: 0; padding-top: 0; border-top: 0; justify-content: stretch;
}
@media (min-width: 360px) {
  .ie-pane .ie-actions { grid-template-columns: repeat(4, 1fr); }
  .ie-pane .ie-btn { flex-direction: column; gap: 3px; font-size: 10px; padding: 6px 3px; }
}
.ie-pane .ie-btn {
  display: flex; align-items: center; justify-content: center; gap: 5px;
  width: 100%; min-height: 40px; padding: 6px 4px;
  background: #fffdf8; border: 1px solid rgba(120, 86, 44, .28); border-radius: 10px;
  color: #5c4a33; font: 600 10px/1.15 var(--font-ui, Poppins, sans-serif);
  transition: background .14s, border-color .14s, color .14s;
}
.ie-pane .ie-btn:hover {
  background: #f6edda; border-color: var(--maroon, #7b3335); color: var(--maroon, #7b3335);
}
.ie-pane .ie-btn:active { transform: translateY(1px); }
.ie-pane .ie-btn svg { width: 15px; height: 15px; }

/* ---- action bar ---- */
/* Save is the primary and takes the row; Reset all / Discard share the one
   below. At 300px a single row wrapped "Reset all" onto two lines. */
.ie-bar {
  display: grid; grid-template-columns: 1fr 1fr; gap: 8px; align-items: center;
}
.ie-bar .ov-primary { grid-column: 1 / -1; order: -1; }
.ie-bar .ie-mini { width: 100%; text-align: center; white-space: nowrap; }
.ie-bar-spacer { display: none; }
.ie-bar .ov-primary { width: auto; margin: 0; padding: 10px 20px; min-height: 44px; }
.ie-bar .ie-mini { min-height: 44px; }

@media (min-width: 861px) {
  /* DESKTOP: header on top, then image | dock side by side.
     .ie-body turns into a row — the PANEL never does, which is what put the
     header in its own empty column and starved the picture. */
  .ie-panel.ie-spread { padding: 8px 12px 12px; }

  /* ---- THE EDITOR SITS OVER THE BOOK, NOT OVER THE BROWSER ----
     reader-media.js measures .book-open and publishes it as --ie-top/left/w/h.
     The panel is still `position: fixed` (it has to be — preserve-3d inside
     .flipbook would break anything else), but it is fixed to the BOOK'S BOX.
     A scrim dims the rest of the page so the book still reads as the subject.
     Falls back to full screen if the measurement is unavailable, so a missing
     custom property can never leave the editor unusable. */
  .ie-panel.ie-spread.ie-inbook {
    top: var(--ie-top, 0px);
    left: var(--ie-left, 0px);
    right: auto; bottom: auto;
    width: var(--ie-w, 100%); height: var(--ie-h, 100%);
    border-radius: 10px;
    box-shadow: 0 24px 70px -18px rgba(0, 0, 0, .75),
                0 0 0 1px rgba(255, 246, 228, .12);
  }
  /* The scrim is a pseudo-element on the panel, so it needs no extra node and
     cannot be left behind when the panel is removed. */
  .ie-panel.ie-spread.ie-inbook::before {
    content: ''; position: fixed; inset: 0; z-index: -1;
    background: rgba(18, 13, 9, .55);
    -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px);
  }

  .ie-body { flex-direction: row; gap: 12px; align-items: flex-start; }
  /* COMPACT AND TOP-ALIGNED. A stretched dock left a large empty lower half of
     cream — the controls are secondary, so the card wraps them and stops.
     align-self:flex-start is what removes the empty area; max-height keeps a
     long pane (Adjust) scrolling instead of growing past the screen. */
  /* ---- THE PANEL USES THE COLUMN IT HAS ----
     It was align-self:flex-start and content-height, which on a 900px screen
     left ~400px of empty book below it while the controls inside scrolled in a
     ~300px box. Stretching it costs the image nothing (the dock's WIDTH is
     fixed, so the workspace never changes when a tab opens) and gives every
     pane room to show its controls instead of scrolling them. */
  .ie-dock {
    flex: 0 0 316px; align-self: stretch;
    height: 100%; max-height: 100%; min-height: 0;
    border-radius: 14px; padding: 14px;
    box-shadow: 0 12px 32px -18px rgba(0,0,0,.8);
  }
  /* Panes get the height; the action bar is pinned to the bottom of the card so
     Save is always in the same place whichever tool is open. */
  .ie-panes { flex: 1 1 auto; }
  .ie-bar { margin-top: auto; }
  /* Comfortable, not cramped: this is a mouse, and there is room. */
  .ie-tab { min-height: 40px; font-size: 11.5px; }
  /* The pane owns the column, and its rows distribute across it. Six rows
     bunched at the top left ~180px of empty card below "Reset adjustments" —
     space that is neither breathing room nor content. Spreading them uses the
     height deliberately and gives each slider a comfortable target. */
  .ie-pane { display: none; }
  .ie-pane.is-active { display: flex; flex-direction: column; height: 100%; }
  .ie-adj-rows { flex: 1 1 auto; justify-content: space-evenly; max-height: 460px; }
  /* Tiles, not pills. Three across reads as a filmstrip and fills the column
     without stretching any one tile out of proportion. */
  .ie-filters { grid-template-columns: repeat(3, 1fr) !important; gap: 8px; align-content: start; }
  .ie-adj-reset { flex: 0 0 auto; align-self: flex-start; }
  .ie-pane .ie-field { margin-bottom: 14px; }
  .ie-adj-row { padding: 8px 10px; }
  .ie-adj-name { font-size: 11px; }
  .ie-adj-rows { gap: 3px; }
  .ie-fit-row { margin-bottom: 10px; }
  .ie-pane .ie-actions { gap: 6px; }
  /* Section rules give the pane a hierarchy instead of a stack of controls. */
  .ie-pane .ie-actions { margin-top: 12px; padding-top: 12px;
                         border-top: 1px solid rgba(120, 86, 44, .16); }
  /* The workspace is the whole column height; the dock no longer dictates it. */
  .ie-body > .ie-workspace { align-self: stretch; height: 100%; }
  .ie-tab { flex-direction: row; gap: 5px; font-size: 11px; min-height: 34px; }
  /* Pointer, not thumb: the 44px floor is a touch rule, not a mouse one. */
  .ie-chip, .ie-adj-chip { min-height: 30px; padding: 5px 11px; font-size: 11px; }
  .ie-pane .ie-actions { grid-template-columns: repeat(2, 1fr); gap: 5px; }
  .ie-pane .ie-btn { flex-direction: row; gap: 5px; font-size: 11px; padding: 7px 8px; min-height: 34px; }
  .ie-mini { min-height: 32px; font-size: 11.5px; }
  .ie-bar .ie-mini { min-height: 32px; }
  .ie-bar .ov-primary { padding: 9px 18px; min-height: 38px; }
  .ie-icon { width: 34px; height: 34px; }
  .ie-icon svg { width: 15px; height: 15px; }
  .ie-panel.ie-spread .ie-head .ov-title { font-size: 14px; }
}
/* Very wide screens: the dock does not grow, the picture does.
   1180px is the desktop step from the standard scale (tools/breakpoints.py);
   an invented 1400px would have added a new threshold for no visual gain. */
@media (min-width: 1180px) {
  .ie-panel.ie-spread { padding: 10px 16px 16px; }
  .ie-dock { flex: 0 0 336px; padding: 16px; }
  .ie-tab { min-height: 42px; font-size: 12px; }
  .ie-adj-row { padding: 9px 11px; }
}

/* One footer row for the whole panel, right-aligned, outside the workspace
   so it never moves as the rails change height. */
.ie-actions {
  display: flex; gap: 8px; align-items: center; justify-content: flex-end;
  margin-top: 12px; padding-top: 12px; flex-wrap: wrap; flex: 0 0 auto;
  border-top: 1px solid rgba(120, 86, 44, .18);
}
/* .ov-primary is a full-width block button elsewhere in the reader, which made
   Apply Changes stretch across the row while Reset and Cancel stayed compact.
   Inside this action row it matches them. */
.ie-actions .ov-primary {
  margin: 0; width: auto; flex: 0 0 auto; display: inline-flex;
  align-items: center; justify-content: center;
  padding: 6px 20px; font-size: 12px; line-height: 1.4;
  border-radius: 14px; min-width: 132px;
}
.ie-actions .ie-mini { min-width: 84px; text-align: center; }

/* =====================================================================
 * Refinement round: backdrop books, media page fit, editor height
 * ===================================================================== */

/* ---- (5) Backdrop books: SAME geometry as the real shelf ----
   The backdrop renders the IDENTICAL .page-home markup, so main.css's
   .page-home shelf cascade (equal flex shares per book, width-driven
   covers with an aspect-ratio height, the intentional -12% lean) applies
   inside it and must be left alone.

   Two earlier backdrop-specific sizing overrides lived here and near the
   end of this file (height: clamp(84px,7.6vw,128px); width: auto;
   max-width: none). They date from when the wrapper's width chain
   collapsed and covers had no width to paint; with the wrapper fixed they
   only destroyed parity — at .reader-room-behind .book .cover-face
   specificity (0-3-0) they beat every real-page rule, freeing each cover
   to render at its natural aspect width, far wider than its flex share
   and with no cap. Covers piled over their neighbours and over the shelf
   boards on every reader backdrop (home, My Library, Poets Corner,
   Couple Diary, In Memory Of). Do not reintroduce sizing on any
   .reader-room-behind selector.

   Only two things are legitimately backdrop-specific: eager cover
   loading ($eagerCovers, PHP-side) and NO shadow — a shadow underneath
   the blur reads as a smear. This rule is the single owner of the
   no-shadow part. */
.reader-room-behind .book .cover-face {
  box-shadow: none;
  filter: none;
}
/* The book currently being read is off the shelf; everything else stays. */
.reader-room-behind .book.is-taken { visibility: hidden; }

/* ---- (1) Image and video pages fit the page — never scroll ----
   Only text entries may scroll. Media pages are laid out as
   Date -> Title -> Media, with the media given a bounded share of the page
   so the whole page is always visible. */
.page-media-block { overflow: hidden; }
.page-media-block .page-media {
  position: relative;
}

/* Belt and braces: a media page must not gain a scrollbar from any source. */
.page-full .page-body { overflow: hidden !important; }
/* ...but a text entry page still scrolls when its content is long. */
.page-body.page-scroll { overflow-y: auto !important; overflow-x: hidden !important; }
/* ---- THE SCROLLBAR BELONGS TO THE PAGE, NOT TO THE TEXT ----
   .page carries the 8% side margin, so a scrolling .page-body was inset by it
   and its scrollbar appeared immediately beside the last word — reading as part
   of the column rather than as the edge of the sheet.
   The scroller is pulled back out to the full page width and the same 8% is
   re-applied INSIDE it, so the text sits exactly where it did and the bar moves
   to the far right of the page. Padding and margin are the same value on
   purpose; changing one without the other shifts the column. */
.page-body.page-scroll {
  margin-left: -8%; margin-right: -8%;
  padding-left: 8%; padding-right: 8%;
}

/* ---- (2) Editor: shorter, image higher, actions reachable sooner ----
   THE inset: 10px HERE IS WHY THE PANEL WAS NEVER FULL SCREEN. It was written
   when the editor was an absolutely positioned panel inset inside .book-open,
   and it kept winning over the new fixed full-screen rule further up (same
   specificity, later in the file). The editor is a viewport-level modal now, so
   the inset and the padding both belong to that rule and not to this one. */
.ie-panel.ie-spread {
  padding: 12px 16px 12px;
}
.ie-panel.ie-spread .ov-title { margin: 0 0 4px; font-size: 19px; }
.ie-panel.ie-spread .overlay-rule { margin: 0 0 8px; }
/* The stage now fills the workspace column. The previous max-height capped
   the image so the controls beneath it stayed reachable — with the controls
   moved into the side rails there is nothing beneath it to reach, and the cap
   only shrank the image. These rules load AFTER the layout block above, so
   leaving them would have silently overridden it. */
.ie-panel.ie-spread .ie-dims { margin-top: 0; }
.ie-panel.ie-spread .ie-status { margin-top: 4px; min-height: 12px; }

/* ---- (3) Free-form crop marquee ---- */
.ie-stage.is-cropping { cursor: crosshair; }
.ie-selection {
  position: absolute; pointer-events: none; z-index: 3;
  border: 1.5px dashed #fff; border-radius: 2px;
  box-shadow: 0 0 0 9999px rgba(30, 20, 10, .45);
}
.ie-selection[hidden] { display: none; }
/* Crop actions live in the rail beside the crop button; they stack so two
   buttons never squeeze into a 208px row. (The old grid-column rule referred
   to the removed .ie-tools grid and no longer applied to anything.) */
.ie-crop-actions { display: flex; flex-direction: column; gap: 5px; margin-top: 5px; }
.ie-crop-actions .ie-mini { width: 100%; text-align: center; }
.ie-crop-actions .ie-mini[hidden] { display: none; }

/* Diary tiles sit on a real shelf, like every other book on the site. */
.cd-books { padding: 0 14px 4px; }

.cd-mini.cd-danger:hover { background: #fbeaea; border-color: #a3302f; }

/* Couple diary covers use the same inset-artwork treatment as the shelf. */
/* BOTH collection pages, not just Couple Diary. These were scoped to
   .page-couple-library, so the In Memory Of shelf (.page-memory-library) never
   received them — which is why its covers overlapped and the shelf sat hard up
   against the Create button. That scoping was the regression. */
.page-couple-library .cover-shell,
.page-memory-library .cover-shell { position: relative; display: inline-block; }
/* #11 REMOVED a per-page size override here.
   Couple Diary and In Memory Of had their own cover height
   (clamp(96px,8vw,138px)) while every other shelf used the homepage value
   (clamp(84px,7.6vw,128px)) — two different book sizes on one site. These
   pages now inherit `.page-home .book .cover-face`, so there is ONE size
   everywhere. Only the non-sizing properties are kept. */
.page-couple-library .cd-book .cover-face,
.page-memory-library .cd-book .cover-face {
  width: auto; max-width: none;
  object-fit: contain; object-position: bottom;
}

/* =====================================================================
 * Couple Diary — Round B (room, creation book, password gate)
 * The room itself needs no new CSS: couple_diary.php reuses .library-room,
 * .room-body, room-left/room-center and .shelf-wall unchanged. Only the
 * collection-specific pieces are styled here.
 * ===================================================================== */

/* Diary tiles on the shelf */
/* NOTE, measured August 2026: this declaration does not decide the wrapping of
   any collection shelf. All four collection pages set `page-home` on <body>,
   and `.page-home .shelf-books { flex-wrap: nowrap }` further down this file is
   (0,2,0) against this rule's (0,1,0) — so the collections have been nowrap all
   along. Left exactly as it was rather than "corrected" to nowrap, which would
   have been a no-op edit dressed up as a fix. The real overflow control is the
   shelf CAPACITY: see shelf_rows() in functions-catalog.php. */
.cd-books { flex-wrap: wrap; align-items: flex-end; }
/* Each tile hugs its cover instead of stretching.
   .page-home .book is `flex: 1 1 0`, so with only one or two couple diaries a
   tile stretched across the whole shelf row — and .book-tooltip, centred on
   that stretched box with left:50%, appeared in the middle of the page instead
   of above the book. Content-width tiles put the tooltip over its own cover,
   for one book or many. */
/* Same note as above: `.page-home .shelf-books .book { flex: 0 1 auto }` wins
   over this rule on every collection page, so the tiles already shrink. That
   shrink is what needed BOUNDING, not enabling — see shelf_rows(). */
.cd-books .book { flex: 0 0 auto; }
.cd-empty { margin: 14px auto; font-size: 13px; color: #8d7c64; }

/* "Create Couple Diary" reads as a book action in the shelf area, not a
   dashboard button. */
.cd-create-wrap { margin: 14px 0 0; text-align: center; }
.cd-create {
  display: inline-block; padding: 9px 26px; border-radius: 22px;
  background: var(--maroon, #7b3335); color: #fff; text-decoration: none;
  font-size: 13.5px; font-weight: 700;
  box-shadow: 0 3px 10px rgba(40, 26, 12, .25);
}
.cd-create:hover { background: var(--maroon-dark, #5f2628); }
.cd-create[hidden] { display: none; }

.cd-status {
  margin: 0 auto 10px; max-width: 520px; padding: 9px 14px; border-radius: 8px;
  text-align: center; font-size: 13px; font-weight: 600;
}
.cd-status[hidden] { display: none; }
.cd-status.is-ok    { background: #e9f5ec; border: 1px solid #b6ddc2; color: #1f5c33; }
.cd-status.is-error { background: #fbeaea; border: 1px solid #e6b6b6; color: #8c2626; }

.cd-invites { max-width: 620px; margin: 0 auto 14px; }
.cd-invites[hidden] { display: none; }
.cd-invite {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 10px 14px; margin-bottom: 8px;
  background: #fdf6e6; border: 1px solid #e3d0a4; border-left: 3px solid #c9a227;
  border-radius: 8px;
}
.cd-invite-text { margin: 0; flex: 1 1 200px; font-size: 12.5px; color: #6b5628; }
.cd-invite .cd-accept { margin: 0; padding: 6px 16px; font-size: 12.5px; }

.cd-mini {
  margin-top: 3px; padding: 4px 11px; border-radius: 12px; cursor: pointer;
  background: #fffdf8; border: 1px solid #d3c6ae; color: #5c4a33;
  font: inherit; font-size: 10.5px; font-weight: 600;
}
.cd-mini:hover { background: #f4ebd9; border-color: var(--maroon, #7b3335); }
.cd-mini.cd-danger { color: #a3302f; border-color: #e0bcbc; }

/* ---- Creation book: cover picker ---- */
.cc-drop {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  padding: 12px; cursor: pointer;
  border: 1px dashed #cbb894; border-radius: 8px; background: #fffdf8;
  font-size: 12.5px; color: #7a6647;
}
.cc-drop:hover { border-color: var(--maroon, #7b3335); color: var(--maroon, #7b3335); }
.cc-drop-ic { font-weight: 700; }
.cc-cover-row {
  display: flex; align-items: center; gap: 10px; margin-top: 8px;
}
.cc-cover-row[hidden] { display: none; }
.cc-cover-thumb {
  width: 52px; height: 65px; object-fit: cover;
  border-radius: 3px; border: 1px solid #dccdb0;
}

/* ---- THE CREATION PAGES' OWN EDITOR HOST — REMOVED, August 2026 ----
   `.cc-editor-host` used to be a fixed scrim holding the Image Editor INSIDE
   the creation page, and `.cc-editor-host .ie-panel` then overrode the shared
   `.ie-panel.ie-spread` rules for it: position:relative, a capped 1100x760 box,
   `background: var(--page-bg)` — the PAGE's parchment rather than the editor's
   own warm stone — a border, a radius, and `overflow:auto` on the host, which
   is where the scrolling came from. The comment above these rules claimed the
   panel was "given the same room it has in the reader, so the identical CSS
   produces the identical result"; it was not, because these declarations beat
   the shared ones and produced a visibly different component: parchment
   background, the static book's centre binding showing around the panel, and
   scrolling the reader's editor does not have.

   The creation pages now mount the editor on <body> through
   assets/js/cover-editor.js, exactly as reader-media.js does, so the approved
   `.ie-panel.ie-spread` rules — including `.ie-inbook` on a desktop — are the
   only ones that apply. Nothing replaces this block; the host element is gone
   from all four templates. */

/* ---- (7) Shared indicator above a shared diary, nothing else under books ---- */
.cd-book { position: relative; }
.cd-shared-badge {
  position: absolute; top: -16px; left: 50%; transform: translateX(-50%);
  padding: 1px 8px; border-radius: 9px; white-space: nowrap;
  background: #f0e4c6; border: 1px solid #d8c396; color: #6b5628;
  font-size: 9.5px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  pointer-events: none;
}
   /* room for the badge above the covers */

/* ---- (3)(4) Couple shelf: spacing and breathing room ----
   Couple tiles are content-width (not stretched like catalogue shelves), so
   they need real gaps between them or they read as a single stack. */
.cd-books {
  justify-content: center;
  padding-top: 22px;            /* room for the Shared badge above a cover */
}
.cd-books .book { margin: 0; }
/* The heading area (title + Add button) is separated from the shelf itself. */
/* SPACING BETWEEN THE HEADING, THE ACTION AND THE SHELF.
   10px above put the button hard under the heading's rule, and 30px below left
   the shelf close enough to read as touching it. Applied to every collection
   room so the four behave identically. */
.page-couple-library .cd-create-wrap,
.page-memory-library .cd-create-wrap,
.page-events-library .cd-create-wrap,
.page-favorites-library .cd-create-wrap { margin: 22px 0 44px; }

.page-couple-library .library-heading,
.page-memory-library .library-heading,
.page-events-library .library-heading,
.page-favorites-library .library-heading { margin-bottom: 18px; }
.page-couple-library .library-group .shelf,
.page-memory-library .library-group .shelf { margin-top: 12px; }

/* ---- (6) Partner panel: grouped, readable, consistent with the book forms ---- */
.cp-body { display: flex; flex-direction: column; gap: 10px; }
.cp-status {
  margin: 0; padding: 11px 13px; border-radius: 8px;
  background: #f6efe1; border: 1px solid #dccdb0;
  font-size: 12.5px; line-height: 1.5; color: #5c4a33;
}
.cp-status.is-accepted { background: #eef7f0; border-color: #bcdcc7; color: #2f6244; }
.cp-status.is-pending  { background: #fdf6e6; border-color: #e3d0a4; color: #6b5628; }
.cp-row {
  display: flex; align-items: baseline; gap: 8px;
  padding: 7px 0; border-bottom: 1px solid rgba(120, 86, 44, .14);
}
.cp-row:last-child { border-bottom: 0; }
.cp-label {
  flex: 0 0 78px; font-size: 10px; letter-spacing: .06em;
  text-transform: uppercase; color: #9a8763;
}
.cp-value { flex: 1 1 auto; min-width: 0; font-size: 13px; color: #3c2a1e; overflow-wrap: anywhere; }
.cp-badge {
  display: inline-block; padding: 2px 9px; border-radius: 10px;
  font-size: 10px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
}
.cp-badge.is-accepted { background: #dff0e4; color: #2c7a4b; }
.cp-badge.is-pending  { background: #fdf0d5; color: #8a6a1d; }
.cp-actions { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; margin-top: 4px; }
.cp-actions[hidden] { display: none; }

/* ---- (7)(8)(9) Guest-access account ---- */
.pf-guest-note {
  margin: 0 0 12px; padding: 8px 12px; border-radius: 7px;
  background: #fdf6e6; border: 1px solid #e3d0a4; border-left: 3px solid #c9a227;
  font-size: 11.5px; line-height: 1.45; color: #6b5628; text-align: center;
}
.pf-guest-note strong { color: #4a3a12; }
.pf-guest-note a { color: var(--maroon, #7b3335); font-weight: 600; }
/* Registration-only fields are hidden for guests, who never supplied them. */
.pf-grid.is-guest #pfPrimaryLanguage,
.pf-grid.is-guest #pfSecondaryLanguage,
.pf-grid.is-guest #pfAccessibility { display: none; }
.pf-grid.is-guest label:has(#pfPrimaryLanguage),
.pf-grid.is-guest label:has(#pfSecondaryLanguage),
.pf-grid.is-guest label:has(#pfAccessibility) { display: none; }

/* Guest accounts: hide anything they never supplied at onboarding. */
.pf-grid.is-guest [data-guest-hide],
.pf-grid.is-guest label:has([data-guest-hide]) { display: none; }

/* =====================================================================
 * In Memory Of — reuses the room, shelf, tiles and editor styling already
 * defined for Couple Diary; only the pieces this module adds are here.
 * ===================================================================== */

/* Pending badge on a shelf tile (owner's review queue) */
.cd-shared-badge.is-pending { background: #fdf0d5; border-color: #e0c88f; color: #8a6a1d; }

/* Toolbar count badge on the Pending Memories tool */
.reader-toolbar .tool { position: relative; }
.tool-badge {
  position: absolute; top: -4px; right: -4px; min-width: 17px; height: 17px;
  display: flex; align-items: center; justify-content: center;
  padding: 0 4px; border-radius: 9px;
  background: var(--maroon, #7b3335); color: #fff;
  font-size: 10px; font-weight: 800; line-height: 1;
}

/* ---- Member picker inside Add Entry ---- */
.ae-member-field { position: relative; }
/* THE DROPDOWN MUST BE ANCHORED, NOT LEFT TO ITS STATIC POSITION.
   This block declared left/right/z-index but no `top`, on the assumption that
   an absolutely positioned box with no `top` falls where it would have flowed
   — i.e. under the input. That is true in a BLOCK container and false here:
   .ae-member-field is `display: flex`, and CSS Flexbox §4.1 gives an
   absolutely positioned child of a flex container the static position it would
   have "as the sole flex item", which is the container's content-box START
   corner. So the list rendered at the top of the field, ON TOP OF the label
   and the search input.

   Measured before the fix at 1280x900: field top 305, input 324.8-368.4, list
   305-404, and document.elementFromPoint() at the input's centre returned an
   option's <span> rather than the input. The input was not merely obscured, it
   was unhittable — which is why the field could not be typed into and why the
   list appeared to stay open until an option was picked. The outside-click and
   Escape handlers were present and correct throughout; they were never the
   fault.

   `top: 100%` anchors the list to the BOTTOM of the field, so it opens below
   the control the way a dropdown is expected to, and the input stays reachable
   the whole time it is open. */
.ae-member-list {
  position: absolute; left: 0; right: 0; top: calc(100% + 4px); z-index: 30;
  max-height: 190px; overflow-y: auto; overflow-x: hidden;
  background: #fffdf8; border: 1px solid #d9c9ab; border-radius: 7px;
  box-shadow: 0 8px 22px rgba(40, 26, 12, .22);
}
.ae-member-list[hidden] { display: none; }
.ae-member-opt {
  display: block; width: 100%; text-align: left; cursor: pointer;
  padding: 7px 10px; background: none; border: 0;
  border-bottom: 1px solid rgba(120, 86, 44, .12); font: inherit;
}
.ae-member-opt:last-child { border-bottom: 0; }
.ae-member-opt:hover { background: #f4ebd9; }
.ae-member-opt strong { display: block; font-size: 12.5px; color: #3c2a1e; }
.ae-member-opt span { display: block; font-size: 10.5px; color: #8d7c64; }
.ae-member-chosen {
  margin: 5px 0 0; padding: 6px 9px; border-radius: 6px;
  background: #eef7f0; border: 1px solid #bcdcc7;
  font-size: 11.5px; font-weight: 600; color: #2f6244;
}
.ae-member-chosen[hidden], .ae-member-warn[hidden] { display: none; }
.ae-member-warn {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin: 5px 0 0; padding: 6px 9px; border-radius: 6px;
  background: #fdf4e3; border: 1px solid #e6cf9e;
  font-size: 11.5px; color: #6b5320;
}

/* ---- Pending memories review queue ---- */
.mp-list { display: flex; flex-direction: column; gap: 9px; }
.mp-item {
  padding: 10px 12px; border-radius: 8px;
  background: #f8f1e2; border: 1px solid #dccdb0;
}

/* ---- (#1) Create Memory: participant summary + dedicated screen ---- */
.mc-participant-summary { display: flex; align-items: center; gap: 10px; margin: 6px 0 2px; flex-wrap: wrap; }
.mc-summary-text { font-size: 12px; font-weight: 600; color: #5c4a33; }
/* A PAGE of the static book, not a modal — the same idea as the reader's
   .page-overlay: it fills the page it belongs to, so the user stays inside the
   book instead of a window opening over the application. Reusable by any
   static-book page (see $bookRightPanelHtml in includes/static-book.php). */
.sb-panel {
  position: absolute; inset: 0; z-index: 6;
  padding: 24px 48px 16px; overflow-y: auto; overflow-x: hidden;
  background: var(--page-bg, #f0e6d2); color: var(--page-ink, #34281c);
}
.sb-panel[hidden] { display: none; }
.sb-panel .sb-title { margin-top: 0; }

/* CLOSE CONTROL — on .sb-panel, not on any one page that uses it.
   Add Participants closes with a text link at the bottom, which works when the
   panel is short but is unreachable without scrolling when it is not. A corner
   X is always in the same place, so any panel opened from any static-book page
   closes the same way. 44px target per the tap-target floor; the glyph is
   smaller than the button so the target is generous without looking heavy. */
/* STICKY, NOT ABSOLUTE. .sb-panel is the scroll container, so an absolutely
   positioned X scrolled away with the content and a visitor had to scroll back
   up just to close. Sticky keeps it in the panel's own top-right corner for the
   whole scroll. It is floated so it takes no row of its own — the panel's title
   still starts at the top, exactly as before — and `top` is measured from the
   panel's padding edge, so the resting position is unchanged. */
.sb-panel-close {
  position: sticky; top: 0; float: right; margin-left: 8px; z-index: 1;
  width: 44px; height: 44px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  background: none; border: 0; cursor: pointer;
  color: var(--maroon, #7b3335); font-size: 26px; line-height: 1;
  border-radius: 50%; transition: background .15s;
}
.sb-panel-close:hover { background: rgba(123, 51, 53, .09); }
.sb-panel-close:focus-visible { outline: 2px solid var(--maroon, #7b3335); outline-offset: 2px; }
/* The floated X reserves its own space, so the title needs no padding to clear
   it; the reservation only applies to the first line, which is where it sits. */
@media (max-width: 720px){
  .sb-panel { padding: 18px 22px 14px; }
}
.mc-rows { display: flex; flex-direction: column; gap: 6px; margin: 10px 0; }
.mc-row { display: flex; gap: 6px; align-items: center; }
.mc-row .ov-input { flex: 1 1 0; min-width: 0; }
.mc-row-del {
  flex: 0 0 auto; width: 26px; height: 26px; cursor: pointer;
  background: #fffdf8; border: 1px solid #d3c6ae; border-radius: 6px;
  color: #a3302f; font-size: 11px; line-height: 1;
}
.mc-row-del:hover { background: #fbeaea; border-color: #a3302f; }
.mc-people-actions { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; margin-top: 6px; }
.mc-people-actions .ov-primary { width: auto; margin: 0; padding: 7px 20px; font-size: 12.5px; border-radius: 15px; }
.mc-people-count { font-weight: 700; }

/* ---- (#4) Participant management cards inside the book ---- */
.mm-list { flex-direction: column; }
.mm-item { border-radius: 8px;
  background: #f6efe1; border: 1px solid #dccdb0;
}
.mm-name { margin: 0; font-weight: 700; color: #3c2a1e; }
.mm-email { color: #7a6647; overflow-wrap: anywhere; }
.mm-status { display: flex; align-items: center; gap: 7px; margin: 0; flex-wrap: wrap; }
.mm-status-label { font-size: 10px; letter-spacing: .06em; text-transform: uppercase; color: #9a8763; }
.mm-memory { font-size: 10.5px; color: #7a6647; }
.mm-actions { display: flex; gap: 7px; }
.mm-add-wrap { display: flex; justify-content: center; margin: 6px 0; }
.mm-add-wrap[hidden], .mm-invite[hidden] { display: none; }

/* ---- (#2) Diary password AS A PAGE of the book ----
   Not an overlay and not a popup: this is the content of the first page, so it
   inherits the page's parchment, spacing and typography like any other page. */
.lock-page {
  display: flex; flex-direction: column; justify-content: center;
  height: 100%; padding: 8px clamp(14px, 4vw, 44px); text-align: center;
}
.lock-title {
  margin: 0 0 6px; font-size: clamp(19px, 2.2vw, 26px);
  font-family: var(--font-display, inherit); color: var(--page-accent, #7b3335);
}
.lock-rule {
  display: block; width: 84px; height: 2px; margin: 0 auto 14px;
  background: linear-gradient(90deg, transparent, #c9a227, transparent);
}
.lock-lede {
  margin: 0 0 18px; font-size: 13.5px; line-height: 1.6;
  color: var(--page-ink, #34281c);
}
.lock-label {
  display: block; text-align: left; margin: 0 0 4px;
  font-size: 11px; letter-spacing: .05em; text-transform: uppercase;
  color: #8d7c64;
}
.lock-input {
  width: 100%; padding: 9px 11px; margin: 0 0 12px;
  border: 1px solid #cbb894; border-radius: 7px;
  background: rgba(255, 253, 248, .85); font: inherit; font-size: 14px;
  color: var(--page-ink, #34281c);
}
.lock-input:focus { outline: none; border-color: var(--page-accent, #7b3335); }
.lock-msg {
  margin: 0 0 10px; padding: 7px 10px; border-radius: 6px;
  background: #fdf4e3; border: 1px solid #e6cf9e;
  font-size: 12px; color: #6b5320;
}
.lock-msg[hidden] { display: none; }
.lock-go {
  align-self: center; padding: 10px 30px; border-radius: 22px; cursor: pointer;
  background: var(--page-accent, #7b3335); color: #fff; border: 0;
  font: inherit; font-size: 13.5px; font-weight: 700;
}
.lock-go:hover { filter: brightness(1.08); }
.lock-go[disabled] { opacity: .6; cursor: default; }

/* ---- (#3) Approval notice in Add Entry ---- */
.ae-approval-note {
  margin: 0 0 10px; padding: 8px 11px; border-radius: 6px;
  background: #fdf6e6; border: 1px solid #e3d0a4; border-left: 3px solid #c9a227;
  font-size: 11.5px; line-height: 1.45; color: #6b5628;
}
.ae-approval-note[hidden] { display: none; }

/* ---- (#4) Review list + detail ---- */
.mp-head-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin: 0 0 1px; }
.mp-name { margin: 0; font-size: 13px; font-weight: 700; color: #3c2a1e; }
.mp-email { margin: 1px 0 5px; font-size: 11.5px; color: #7a6647; overflow-wrap: anywhere; }
.mp-status { display: flex; align-items: center; gap: 7px; margin: 0 0 8px; flex-wrap: wrap; }
/* ONE ROW, ONE HEIGHT — and the reason it was neither.
   A second `.mp-actions .ov-primary` rule earlier in this file carried
   `min-width: 96px`. The later rule below overrode everything the two had in
   common, so it looked superseded and was easy to miss, but the floor survived
   and applied to the primary button alone: Approve was held at 96px while
   Reject and View beside it sized to their own labels. That is the uneven,
   unbalanced row. The duplicate is gone; these are now the only rules.

   `align-items: center` keeps the controls on one baseline when the row wraps,
   and `white-space: nowrap` with a shared `min-height` stops a longer label
   wrapping to two lines and making one button taller than its neighbours —
   the vertical jumping. Sizing comes from shared padding, not from a width, so
   nothing is made enormous to fit its text. */
.mp-actions {
  display: flex; gap: 6px; justify-content: flex-end;
  flex-wrap: wrap; align-items: center;
}
.mp-actions .ie-mini,
.mp-actions .ov-primary {
  padding: 6px 14px; font-size: 12px; border-radius: 14px; margin: 0; width: auto;
  line-height: 1.3; min-height: 30px; white-space: nowrap;
  display: inline-flex; align-items: center; justify-content: center;
}
/* Narrow phones: the row spans the card and the controls share it evenly,
   rather than three different widths crowding against the right edge. */
@media (max-width: 430px) {
  .mp-actions { justify-content: stretch; }
  .mp-actions .ie-mini,
  .mp-actions .ov-primary { flex: 1 1 0; min-width: 0; padding: 6px 8px; }
}
.mv-meta { margin: 0 0 10px; font-size: 11.5px; color: #7a6647; }
/* Labelled sections give the review page its structure: Memory Text, then
   Image, then Video — each in its own block, absent media simply absent. */
.mv-section { margin: 0 0 12px; }
.mv-sec-head {
  margin: 0 0 5px; font-size: 10.5px; font-weight: 700;
  letter-spacing: .07em; text-transform: uppercase; color: #8d7c64;
}
.mv-text {
  margin: 0 0 12px; padding: 10px 12px; border-radius: 7px;
  background: #f8f1e2; border: 1px solid #dccdb0;
  font-size: 13px; line-height: 1.6; color: #3c2a1e; white-space: pre-wrap;
}
.mv-media { display: flex; flex-direction: column; gap: 10px; margin-bottom: 12px; }
.mv-image, .mv-video {
  display: block; width: 100%; max-height: 300px; object-fit: contain;
}
/* One action row, one height: Back pinned left, the decision group right,
   Approve as the visual primary. Every control shares the same size. */
.mv-actions { display: flex; gap: 8px; align-items: center; justify-content: flex-end; flex-wrap: wrap; }
.mv-actions .ie-mini,
.mv-actions .ov-primary { width: auto; margin: 0; padding: 7px 20px; font-size: 12.5px; border-radius: 15px; line-height: 1.3; }
.mv-actions[hidden] { display: none; }

/* ---- (#5) Participant entry rows in the reader ---- */
.map-rows { display: flex; flex-direction: column; gap: 6px; margin: 8px 0; }
.map-row { display: flex; gap: 6px; align-items: center; }
.map-row .ov-input { flex: 1 1 0; min-width: 0; }
.map-del {
  flex: 0 0 auto; width: 26px; height: 26px; cursor: pointer;
  background: #fffdf8; border: 1px solid #d3c6ae; border-radius: 6px;
  color: #a3302f; font-size: 11px; line-height: 1;
}
.map-actions { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; margin-top: 6px; }
.map-actions .ov-primary { width: auto; margin: 0; padding: 7px 20px; font-size: 12.5px; border-radius: 15px; }
.map-count { font-weight: 700; }

/* =====================================================================
 * Shelf presentation — real shelf, real book silhouettes
 * ===================================================================== */

/* Covers are transparent PNGs of CURVED books. Any box-shadow on them draws a
   rectangle around the artwork; drop-shadow follows the alpha channel so the
   shadow matches the book's real shape. Applied everywhere a cover appears. */
/* SCOPED TO img.cover-face ON PURPOSE.
   The reader's OPEN book uses <div class="cover-face cover-front"> for the
   maroon cover, and the shelf uses <img class="cover-face"> for the artwork —
   the same class name, two completely different things. An unscoped rule here
   set background:transparent and border:0 on both, which is what made the
   maroon book cover vanish inside every open book while the uploaded image
   stayed. `img.` can only ever match the shelf's artwork. */
img.cover-face {
  box-shadow: none !important;
  filter: drop-shadow(2px 4px 6px rgba(40, 24, 8, .42));
  background: transparent;
  border: 0;
  border-radius: 0;
}
.shelf-books .cover-shell { background: transparent; border: 0; box-shadow: none; }
.shelf-books .book { background: transparent; border: 0; }

/* A shelf with visible thickness and brackets, rather than a flat bar. */
.board-fallback {
  display: block;
  height: 14px;
  border-radius: 2px 2px 3px 3px;
  background:
    linear-gradient(180deg, #d8b988 0%, #c2a06c 18%, #9c7a4a 55%, #7d5c33 100%);
  box-shadow:
    0 1px 0 rgba(255, 240, 214, .55) inset,
    0 10px 16px -6px rgba(52, 32, 12, .55),
    0 2px 3px rgba(52, 32, 12, .35);
  position: relative;
}
/* The two supporting brackets under the board. */
.board-fallback::before,
.board-fallback::after {
  content: '';
  position: absolute; top: 100%;
  width: 9px; height: 17px;
  background: linear-gradient(180deg, #6d4b26, #4e341a);
  border-radius: 0 0 2px 2px;
  box-shadow: 0 3px 5px rgba(52, 32, 12, .4);
}
.board-fallback::before { left: 11%; }
.board-fallback::after  { right: 11%; }

.shelf-board { min-height: 14px; }

/* ---- Pending review: owner's edit fields ---- */
/* Edit view — organised like Add Entry. Compact label+input rows per the
   FORM DENSITY STANDARD; the current media stays visible as a preview above
   its fields; a right-aligned Cancel/Save row closes the form. */
.mv-edit { display: flex; flex-direction: column; gap: 0; margin-bottom: 12px; }
.mv-edit[hidden] { display: none; }
.mv-row { display: flex; align-items: center; gap: 8px; margin: 0 0 6px; }
.mv-row .ov-label { flex: 0 0 84px; margin: 0; font-size: 11px; }
.mv-row .ov-input { flex: 1 1 auto; margin: 0; min-width: 0; }
.mv-edit .mv-text-edit { margin: 0; }
.mv-edit-img-preview, .mv-edit-vid-preview {
  display: block; width: 100%; max-height: 220px; object-fit: contain;
  border-radius: 6px; border: 1px solid #dccdb0; background: #efe6d4; margin: 0 0 8px;
}
.mv-edit-img-preview[hidden], .mv-edit-vid-preview[hidden] { display: none; }
.mv-file { display: flex; align-items: center; gap: 8px; cursor: pointer; margin: 2px 0 0; }
.mv-file-btn {
  display: inline-block; padding: 5px 14px; border-radius: 13px;
  border: 1px solid #b99d6b; background: #f8f1e2; color: #6a4f2e;
  font-size: 11.5px; font-weight: 600;
}
.mv-file:hover .mv-file-btn { border-color: var(--maroon, #7b3335); color: var(--maroon, #7b3335); }
.mv-file-name { font-size: 11px; color: #7a6647; overflow-wrap: anywhere; }
.mv-edit-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 4px; }
.mv-edit-actions .ie-mini,
.mv-edit-actions .ov-primary { width: auto; margin: 0; padding: 7px 20px; font-size: 12.5px; border-radius: 15px; line-height: 1.3; }

/* ---- Member search: dropdown + jumps ---- */
.ms-pick { width: 100%; }
.ms-jumps { display: flex; gap: 8px; justify-content: center; margin-top: 10px; }

/* =====================================================================
 * (#9) GLOBAL DECORATIVE MEDIA FRAME
 * A layered gold surround — braided outer band, bevel, parchment mount —
 * applied by the shared pagination engine so every image and video in every
 * module gets the identical treatment. Built from gradients rather than an
 * image so it scales to any media size and needs no new asset.
 * ===================================================================== */
/* MEDIA ITSELF: fills the frame, never carries it.
   =====================================================================
   THIS RULE USED TO UNDO THE FILL. The base rule further up already says
   `width:100%; height:100%; object-fit:cover`, which is correct — but this
   one is more specific AND later, so `width:auto; height:auto;
   object-fit:contain` won. The result was a photo floating at its own aspect
   ratio with the parchment mount visible around it: the gaps inside the gold
   border.

   IMAGE and VIDEO are now split, because they want opposite things:
     - an IMAGE fills the frame; cropping the edges of a photo is what the
       frame is for, and gaps read as a rendering fault
     - a VIDEO still fits inside it; cropping a video removes content the
       viewer cannot get back by any means, and a letterboxed video reads as
       normal because every player does it
   ===================================================================== */
.page-media-block .page-media img {
  width: 100%; height: 100%;
  object-fit: cover;
  object-position: center;
  display: block;
  box-sizing: border-box;
  border: 0;
  border-radius: 1px;
  box-shadow: none;
  background: transparent;
}
.page-media-block .page-media video {
  max-width: 100%;
  width: 100%; height: 100%;
  object-fit: contain;
  display: block;
  box-sizing: border-box;
  border: 0;
  border-radius: 1px;
  box-shadow: none;
  /* Transparent, so the letterboxing a video legitimately has shows the
     frame's parchment mount rather than the black a <video> paints. */
  background: transparent;
}
.reader-room.night .page-media-block .page-media img,
.reader-room.night .page-media-block .page-media video {
  background:
    linear-gradient(#2b241c, #221c16) padding-box,
    repeating-linear-gradient(135deg,
      #b8952f 0 4px, #8a6a1d 4px 8px, #5c4713 8px 12px, #8a6a1d 12px 16px) border-box;
  box-shadow:
    0 0 0 1px rgba(90, 70, 20, .8),
    0 0 0 1px rgba(255, 248, 226, .1) inset,
    0 10px 24px -8px rgba(0, 0, 0, .65);
}

/* The review page uses the same frame, so approving a memory shows exactly
   what it will look like once published. */
.mv-image, .mv-video {
  padding: 8px;
  border: 7px solid transparent;
  border-radius: 2px;
  background:
    linear-gradient(#fffdf6, #f4e9d4) padding-box,
    repeating-linear-gradient(135deg,
      #f3dc9c 0 4px, #c9a227 4px 8px, #8a6a1d 8px 12px, #c9a227 12px 16px) border-box;
  box-shadow: 0 0 0 1px rgba(122, 92, 24, .5), 0 8px 18px -7px rgba(52, 32, 12, .45);
}

/* ---- (#10) Edit and Delete belong to the TEXT page only ----
   flipbook.js already withholds them from media pages; this makes it
   impossible for any other path to reintroduce them there. */
.page-full .entry-edit,
.page-full .entry-delete,
.page-media-block .entry-edit,
.page-media-block .entry-delete { display: none !important; }

/* ---- (#7)(#8) Limited-access account: what you can reach ---- */
.pf-shared { margin: 0 0 14px; }
.pf-shared .sb-title-sm { margin: 0 0 8px; font-size: 15px; }
/* Leads the limited-access page in place of the Subscription Details
   heading, so it carries that heading's spacing rather than a banner's. */
.pf-shared { margin-bottom: 12px; }
.pf-shared-empty { margin: 0; font-size: 12px; color: #8d7c64; }
.pf-shared-item {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 9px 12px; margin-bottom: 7px; border-radius: 8px;
  background: #f6efe1; border: 1px solid #dccdb0;
}
.pf-shared-module {
  flex: 0 0 auto; padding: 2px 8px; border-radius: 9px;
  background: #efe3c8; color: #6b5628;
  font-size: 9.5px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase;
}
.pf-shared-title { flex: 1 1 120px; min-width: 0; font-size: 13px; font-weight: 600; color: #3c2a1e; }
/* ---- A shared item that can only be READ ----
   Placed here, immediately after the row rules it extends, rather than
   appended at the end of this file: mw.css's rule ORDER is checked against a
   baseline (gate 35) and appending an equal-specificity rule elsewhere is the
   documented way a collision gets introduced. Nothing above targets these two
   selectors, so no existing declaration changes.

   The badge is inline INSIDE .pf-shared-title so it wraps with the title on a
   narrow screen instead of forcing a third flex column that would crush the
   name — the row already wraps (flex-wrap) and the title is the flexible
   member. */
.pf-shared-badge {
  display: inline-block; margin-left: 8px; vertical-align: 1px;
  padding: 1px 7px; border-radius: 9px;
  background: #e6dcc6; color: #6b5628;
  font-size: 9.5px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase;
}
.pf-shared-readonly { background: #f3ece0; }
.pf-shared-open {
  flex: 0 0 auto; padding: 6px 16px; border-radius: 14px; text-decoration: none;
  font-size: 12px; font-weight: 700;   /* typography floor */
}
.pf-shared-open:hover { background: var(--maroon-dark, #5f2628); }

/* #1 The media block itself is a column: head, title, then the media filling
   what is left. Without an explicit height the percentages above have nothing
   to resolve against. */
/* The whole chain must be a flex column with no minimum, or the media area
   cannot grow into the space the title leaves. */
.page-full .page-body { display: flex; flex-direction: column; min-height: 0; }
.page-media-block {
  display: flex; flex-direction: column;
  flex: 1 1 auto; height: 100%; min-height: 0;
}
.page-media-block .page-media-title { flex: 0 0 auto; }

/* ---- (#1) The reader's maroon cover is untouchable ----
   Stated explicitly so a future shelf rule cannot erase it again. */
.book-cover .cover-face {
  background: var(--cover-maroon, linear-gradient(145deg, #7d2f31, #5d2022));
}

/* ---- (#6) Books sit ON the shelf, slightly closer together ---- */
.shelf-books { gap: clamp(10px, 1.6vw, 22px); }
.cd-books { gap: clamp(12px, 2vw, 26px); }
/* A small, deliberate overlap — books lean together rather than standing apart. */
/* Seated on the board, not hovering above it. */

/* ---- (#7) Uploaded cover follows the book's angled front face ----
   The artwork PNG is a 3D render, so its front face is a quadrilateral, not a
   rectangle. clip-path traces that face and a slight skew follows the
   perspective, so a custom cover sits ON the book instead of floating over it
   as a flat square. Percentages match the 98x158 artwork. */

/* ---- (#2) Title page: higher, and readable at a glance ---- */
.title-page {
  justify-content: flex-start;
  padding-top: clamp(18px, 4vh, 46px);
}
.title-page h1 {
  font-size: clamp(22px, 2.6vw, 32px);
  line-height: 1.2;
  margin-bottom: 4px;
}
.tp-dates { margin: 16px 0 6px; }
.tp-dates p {
  margin: 7px 0;
  font-size: clamp(14px, 1.35vw, 17px);
  font-weight: 600;
  line-height: 1.35;
  color: var(--page-ink, #34281c);
}
.tp-dates span {
  display: block; margin-bottom: 2px;
  font-size: 10.5px; letter-spacing: .11em; text-transform: uppercase;
  font-weight: 700; color: #9a8763;
}

/* ---- (#5) Open Book button: legible without hovering ---- */
.pf-shared-open {
  background: var(--maroon, #7b3335);
  color: #fff !important;
  text-shadow: none;
  border: 1px solid rgba(0, 0, 0, .18);
}
.pf-shared-open:hover,
.pf-shared-open:focus { background: var(--maroon-dark, #5f2628); color: #fff !important; }
/* A limited-access profile is a preview, so its fields read as such. */
.pf-grid.is-guest input[readonly],
.pf-grid.is-guest input[disabled] {
  background: rgba(120, 86, 44, .06);
  color: #5c4a33;
  cursor: default;
}

/* =====================================================================
 * Media: fill the page, and no black letterbox around video
 * ===================================================================== */

/* THE BLACK BACKGROUND AROUND VIDEO.
   A <video> element defaults to a 300x150 box and paints its unused area
   black. With max-width/max-height alone the BOX stays a different shape from
   the footage, so black bars appear inside the gold frame. Giving it a
   definite height and letting the width follow makes the element adopt the
   footage's own aspect ratio, so there is no unused area left to paint. */
.page-media-block .page-media.page-video {
  align-items: stretch;
  justify-content: center;
}

/* Firefox/Safari paint the intrinsic background on the element itself. */
.page-media-block .page-media video::-webkit-media-controls-enclosure {
  background: transparent;
}

/* MEDIA FILLS THE PAGE.
   The page's own padding was taking a visible share on media pages, and the
   media block was competing with it. On a media page the page body gives its
   space to the media and keeps only what the date and title need.

   THE 10px IS SET AT `.page-full .page-body` EARLIER IN THIS FILE, not here.
   This rule is kept, and kept in agreement, because it is the one a reader
   looking for media-page padding finds first — but it cannot win on its own:
   the earlier rule carries `!important`, which outranks this rule's higher
   specificity. Changing the number here alone would do nothing at all. */
.page.page-full .page-body { padding-left: 10px; padding-right: 10px; }
.page-media-block { padding: 0; }

/* ============ ONE FRAME WIDTH, WHICHEVER PAGE IT LANDS ON ============
   Reported as "frames can appear wider on some pages". Measured, on a 1280px
   desktop: a media frame on a full-bleed page is 412.78px wide, and the
   IDENTICAL block on a page that also carries entry text is 432.78px — 20px
   wider, reaching 10px nearer each edge.

   The cause is directly above. pagination.js marks a page `full` only for a
   fullBleed unit, so `.page-full .page-body`'s 10px horizontal padding applies
   to a media-only page and not to a media block sharing a page with text. The
   frame therefore took its width from WHICH KIND OF PAGE it happened to land
   on — and a spread showing one of each puts the two side by side, which is
   what makes the difference obvious.

   Equalised by bringing the text-page block IN to the full-bleed frame, never
   by widening the full-bleed page: that geometry is the approved
   "media fills the page" treatment and the number above is the one the design
   was signed off on. The media itself is unaffected — `.page-media-sharp` is
   `object-fit: contain` and the blurred backdrop is `cover`, so a larger
   picture is still scaled and cropped INSIDE the frame rather than growing it.

   `:not(.page-full)` rather than a second copy of the 10px, so the two can
   never fall out of step: whichever page type applies, the inset is the same
   10px stated once above. Specificity (0,3,1) clears `.page-media-block`
   (0,1,0) on the line above without needing !important.

   Verified by tools/media_symmetry.py, which measures all four rendering slots
   plus both page types and fails if any pair disagrees by more than 1px. */
.page:not(.page-full) .page-body > .page-media-block {
  padding-left: 10px;
  padding-right: 10px;
}

.page-media-block .page-media img,
.page-media-block .page-media video { max-height: 100%; }

/* =====================================================================
 * Blurred backdrop: books must look like the real shelf
 * ===================================================================== */

/* SIZING OWNED ELSEWHERE. The backdrop inherits the real .page-home shelf
   cascade wholesale; the single backdrop-specific rule (no shadow, plus
   is-taken) lives in the "(5) Backdrop books" block earlier in this file.
   A second height-driven sizing copy lived HERE and, loading later, was
   the one actually winning — it is what piled oversized covers over their
   neighbours and the shelf boards. Do not re-add sizing rules here. */
/* #14 A doubled selector also lived here (.reader-room-behind
   .reader-room-behind .shelf-books) — nothing is nested inside itself, so the
   rule never matched and its declarations were dead. Removed rather than
   corrected: the backdrop should INHERIT the homepage's shelf spacing, not
   define its own. */

/* =====================================================================
 * ONE SHARED MEDIA FRAME COMPONENT  (used by every module)
 *
 * The frame lives on the FIGURE, not on the <img>/<video>. That is what makes
 * it identical everywhere: the figure is a fixed share of the page, so the
 * frame is the same size whatever is dropped inside it, and the media simply
 * scales to fit. Previously the frame was drawn on the media element itself,
 * so its size followed the media — a portrait photo and a landscape video got
 * visibly different frames — and a later rule setting `background` on the
 * video wiped the frame's gradient entirely, which is why video had no border
 * at all while images did.
 *
 * Layout per media page:  date + title on top, frame fills the rest (~78%).
 * ===================================================================== */
.page-media-block .page-media {
  flex: 1 1 auto;
  min-height: 0;
  width: 100%;
  display: flex; align-items: center; justify-content: center;
  margin: 0 auto;
  box-sizing: border-box;

  padding: 12px;
  border: 10px solid transparent;
  border-radius: 3px;
  background:
    linear-gradient(#fffdf6, #f4e9d4) padding-box,
    repeating-linear-gradient(135deg,
      #f6e3ad 0 4px, #c9a227 4px 8px, #8a6a1d 8px 12px, #c9a227 12px 16px) border-box;
  box-shadow:
    0 0 0 1px rgba(122, 92, 24, .6),
    0 0 0 1px rgba(255, 248, 226, .9) inset,
    0 12px 26px -10px rgba(52, 32, 12, .55),
    0 2px 5px rgba(52, 32, 12, .28);
  overflow: hidden;
}
.reader-room.night .page-media-block .page-media {
  background:
    linear-gradient(#2b241c, #221c16) padding-box,
    repeating-linear-gradient(135deg,
      #b8952f 0 4px, #8a6a1d 4px 8px, #5c4713 8px 12px, #8a6a1d 12px 16px) border-box;
  box-shadow:
    0 0 0 1px rgba(90, 70, 20, .85),
    0 0 0 1px rgba(255, 248, 226, .1) inset,
    0 12px 26px -10px rgba(0, 0, 0, .7);
}

/* No black behind video: these earlier rules set an opaque dark background on
   the element, which showed as bars whenever the footage did not exactly fill
   its box. The frame's parchment shows instead. */
.page-video video,
.page-media.page-video video,
.page-media-block .page-media.page-video video { background: transparent; }
/* The play badge belongs over the media, inside the frame. */
.page-media-block .page-video .video-play { z-index: 2; }

/* =====================================================================
 * SHELF: books rest ON the board, and stand together
 * ===================================================================== */

/* `.page-home .book { flex: 1 1 0 }` gave every book an equal SHARE of the
   row, so a shelf holding two books put each in the middle of its own half —
   the large gap on the second Private Library shelf. Content-width books plus
   the existing centring keeps them together.

   flex-basis auto, shrink ENABLED, and NOWRAP — deliberately, all three:
   a wrap threshold is a discontinuity, and this row used to sit within a
   few pixels of it, so any small width difference between two renders of
   the same shelf (the reader backdrop has no scrollbar; the live page
   does) flipped a book onto a second row and shifted every shelf below.
   With nowrap + shrink, a small width deficit shrinks every book evenly
   and imperceptibly instead (min-width:0 on .book already allows it, and
   the cover's width:100% chain follows the shrink while keeping its
   aspect ratio). Same inputs at any width now give the same layout. */
.page-home .shelf-books .book { flex: 0 1 auto; }
.page-home .shelf-books { justify-content: center; flex-wrap: nowrap; }

/* Consolidated seating — several earlier rules set competing margins here.
   The asset has no transparent margin at its base (measured: opaque to the
   last pixel), so the books' own bottom edge IS the shelf line; the board is
   pulled up just enough to meet it. */

/* =====================================================================
 * SHELF + COVER — SINGLE SOURCE OF TRUTH
 *
 * Every shelf spacing/seating rule now lives here and nowhere else. There were
 * NINE competing declarations for these same properties scattered through this
 * file, each added by a different round of fixes; whichever happened to be
 * last silently won, which is why adjusting the overlap or the seating kept
 * appearing to do nothing. If a shelf measurement needs changing, change it
 * HERE — do not append a new rule elsewhere.
 * ===================================================================== */

/* Seating. The cover image is inline by default, so it sits on the text
   baseline and leaves descender space beneath it — that space is the gap under
   the books, not a margin. display:block removes it. (Measured: the artwork is
   opaque to its final pixel, so the image's bottom edge IS the shelf line.)
   The board is pulled up 5px so the books' bottom edge tucks BEHIND it —
   the board paints above the books (z-index 2 !important vs the books'
   layer), so the tuck reads as resting on the shelf rather than floating
   at its front edge. The .page-home variant is required: main.css carries
   `.page-home .shelf-board { margin-top: -1px }` which out-ranks the bare
   selector on every origin page. */
img.cover-face { display: block; }
.shelf-books { align-items: flex-end; }
.shelf-books .book { margin-bottom: 0; }
.shelf-board,
/* -1px, not -5px. The -5px was cancelling the baseline descender measured
   above; with that gone it would have buried the bottom 5px of every cover
   under the plank. One pixel is the tuck, so the books meet the board instead
   of hovering over it or sinking into it. */
.page-home .shelf-board { margin-top: -1px; }   /* books tuck into the board */

/* Spacing. A slight lean, not a stack: -8px was too aggressive. */
.shelf-books .book + .book { margin-left: -3px; }
.page-home .shelf-books { gap: clamp(6px, 1vw, 14px); }

/* Custom cover, fitted INSIDE the visible front face.
   Measured from the artwork (98x158): the face spans x 22%-96%; its top edge
   sits below the page block and its bottom rises from ~98% on the left to
   ~86% on the right as the book turns away. These bounds are inset from those
   extremes so the cover can never spill above or beyond the face. */
.book-user-art {
  position: absolute;
  left: 23%; width: 71%;
  top: 0; height: 100%;
  background-size: cover;
  background-position: center;
  /* MEASURED FROM THE ARTWORK, NOT ESTIMATED FROM IT.
     Scanning the 98x158 covers column by column for the first fully opaque
     dark pixel — the printed cover face, below the gold/maroon top edge —
     gives a face that begins at 10.3% of the height at x=23% and rises to
     4.9% at x=94%, and a bottom that falls from 98.1% to 84.2% across the
     same span. The polygon claimed 8% on the right where the face actually
     starts at 4.9%, so a ~3% band of the underlying book showed above the
     uploaded cover along its top-right — which is the reported misfit — and
     it stopped ~1.7% short along the bottom for the same reason.
     Inset ~0.2% from the measured edges so the cover can never spill past
     the face it is sitting on. */
  clip-path: polygon(0% 10.5%, 100% 5.2%, 100% 85.2%, 0% 97.4%);
  transform: none;
  border-radius: 0;
  box-shadow: none;
  pointer-events: none;
}

/* The blurred backdrop renders the same shelf smaller; it keeps the seating
   but not the overlap, which would squash it at that scale. */

/* =====================================================================
 * PANEL DENSITY STANDARD  (Add Entry, Edit Entry, Search, Change Cover)
 * One place defining how reader panels use space, so future panels inherit it
 * instead of each one being tuned separately.
 * ===================================================================== */

/* A label and its control share one row rather than stacking. */
.ov-row-inline {
  display: flex; align-items: center; gap: 10px;
  margin-bottom: 8px;
}
.ov-row-inline .ov-label { margin: 0; flex: 0 0 auto; white-space: nowrap; }
.ov-row-inline .ov-input { flex: 0 1 auto; width: auto; min-width: 0; }
/* A date field only needs to be as wide as a date. */
.ov-input[type="date"] { width: auto; min-width: 148px; padding: 5px 8px; font-size: 13px; }

/* Primary actions: compact and centred, never stretched across the panel. */
.ov-panel .ov-primary {
  display: block; width: auto; margin: 10px auto 0;
  padding: 7px 22px; font-size: 12.5px; border-radius: 15px;
  min-width: 132px; text-align: center;
}

/* Small circular control used where a full block would be wasteful
   (cover preview, image editor). */
.ov-icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; padding: 0; cursor: pointer;
  background: #fffdf8; border: 1px solid #d3c6ae; border-radius: 50%;
  color: #5c4a33; flex: 0 0 auto;
}
.ov-icon-btn svg { width: 15px; height: 15px; fill: currentColor; }
.ov-icon-btn:hover,
.ov-icon-btn.is-active { border-color: var(--maroon, #7b3335); color: var(--maroon, #7b3335); }
.ec-compact { gap: 8px; flex-wrap: wrap; }

/* Instructional text is a hint, not a paragraph. */
.ov-panel .ov-note { font-size: 10.5px; line-height: 1.4; margin: 4px 0; color: #8d7c64; }

/* A message that must actually be NOTICED — the picture-titles hint, and
   errors like "an entry already exists for this date". Both previously
   rendered as .ov-note: 10.5px in a muted brown, indistinguishable from
   decoration. Deliberately compact — a notice, not a banner. */
.ov-panel .ov-notice {
  margin: 8px 0; padding: 7px 10px; border-radius: 7px;
  font-size: 11.5px; line-height: 1.45; font-weight: 500;
  border: 1px solid transparent;
}
.ov-panel .ov-notice[hidden] { display: none; }
.ov-panel .ov-notice.ov-notice-info,
.ov-panel .ov-notice.is-info {
  background: #f7f0e1; border-color: #ddc9a3; color: #6b552f;
}
.ov-panel .ov-notice.is-error {
  background: #fbeaea; border-color: #e6b6b6; color: #8c2626;
}

/* Writing space is the point of an entry panel: the textarea takes what is
   left after the compact rows above it. */
.ov-panel .ov-form { display: flex; flex-direction: column; min-height: 0; }
.ov-panel textarea.ov-input { flex: 1 1 auto; min-height: 150px; resize: vertical; }
/* THE RULE SITS UNDER THE TITLE IT BELONGS TO. `margin: 6px 0 8px` left it
   hard against the panel's left padding while .ov-title above it is centred,
   so every overlay had a centred heading with an off-centre divider. `auto`
   on the sides centres it, which is what reader.css intended before this rule
   overrode the whole shorthand to change the vertical spacing.
   One shared declaration, so Participants, Add Entry, Find a Memory, Pending
   Memories, Share This Book and every other .ov-panel move together. */
.ov-panel .overlay-rule { margin: 6px auto 8px; }
.ov-panel .ov-title { margin-bottom: 2px; }
/* THE SPECIAL EVENTS MEDIA TITLE CARRIES A DATE, so it is several times longer
   than "Edit Entry" or "Memory" — at the shared clamp's upper bound it filled
   two lines and dwarfed the picture beneath it. Held at the clamp's LOWER bound
   so it matches the entry panels' title rather than shouting over them; the
   family, weight, colour and centring are inherited unchanged. */
.ov-panel .evm-title { font-size: clamp(17px, 1.7vw, 20px); line-height: 1.25; }

/* =====================================================================
 * THE END page
 * ===================================================================== */
.end-page {
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  height: 100%; text-align: center; gap: 10px;
}
.end-flourish { font-size: 26px; color: var(--page-accent, #7b3335); opacity: .8; }
.end-title {
  margin: 0;
  font-family: var(--font-display, Georgia, serif);
  font-size: clamp(26px, 3.4vw, 44px);
  letter-spacing: .18em;
  font-weight: 400;
  color: var(--page-ink, #34281c);
}
.end-rule {
  display: block; width: 110px; height: 2px;
  background: linear-gradient(90deg, transparent, #c9a227, transparent);
}

/* The back cover PAGE styling that used to live here is gone: the back cover
   is a BOARD (.book-back-cover, styled in reader.css beside .book-cover), and
   the closing leaf is type 'inside-cover' — the exact leaf the front uses —
   so reader.css's is-inside-cover rules cover it with no parallel CSS here. */

/* =====================================================================
 * #14 BACKDROP SHELF = HOMEPAGE SHELF
 * The blurred backdrop renders the same markup as the page it came from, so it
 * should look the same. Previously it carried its own spacing overrides (no
 * overlap, different board offset) which is exactly why its shelves sat
 * closer together and overlapped. Those are gone; the backdrop now inherits
 * every shelf measurement from the single source of truth above.
 *
 * Only two things remain backdrop-specific, and both are necessary:
 *   - covers load eagerly (lazy images never load in a fixed, clipped box)
 *   - no shadow (a shadow on top of a blur reads as a smear)
 * The no-shadow rule itself lives in the "(5) Backdrop books" block earlier
 * in this file — ONE owner; a duplicate filter:none one-liner here is gone.
 * ===================================================================== */

/* One shelf rhythm, expressed twice ON PURPOSE — this is not a duplicate.
   main.css sets `.page-home .shelf { margin-bottom: 0 }`, which is more
   specific than `.shelf`, so the homepage needs the value restated at matching
   specificity or it would fall back to 0. Both carry the SAME number; change
   them together. */
.shelf { margin-bottom: 26px; }
.page-home .shelf { margin-bottom: 26px; }

/* =====================================================================
 * #13 PARTICIPANTS PAGE
 * ===================================================================== */

/* Heading: prominent and left aligned, not a muted note. */
.mm-heading {
  margin: 4px 0 8px;
  text-align: left;
  font-family: var(--font-display, Georgia, serif);
  font-size: 15px; font-weight: 600; letter-spacing: .01em;
  color: var(--page-accent, #7b3335);
}

/* ONE PARTICIPANT PER ROW.
   This used to be `repeat(auto-fit, minmax(190px, 1fr))`, which put two cards
   side by side on any panel wider than ~400px. The compact two-column item
   below was then trying to fit an identity column AND an actions column into
   about 207px. Measured at 1280x900 with four participants: each card 227.6px
   wide, the name column squeezed to 60.6px, a long name wrapping to four lines
   and cards 120-145px tall — the "large boxes" in the report. The item was
   rebuilt as a compact row in an earlier pass but was never given a row's
   width, so the rebuild could not show.
   A full-width row leaves the name real space at every panel size. */
.mm-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin: 8px 0;
}
.mm-item { padding: 7px 10px; }
.mm-name { font-size: 12.5px; }
.mm-email { font-size: 11px; margin: 1px 0 4px; }
.mm-actions { margin-top: 6px; }
.mm-actions .ie-mini { padding: 4px 10px; font-size: 10.5px; }

/* Add Participant / View Participants: two actions, not two banners.
   .ov-primary is full width by default, which would stack them as slabs;
   these are peers on one line, wrapping only when the panel is too narrow to
   hold both. The 34px height is a compromise the panel forced: the 44px floor
   applies to the site's own controls, and a pair of 44px slabs directly under
   the heading dominated a panel whose subject is the list below them. */
.mm-actions-row {
  display: flex; flex-wrap: wrap; gap: 8px;
  align-items: center; justify-content: center;
  margin: 6px 0 2px;
}
/* THE WRAPPER GENERATES NO BOX, so both buttons are direct flex items of the
   row and are laid out identically. It was an ordinary flex container, which
   made Add a grandchild and View a child of the row — measured 5px apart
   vertically, Add at y=362 against View at y=367, because View still carried
   .ov-primary's own `margin: 8px auto 2px` while Add's was reset by a
   descendant selector. `display: contents` removes the asymmetry at its
   source rather than compensating for it.
   The wrapper still carries `hidden`, which keeps working because main.css
   declares [hidden] { display: none !important } and that beats this. */
.mm-actions-row .mm-add-wrap { display: contents; }
/* Scoped to the row so it outranks `.ov-primary` on specificity rather than on
   file order. Written as a single-class rule first, this lost to a later
   `.ov-primary` at equal specificity — which is exactly the silent-override
   risk gate 4 counts. */
.mm-actions-row .ov-primary {
  width: auto; min-width: 148px; min-height: 34px;
  margin: 0; padding: 8px 18px;
  font-size: 12px; border-radius: 17px;
}
/* Occupies the list while it loads, so the box is never shown empty. */
.mm-loading {
  margin: 0; padding: 10px 2px;
  font-size: 12px; color: #9a8763; text-align: center;
}

/* Back control on a panel, matching the close button's position. */
.ov-back {
  position: absolute; top: 10px; left: 12px;
  padding: 4px 10px; border-radius: 12px; cursor: pointer;
  background: #fffdf8; border: 1px solid #d3c6ae; color: #5c4a33;
  font: inherit; font-size: 11.5px; font-weight: 600;
}
.ov-back:hover { background: #f4ebd9; border-color: var(--maroon, #7b3335); }
.ec-optional { font-weight: 400; text-transform: none; letter-spacing: 0; color: #9a8763; }

/* =====================================================================
 * (#7) GUEST STATIC BOOK — Private Library preview for non-subscribers
 * Image left, description right, on the shared static-book layout. Follows
 * the FORM DENSITY STANDARD: compact rows, small centred actions.
 * ===================================================================== */
/* LEFT PAGE — the artwork carries the page.
   Previously `width:auto; height:auto` meant the image drew at its INTRINSIC
   size: the shelf covers are 98x158 thumbnails, so it painted a postage stamp
   in the middle of a full page. It now fills ~88% of the usable page area
   while keeping its aspect ratio (object-fit: contain never crops or
   stretches), with the gilt frame kept as the border treatment. */
/* The left page IS the artwork. The page's own body padding is pulled right
   in so the image reaches close to the page edges, leaving only a margin;
   the image then takes the entire remaining box. The earlier 88% was
   shrinking it inside an already-padded page, which is what left the large
   empty areas. object-fit: contain still guarantees no crop or distortion. */
.page-guest-book .page-left .sb-body { padding: 3.2%; }
/* ============ INFORMATIONAL BOOK — PROPORTIONS ============
   THE BOOK WAS SIZED FOR A READER SPREAD, not for a page of prose. At
   min(97vw, 1500px) it filled the window, so three paragraphs sat in an
   enormous sheet with a full-bleed picture beside them — the "large empty
   document" effect. Constraining the spread on these pages is what fixes it;
   nothing about the layout, the reader or the flipbook changes, and the same
   boards, spine and paper are still used. */
.page-guest-book .flipbook {
  width: min(88vw, 1120px, calc((100vh - var(--reader-chrome, 200px)) * 1.60));
}
.page-guest-book .book-open { min-height: 420px; max-height: 720px; }
.page-static-book .reader-room { padding-bottom: clamp(18px, 2.6vw, 38px); }

.page-guest-book .gb-art {
  display: flex; align-items: center; justify-content: center;
  /* THE ARTWORK FILLS THE PAGE. A previous round inset it with `padding: 9%
     10%` and capped the image at its intrinsic size, on the reasoning that
     the picture is "an illustration, not the page". That shrank it to well
     inside the board and left the empty margin the client reported. Reverted:
     the page's own 3.2% body padding above is the only inset, so the image
     reaches close to the page edges as it did originally. */
  width: 100%; height: 100%; padding: 0;
  box-sizing: border-box;
}
.page-guest-book .gb-art img {
  display: block;
  width: 100%; height: 100%;
  object-fit: contain;            /* fills the box, never crops or distorts */
  border-radius: 2px;
  /* The gilt frame treatment, kept. */
  border: 7px solid transparent;
  border-image: linear-gradient(145deg, #f3e2b4, #b8933f 28%, #fbf0cf 52%, #a8843a 78%, #e6cd92) 1;
  box-shadow: 0 18px 42px rgba(52, 35, 18, .48);
}
.page-guest-book .gb-art img.img-missing { display: none; }
.page-guest-book .gb-art img.img-missing + .gb-art-fallback { display: block; }
.gb-art-fallback {
  display: none;
  font-family: var(--font-display, Georgia, serif);
  font-size: clamp(20px, 2.4vw, 30px); color: var(--page-ink, #34281c);
  text-align: center;
}
/* RIGHT PAGE — a product page, not a paragraph dump.
   Hierarchy: title, rule, a larger italic lede, then body copy with generous
   leading; the first paragraph takes a drop-cap-ish emphasis so the eye has a
   clear entry point. Sizes use clamp() so the page reads well from a laptop
   to a large monitor without a media query. */
/* THE TEXT COLUMN. Tighter margins and a measured line length: the previous
   7.5% side padding on an already-narrow page left a thin ribbon of text down
   the middle with space either side, which is what made it feel empty. Long
   text scrolls within the page rather than stretching it. */
.page-guest-book .page-right .sb-body {
  padding: 5.5% 6.5%;
  overflow-y: auto; overflow-x: hidden;
  max-height: 100%;
  scrollbar-width: thin;
  /* THE TITLE STARTS AT THE TOP. A previous round set
     `justify-content: center` here so short pages would sit mid-sheet. On a
     real page that pushed the heading down and opened a large blank area
     above it — the client reported this twice. The column starts at the top
     and the copy flows below it, which is the original behaviour; long text
     still scrolls within the page. */
  display: flex; flex-direction: column; justify-content: flex-start;
}
.page-guest-book .page-right .sb-body > * { flex: 0 0 auto; }
.page-guest-book .sb-title {
  font-size: clamp(21px, 2.05vw, 30px);
  line-height: 1.18; margin: 0 0 5px;
}
/* Subheadings, for the informational pages that use them (About Us). */
.page-guest-book .sb-subtitle {
  font-family: var(--font-display, Georgia, serif);
  font-size: clamp(15px, 1.3vw, 19px);
  line-height: 1.3; margin: 18px 0 7px;
  color: var(--page-accent, #7b3335);
}
.page-guest-book .sb-subtitle:first-of-type { margin-top: 12px; }
.gb-lede {
  margin: 8px 0 14px;
  font-family: var(--font-display, Georgia, serif);
  font-size: clamp(14px, 1.2vw, 17px);
  line-height: 1.4; font-style: italic; font-weight: 500;
  color: var(--page-accent, #7b3335);
}
.gb-para {
  margin: 0 0 11px;
  font-size: clamp(12.5px, .95vw, 14.5px);
  /* 1.8 leading on a narrow column is what made short pages look sparse. */
  line-height: 1.65;
  color: var(--page-ink, #34281c);
  text-align: left;
}
.gb-para:first-of-type { font-size: clamp(13px, 1vw, 15px); }
.gb-para:last-of-type { margin-bottom: 0; }
.gb-actions {
  display: flex; gap: 10px; justify-content: center; flex-wrap: wrap;
  margin-top: 18px; padding-top: 13px;
  border-top: 1px solid rgba(120, 86, 44, .22);
}
.gb-cta, .gb-back {
  display: inline-block; padding: 9px 26px; border-radius: 21px;
  font: 600 13px/1.3 var(--font-ui, Poppins, sans-serif);
  text-decoration: none; border: 1px solid transparent;
  position: relative; z-index: 1;      /* above the page's texture layer */
  transition: background .15s, color .15s, border-color .15s;
}
.gb-cta { background: var(--maroon, #7b3335); color: #fff; }
.gb-cta:hover { background: var(--maroon-dark, #5f2426); }
.gb-back { background: #fff; color: var(--maroon, #7b3335); border-color: #cbb894; }
.gb-back:hover { background: #f8f1e2; }

/* (#3) One upload tool at a time, in ONE toolbar slot. The back-cover
   button carries BOTH classes (.tool-upload .tool-upload-back) so the
   closed-state rule above keeps it alive; these two rules then swap the
   pair by closed state — front-closed shows the front upload, back-closed
   shows the back upload, never both. (The on-board icon is gone: the
   toolbar owns the entry point for both covers.) */
.reader-room.book-closed-back .reader-toolbar .tool-upload:not(.tool-upload-back) { display: none !important; }
.reader-room:not(.book-closed-back) .reader-toolbar .tool-upload-back { display: none !important; }

/* Pending invitations in My Shared Books. Same row as an accepted book so the
   list reads as one thing, with a gold edge and a filled action to mark that
   this one still needs a decision. */
.pf-invite-item { border-color: #c9a227; background: #fbf4e2; }
.pf-invite-from { font-weight: 400; font-size: 12px; color: #7a6647; }   /* typography floor */
.pf-invite-accept {
  flex: 0 0 auto; padding: 6px 16px; border-radius: 14px; border: 0;
  background: var(--maroon, #7b3335); color: #fff; cursor: pointer;
  font: 600 12px/1.3 var(--font-ui, Poppins, sans-serif);
}
.pf-invite-accept:hover:not([disabled]) { background: var(--maroon-dark, #5f2628); }
.pf-invite-accept[disabled] { opacity: .6; cursor: default; }
.pf-invite-msg { margin: 6px 0 0; font-size: 12px; }
.pf-invite-msg[hidden] { display: none; }
.page-media-block[data-media-row] { position: relative; }

/* Metadata panel */
.evm-preview { margin: 0 0 8px; text-align: center; }
.evm-preview-media {
  display: block; width: 100%; max-height: 200px;
  object-fit: contain; border-radius: 6px;
  border: 1px solid #dccdb0; background: #efe6d4;
}
.evm-actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; margin-top: 10px; }
.evm-actions .ie-mini,
.evm-actions .ov-primary { width: auto; margin: 0; padding: 7px 18px; font-size: 12.5px; border-radius: 15px; }

/* Image order: one row per picture, thumbnail + label + two nudge buttons. */
.eo-list { display: flex; flex-direction: column; gap: 6px; margin: 8px 0; }

/* ---- (#6) Add Event Entry: compact, top-weighted ----
   The panel previously used full-width stacked blocks with the shared
   .ov-field spacing, which pushed the date selector down and Save below the
   fold. Rows are tighter, and the two upload areas share a line. */
.ev-entry-panel .ev-row { display: flex; align-items: center; gap: 9px; margin: 0 0 6px; }
.ev-entry-panel .ev-row .ov-label { flex: 0 0 46px; margin: 0; }
.ev-entry-panel .ev-row .ov-input { flex: 1 1 auto; margin: 0; min-width: 0; }
.ev-entry-panel .ev-block { display: flex; flex-direction: column; gap: 3px; margin: 0 0 7px; }
.ev-entry-panel .ev-block .ov-input { margin: 0; }
.ev-entry-panel .ev-uploads { display: flex; gap: 8px; align-items: flex-start; }
.ev-entry-panel .ev-uploads .ev-block { flex: 1 1 0; min-width: 0; }
.ev-entry-panel .ev-drop { padding: 8px 10px; }
.ev-entry-panel .ev-drop .ov-drop-text { font-size: 11px; }
.ev-entry-panel .ev-meta-hint { margin: 6px 0 8px; }
.ev-entry-panel .ee-save { margin-top: 2px; }

/* =====================================================================
   SPECIAL EVENTS — media page controls and Edit Entry media management
   ===================================================================== */

/* The date text inside a media page's head, so the action icons can sit
   beside it rather than inside the same text node. */
.ev-head-text { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }

/* Action icons in a media page head.
   Their own class and their own styling: `.page-full .entry-edit` and
   `.page-media-block .entry-edit` are hidden with !important elsewhere in this
   file (Edit/Delete belong to an entry's text page), so borrowing that class
   for these made them invisible on exactly the pages they belong on. */
.page-head .ev-media-btn {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  width: 23px; height: 23px; margin-left: 4px; padding: 0;
  border: 1px solid #d8c7a4; border-radius: 50%;
  background: rgba(255, 252, 245, .94); color: #6b5335;
  cursor: pointer; transition: background .15s, border-color .15s, color .15s;
}
.page-head .ev-media-btn svg { width: 13px; height: 13px; fill: currentColor; }
.page-head .ev-media-btn:hover {
  background: #fff; border-color: var(--maroon, #7b3335); color: var(--maroon, #7b3335);
}
.page-head .ev-media-btn.ev-meta-del:hover {
  background: #fbeaea; border-color: #b3413f; color: #a3302f;
}

/* A drop control used inside an already-dense panel. */
.ov-drop-sm { padding: 7px 10px; }
.ov-drop-sm .ov-drop-text { font-size: 11px; }

/* ---- Linked items on an entry page (generic `chapter.links`) ----
   Used by Special Events to surface an event inside the diary it was linked
   to, but the block itself names no module. Presented as a card with a gold
   rule and a clear call to open it — as a plain tinted strip it read as a
   disabled notice rather than something to click. */
.blk-entry-links { margin: 14px 0 0; display: flex; flex-direction: column; gap: 8px; }
.blk-entry-links::before {
  content: 'Linked event';
  display: block; margin-bottom: 2px;
  font-size: 10px; font-weight: 700; letter-spacing: 1.1px; text-transform: uppercase;
  color: #a08a5c;
}
.entry-link {
  position: relative; display: flex; align-items: center; justify-content: space-between;
  gap: 12px; padding: 11px 14px 11px 16px; text-decoration: none;
  border-radius: 9px; overflow: hidden;
  background: linear-gradient(135deg, #fffdf7 0%, #f6eeda 100%);
  border: 1px solid #ddc9a3;
  box-shadow: 0 2px 5px -3px rgba(82, 60, 24, .45);
  transition: box-shadow .16s ease, transform .16s ease, border-color .16s ease;
}
.entry-link::before {
  content: ''; position: absolute; inset: 0 auto 0 0; width: 4px;
  background: linear-gradient(180deg, #d8b45e, var(--maroon, #7b3335));
}
.entry-link:hover {
  border-color: var(--maroon, #7b3335);
  box-shadow: 0 6px 14px -8px rgba(82, 60, 24, .6);
  transform: translateY(-1px);
}
.entry-link-label {
  font-family: 'Cormorant Garamond', Georgia, serif;
  font-size: 19px; font-weight: 700; color: #4a3218; line-height: 1.2;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.entry-link-go {
  flex: 0 0 auto; display: inline-flex; align-items: center; gap: 5px;
  padding: 6px 14px; border-radius: 999px;
  background: var(--maroon, #7b3335); color: #fff;
  font-size: 10px; font-weight: 700; letter-spacing: .7px; text-transform: uppercase;
  box-shadow: 0 2px 6px -3px rgba(90, 40, 40, .8);
}
.entry-link-go::after { content: '\2197'; font-size: 11px; line-height: 1; }
.entry-link:hover .entry-link-go { background: #5f2628; }

/* Expanded previews inside a chip's inline preview area. */
.ev-prev-cell { position: relative; display: inline-block; margin: 0 5px 5px 0; }
.ev-prev-thumb {
  width: 46px; height: 46px; object-fit: cover; display: block;
  border-radius: 5px; border: 1px solid #dccdb0; background: #efe6d4; cursor: pointer;
}
.ev-prev-thumb:hover { border-color: var(--maroon, #7b3335); }
.ev-prev-x {
  position: absolute; top: -5px; right: -5px;
  width: 16px; height: 16px; line-height: 14px; padding: 0;
  border-radius: 50%; border: 1px solid #e6b6b6;
  background: #fbeaea; color: #8c2626; font-size: 9px; cursor: pointer;
}
.ev-prev-x:hover { background: #8c2626; color: #fff; border-color: #8c2626; }
.ev-prev-video { display: block; width: 100%; max-height: 110px; border-radius: 5px; background: #000; }
.ev-prev-del { width: auto !important; margin: 6px 0 0 !important; padding: 5px 12px; font-size: 11.5px; }
.et-image-count { font-weight: 400; }


/* Settings checkboxes: one per line, compact. */
.eg-check { display: flex; align-items: center; gap: 7px; font-size: 12px; color: #5c4a2f; margin: 2px 0; cursor: pointer; }
.eg-check input { width: 14px; height: 14px; margin: 0; flex: 0 0 auto; }
.eg-couple-list { display: flex; flex-direction: column; }
.eg-range { margin: 2px 0 8px; }

/* "Current images" / "Current video" above the upload control, so the panel
   says what it is showing instead of just presenting a chip. */
.ev-current-label {
  font-size: 9.5px; font-weight: 700; letter-spacing: .8px; text-transform: uppercase;
  color: #a08a5c; margin: 2px 0 1px;
}
.ev-current-label[hidden] { display: none; }

/* Replace + Edit Image sit on one row rather than stacking. */
.evm-swap { display: flex; align-items: stretch; gap: 8px; margin: 6px 0 2px; }
.evm-swap .ov-drop { flex: 1 1 auto; margin: 0; }
.evm-swap .ie-mini { flex: 0 0 auto; width: auto; margin: 0; padding: 0 14px; font-size: 11.5px; }
.evm-swap .ie-mini[hidden] { display: none; }

/* The metadata fields: label above field, consistent gaps, full width. */
.ov-panel[data-overlay="eventMedia"] .ov-row-inline,
.ov-panel[data-overlay="eventMedia"] .ov-field { margin: 0 0 8px; }
.ov-panel[data-overlay="eventMedia"] .ov-input { margin: 0; width: 100%; }
.ov-panel[data-overlay="eventMedia"] textarea.ov-input { min-height: 68px; resize: vertical; }

.eg-linked {
  display: inline-flex; align-items: center; gap: 6px; margin: 5px 5px 0 0;
  padding: 4px 6px 4px 10px; border-radius: 999px;
  background: #f7f0e1; border: 1px solid #ddc9a3; color: #6b552f;
  font-size: 11.5px; font-weight: 600;
}
.eg-linked-name { max-width: 130px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.eg-linked-x {
  width: 15px; height: 15px; line-height: 13px; padding: 0; border-radius: 50%;
  border: 1px solid #e6b6b6; background: #fbeaea; color: #8c2626; font-size: 8.5px; cursor: pointer;
}
.eg-linked-x:hover { background: #8c2626; color: #fff; border-color: #8c2626; }

/* A panel heading that also carries a date needs less weight than a bare
   one, or it wraps and crowds the rule beneath it. */
.ov-panel .ov-title-sm { font-size: 17px; line-height: 1.25; margin: 0 34px 2px 0; }
.ov-panel .ov-title-sm + .overlay-rule { margin: 4px 0 10px; }

/* Grouped sections inside the picture panel: the item and its tools, then
   the words. Interleaved, Replace/Edit read as unrelated to the preview. */
.evm-group { padding: 9px 10px; border-radius: 8px; background: #faf5ea; border: 1px solid #e6d8ba; }
.evm-group + .evm-group { margin-top: 8px; }
.evm-group .ov-field { margin: 0 0 6px; }
.evm-group .ov-field:last-child { margin-bottom: 0; }
.evm-group .evm-swap { margin: 8px 0 0; }
.evm-preview-sm .evm-preview-media { max-height: 78px; width: auto; max-width: 100%; margin: 0 auto; }

/* Drag-to-reorder rows. */
.eo-item {
  display: flex; align-items: center; gap: 9px; padding: 6px 9px; margin: 0 0 5px;
  border-radius: 7px; background: #fffdf7; border: 1px solid #e2d3b4;
  cursor: grab; user-select: none;
}
.eo-item:hover { border-color: #cbb894; }
/* The row being dragged lifts rather than fading: it is the thing under the
   pointer, so it should read as picked up, not as removed. No transition on
   transform while dragging — the transform IS the pointer position, and
   easing it makes the row lag behind the cursor. */
.eo-item.is-dragging {
  cursor: grabbing; z-index: 5; position: relative;
  border-color: var(--maroon, #7b3335); background: #fffdf7;
  box-shadow: 0 10px 20px -10px rgba(60, 42, 20, .55);
}
/* Children must not swallow the pointer, and no native drag anywhere. */
.eo-item * { pointer-events: none; }
.eo-item img { -webkit-user-drag: none; user-select: none; }
/* touch-action: none is what stops a touch drag scrolling the panel instead
   of moving the row. */
.eo-item { touch-action: none; }
/* Only the OTHER rows animate as they make room. */
.eo-item:not(.is-dragging) { transition: transform .14s ease, border-color .12s ease; }
.eo-grip { flex: 0 0 auto; width: 15px; height: 15px; }
.eo-grip svg { width: 15px; height: 15px; fill: #bda87e; }
.eo-pos {
  flex: 0 0 auto; width: 19px; height: 19px; line-height: 19px; text-align: center;
  border-radius: 50%; background: #efe3c9; color: #6b552f; font-size: 10.5px; font-weight: 700;
}
.eo-thumb { flex: 0 0 auto; width: 34px; height: 34px; object-fit: cover; border-radius: 4px; border: 1px solid #dccdb0; }
.eo-label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; color: #5c4a2f; }

/* Settings: one box, one idea. */
.eg-box { padding: 11px 12px; border-radius: 9px; background: #faf5ea; border: 1px solid #e6d8ba; margin: 2px 0 10px; }
.eg-box-title {
  display: block; margin: 0 0 7px; font-size: 9.5px; font-weight: 700;
  letter-spacing: .9px; text-transform: uppercase; color: #a08a5c;
}
.eg-couple { margin-top: 10px; padding-top: 9px; border-top: 1px solid #e6d8ba; }
.eg-sub { display: block; margin: 0 0 5px; font-size: 11.5px; font-weight: 600; color: #6b552f; }
.eg-picker { display: flex; align-items: center; gap: 6px; max-width: 260px; }
.eg-picker .ov-input { flex: 1 1 auto; min-width: 0; max-width: 172px; margin: 0; font-size: 12px; padding: 6px 8px; }
.eg-picker .ie-mini { flex: 0 0 auto; width: auto; margin: 0; padding: 0 13px; font-size: 11.5px; }

/* Save and Delete pushed apart, and the pair centred as a group. */
.ev-entry-panel .et-actions,
.ov-panel[data-overlay="eventSettings"] .et-actions {
  display: flex; justify-content: center; align-items: center;
  gap: 22px; flex-wrap: wrap; margin-top: 10px;
}
.ov-panel[data-overlay="eventSettings"] .et-actions .ie-mini,
.ov-panel[data-overlay="eventSettings"] .et-actions .ov-primary { width: auto; margin: 0; }

/* ---- Sharing panel ---- */
.sh-row { display: flex; align-items: center; gap: 6px; margin: 0 0 6px; }
.sh-row .ov-input { flex: 1 1 0; min-width: 0; margin: 0; font-size: 12px; padding: 6px 8px; }
.sh-row-x, .sh-item-x {
  flex: 0 0 auto; width: 20px; height: 20px; line-height: 18px; padding: 0;
  border-radius: 50%; border: 1px solid #e6b6b6; background: #fbeaea; color: #8c2626;
  font-size: 9px; cursor: pointer;
}
.sh-row-x:hover, .sh-item-x:hover { background: #8c2626; color: #fff; border-color: #8c2626; }
.sh-actions { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 8px; }
.sh-actions .ie-mini, .sh-actions .ov-primary { width: auto; margin: 0; padding: 7px 16px; font-size: 12px; }
.sh-link-out { display: flex; align-items: center; gap: 6px; margin-top: 8px; }
.sh-link-out .ov-input { flex: 1 1 auto; min-width: 0; margin: 0; font-size: 11px; padding: 6px 8px; }
.sh-link-out .ie-mini, .eg-box > .ie-mini { width: auto; margin: 0; padding: 6px 14px; font-size: 11.5px; }
.sh-item {
  display: flex; align-items: center; gap: 8px; padding: 6px 8px; margin: 0 0 5px;
  border-radius: 6px; background: #fffdf7; border: 1px solid #e2d3b4;
}
.sh-who { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; color: #5c4a2f; font-weight: 600; }
.sh-state { flex: 0 0 auto; font-size: 9.5px; letter-spacing: .4px; text-transform: uppercase; color: #a08a5c; }

/* ---- One link, and access as a screen rather than a list ----
   The panel used to print every recipient inline under "Who has access", so a
   book shared with a dozen people grew past the controls. The count stays on
   the main screen and the names move behind View Access.

   AND IT WAS FAR TALLER THAN IT NEEDED TO BE. The intro was a full
   .ov-notice-info banner, .ov-form's clamp gap put up to 17px between boxes,
   and each .eg-box carried the panel's default padding — so Who has access sat
   below the fold on a laptop and needed scrolling to reach. The measurements
   below trim the gaps and the type, not the information. */
/* 8px read as two boxes touching. 12px separates them without adding a
   scroll: View Access still sits inside the panel at every measured size. */
.sh-panel .ov-form { gap: 12px; }
/* THREE PARAGRAPHS OF PROSE WERE COSTING 130px OF A 523px PANEL.
   Measured on a 390px phone: the intro 74.2px and the link explanation 57px,
   against a Share Link box of 152px and an Email Access box of 215px. Both are
   still here — the operator asked for tighter, not shorter — but at the size a
   supporting note should be rather than at body size, and with the margins the
   .eg-box wrapper was already providing removed. */
.sh-intro { margin: 0 0 2px; font-size: 10.5px; line-height: 1.35; }
.sh-box { padding: 10px 11px; margin: 0 !important; }
.sh-box .eg-box-title { margin-bottom: 5px; }
.sh-panel .sh-rows { gap: 5px; }
.sh-panel .sh-row { gap: 5px; }
.sh-panel .sh-row .ov-input { padding: 6px 8px; font-size: 11.5px; }
.sh-actions { margin-top: 6px; }
/* THE NOTE WAS STARTING AT THE BUTTON'S EDGE. Measured 0px between the bottom
   of Get Share Link and the top of the line under it, so the control read as
   part of the paragraph rather than as a button sitting in its own box. The
   5px declared here was being consumed: .sh-link-actions is a flex row with no
   bottom margin, so the note's top margin had nothing to clear. Space is now
   put on BOTH sides of the actions row, which is the thing that needs to stand
   apart from the text above and below it. */
/* Scoped to .sh-box so these outrank `.ov-note` on specificity rather than on
   file order. Written as bare single-class rules first, the note's margin-top
   was silently reset to 0 by a later .ov-note — measured 0px between the button
   and the line under it even after the margin was declared. Same equal-
   specificity trap as .mm-actions-row .ov-primary. */
/* THE SPACE IS ON THE ACTIONS ROW, both sides, and deliberately so.
   Putting it on the note's margin-top does not work: a fresh <p class="ov-note
   sh-link-help"> cloned into the same parent computes 11px from the rule
   below, while the note actually in the DOM computes 0px — some positional
   selector is zeroing it and it was not worth more of the budget to isolate,
   because the row is the thing that has to stand apart from the text above and
   below it anyway. Measured before: 0px between the bottom of Get Share Link
   and the line under it, so the button read as part of the paragraph. */
.sh-box .sh-link-actions { margin: 10px 0 11px; }
.sh-box .sh-link-help { font-size: 10px; line-height: 1.3; }
.sh-link-actions { display: flex; flex-wrap: wrap; gap: 8px; }
.sh-link-actions .ie-mini { width: auto; margin: 0; padding: 6px 14px; font-size: 11.5px; }
/* Turning the link off is destructive for everyone holding it, so it does not
   look like the neutral control beside it. */
.sh-link-actions .ie-mini.is-danger { color: #8c2626; border-color: #d9b3b3; }
.sh-link-actions .ie-mini.is-danger:hover { background: #8c2626; color: #fff; border-color: #8c2626; }
.sh-link-out { margin: 0 0 8px; }
.sh-access-summary { margin: 6px 0 5px; font-size: 10.5px; }
.sh-box-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 8px; margin-bottom: 4px;
}
.sh-box-head .eg-box-title { margin: 0; }
.sh-box-head .ie-mini.sh-view-access {
  width: auto; margin: 0; padding: 5px 13px; font-size: 11.5px; flex: 0 0 auto;
}
.sh-access-screen .sh-access-back {
  width: auto; margin: 0 0 8px; padding: 5px 12px; font-size: 11.5px; align-self: flex-start;
}
/* ONE WAY OUT OF THE SUB-SCREEN. The panel's X is still the right control for
   the panel, but while the access screen is up it did something different from
   the Back beside it — it closed the book's overlays and left the reader on the
   title page. Hiding it leaves exactly one way back, which goes where it says. */
.sh-panel.is-access .overlay-close { display: none; }
.ov-subtitle {
  margin: 0 0 8px; font-size: 13px; font-weight: 700; color: #5c4a2f;
}

/* ---- The read-only share viewer ---- */
/* ============ THE SHARED BOOK IS SIZED LIKE ANY OTHER ============
   REPORTED, AUGUST 2026: a shared Special Event, and before it a shared In
   Memory Of, opened on a phone as a tiny box in the middle of the screen. It
   could be flipped, but it could not be read. Not an In Memory Of problem and
   not a Special Events one — every shared book, because the cause is this
   rule and this rule is on the container.

   MEASURED, 390x844:

     owned book   .book-stage 375 x 822   .flipbook 345 x 539.1
     shared book  .book-stage   0 x 768   .flipbook   0 x  25

   `align-items: center` on a flex COLUMN sets the cross axis, which is the
   horizontal one — so every child shrink-wrapped to its content instead of
   filling the room. That is harmless for the banner, and fatal for the stage:
   in single-page mode `.book-stage` is `container-type: size` and the book is
   sized from `100cqw`. A shrink-to-fit stage has no width of its own to give,
   the book asks the stage how wide it is, the stage answers with the book —
   and the loop settles at zero.

   Desktop never showed it. There the flipbook takes its width from `97vw`
   rather than from the container, so it sizes itself and the stage
   shrink-wraps around it: 1062.4px on both pages, identical. The defect was
   only ever visible where the container query is used, which is exactly the
   phone.

   So the room no longer centres its children. The banner centres ITSELF with
   `align-self`, which is what was actually wanted, and the stage stretches to
   the room exactly as it does for the owner. Nothing about the read-only
   behaviour is touched: this is the geometry, not the permissions. */
.share-room { display: flex; flex-direction: column; }
.share-banner {
  align-self: center;
  margin: 14px auto 6px; padding: 7px 16px; border-radius: 999px;
  background: #f7f0e1; border: 1px solid #ddc9a3; color: #6b552f;
  font-size: 12px; font-weight: 600;
}
.share-banner span {
  margin-left: 6px; padding: 2px 9px; border-radius: 999px;
  background: var(--maroon, #7b3335); color: #fff;
  font-size: 9.5px; font-weight: 700; letter-spacing: .6px; text-transform: uppercase;
}

/* ---- Panel alignment ----
   .ov-panel textarea.ov-input grows to fill the form (flex: 1 1 auto), which
   is right for a diary entry whose text IS the page, but in the event panels
   it pushed Image Order and Save to the very bottom of the book page. These
   panels have media controls under the text, so the textarea is sized to its
   rows and the actions follow the content. */
.ev-entry-panel textarea.ov-input { flex: 0 0 auto; min-height: 76px; }
.ev-entry-panel .ov-form { justify-content: flex-start; }

/* Entry editor actions: Image Order beside Save, right aligned. */
.et-actions { display: flex; gap: 8px; justify-content: flex-end; align-items: center; margin-top: 6px; }
.et-actions .ie-mini,
.et-actions .ov-primary { width: auto; margin: 0; padding: 7px 18px; font-size: 12.5px; border-radius: 15px; }
.et-actions .ie-mini[hidden] { display: none; }

/* =====================================================================
   MY LIFE IN PICTURES — per-media page controls

   Same visual language as the Special Events media buttons, and separate
   classes for the same hard reason: `.page-full .entry-edit` and
   `.page-media-block .entry-edit` are hidden with !important earlier in this
   file, because Edit and Delete belong to an entry's TEXT page. Reusing those
   classes here would put the icons in the DOM and then hide them on exactly
   the pages they are meant for.

   Not merged with the .ev-* rules either: one module changing its icon size
   should not silently move another module's, and these two sets are free to
   diverge.
   ===================================================================== */

/* The date text inside a media page head, wrapped so icons sit beside it. */
.lp-head-text { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }

.page-head .lp-media-btn {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  width: 23px; height: 23px; margin-left: 4px; padding: 0;
  border: 1px solid #d8c7a4; border-radius: 50%;
  background: rgba(255, 252, 245, .94); color: #6b5335;
  cursor: pointer; transition: background .15s, border-color .15s, color .15s;
}
.page-head .lp-media-btn svg { width: 13px; height: 13px; fill: currentColor; }
.page-head .lp-media-btn:hover {
  background: #fff; border-color: var(--maroon, #7b3335); color: var(--maroon, #7b3335);
}
/* Delete reads as destructive on hover only — coloured at rest, it would pull
   the eye away from the picture, which is the point of the page. */
.page-head .lp-media-btn.lp-del:hover {
  background: #fbeaea; border-color: #b3413f; color: #a3302f;
}

/* The per-media panel — laid out to match the Special Events media panel.

   Its own .lp-* classes rather than reusing .evm-*: the two panels should be
   free to diverge, and one module changing a shared layout rule out from under
   another is exactly the coupling this project keeps hitting. The VALUES are
   deliberately identical, so the two read as the same panel; only the
   selectors differ. Generic classes (.ov-panel, .ov-form, .ov-field,
   .ov-label, .ov-input, .ov-drop, .ie-mini, .ov-note, .ov-notice) are shared
   as intended. */
.lp-group {
  padding: 9px 10px; border-radius: 8px;
  background: #faf5ea; border: 1px solid #e6d8ba;
}
.lp-group + .lp-group { margin-top: 8px; }
.lp-group .ov-field { margin: 0 0 6px; }
.lp-group .ov-field:last-child { margin-bottom: 0; }

.lp-preview { margin: 0 0 8px; text-align: center; }
.lp-preview-media {
  display: block; width: 100%; max-height: 200px;
  object-fit: contain; border-radius: 6px;
  border: 1px solid #dccdb0; background: #efe6d4;
}

.lp-swap { display: flex; align-items: stretch; gap: 8px; margin: 6px 0 2px; }
.lp-swap .ov-drop { flex: 1 1 auto; margin: 0; }
.lp-swap .ie-mini { flex: 0 0 auto; width: auto; margin: 0; padding: 0 14px; font-size: 11.5px; }
.lp-swap .ie-mini[hidden] { display: none; }

.lp-replace-name:empty { display: none; }

.ov-panel[data-overlay="lpMedia"] .ov-field { margin: 0 0 8px; }
.ov-panel[data-overlay="lpMedia"] .ov-input { margin: 0; width: 100%; }

.lp-actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; margin-top: 10px; }
.lp-actions .ov-primary { width: auto; margin: 0; padding: 7px 18px; font-size: 12.5px; border-radius: 15px; }

/* =====================================================================
   "CURRENT IMAGE / CURRENT VIDEO" in the Edit Entry panel

   Shows what is already on the page, above the control that would replace it.
   Deliberately small: this is context, not the subject of the panel, and the
   panel already has to fit a date, two media controls, two metadata pairs and
   a save button inside a book page.
   ===================================================================== */
.ee-current {
  display: flex; align-items: center; gap: 8px;
  margin: 0 0 7px; padding: 6px 9px;
  background: #faf5ea; border: 1px solid #e6d8ba; border-radius: 7px;
}
.ee-current-label {
  flex: 1 1 auto; min-width: 0;
  font-size: 11.5px; font-weight: 700; letter-spacing: .2px; color: #6b5335;
}
/* The thumbnail is NOT shown inline any more. It was a 74px preview sitting in
   every panel whether or not anyone wanted to look at it, which made a page
   holding an image and a video noticeably taller. The eye reveals it on
   demand, matching the compact row the Special Events media panel already
   uses — and matching what the eye means everywhere else in the reader. */
.ee-current-media {
  display: none;
  width: 100%; max-height: 190px; object-fit: contain;
  border-radius: 5px; border: 1px solid #dccdb0; background: #efe6d4;
}
.ee-current.is-open { flex-wrap: wrap; }
.ee-current.is-open .ee-current-media { display: block; margin-top: 7px; }

/* =====================================================================
   POEM PAGE — Poems Collection and Forty Poems One Love

   The right-hand page of every poem spread. The left page is the shared
   media frame, unchanged: poem books reuse it exactly as every other module
   does, which is why there is no image styling here at all.
   ===================================================================== */
.poem-page {
  display: flex; flex-direction: column;
  height: 100%; min-height: 0;
  padding: 4px 2px 0;
  text-align: center;
}

.poem-title {
  flex: 0 0 auto;
  margin: 0 0 3px;
  font-family: var(--font-display, 'Cormorant Garamond', serif);
  /* Reduced from 21-28px, then again from 18-23px to the shared scale in
     main.css — see --rp-title there for why the quote page reads the same
     token. Still the most prominent thing on the page. */
  font-size: var(--rp-title); font-weight: 700; line-height: 1.18;
  color: #9b2118;                 /* red, as specified */
  letter-spacing: .01em;
}
.poem-author {
  flex: 0 0 auto;
  margin: 0 0 9px;
  /* BLACK and bold. It was a muted brown at 13px, which read as a caption
     rather than as the author's name — the one piece of attribution on the
     page should not be the faintest thing on it.

     On the shared scale it also stays a step ABOVE the body at every width
     (13.5/13 wide, 12.5/12 narrow), so its prominence is now carried by size
     as well as by weight and colour. */
  font-size: var(--rp-author); font-weight: 700; letter-spacing: .02em;
  color: #1c1510;
}
.poem-rule {
  flex: 0 0 auto;
  width: 64px; height: 2px; margin: 0 auto 12px;
  background: linear-gradient(90deg, transparent, #c9a24e, transparent);
}

/* THE POEM ITSELF — EVERY LINE CENTRED.
   `pre-line` preserves every newline and every blank line between stanzas, so
   the shape of the verse is exactly as stored, and it still wraps a line too
   long for the page rather than forcing a horizontal scrollbar.

   WHY NOT pre-wrap. pre-wrap also preserves LEADING SPACES, and a preserved
   leading space is part of the line for centring purposes — so an indented
   line gets pushed off-axis by its own indentation. That is the defect the
   previous round worked around by centring the poem as a block instead: the
   text sat in an inline-block sized to its LONGEST line, with every line
   ranged left inside it. On a poem whose lines vary in length that reads as
   left-aligned, and on an indented one as pushed right — which is exactly the
   "shifted left or right" being reported.

   `pre-line` collapses the leading whitespace that was skewing the axis while
   keeping every line break, so each line can now centre on the true middle of
   the page. The trade-off is deliberate and worth stating: relative
   indentation is NOT preserved any more. Line breaks and stanza breaks are;
   horizontal indentation is not. The two cannot both hold — centring a line
   discards its horizontal offset by definition. */
.poem-body {
  flex: 1 1 auto; min-height: 0;
  overflow-y: auto; overflow-x: hidden;               /* long poems scroll; they never spill */
  -webkit-overflow-scrolling: touch;
  white-space: pre-line;
  text-align: center;
  /* No reserved gutter and no max-width: the scrollbar now sits near the page
     edge and the text takes back the space it was giving away. */
  margin: 0; max-width: none;
  padding: 0 6px 6px;
  font-family: var(--font-body, Georgia, serif);
  font-size: var(--rp-body); line-height: 1.62;
  color: #3c2f1e;
}

/* A poem book has no dates and no ornament rule. Hidden rather than
   conditionally omitted, so a shared component that emits one cannot leave a
   stray element on the page.

   .favorite-book is listed for the FORMAT properties only. The entry controls
   are split out below, because a favourite book DOES have them. */
.poem-book .page-head,
.favorite-book .page-head,
.poem-book .blk-entry-open,
.favorite-book .blk-entry-open { display: none !important; }

/* Read-only poem books keep hiding the row controls. A favourite book does not
   appear here: its owner edits and deletes their own poems. */
.poem-book .entry-edit,
.poem-book .entry-delete { display: none !important; }

/* Give the image page as much of the sheet as possible — the poem's picture
   is half the spread and should not sit in a small box.

   Both classes: a favourite book's image page has to look identical to a
   Poems Collection one. Nothing above the picture either — the payload sends
   no caption and no description, so pagination emits no title block at all. */
.poem-book .page-media-block,
.favorite-book .page-media-block { padding: 0; }
.poem-book .page-media-block .page-media,
.favorite-book .page-media-block .page-media { flex: 1 1 auto; min-height: 0; }

/* =====================================================================
   POEM BOOKS ARE READ-ONLY — remove every editing affordance.

   Hidden in CSS keyed off the .poem-book class the reader already applies,
   rather than by omitting markup: the toolbar and cover are shared components
   used by every book, and stripping elements from them for one book type is
   how a shared template starts growing per-module branches. The ACTIONS are
   already unreachable — these books set no apiBase, so isDiary() is false and
   nothing wires them up — this simply stops the buttons being visible.
   ===================================================================== */
/* .readonly-book takes the READ-ONLY half of the poem-book treatment and none
   of the format half: Daily Quotes & Weekly Poems is chronological and keeps
   its page heads, which .poem-book hides. Adding a selector to a comma list
   cannot change what the existing one matches. */
.reader-room.readonly-book .reader-toolbar .tool-add,
.reader-room.readonly-book .reader-toolbar .tool-upload,
.reader-room.readonly-book .reader-toolbar .tool-upload-back,
.reader-room.readonly-book .reader-toolbar .tool-share,
.reader-room.readonly-book .reader-toolbar .tool-members,
.reader-room.readonly-book .reader-toolbar .tool-pending,
.reader-room.readonly-book .reader-toolbar .tool-diarypw,
.reader-room.readonly-book .reader-toolbar .tool-book-settings,
.reader-room.readonly-book .reader-toolbar .tool-event-add,
.reader-room.readonly-book .reader-toolbar .tool-event-settings,
.reader-room.readonly-book .entry-edit,
.reader-room.readonly-book .entry-delete { display: none !important; }

.reader-room.poem-book .reader-toolbar .tool-add,
.reader-room.poem-book .reader-toolbar .tool-upload,
.reader-room.poem-book .reader-toolbar .tool-upload-back,
.reader-room.poem-book .reader-toolbar .tool-share,
.reader-room.poem-book .reader-toolbar .tool-members,
.reader-room.poem-book .reader-toolbar .tool-pending,
.reader-room.poem-book .reader-toolbar .tool-diarypw,
.reader-room.poem-book .reader-toolbar .tool-event-add,
.reader-room.poem-book .reader-toolbar .tool-event-search,
.reader-room.poem-book .reader-toolbar .tool-event-settings { display: none !important; }

/* A FAVOURITE BOOK IS WRITABLE. It keeps the tools that belong to other
   modules off, but Add Entry and both cover uploads stay — that is the whole
   difference from a read-only poem book, and it is expressed by this list
   being SHORTER, not by overriding anything above. */
.reader-room.favorite-book .reader-toolbar .tool-share,
.reader-room.favorite-book .reader-toolbar .tool-members,
.reader-room.favorite-book .reader-toolbar .tool-pending,
.reader-room.favorite-book .reader-toolbar .tool-diarypw,
.reader-room.favorite-book .reader-toolbar .tool-event-add,
.reader-room.favorite-book .reader-toolbar .tool-event-search,
.reader-room.favorite-book .reader-toolbar .tool-event-settings { display: none !important; }

/* ---- FILTER / CLEAR FILTER: one button, three glyphs -------------------
   Exactly one renders at a time, so the button never changes size.

   Books with a real date search keep the magnifier — nothing below applies to
   them, because every rule is scoped to .poem-book. A poem book shows a
   funnel, and once a filter is in force the same control clears it, so it
   shows the funnel struck through: the glyph states what the button will DO,
   which a bare cross did not.

   The maroon fill is deliberate reinforcement, not decoration: a filtered book
   is showing a SUBSET, and that should be readable at a glance rather than
   from memory. */
.reader-toolbar .tool-search .tool-ic-filter,
.reader-toolbar .tool-search .tool-ic-clear { display: none; }

.reader-room.poem-book .reader-toolbar .tool-search .tool-ic-find,
.reader-room.favorite-book .reader-toolbar .tool-search .tool-ic-find { display: none; }
.reader-room.poem-book .reader-toolbar .tool-search .tool-ic-filter,
.reader-room.favorite-book .reader-toolbar .tool-search .tool-ic-filter { display: block; }
.reader-room.poem-book .reader-toolbar .tool-search.is-filtered .tool-ic-filter,
.reader-room.favorite-book .reader-toolbar .tool-search.is-filtered .tool-ic-filter { display: none; }
.reader-room.poem-book .reader-toolbar .tool-search.is-filtered .tool-ic-clear,
.reader-room.favorite-book .reader-toolbar .tool-search.is-filtered .tool-ic-clear { display: block; }

.reader-toolbar .tool-search.is-filtered {
  background: var(--maroon, #7b3335);
  box-shadow: 0 2px 9px rgba(123, 51, 53, .34);
}
.reader-toolbar .tool-search.is-filtered svg { fill: #fff; }
.reader-toolbar .tool-search.is-filtered:hover { background: var(--maroon-dark, #5f2628); }

/* ---- COVER: artwork only -------------------------------------------------
   These books ship dedicated cover art that already contains the title, so
   the generated title block would print it twice. The artwork fills the whole
   cover face, using the same boundaries every other book uses. */
/* Poems Collection and Forty Poems carry their titles INSIDE the artwork, so
   printing it again over the plate doubles it. A favourite book does not: its
   cover is whatever the owner uploaded, or the stock plate, and neither has
   the book's name on it. So .favorite-book is deliberately NOT in this rule —
   it was, and that is why a favourite book opened with a blank cover. */
.reader-room.poem-book .cover-book-title,
.reader-room.full-cover .cover-book-title { display: none !important; }

/* THE ARTWORK STAYS INSIDE THE BOARD'S PLATE.
   An earlier attempt used `inset: 0`, which made the image the whole cover
   and swallowed the maroon board — the book stopped looking like a book.
   The plate keeps its normal insets; only the TOP one is reduced, because
   these covers hide the generated title and that 15% gap existed to clear it.
   Left inset is untouched: that is the spine side. */
/* POEM BOOKS ONLY. The reduced TOP inset exists because a poem collection's
   artwork already contains its title, so the band that would have cleared a
   printed title is not needed. A favourite book DOES print its title — its
   cover is the owner's upload or the stock plate, and neither carries the
   name — so it keeps the standard plate and the title keeps its own band
   above the image. Listing .favorite-book here is what made the two overlap. */
.reader-room.poem-book .cover-front .cover-art,
.reader-room.full-cover .cover-front .cover-art {
  inset: 7% 5.5% 6% 12.5% !important;
}
/* `cover` rather than `contain` here: the artwork is made for this plate, so
   filling it edge to edge is right, and any slight ratio difference should be
   cropped rather than leave bands inside the frame. */
.reader-room.poem-book .cover-front .cover-art img,
.reader-room.full-cover .cover-front .cover-art img,
.reader-room.favorite-book .cover-front .cover-art img {
  object-fit: cover !important;
}
/* NO favourites-specific cover geometry. It had its own title position and a
   22% plate, which opened a large gap that no other book had. A favourite book
   prints a title exactly like a diary does, so it takes the same defaults —
   one rule for every title-bearing book instead of a per-module copy. */

/* ---- POEM TEXT: centred, and using the width it should ------------------
   Centred as specified. The body previously reserved a wide gutter and the
   scrollbar sat well inside the page; it now runs near the page edge with the
   text taking the space back.

   scrollbar-width/`::-webkit-scrollbar` give a slim gold-brown bar instead of
   the platform default, which was both wide and visually foreign to the book. */
/* A full-width block, so each line centres on the page's axis rather than on
   the axis of the poem's longest line. This was an inline-block with its lines
   ranged left, which is what made short lines hug the left edge. */
.poem-body > .poem-text {
  display: block;
  text-align: center;
  white-space: pre-line;
  max-width: 100%;
}

/* ---- MISSING IMAGE -------------------------------------------------------
   A poem with no picture keeps its left page and its frame; only the contents
   change. Page STRUCTURE never varies, so every spread is image-left and
   poem-right whether or not a picture exists. */
.poem-noimage {
  display: flex; align-items: center; justify-content: center;
  width: 100%; height: 100%; min-height: 180px;
  background: #efe7d8;
  font-family: var(--font-display, 'Cormorant Garamond', serif);
  font-size: clamp(14px, 1.6vw, 19px); font-style: italic;
  color: #9a8663; letter-spacing: .02em;
}

/* ---- POEM FILTER PANEL ---------------------------------------------------
   Rows rather than a stack of loose fields. Author and Poem Name share the
   first row because they are the two most-used, genre takes the second, and
   the two actions sit together in a third.

   Apply and Clear are sized identically. Clear previously inherited the full
   width of a primary button while Apply did not, which is what made it read as
   an oversized afterthought rather than the other half of a pair.
   ------------------------------------------------------------------------- */
.rs-filter { display: flex; flex-direction: column; gap: 2px; }
.rs-filter-row { display: flex; gap: 10px; align-items: flex-end; }
.rs-filter-row .ov-field { flex: 1 1 0; min-width: 0; margin: 0 0 8px; }
.rs-filter-row .ov-input { width: 100%; margin: 0; }

.rs-filter-actions {
  display: flex; gap: 8px; justify-content: center; flex-wrap: wrap;
  margin: 4px 0 0;
}
.rs-filter-actions .ov-primary,
.rs-filter-actions .ov-secondary {
  width: auto; margin: 0;
  padding: 8px 20px; font-size: 12.5px; border-radius: 16px;
}
.rs-poem-result { margin: 2px 0 0; }

/* The shared First / Last row sits under the actions for these books, so all
   four controls read as one group. */
.reader-room.poem-book .ov-jump,
.reader-room.favorite-book .ov-jump { margin-top: 8px; }

/* =====================================================================
   ADD TO FAVORITES — the heart on a poem page

   Belongs to the capability module, so it is scoped to its own classes and
   named nowhere in the poem books' own rules. Note the class is .af-fav-btn
   and NOT .entry-edit: `.poem-book .entry-edit` is display:none !important,
   and reusing that class would have made the control invisible in exactly the
   two books it exists for.
   ===================================================================== */
.af-fav-btn {
  position: absolute; top: 10px; right: 12px; z-index: 6;
  width: 30px; height: 30px; padding: 0;
  display: grid; place-items: center;
  border: 1px solid rgba(150, 120, 70, .38); border-radius: 50%;
  background: rgba(255, 253, 248, .92);
  cursor: pointer;
  transition: background .15s, border-color .15s, transform .12s;
}
.af-fav-btn svg { width: 16px; height: 16px; fill: #b9a684; transition: fill .15s; }
.af-fav-btn:hover { background: #fff; border-color: var(--maroon, #7b3335); transform: scale(1.06); }
.af-fav-btn:hover svg { fill: var(--maroon, #7b3335); }
/* Added: filled, so the state reads at a glance from across the page. */
.af-fav-btn.is-favorited { background: var(--maroon, #7b3335); border-color: var(--maroon, #7b3335); }
.af-fav-btn.is-favorited svg { fill: #fff; }

/* The picker rows. */
.af-poem {
  margin: 0 0 10px; text-align: center;
  font: italic 400 13px/1.4 var(--font-ui, Poppins, sans-serif); color: #8d7c64;
}
.af-list { display: flex; flex-direction: column; gap: 8px; }
.af-row {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 12px; border: 1px solid rgba(150, 120, 70, .28); border-radius: 6px;
  background: #fffdf8;
}
.af-row-name { flex: 1 1 auto; font: 600 13px/1.3 var(--font-ui, Poppins, sans-serif); }
.af-row-count { flex: 0 0 auto; font: 400 11.5px/1 var(--font-ui, Poppins, sans-serif); color: #9a866a; }
.af-row-add { flex: 0 0 auto; }
.af-row.is-added { border-color: rgba(123, 51, 53, .45); }
.af-row.is-full { opacity: .62; }
.af-note a { color: var(--maroon, #7b3335); font-weight: 600; }

/* ---- Create Favorite Book: the two covers as a pair --------------------
   Equal columns so the drop zones match, and a single-column fallback below
   620px where two would each be too narrow to aim at. */
.fc-covers {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
  align-items: start;
}
.fc-covers .sb-field { margin: 0; }
.fc-covers .cc-drop { min-height: 0; padding: 14px 10px; }
.fc-covers .cc-drop-text { font-size: 12px; text-align: center; }
@media (max-width: 620px){
  .fc-covers { grid-template-columns: 1fr; }
}

/* ---- My Favorites: compact image controls ------------------------------
   `.ov-chip` is `flex: 1 1 auto` platform-wide, which makes a chip fill its
   row. That is right in a diary panel, where the chip carries a filename and
   an upload percentage; here the rows say "Current image" and "New image" and
   stretching them across the panel made two short labels look like two empty
   bars. Scoped to these two panels only — the diaries are untouched. */
[data-overlay="addPoem"] .ov-media-row,
[data-overlay="editPoem"] .ov-media-row {
  flex-wrap: wrap;
  justify-content: flex-start;
}
[data-overlay="addPoem"] .ov-chip,
[data-overlay="editPoem"] .ov-chip {
  flex: 0 0 auto;
}
.quote-page .quote-author {
  margin: 0 0 4px;
  text-align: center;
  font-family: var(--font-display, 'Cormorant Garamond', serif);
  /* THE SAME TOKEN THE POEM TITLE READS. A quote page has no separate title:
     this heading occupies the poem title's slot, in the same markup, and so
     takes the same step of the scale. It was clamp(17px, 2.2vw, 25px), two
     pixels above the poem title at the top and on its own separate curve —
     which is how a quote came to read as a bigger thing than a poem. */
  font-size: var(--rp-title);
  /* 700, MATCHING THE POEM TITLE. It asked for 800, which this family does not
     have: header.php loads Cormorant Garamond at 500/600/700, and the family
     ships nothing above 700 anyway. Measured — the same string at 700 and at
     800 renders to the identical width, so 800 was never producing a heavier
     face, only a heavier NUMBER. Stating 700 makes the two headings agree in
     the stylesheet as well as on screen. */
  font-weight: 700;
  color: var(--maroon, #7b3335);
  letter-spacing: .01em;
}
/* The quote itself reads as prose, not as verse: normal wrapping, centred.
   IT IS NO LONGER LARGER THAN A POEM'S BODY — same token, same size. The
   leading stays looser than the poem's 1.62, which is a prose/verse
   distinction rather than a size one and is the only thing that still differs
   between the two bodies. */
.quote-page .poem-body { white-space: normal; }
.quote-page .poem-text {
  white-space: normal;
  font-size: var(--rp-body);
  line-height: 1.72;
}

/* The hearts, and Remove on a diary quote page. Same shell as My Favorites'
   page controls — .fp-icon and .fp-actions are already the shared pair. */
.qd-heart svg { fill: #b9a684; transition: fill .15s; }
.qd-heart:hover { background: var(--maroon, #7b3335); border-color: var(--maroon, #7b3335); }
.qd-heart:hover svg { fill: #fff; }
.qd-del:hover { background: var(--maroon, #7b3335); border-color: var(--maroon, #7b3335); }
.qd-del:hover svg { fill: #fff; }

/* ---- Quote page: the diary version ------------------------------------
   The date is in the page head, where every diary page carries it. What the
   page adds is a kicker that says what this page IS, so a quote reads as a
   quote and not as something the diary's owner wrote. */
.quote-in-diary .quote-kicker {
  margin: 0 0 6px;
  text-align: center;
  /* The smallest step of the shared scale. An uppercase letter-spaced
     eyebrow, not read text, so it sits with .cd-shared-badge in the class
     A7b lists as deliberately below the 12px floor. */
  font: 700 var(--rp-kicker)/1 var(--font-ui, Poppins, sans-serif);
  letter-spacing: .22em;
  text-transform: uppercase;
  color: #b09a72;
}
.quote-in-diary .quote-author { margin-bottom: 2px; }

/* ---- Add-to-diary dialog ----------------------------------------------
   The quote leads, so the dialog is visibly about the page behind it. Then
   the two decisions, then the action — one idea per band, with the spacing
   doing the grouping rather than rules and boxes. */
.qd-card {
  margin: 0 0 18px;
  padding: 14px 16px 12px;
  border-left: 3px solid var(--maroon, #7b3335);
  border-radius: 0 6px 6px 0;
  background: #f8f2e6;
}
.qd-card .qd-text {
  margin: 0 0 8px;
  font-family: var(--font-display, 'Cormorant Garamond', serif);
  font-size: clamp(14px, 1.7vw, 17px);
  line-height: 1.6;
  color: #4a3722;
  /* Four lines is enough to recognise a quote by; a long one is not the
     subject of this dialog, the date is. */
  display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical;
  overflow: hidden;
}
.qd-card .qd-quote {
  display: block;
  font: 600 11.5px/1.3 var(--font-ui, Poppins, sans-serif);
  font-style: normal;
  letter-spacing: .04em;
  color: var(--maroon, #7b3335);
}
[data-overlay="quoteToDiary"] .ov-field { margin-bottom: 14px; }
[data-overlay="quoteToDiary"] .ov-hint {
  display: block; margin-top: 5px;
  font: 400 11px/1.4 var(--font-ui, Poppins, sans-serif); color: #9a866a;
}
[data-overlay="quoteToDiary"] .qd-save { margin-top: 4px; }

/* ---- A heart that is already linked ------------------------------------
   Filled, not merely tinted: the question it answers is yes or no, and a
   half-state reads as neither. */
.qd-heart.is-linked { background: var(--maroon, #7b3335); border-color: var(--maroon, #7b3335); }
.qd-heart.is-linked svg { fill: #fff; }
.qd-heart.is-linked:hover { background: var(--maroon-dark, #5f2628); }

/* ---- Add-to-diary dialog: label beside control -------------------------- */
[data-overlay="quoteToDiary"] .qd-row {
  display: grid;
  grid-template-columns: 74px 1fr;
  align-items: center;
  gap: 12px;
  margin-bottom: 12px;
}
[data-overlay="quoteToDiary"] .qd-row .ov-label { margin: 0; text-align: right; }
[data-overlay="quoteToDiary"] .qd-row .ov-input { width: 100%; }
[data-overlay="quoteToDiary"] .qd-hint {
  display: block; margin: -4px 0 16px 86px;
  font: 400 11px/1.4 var(--font-ui, Poppins, sans-serif); color: #9a866a;
}
@media (max-width: 520px){
  [data-overlay="quoteToDiary"] .qd-row { grid-template-columns: 1fr; gap: 5px; }
  [data-overlay="quoteToDiary"] .qd-row .ov-label { text-align: left; }
  [data-overlay="quoteToDiary"] .qd-hint { margin-left: 0; }
}

/* ---- The heart, and the badge that qualifies it -------------------------
   The badge belongs to its heart: it appears with it, disappears with it, and
   cannot push the quote around, because the pair is one inline-flex unit
   inside the same absolutely positioned .fp-actions row. */
/* No badge beside the heart. It widened the control row until it overlapped
   the author, and one label cannot represent a quote linked to several dates
   in several diaries. The dates are listed inside the panel instead. */
.qd-existing {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
  margin: 0 0 16px;
}
.qd-existing-lead {
  font: 600 10.5px/1 var(--font-ui, Poppins, sans-serif);
  letter-spacing: .1em; text-transform: uppercase; color: #a07a52;
}
.qd-existing-pill {
  padding: 4px 10px; border-radius: 11px;
  background: rgba(123, 51, 53, .08);
  border: 1px solid rgba(123, 51, 53, .22);
  font: 600 11px/1.3 var(--font-ui, Poppins, sans-serif);
  color: var(--maroon, #7b3335);
  white-space: nowrap;
}

/* =====================================================================
   PANEL FEEDBACK STAYS IN VIEW

   Two complaints with one cause. A panel scrolls, and both the upload
   progress bar and the validation message live in the document flow — so a
   long form, or an expanded image preview, pushes them below the visible
   area. The person is then either watching nothing while an upload runs, or
   being told about a missing field they cannot see.

   Sticky rather than moved. Repositioning ten progress bars and ten notes in
   the markup would have to be done panel by panel, would be undone by the
   next layout change, and gives no guarantee at all once a preview expands.
   Sticky guarantees it at every scroll position, in every panel, for free —
   including panels added later.

   The progress bar sticks to the BOTTOM: it belongs with the upload controls
   it reports on and following the content down keeps it beside them.
   The message sticks to the TOP: an error has to be the first thing seen, and
   the top edge is where the eye returns after pressing Save.
   ===================================================================== */
.ov-panel .ov-progress,
.page-overlay .ov-progress {
  position: sticky;
  bottom: 0;
  z-index: 6;
  /* Opaque, so content scrolling underneath does not read through it. */
  background: var(--page-bg, #f0e6d2);
  padding: 8px 0 6px;
  margin-top: 10px;
}

/* :not(:empty) MATTERS. `.ov-note` is used for more than messages — Special
   Events carries an empty one as a filename slot — and styling every instance
   turned those into empty red boxes the moment this rule arrived. A note with
   nothing in it is not a message and gets nothing. */
.ov-panel .ov-note:not([hidden]):not(:empty),
.page-overlay .ov-note:not([hidden]):not(:empty) {
  position: sticky;
  top: 0;
  z-index: 7;
  margin: 0 0 12px;
  padding: 10px 12px;
  border-radius: 6px;
  border: 1px solid rgba(150, 120, 70, .34);
  background: #f8f2e6;
  color: #6b5940;
  font: 600 12px/1.45 var(--font-ui, Poppins, sans-serif);
  text-align: left;
  box-shadow: 0 4px 12px rgba(60, 40, 20, .12);
}
/* Red is for an actual failure, whether marked by class or left unmarked by a
   panel that only ever writes errors into its note. */
.ov-panel .ov-note.is-error:not([hidden]):not(:empty),
.page-overlay .ov-note.is-error:not([hidden]):not(:empty) {
  border-color: rgba(163, 48, 47, .34);
  background: #fbf0ee;
  color: #8c2f2f;
}

/* ---- Special Events: the replacement chip uses the shared component ---- */
.evm-swap-row { margin-top: 8px; }

/* ---- Destructive actions, set apart --------------------------------------
   A delete that removes a whole book does not belong among the ordinary
   controls. Its own band, its own rule above it, and the consequence stated
   before the button rather than only in the confirmation. */
.ov-danger { margin-top: 20px; }
.ov-danger-rule {
  display: block; height: 1px; margin-bottom: 12px;
  background: linear-gradient(to right, transparent, rgba(163, 48, 47, .3), transparent);
}
.ov-danger-note {
  margin: 0 0 10px;
  font: 400 11.5px/1.5 var(--font-ui, Poppins, sans-serif);
  color: #8c6f5a;
  text-align: center;
}
.ov-danger-btn {
  display: block; width: 100%;
  padding: 9px 14px;
  border: 1px solid rgba(163, 48, 47, .45);
  border-radius: 7px;
  background: #fbf0ee;
  color: #8c2f2f;
  font: 600 12.5px/1 var(--font-ui, Poppins, sans-serif);
  cursor: pointer;
  transition: background .15s, color .15s, border-color .15s;
}
.ov-danger-btn:hover { background: #a3302f; border-color: #a3302f; color: #fff; }
.ov-danger-btn:disabled { opacity: .55; cursor: default; }

/* The favourite-book delete used to be a hover-only icon on the shelf tile.
   Removed: the tile and the icon are separate hover targets, so moving the
   cursor towards the icon left the tile and the icon vanished. A destructive
   action cannot depend on a hover state it fights with. It now lives in the
   book's own settings panel, where it is always visible. */

/* Book settings is a favourites-only tool. Hidden by default and shown only on
   a favourite book — the same shape every other module-specific tool uses. */
.reader-toolbar .tool-book-settings { display: none; }
/* inline-flex here used to override .tool's own `display: grid; place-items:
   center`, which is what un-centred the glyph inside the circular button.
   inline-grid keeps the show/hide behaviour and restores the centring. */
.reader-room.favorite-book .reader-toolbar .tool-book-settings {
  display: inline-grid; place-items: center;
}

/* ---- Page controls hug the binding -------------------------------------
   On a right-hand page they sit upper-LEFT, on a left-hand page upper-RIGHT —
   both next to the spine. That keeps them clear of the titles, which are
   centred, and clear of the page-turn arrows, which live on the outer edges.
   The default (outer corner) still applies to books that do not set a side. */
/* Higher, and further out. At 10px from the spine and 4px down they crowded
   the poem title, which is centred and begins near the top of the page. Moving
   them up and outward gives the title its band back while staying well inside
   the page-turn arrows, which sit on the outer edges of the BOOK rather than
   of the page. */
/* ---- The deliberate spacer before a Saturday poem ----------------------
   The page exists so the poem opposite keeps its image on the left. Saying so
   makes it read as intentional rather than as a page that failed to render. */
.page-spacer-note {
  height: 100%;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 0;
  padding: 0 8%;
}
/* A CARD, not a caption on an empty sheet.
   A centred device with one grey line still read as a page that had failed to
   render. Framing it — rule, ornament, label, title, rule — makes the page an
   intentional half-title, which is exactly what a printed book puts opposite a
   plate. */
.page-spacer-note::before {
  content: '\2766';
  font-family: var(--font-display, 'Cormorant Garamond', serif);
  font-size: clamp(30px, 3.8vw, 44px);
  line-height: 1;
  color: rgba(155, 33, 24, .6);
  margin-bottom: 18px;
}
.page-spacer-note .spacer-label {
  font: 700 10px/1 var(--font-ui, Poppins, sans-serif);
  letter-spacing: .3em;
  text-transform: uppercase;
  color: #b09a72;
  margin-bottom: 14px;
}
.page-spacer-note p {
  margin: 0;
  max-width: 88%;
  font-family: var(--font-display, 'Cormorant Garamond', serif);
  font-size: clamp(17px, 2.3vw, 24px);
  font-style: italic;
  letter-spacing: .02em;
  line-height: 1.5;
  color: #6b5236;
  text-align: center;
}
.page-spacer-note .spacer-sub {
  margin-top: 14px;
  font: 400 11.5px/1.5 var(--font-ui, Poppins, sans-serif);
  font-style: normal;
  letter-spacing: .06em;
  color: #a4917a;
}
.page-spacer-rule {
  display: block; width: 118px; height: 1px;
  margin: 20px 0;
  background: linear-gradient(to right, transparent, rgba(150, 120, 70, .6), transparent);
}

/* ---- Search result highlight -------------------------------------------
   A soft glow on the page a search landed on, so it is obvious which of the
   two matched. Removed by the reader after a few seconds; the animation is on
   a pseudo-element so nothing about the page's own layout or paint changes. */
.page.is-search-hit::after {
  content: '';
  position: absolute; inset: 0;
  border-radius: inherit;
  pointer-events: none;
  z-index: 30;
  /* A LIT PAGE, not a flashing one.
     Earlier versions pulsed, which reads as an alert rather than as "here it
     is". This holds one steady state instead: a solid gold frame, a warm wash
     across the page so it is visibly brighter than its neighbour, and a bloom
     that lifts it off the spread. It arrives quickly, sits still while it is
     read, and fades out. Nothing blinks. */
  box-shadow: inset 0 0 0 3px var(--gold, #c9a24e),
              inset 0 0 0 6px rgba(201, 162, 78, .22),
              0 0 34px 6px rgba(201, 162, 78, .55);
  background: linear-gradient(160deg,
              rgba(201, 162, 78, .20), rgba(201, 162, 78, .07) 55%, transparent);
  animation: mw-search-hit 3.4s ease-in-out forwards;
}
/* A corner marker, so the eye is drawn even before the frame registers. */
.page.is-search-hit::before {
  content: '';
  position: absolute; top: 0; left: 0; right: 0;
  height: 4px;
  z-index: 31;
  pointer-events: none;
  background: linear-gradient(to right, var(--gold, #c9a24e), rgba(201, 162, 78, .1));
  animation: mw-search-hit 3.4s ease-in-out forwards;
}
@keyframes mw-search-hit {
  0%   { opacity: 0; }
  6%   { opacity: 1; }
  76%  { opacity: 1; }
  100% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce){
  /* The marker bar too — it shares the animation, so without this it would
     stay at opacity 0 and never appear for these readers. */
  .page.is-search-hit::after,
  .page.is-search-hit::before { animation: none; opacity: 1; }
}

/* The gear reads better a touch larger than the line icons, and `place-items:
   center` on .tool only centres the SVG BOX — a path that is not centred
   within its own viewBox still looks off. This one is drawn symmetrically
   about (12,12), so the box and the mark agree. */
.reader-toolbar .tool-book-settings svg { width: 21px; height: 21px; display: block; margin: 0; }

/* =====================================================================
 * ICON ALIGNMENT — public books (Poems, Forty Poems, Quotes, Daily Quotes)
 *
 * A page is rendered into FOUR slots, not two: the two standing pages and the
 * two faces of the turning sheet. Rules written only for .page-left/.page-right
 * leave the sheet faces unstyled, which is what made icons appear to jump or
 * vanish mid-turn in earlier attempts. The mapping is fixed and direction-
 * independent — flipbook.js fills sheet-front from an ODD page index (a right
 * page) and sheet-back from an EVEN one (a left page) in both directions — so
 * .sheet-front pairs with .page-right and .sheet-back with .page-left.
 *
 * POSITIVE OFFSETS ONLY. .page-body is `position: relative; overflow: hidden`,
 * so it is both the positioning ancestor and the clipping ancestor. A negative
 * offset is clipped by the same box that positions it; that is the mechanism
 * behind "icons clipped, partly hidden, or missing entirely".
 *
 * Two-page layout only. flipbook.js switches to a single page at 860px, where
 * there is no binding to align to, so below that everything stays as it was.
 * ===================================================================== */
@media (min-width: 861px){

  /* Right-hand page — binding is on its LEFT. */
  .page-right .fp-actions,
  .sheet-front .fp-actions {
    right: auto;
    left: 6px;
    top: 2px;
  }
  .page-right .af-fav-btn,
  .sheet-front .af-fav-btn {
    right: auto;
    left: 2px;
    top: 4px;
  }

  /* Left-hand page — binding is on its RIGHT, so these were already on the
     correct side and only move a little closer to it. */
  .page-left .fp-actions,
  .sheet-back .fp-actions {
    right: 6px;
    top: 2px;
  }
  .page-left .af-fav-btn,
  .sheet-back .af-fav-btn {
    right: 2px;
    top: 4px;
  }
}

/* =====================================================================
 * ICON ALIGNMENT — date-based books, ONE reusable pattern
 *
 * Personal Diary, Couple Diary, In Memory Of, My Life in Pictures,
 * Special Events, My Favorites — and anything added later that puts an icon in
 * the page head. There is no per-module rule here and there should never be
 * one: every head icon lives in .page-head-actions, so this positions the
 * container and the icons come along.
 *
 * WHY A CONTAINER AND NOT OFFSETS PER ICON.
 * .page-head is `display: flex; justify-content: center`. An icon appended
 * loose is a flex item: it shifts the date off centre and sits at whichever end
 * the row happens to finish — the arrow side on a right-hand page. Absolutely
 * positioning each icon instead would have needed a fixed offset per class to
 * keep them together, which breaks the moment an icon's width or count changes.
 * That fragility is what produced the earlier revert cycles. One positioned
 * container is count-independent and width-independent.
 *
 * FOUR RENDERING SLOTS, NOT TWO. flipbook.js fills .page-left, .page-right and
 * the two faces of the turning sheet. sheet-front always takes an ODD page
 * index (a right-hand page) and sheet-back an EVEN one, in both directions, so
 * the pairing below is fixed. Styling only the two standing pages is what made
 * icons appear to jump mid-turn before.
 *
 * POSITIVE OFFSETS ONLY. .page-head is both the positioning ancestor
 * (position: relative) and the clipping ancestor (overflow: hidden), so a
 * negative offset is clipped by the same box that positions it.
 * ===================================================================== */
.page-head-actions {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

@media (min-width: 861px){

  /* Out of the centred row entirely, vertically centred against the head. */
  .page-head-actions {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    z-index: 8;
  }

  /* Left-hand page: binding is on its RIGHT.  Right-hand page: on its LEFT. */
  .page-left  .page-head-actions,
  .sheet-back .page-head-actions { right: 6px; }

  .page-right  .page-head-actions,
  .sheet-front .page-head-actions { left: 6px; }

  /* The per-icon margins existed to space them inside the flex row. The
     container's `gap` does that now, and leaving them in would push the group
     off its offset. */
  .page-head .entry-edit,
  .page-head .entry-delete,
  .page-head .ev-media-btn,
  .page-head .lp-media-btn { margin-left: 0; }

  /* ROOM FOR THE GROUP.
     The head's padding was reduced to 16px when the icons were inline and took
     their own space in the row. Out of flow they no longer do, so the padding
     has to reserve it or a long date runs underneath them. Symmetric, so the
     date still reads as centred: the widest group is Edit (30) + gap (6) +
     Delete (24) = 60, plus the 6px offset = 66. */
  .page-head {
    padding-left: 68px !important;
    padding-right: 68px !important;
  }
}

/* =====================================================================
 * POEM TITLE — clearance for the favourite heart.
 *
 * Moving the heart alone could never fix this, and the offset number is
 * misleading about why. .af-fav-btn is positioned against .page-body, and
 * .page-body sits INSIDE .page's `padding: 24px 10% 16px` — so `left: 2px` is
 * 2px into the TEXT COLUMN, not 2px from the page edge. It is already as far
 * toward the binding as it can go: any further needs a negative offset, and
 * .page-body is `overflow: hidden`, so a negative offset is clipped by the
 * same box that positions it.
 *
 * The title is centred across the full column, so a long one grows outward
 * until it reaches the icon. Reserving the icon's own width on BOTH sides is
 * what actually prevents that, and doing it symmetrically keeps the title
 * centred. The heart occupies 2px + 30px = 32px; 34px leaves 2px of air.
 *
 * The title does not move down. A very long title now wraps to a second line
 * slightly sooner than it did — that is the trade for never overlapping.
 * ===================================================================== */
@media (min-width: 861px){
  .poem-title {
    padding-left: 34px;
    padding-right: 34px;
  }
}


/* ---- CONTACT FORM STATES (Phase 7) --------------------------------------
   The enquiry form finally goes somewhere, so it needs the two states it never
   had: sent, and please-check-that. Scoped to .sb- so nothing else moves. */
.sb-form-ok{margin:0 0 18px;padding:12px 15px;border-radius:8px;
  background:rgba(46,110,64,.10);border:1px solid rgba(46,110,64,.35);color:#25502f;font-size:15px}
.sb-form-err{margin:0 0 18px;padding:12px 15px;border-radius:8px;
  background:rgba(138,43,43,.08);border:1px solid rgba(138,43,43,.35);color:#7a2626;font-size:15px}
/* The honeypot: off-screen rather than display:none, which some bots skip. */
.sb-hp{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}


/* ---- PLAN HIERARCHY (Phase 7) -------------------------------------------
   Poet's Corner and its features, on the public pricing book. Scoped to .plan-
   so nothing else moves. */
.plan-row.plan-group{border:0;padding:10px 0 2px}
.plan-row.plan-group .pl-name{font-size:11px;letter-spacing:.07em;text-transform:uppercase;
  opacity:.6}
.plan-row.plan-sub-item .pl-name{padding-left:14px;position:relative}
.plan-row.plan-sub-item .pl-name::before{content:'';position:absolute;left:3px;top:.6em;
  width:5px;height:1px;background:currentColor;opacity:.4}


/* ---- A LA CARTE PICKER (Phase 7) ---------------------------------------- */
.ac-wrap{max-width:1000px;margin:0 auto;padding:38px 22px 70px}
.ac-head h1{margin:0 0 6px;font-size:30px}
.ac-head p{margin:0 0 28px;opacity:.85;max-width:60ch}
.ac-grid{display:grid;grid-template-columns:minmax(0,1fr) 300px;gap:30px;align-items:start}
.ac-choices{display:flex;flex-direction:column}
.ac-group{margin:20px 0 4px;font-size:11px;letter-spacing:.08em;text-transform:uppercase;opacity:.6}
.ac-group:first-child{margin-top:0}
.ac-item{display:flex;align-items:center;gap:12px;padding:12px 4px;cursor:pointer;
  border-bottom:1px solid rgba(0,0,0,.08)}
.ac-item.is-child{padding-left:16px}
.ac-item input{width:17px;height:17px;flex:0 0 17px;cursor:pointer}
.ac-name{flex:1;min-width:0;font-size:15px}
.ac-price{font-size:14px;opacity:.75;white-space:nowrap}
/* The summary follows you down a long list. */
.ac-summary{position:sticky;top:22px;padding:22px;border:1px solid rgba(0,0,0,.12);
  border-radius:12px;background:rgba(255,255,255,.55)}
.ac-summary h2{margin:0 0 14px;font-size:17px}
.ac-totals{display:grid;grid-template-columns:auto auto;gap:8px 14px;margin:0 0 14px;font-size:14px}
.ac-totals dt{opacity:.75}
.ac-totals dd{margin:0;text-align:right;font-variant-numeric:tabular-nums}
.ac-pay{display:flex;justify-content:space-between;align-items:baseline;gap:12px;
  margin:0 0 18px;padding-top:14px;border-top:1px solid rgba(0,0,0,.12)}
.ac-pay strong{font-size:24px;font-variant-numeric:tabular-nums}
.ac-update,.ac-continue{display:block;width:100%;text-align:center;padding:11px 14px;
  border-radius:9px;font-size:15px;cursor:pointer;margin-bottom:9px;border:0}
.ac-continue{background:#6b1f2a;color:#fff;text-decoration:none}
.ac-continue[aria-disabled="true"]{opacity:.45;pointer-events:none}
.ac-update{background:transparent;border:1px solid rgba(0,0,0,.25)}
.ac-back{display:block;text-align:center;font-size:13px;opacity:.75}
.ac-empty{padding:30px 0;opacity:.8}
@media (max-width: 820px){
  .ac-grid{grid-template-columns:minmax(0,1fr)}
  .ac-summary{position:static}
}


/* ---- PLAN CARDS, REBUILT (Phase 7) --------------------------------------
   A BOOK PAGE IS A FIXED BOX: .plan-page is height:100%; overflow:hidden.
   Everything below is written for that. Nothing may rely on the page growing,
   and anything that overflows is simply gone — which is exactly how the
   À La Carte list lost half its modules. */

/* The hero was padded as though it had a screen to breathe in; on a book page
   that space comes straight out of the list underneath it. */
.plan-hero{text-align:center;padding:0 0 8px;border-bottom:1px solid rgba(150,120,70,.28);
  margin-bottom:10px;flex:0 0 auto}
.plan-pay-big{margin:0;line-height:1;display:flex;flex-direction:column;align-items:center;gap:3px}
/* NAMED CLASSES, NOT A BARE `span`. The old descendant selector styled the
   price itself as the small faded unit label — see the note in plans.php. */
.plan-pay-big .pl-figure{font:800 clamp(24px,3vw,38px)/1 var(--font-display,serif);
  letter-spacing:-.02em;color:var(--maroon,#7b3335)}
.plan-pay-big .pl-unit{font-size:11px;font-weight:400;letter-spacing:.09em;
  text-transform:uppercase;opacity:.6}
.plan-was{margin:5px 0 0;font-size:12px;opacity:.7}
.plan-blurb{margin:0 0 10px;font-size:13px;line-height:1.45;opacity:.85;text-align:center;
  flex:0 0 auto}
.plan-row .pl-price i{font-style:normal;font-size:11px;opacity:.55}

/* ---- À LA CARTE, INLINE --------------------------------------------------
   TWO COLUMNS, BECAUSE THE PAGE CANNOT GROW. Thirteen modules in one column
   overflow the page, and overflow is clipped — so the last several were
   invisible AND unselectable. Two columns halves the height; the category
   heading spans both so the grouping still reads. No internal scrolling. */
.ac-inline{display:flex;flex-direction:column;height:100%;min-height:0}
/* ONE MODULE PER ROW, AT EVERY WIDTH.
   This was `repeat(2, minmax(0, 1fr))` above 620px, which halved the space
   available to a name. The catalogue's real names are long — "Daily Quotes &
   Weekly Poems", "Forty Poems One Love", "Quotes Collection" — so a half-width
   tile broke them mid-word ("Quotes Collecti / on") and made the tiles tall to
   hold the wrap. A full-width row fits every name on one line and is shorter,
   not taller, than the two-column version it replaces. The list still scrolls
   (overflow-y: auto, declared further down), so a long catalogue is contained
   exactly as before. */
.ac-inline-list{display:grid;grid-template-columns:minmax(0,1fr);
  gap:0 16px;align-content:start;flex:1 1 auto;min-height:0;overflow:visible}
.ac-inline-group{grid-column:1/-1;margin:6px 0 1px;padding:0;border:0;
  font-size:10px;letter-spacing:.08em;text-transform:uppercase;opacity:.6}
.ac-inline-group:first-child{margin-top:0}
.ac-inline-item{display:flex;align-items:center;gap:8px;padding:4px 2px;cursor:pointer;
  border-bottom:1px solid rgba(150,120,70,.20);font-size:clamp(10px,.92vw,12.5px);
  line-height:1.2;color:#4a3826;min-width:0}
.ac-inline-item.is-child{padding-left:9px}
.ac-inline-item input{width:14px;height:14px;flex:0 0 14px;cursor:pointer;accent-color:#7b3335}
.ac-inline-item .pl-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;
  white-space:nowrap}
.ac-inline-item .pl-price{flex:0 0 auto;font-weight:700;font-size:11.5px}
.ac-inline-note{margin:6px 0 0;font-size:11px;opacity:.7;text-align:center}
button.plan-subscribe{border:0;cursor:pointer;font:inherit;width:max-content}
button.plan-subscribe:disabled{opacity:.45;cursor:default}

/* ---- THE LAST PAGE ------------------------------------------------------
   A book closes; it does not ask a question. */
.plan-page-end{text-align:center;display:flex;flex-direction:column;
  align-items:center;justify-content:center;height:100%}
.plan-end-mark{margin:0 0 18px;font-size:22px;color:var(--maroon,#7b3335);opacity:.5}
.plan-end-imprint{margin:0 0 6px;font:700 clamp(15px,1.7vw,20px)/1.2 var(--font-display,serif);
  color:var(--maroon,#7b3335);letter-spacing:.02em}
.plan-end-copy{margin:0 0 22px;font-size:12.5px;opacity:.7}
.plan-end-back{font-size:12px;opacity:.75;text-decoration:underline}

/* =====================================================================
   SUBSCRIPTION PLANS ON A PHONE — READ, DON'T TRUNCATE.

   The plans book is a reader page, so its content had to fit a fixed box.
   Everything about the mobile layout followed from that: `.pl-name` carried
   `white-space: nowrap` with `text-overflow: ellipsis`, so plan and A La Carte
   item names were CUT OFF rather than merely tight — the "compressed content"
   reported, and worst on A La Carte, which has the longest names and the most
   rows.

   The trade the client asked for explicitly: some vertical scrolling is
   acceptable, unreadable text is not. So on phones the page scrolls its own
   content instead of squeezing it, names wrap to as many lines as they need,
   and the rows get real vertical rhythm.

   Scoped to the plans book and to phones — the reader's own pages, its
   geometry and its turn are untouched, and desktop keeps the fixed-page
   layout it was designed with. */
@media (max-width: 860px) {
  .plans-flipbook .plan-page {
    overflow-y: auto; overflow-x: hidden;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
  }

  /* The truncation, removed. A name that wraps is readable; a name with an
     ellipsis through it is not a name. */
  .plans-flipbook .plan-row .pl-name,
  .plans-flipbook .ac-inline-item .pl-name {
    white-space: normal;
    overflow: visible;
    text-overflow: clip;
    line-height: 1.35;
  }

  /* Rows breathe. Aligned to the top so a wrapped name does not drag its
     price out of line with it. */
  .plans-flipbook .plan-row {
    align-items: flex-start;
    gap: 10px;
    padding-top: 9px;
    padding-bottom: 9px;
  }
  .plans-flipbook .plan-blurb { line-height: 1.5; }

  /* Titles one step down, which is what buys the rows their space. */
  .plans-flipbook .plan-hero .pl-name { font-size: 1.05em; }
  .plans-flipbook .pl-figure { font-size: 1.15em; }

  /* A La Carte is the densest page: nested child items need to read as
     children without losing width to indentation. */
  .plans-flipbook .ac-inline-item {
    align-items: flex-start;
    padding-top: 8px;
    padding-bottom: 8px;
  }
  .plans-flipbook .ac-inline-item.is-child { padding-left: 12px; }
  .plans-flipbook .ac-inline-note { line-height: 1.45; }

  /* The action stays a comfortable target while everything around it relaxes. */
  .plans-flipbook .plan-subscribe { min-height: 44px; padding: 10px 18px; }
}

/* =====================================================================
   ONE STANDARD BOOK BOX FOR THE BOOK-SHAPED PAGES THAT SCROLL.

   THE ROOT CAUSE OF "PLANS IS SMALLER THAN THE OTHER BOOKS": three different
   sizing systems were in play.

     - The READER sizes from its stage (`cqh`) because it scroll-locks and
       hides the footer, so the stage IS the viewport. Correct there.
     - PLANS carries `page-reader`, so it inherited that same height-driven
       formula — but it does NOT scroll-lock and it KEEPS its footer. Its stage
       is therefore ~160px shorter, the height term binds instead of the width
       term, and the book comes out visibly narrower than every other book on
       the site. Same CSS, different page contract.
     - STATIC BOOKS used neither: `height: auto` sized them to their content,
       so a book's proportions changed with how much text it happened to hold.

   The fix is to say what a book IS, once, for every page that shows a book and
   scrolls: a standard WIDTH taken from the viewport, height from the shared
   `--book-ratio`, and content that scrolls INSIDE the page rather than
   resizing the book around itself. That is the client's own rule — "keep the
   book the same size, allow scrolling inside the page" — and it makes plans,
   About Us, Contact and the info books all the same object.

   The reader is deliberately excluded and unchanged: it locks scrolling, so
   fitting its stage exactly is right for it and only for it.

   In mw.css because it loads last. `.page-plans-book .book-stage .flipbook`
   and `.page-reader .book-stage .flipbook` are both (0,3,0), so load order is
   what decides — the documented convention rather than an !important.

   THE CONDITION CARRIES A LANDSCAPE TERM, AND IT HAS TO. A phone on its side
   is 844 or 932 CSS px WIDE, so `max-width: 860px` alone missed every landscape
   phone over 860 — and the reader's own landscape block (which IS keyed on
   height) was still stripping the min/max-height floors off `.book-open` there.
   Measured at 932x430 before this term was added: every static book rendered
   334x122, ratio 2.74. The term is spelled exactly as reader.css spells it, so
   there is one definition of "landscape phone" in the codebase, not two.

   THE SELECTOR IS KEYED ON .page-static-book, NOT .page-auth-book. Every static
   book page carries .page-static-book — the auth pages, the create forms AND
   the six guest books, which are `page-static-book page-guest-book` and were
   therefore excluded from the shape rule while still receiving the width rule.
   Same specificity (0,3,0); strictly more of the pages this block already
   claims to cover. */
.sb-scroll { overflow-x: hidden; }   /* pairs reader.css's bare overflow-y; see the note below */

@media (max-width: 860px), (max-height: 520px) and (orientation: landscape) {

  .page-plans-book  .book-stage .flipbook,
  .page-static-book .book-stage .flipbook,
  .page-auth-book   .book-stage .flipbook {
    width: min(calc(100vw - 22px), 540px);
    max-width: none;
  }

  .page-plans-book  .book-open,
  .page-static-book .static-book .book-open {
    aspect-ratio: var(--book-ratio, 0.64) / 1;
    height: auto;
    min-height: 0;
    max-height: none;
  }

  /* CONTENT SCROLLS; THE BOOK DOES NOT MOVE.
     `.plan-table` was `flex: 1 1 auto; min-height: 0` with NO overflow, so
     when the page shrank it kept its full content height and spilled OUT of
     the table — which is why the feature rows painted straight through the
     Subscribe and Continue buttons. It was never a spacing problem; the
     overflow simply had nowhere to go. */
  /* OVERFLOW-Y ALONE MAKES AN ELEMENT SCROLL SIDEWAYS TOO.
     `overflow-x` defaults to `visible`, and `visible` CANNOT pair with a
     non-visible value — the spec computes it to `auto`. So every container
     given `overflow-y: auto` here silently became horizontally scrollable as
     well, and any content a pixel too wide let the whole form slide left and
     right under a finger. Measured on .plan-table at 393px: computed
     overflow-x resolved to `auto`, not `visible`.

     The axis is named explicitly wherever a container is meant to scroll in one
     direction only. Nothing here is meant to scroll sideways: a form that moves
     horizontally while being read is a defect, not a feature. */
  .page-plans-book .plan-table,
  .page-static-book .sb-scroll {
    overflow-y: auto;
    overflow-x: hidden;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
    min-height: 0;
  }

  /* The action sits below the scrolling list, in flow, never over it. */
  .page-plans-book .plan-subscribe,
  .page-plans-book .ac-continue { flex: 0 0 auto; position: relative; z-index: 1; }

  /* The page itself no longer clips what the table is now scrolling. */
  .plans-flipbook .page-body { overflow: hidden; display: flex; flex-direction: column; }

  /* ------------------------------------------------------------------
     PLANS TAKES THE SCROLLING PAGE'S CONTRACT, NOT THE READER'S.

     THE DEFECT THIS FIXES, measured at 393x749: the book rendered 371x580
     inside a stage only ~480px tall. `.book-stage` centres its child, so the
     100px of overflow split EVENLY — 50px up behind the Return button, which
     is why the toolbar painted over the price and the plan name, and 50px
     down past `.reader-room { overflow: hidden }`, which is why the lower gold
     corners vanished and only 15px of the 48px next-plan arrow was reachable.
     `document.scrollHeight` equalled the viewport exactly, so none of it could
     be scrolled to. That is the whole of "pushed against the edges", "does not
     fit inside the book" and "stuck and cannot be flipped".

     ROOT CAUSE: plans.php declares `page-reader`, so it inherits the reader's
     VIEWPORT CONTRACT — body pinned to 100svh, room pinned and clipped, book
     sized to fit the stage. Every one of those is correct for the reader,
     which scroll-locks and hides its footer. Plans does NEITHER. The block
     above already gave plans the book box of a page that scrolls; it never
     gave it the page contract that goes with it, so the book was sized by
     width while its container was still sized by a locked viewport. The two
     halves of one decision, only one of which shipped.

     This completes it rather than compensating for it: no size is capped and
     no number is restated. The document flows, the room grows to whatever the
     book needs, and the stage stops centring a child it is no longer too
     short for. Scoped to .page-plans-book, so the reader is untouched.

     Three releases, and each one names the reader mechanism it is opting out
     of. Nothing here restates a size:

       body       — the reader pins the document to 100svh so there is nothing
                    to scroll. Plans has a reachable footer and a book that may
                    exceed the fold, so the document flows and the viewport
                    becomes a FLOOR rather than a cap.
       .reader-room — `flex: 1` computes flex-basis 0, which DISCARDS any height
                    declared on the room (the trap recorded in A8ter). `flex:
                    none` is what lets the room take its content height; without
                    it the `height: auto` below is silently ignored. `overflow:
                    visible` stops the room amputating a book taller than it.
       .book-stage — the reader makes this a `container-type: size` box so the
                    book can be sized from `cqh`. Size containment means the
                    stage IGNORES ITS CONTENT for sizing, so with no definite
                    height it collapsed to its own 16px of padding and the
                    580px book overflowed it centred, landing at top: -159px.
                    `container-type: normal` cancels the containment; the stage
                    then measures the book it holds, as an ordinary flex item.
     ------------------------------------------------------------------ */
  body.page-plans-book { height: auto; min-height: 100svh; }
  .page-plans-book .reader-room {
    flex: none;
    height: auto;
    min-height: 0;
    overflow: visible;
  }
  .page-plans-book .book-stage { container-type: normal; flex: 0 0 auto; }

  /* The Stage-2 two-content-page exception stood here. It existed for ONE page:
     Profile, which carried its identity form on the left and Subscription on the
     right, so its book had to grow to hold both. Subscription now opens as an
     .sb-panel and Profile has a single content page like every other static
     book, so the exception has no subject left and the standard box applies
     unconditionally again. Removed rather than left dormant — a rule kept "just
     in case" is a rule nobody dares delete later. */
}

/* =====================================================================
   MOBILE INPUT ZOOM — THE STANDARDS-BASED FIX.

   iOS Safari zooms the page whenever a focused input's computed font-size is
   below 16px, and it does not zoom back out afterwards. Every field on this
   site was 13.5px, so every tap into a form left the visitor pinched in.

   16px is the whole fix. It is not a hack and it is not `user-scalable=no` —
   that would disable pinch zoom for everyone, which is an accessibility
   regression and is ignored by current iOS anyway. Making the text meet the
   threshold means the browser has no reason to zoom, and the field stays
   readable for people who need it.

   Applied to controls only, so surrounding copy keeps its scale. */
@media (max-width: 860px) {
  .sb-field input,
  .sb-field select,
  .sb-field textarea,
  .sb-input-wrap input,
  input[type="text"], input[type="email"], input[type="password"],
  input[type="search"], input[type="tel"], input[type="url"],
  input[type="number"], input[type="date"], select, textarea {
    font-size: 16px;
  }

  /* ---------------------------------------------------------------
     THE HINT TEXT DOES NOT NEED THE 16px, AND LOOKS WRONG WEARING IT.

     16px above is load-bearing: below it, iOS Safari zooms on focus and does
     not zoom back. But that threshold is measured against the INPUT's own
     computed font-size, not against ::placeholder — so the hint can be set
     back down without giving the zoom any reason to fire. Typed text stays at
     16px; only the empty-field prompt shrinks.

     Reported as "First Name / Last Name / Password / Phone Number look too
     large" on registration and contact. They were: every placeholder jumped
     2.5px on a phone while its label stayed at 12px, so the hint read larger
     than the thing naming it.

     14px, not smaller — this is text a visitor reads to know what a field
     wants, and the 12px floor elsewhere is for supporting copy, not for
     instructions. Written as three separate rules because one invalid
     selector in a combined list voids the whole list, and the prefixed
     spellings are exactly the engines this is aimed at. */
  .sb-field input::placeholder,
  .sb-input-wrap input::placeholder,
  input::placeholder, textarea::placeholder { font-size: 14px; }

  .sb-field input::-webkit-input-placeholder,
  .sb-input-wrap input::-webkit-input-placeholder,
  input::-webkit-input-placeholder,
  textarea::-webkit-input-placeholder { font-size: 14px; }

  .sb-field input::-moz-placeholder,
  .sb-input-wrap input::-moz-placeholder,
  input::-moz-placeholder, textarea::-moz-placeholder { font-size: 14px; }
}

/* =====================================================================
   THE DAILY PANELS READ AS CARDS, NOT AS STRIPS.

   Reported as "slightly too narrow… more like a thin rectangle than a
   balanced content card". Measured, the width was not the problem: the frame
   already takes 93% of a 390px phone. The PROPORTION was. A short quote inside
   a frame with desktop's horizontal-only padding produces a wide, shallow band
   — correct in a narrow desktop column, wrong as the full-width card it
   becomes when the room stacks.

   So the fix is vertical, not horizontal: real padding above and below, and a
   floor on the frame's height so a one-line quote and a four-line poem still
   read as the same kind of object. A couple of pixels of width are given back
   at the same time. */
@media (max-width: 760px) {
  .page-home .room-col { padding-left: 11px; padding-right: 11px; }
  .page-home .daily-frame { padding: 12px 11px; }
  .page-home .frame-inner {
    padding: 20px 16px;
    min-height: 128px;
    display: flex;
    flex-direction: column;
    justify-content: center;
  }
  .page-home .daily-section { width: 100%; }
}

/* =====================================================================
 * BOTTOM NAVIGATION — see includes/bottom-nav.php for how to remove.
 * Every rule below is inside a mobile media query, so desktop is untouched
 * whether the feature is on or off.
 *
 * FIVE EQUAL SLOTS. `flex: 1 1 0` + `min-width: 0` means the slots stay equal
 * no matter how long a label is, so the bar cannot go lopsided when "Log out"
 * replaces "Plans". Measured slot width: 64px at 320px, 78px at 390px — both
 * above the 44px touch floor, and the whole bar is 56px tall so every target
 * clears the floor on BOTH axes.
 *
 * LABELS ARE 12px, NOT 9.5px. The previous 9.5px was the single largest
 * accessibility finding on the public site (260 of the 560 sub-12px results in
 * the responsive audit came from this one declaration). 12px is the floor the
 * audit enforces everywhere else and every label still fits its slot at 320px.
 * ===================================================================== */
@media (max-width: 860px) {
  .mw-bottom-nav {
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 60;
    display: flex; align-items: stretch;
    /* 56px of bar plus whatever the phone's home indicator needs. env() is 0 on
       hardware that has no inset, so this is one rule for both. */
    height: calc(56px + env(safe-area-inset-bottom, 0px));
    padding-bottom: env(safe-area-inset-bottom, 0px);
    /* Near-opaque rather than translucent-plus-heavy-blur: this element is
       fixed over scrolling content, and a backdrop filter on a fixed layer is
       re-rasterised every frame of every scroll. At .96 the blur is a finishing
       touch rather than the thing making the bar readable, so the cheap
       fallback below looks the same. */
    background: rgba(247, 238, 222, .96);
    border-top: 1px solid rgba(120, 86, 44, .22);
    box-shadow: 0 -2px 10px rgba(60, 42, 30, .10);
  }
  @supports ((backdrop-filter: blur(6px)) or (-webkit-backdrop-filter: blur(6px))) {
    .mw-bottom-nav {
      background: rgba(247, 238, 222, .88);
      -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
    }
  }

  .mwn-item {
    flex: 1 1 0; min-width: 0;
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: 3px; text-decoration: none; color: #7d6a50;
    font: 600 12px/1 var(--font-ui, Poppins, sans-serif); letter-spacing: .1px;
    -webkit-tap-highlight-color: transparent;
  }

  /* A long label must never wrap or push its neighbours: the slot is fixed, so
     the LABEL is clipped.

     THE CLIPPING LIVES ON THE LABEL, NOT ON .mwn-item — and that distinction is
     the whole rule. `overflow: hidden` on the item clips its children, and
     .mwn-home-disc deliberately sits OUTSIDE the item's box via a negative
     top margin. Putting the ellipsis on the item cut 10px off the top of a 44px
     Home disc (23% of the circle) on every phone, while the label behaved
     identically either way. If a future change needs the item to clip, the disc
     has to stop being raised first. */
  .mwn-item > span {
    max-width: 100%;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .mwn-item svg { width: 21px; height: 21px; fill: currentColor; flex: none; }

  /* CURRENT STATE. Colour alone is not the whole signal — the label also goes
     to full weight — because a maroon-on-parchment shift is weak in sunlight. */
  .mwn-item.is-current { color: var(--maroon, #7b3335); font-weight: 700; }
  /* Touch devices have no pointer to move away, so :hover would latch after a
     tap. Only :active is defined here, deliberately. */
  .mwn-item:active { color: var(--maroon, #7b3335); }
  .mwn-item:focus-visible {
    outline: 2px solid var(--maroon, #7b3335); outline-offset: -3px; border-radius: 6px;
  }

  /* HOME IS RAISED, NOT MERELY LARGER: a disc breaks the row so the eye finds
     it without reading, which is the whole reason it is centred.

     THE DISC IS NEUTRAL UNTIL YOU ARE ACTUALLY ON HOME. It used to be maroon
     unconditionally, with the "current" state a slightly darker maroon — so
     Home read as selected on every page of the site, and the real current state
     was invisible. Default is now parchment with a maroon glyph; current fills
     with maroon. */
  .mwn-home { position: relative; }
  .mwn-home-disc {
    display: flex; align-items: center; justify-content: center;
    width: 44px; height: 44px; margin-top: -16px; border-radius: 50%;
    background: #fdf6e8; color: var(--maroon, #7b3335);
    border: 1.5px solid rgba(123, 51, 53, .32);
    box-shadow: 0 2px 7px rgba(60, 42, 30, .20);
    transition: background .15s, color .15s, border-color .15s;
  }
  .mwn-home-disc svg { width: 21px; height: 21px; fill: currentColor; }
  .mwn-home.is-current .mwn-home-disc {
    background: var(--maroon, #7b3335); color: #fdf6e8;
    border-color: var(--maroon, #7b3335);
    box-shadow: 0 3px 9px rgba(123, 51, 53, .38);
  }
  .mwn-home:active .mwn-home-disc { transform: translateY(1px); }
  @media (prefers-reduced-motion: reduce) {
    .mwn-home-disc { transition: none; }
    .mwn-home:active .mwn-home-disc { transform: none; }
  }

  /* THE BAR MUST NOT COVER THE LAST LINE OF A PAGE. The footer is the last
     thing in flow on every page that has one — verified as a DIRECT child of
     body on every rendered page, which is what the `>` combinator needs — so
     the reservation goes there and nothing else needs to know the bar exists.
     Hit-tested at 320x568, 390x844 and 844x390 across eight pages: zero
     controls unreachable at full scroll. */
  body:has(.mw-bottom-nav) > footer.site-footer {
    padding-bottom: calc(64px + env(safe-area-inset-bottom, 0px));
  }
  /* Anchored jumps must not land under the bar either. */
  body:has(.mw-bottom-nav) { scroll-padding-bottom: calc(64px + env(safe-area-inset-bottom, 0px)); }

  /* HIDDEN WITH THE BOOK, THE SAME WAY THE FOOTER IS. `book-open-mode` already
     means "a book is open" — reader.php sets it and shelf.js adds and removes it
     around the open/close animation — so the bar leaves and returns by itself
     with no JS and no second definition to keep in step. .page-reader covers the
     reader and Plans, which own the full viewport. */
  body.book-open-mode .mw-bottom-nav,
  body.page-reader .mw-bottom-nav { display: none; }
}

/* LANDSCAPE PHONES: SAME BAR, LESS OF IT.
   A landscape phone is ~390px tall. The site header takes 68px and the bar took
   another 56px, so a third of the screen was permanently chrome on a viewport
   that is already the tightest the site supports. The bar is kept — it is more
   useful in landscape, not less, because the hamburger is further from the
   thumb — but compacted to 46px by tightening the gap and the icon. The label
   stays at 12px; shrinking text to buy height is what created the finding this
   round removed. */
@media (max-height: 520px) and (orientation: landscape) {
  .mw-bottom-nav { height: calc(46px + env(safe-area-inset-bottom, 0px)); }
  .mwn-item { gap: 1px; font-size: 12px; }
  .mwn-item svg { width: 17px; height: 17px; }
  .mwn-home-disc { width: 36px; height: 36px; margin-top: -12px; }
  .mwn-home-disc svg { width: 17px; height: 17px; }
  body:has(.mw-bottom-nav) > footer.site-footer {
    padding-bottom: calc(54px + env(safe-area-inset-bottom, 0px));
  }
  body:has(.mw-bottom-nav) { scroll-padding-bottom: calc(54px + env(safe-area-inset-bottom, 0px)); }
}

/* Desktop never renders it. The hamburger remains the navigation system. */
@media (min-width: 861px) {
  .mw-bottom-nav { display: none; }
}


/* ===================================================================
   DAILY QUOTE NOTIFICATIONS TOGGLE  (Aug 2026)

   Appended at the END of the file, which is where mw.css says new rules go:
   appending cannot invert an existing contested pair, and moving blocks can.
   Every selector here is new (.pf-toggle*), so nothing existing is contested
   and gate 35 has no pair to check.

   THE CONTROL IS A REAL CHECKBOX. The input is taken out of the flow with
   opacity rather than `display:none` or `visibility:hidden`, because both of
   those remove it from the tab order and from the accessibility tree — the
   switch would then be unreachable by keyboard and silent to a screen reader.
   Kept in the layer, sized to the track, and painted over.

   TAP TARGET: the track is 30px tall inside a 44px-tall row, and the whole
   .pf-toggle label is clickable, so the target is the row — comfortably past
   the 44px floor the responsive audit checks.
   =================================================================== */
.pf-toggle-row {
  margin: 14px 0 0;
  padding: 13px 0 0;
  border-top: 1px solid #ece0c8;
}

/* NOT A <label>'s usual behaviour: see the note on .pf-toggle-input. The text
   is informational, so it does not take the pointer cursor and clicking it does
   nothing — `pointer-events: none` on the text is what actually prevents the
   implicit label activation that a <label> would otherwise give it. */
.pf-toggle {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 44px;
  padding: 4px 0;
  cursor: default;
}

/* The switch, and the ONLY clickable thing in the row. Positioned so the
   transparent input can be sized over it. */
.pf-toggle-switch {
  position: relative;
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  cursor: pointer;
}

/* Informational. Not inside the <label>, so clicking it does nothing at all —
   pointer-events alone would only have passed the click through to the label. */
.pf-toggle-text { cursor: default; }

/* Sized and stacked over the track rather than hidden — see the note above.
   All offsets are `auto`, so the box resolves to its STATIC position: exactly
   where it would have sat in the flex line, which is precisely over the track.
   Measured at 390px: input and track both (0, 35, 52, 30). Because the offsets
   are auto, the containing block is irrelevant, so a positioned or transformed
   ancestor cannot pull the two apart.

   z-index PUTS THE TRANSPARENT INPUT ABOVE THE TRACK, and it is load-bearing.
   Without it the track — later in DOM order, and positioned — paints over the
   input and swallows every pointer event aimed at it. The switch still worked,
   because both are inside the <label> and label activation toggles the input,
   but the input itself was unclickable: automated checks could not operate the
   control, and anything that clicks the element rather than the label would
   have silently done nothing. Found by the Playwright probe refusing to click
   it. */
/* THE HIT AREA IS THE SWITCH, NOT THE WHOLE ROW.
   Reported as "clicking the text does nothing" and "the switch is difficult to
   operate". Both came from the same thing: the input was stretched over the
   track and given z-index, so it sat ON TOP of the label's own text. A click on
   the words landed on a transparent input box only where the two overlapped,
   and elsewhere hit the label — which worked — so the control felt
   intermittent rather than broken, which is worse.

   The input now covers the ENTIRE label. It is still the real checkbox, so a
   click anywhere on the row, a tap, the spacebar and a screen reader all
   operate the same element, and the label needs no click handling of its own. */
.pf-toggle-input {
  position: absolute;
  z-index: 1;
  /* 60x44: the 52px track plus a small margin, at the 44px tap-target floor.
     Wide enough to hit comfortably on a phone, contained enough that reading
     the label cannot change a mailing preference by accident. */
  left: -4px; top: 50%;
  transform: translateY(-50%);
  width: 60px; height: 44px;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}

.pf-toggle-track {
  position: relative;
  flex: 0 0 auto;
  width: 52px; height: 30px;
  border-radius: 999px;
  background: #ded0b4;
  border: 1px solid #cdbb98;
  transition: background .18s ease, border-color .18s ease;
}

.pf-toggle-knob {
  position: absolute;
  top: 3px; left: 3px;
  width: 22px; height: 22px;
  border-radius: 50%;
  background: #fffdf7;
  box-shadow: 0 1px 3px rgba(60, 47, 30, .35);
  transition: transform .18s ease;
}

.pf-toggle-input:checked ~ .pf-toggle-track {
  background: var(--maroon, #7b3335);
  border-color: var(--maroon, #7b3335);
}
.pf-toggle-input:checked ~ .pf-toggle-track .pf-toggle-knob {
  transform: translateX(22px);
}

/* Keyboard focus must be visible on the TRACK, since the input it belongs to is
   transparent. Without this the switch is reachable by tab and gives no sign of
   it. */
.pf-toggle-input:focus-visible ~ .pf-toggle-track {
  outline: 2px solid var(--maroon, #7b3335);
  outline-offset: 2px;
}

/* Saving in progress: the control stops accepting input but does not move, so
   the switch never shows a state the server has not confirmed. */
.pf-toggle-input:disabled ~ .pf-toggle-track { opacity: .55; }
.pf-toggle-input:disabled { cursor: default; }

.pf-toggle-text { display: block; }

.pf-toggle-name {
  display: block;
  font: 600 13px/1.3 var(--font-ui, Poppins, sans-serif);
  color: #3c2f1e;
}

/* TYPOGRAPHY FLOOR: this is read while deciding whether to switch the setting
   on, so it sits at the 12px floor rather than below it. */
.pf-toggle-hint {
  display: block;
  margin: 2px 0 0;
  font: 400 12px/1.4 var(--font-ui, Poppins, sans-serif);
  color: #8d7c64;
}

.pf-toggle-status {
  margin: 7px 0 0;
  font: 500 12px/1.4 var(--font-ui, Poppins, sans-serif);
  color: #6b7a4a;
}
.pf-toggle-status.is-error { color: var(--maroon, #7b3335); }



/* ===================================================================
   PLANS — SERVER-RENDERED FIRST SPREAD, and the subscriber's own card.
   Appended at the end of the file, where mw.css says new rules go.
   =================================================================== */

/* display:contents, NOT block. .plan-page is height:100%, which resolves
   against its parent; a wrapper that generated a box would become that parent
   and the page would collapse to nothing. display:contents makes the wrapper
   generate no box, so .plan-page remains the layout child of .page-body and
   the served markup lays out identically to the JavaScript-built one.
   The spread half is the default because the spread is the wider layout. */
.plans-flipbook .pp-boot { display: contents; }
.plans-flipbook .pp-boot-single { display: none; }
.plans-flipbook .page-head .pp-boot-spread { display: inline; }

/* THE SINGLE-PAGE HALF MUST FOLLOW THE BASE RULES, NOT PRECEDE THEM.
   These selectors are equal in specificity to the three above (0,2,0), so the
   later one wins and a media query buys no priority whatsoever. The first
   attempt put this block earlier in the file, mid-way through mw.css, and the
   base rules appended here silently overrode it: phones and tablets rendered
   SILVER as the opening page and showed both page titles at once. Caught by
   measuring a purpose-built fixture — the standard harness cannot render this
   page because it is PHP-generated, and its stale plans.html fixture reported
   a completely different book.

   This is the equal-specificity order trap gate 35 exists for. Keeping the two
   halves adjacent, base first, is what makes the relationship visible.

   860px is the breakpoint reader.css uses to hide .page-left; it is already
   used by this book elsewhere in this file and is on the grandfathered list
   gate 36 checks. */
@media (max-width: 860px) {
  .plans-flipbook .pp-boot-spread { display: none; }
  .plans-flipbook .pp-boot-single { display: contents; }
  .plans-flipbook .page-head .pp-boot-spread { display: none; }
  .plans-flipbook .page-head .pp-boot-single { display: inline; }
}

/* THE CURRENT PLAN'S BADGE. Deliberately not a button and not maroon: it is a
   statement of fact, not an invitation, and making it look actionable is the
   thing this change exists to stop. Same box as .plan-subscribe so the card's
   rhythm is unchanged, in the muted parchment/green of a settled state. */
.plan-subscribe.is-current {
  background: #eef1e4;
  color: #4f5b33;
  border: 1px solid #c8d2ac;
  box-shadow: none;
  cursor: default;
}
.plan-subscribe.is-current:hover { background: #eef1e4; }

.plan-cta-tick {
  margin-right: 7px;
  font-weight: 700;
}


/* ===================================================================
   SUBSCRIPTION NOTIFICATION BAR  (Aug 2026)

   SITE CHROME. Sits above the header and is counted in --site-header-h, so
   every book formula already subtracts it. Appended at the end of the file,
   where mw.css says new rules go; every selector is new.

   HEIGHT IS DECLARED, NOT MEASURED. main.css does
   calc(var(--site-header-base) + var(--mw-subbar-h)); the bar is then given
   exactly --mw-subbar-h, so the arithmetic and the pixels cannot drift. That is
   why the copy is one line, clipped rather than wrapped: a bar that wrapped to
   two lines would be taller than the number every book was sized against.

   An earlier revision made this a content-flow block sized by its own text. It
   measured 159px at 390x844 and 177px at 320x568 — a fifth of the viewport —
   and it changed page height, so it had to be kept off every book page by an
   exclusion list. Declaring the height instead removed both problems and the
   exclusion list with them.

   TWO STATES, ONE SHAPE. Expiring is the site's parchment and gold — a notice,
   not an alarm. Expired is the maroon every consequential action already uses.
   Neither is red: red is not in this palette, and a memorial site is the wrong
   place to introduce it.
   =================================================================== */
body.has-subbar { --mw-subbar-h: 40px; }

/* STICKY, WITH THE HEADER. The header is `position: sticky; top: 0; z-index: 60`
   and the bar sits immediately above it, so on scroll the bar left and the
   header stayed — hiding the one message a lapsed customer most needs.

   The bar sticks at top:0 and the header is pushed down to stick BELOW it (see
   the .site-header rule that follows), so the two travel together as one piece
   of chrome. z-index is one higher than the header's so the bar is never
   overlapped by it during the transition.

   NO LAYOUT CHANGE. Both elements were already in the flow and still are —
   `position: sticky` reserves its space, unlike `fixed` — so --site-header-h
   still describes the total chrome height exactly, and every book formula that
   subtracts it is unaffected. */
.mw-subbar {
  position: sticky;
  top: 0;
  z-index: 61;
  height: var(--mw-subbar-h);
  overflow: hidden;
  border-bottom: 1px solid;
  font-family: var(--font-ui, Poppins, sans-serif);
}

/* The header sticks beneath the bar rather than under the viewport top. Scoped
   to `body.has-subbar` so a page with no bar keeps `top: 0` exactly as before. */
body.has-subbar .site-header {
  top: var(--mw-subbar-h);
}

.mw-subbar-in {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  height: 100%;
  max-width: 1180px;
  margin: 0 auto;
  padding: 0 clamp(12px, 3vw, 44px);
}

.mw-subbar-mark {
  flex: 0 0 auto;
  width: 7px; height: 7px;
  border-radius: 50%;
  background: currentColor;
}

/* TYPOGRAPHY FLOOR: both strings are read while deciding whether to renew. */
.mw-subbar-msg {
  flex: 0 1 auto;
  min-width: 0;
  font-size: 12.5px;
  font-weight: 700;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.mw-subbar-detail {
  flex: 0 1 auto;
  min-width: 0;
  font-size: 12px;
  opacity: .85;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.mw-subbar-cta {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  height: 26px;
  padding: 0 14px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 700;
  text-decoration: none;
  white-space: nowrap;
}

.mw-subbar-expiring {
  background: #fdf6e6;
  border-bottom-color: #e3d0a4;
  color: #6b5628;
}
.mw-subbar-expiring .mw-subbar-cta { background: #c9a24e; color: #3c2f1e; }

.mw-subbar-expired {
  background: #f7ecec;
  border-bottom-color: #e0c3c3;
  color: var(--maroon, #7b3335);
}
.mw-subbar-expired .mw-subbar-cta { background: var(--maroon, #7b3335); color: #fffdf7; }

/* THE SECOND STRING GOES ON A PHONE, not the action. The headline carries the
   fact and the button carries the response; the detail line repeats the expiry
   date, which is on Profile and in every reminder email. Dropping it keeps the
   bar one line at 320px instead of clipping the button off the end. Removed
   from the layout only — a screen reader still reads it. */
@media (max-width: 620px) {
  .mw-subbar-detail {
    position: absolute;
    width: 1px; height: 1px;
    margin: -1px; padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  .mw-subbar-in { gap: 8px; }
}

/* The Profile renewal link when the subscription needs attention. The banner
   cannot render on a static book page, so this link carries the urgency there
   instead — same maroon the expired banner uses, so the two read as one
   system. */
.pf-renew.is-urgent {
  background: var(--maroon, #7b3335);
  color: #fffdf7;
  border-color: var(--maroon, #7b3335);
}

/* ===================================================================
   MODULES KEPT UNTIL EXPIRY, NOT RENEWED  (Aug 2026)

   Shown on Profile when a customer has dropped an A La Carte module during a
   plan change. Deliberately NOT styled as a warning: nothing has gone wrong and
   nothing has been taken away — it is a statement of what they still have.
   Parchment and gold, the same furniture as the expiring notice.
   =================================================================== */
.pf-dropped {
  margin: 14px 0 0;
  padding: 11px 13px;
  background: #fdf6e6;
  border: 1px solid #e3d0a4;
  border-left: 3px solid #c9a24e;
  border-radius: 7px;
}

.pf-dropped-head {
  margin: 0 0 6px;
  font: 700 12px/1.4 var(--font-ui, Poppins, sans-serif);
  color: #6b5628;
}

.pf-dropped-list {
  margin: 0 0 7px;
  padding-left: 17px;
  font: 400 12px/1.55 var(--font-ui, Poppins, sans-serif);
  color: #3c2f1e;
}

/* TYPOGRAPHY FLOOR: this explains why access continues, which is the sentence
   that stops a support email. It sits at 12px, not below. */
.pf-dropped-note {
  margin: 0;
  font: 400 12px/1.5 var(--font-ui, Poppins, sans-serif);
  color: #6b5628;
}
.pf-dropped-note a { color: var(--maroon, #7b3335); font-weight: 600; }


/* ===================================================================
   SCHEDULED SUBSCRIPTION CHANGE  (Aug 2026)

   Shown on Profile when a downgrade or switch is waiting for renewal. Styled as
   information, not as a warning: nothing has gone wrong and nothing has been
   lost — the customer asked for this, and the block exists so they can see it
   and undo it.
   =================================================================== */
.pf-pending {
  margin: 14px 0 0;
  padding: 12px 14px;
  background: #eef1f6;
  border: 1px solid #c9d2e0;
  border-left: 3px solid #6b7fa3;
  border-radius: 7px;
}

.pf-pending-head {
  margin: 0 0 5px;
  font: 700 12px/1.4 var(--font-ui, Poppins, sans-serif);
  color: #3d4a60;
}

/* TYPOGRAPHY FLOOR: this sentence is what stops a support email asking why the
   plan has not changed yet. 12px, not below. */
.pf-pending-text {
  margin: 0 0 9px;
  font: 400 12px/1.55 var(--font-ui, Poppins, sans-serif);
  color: #3c2f1e;
}

.pf-pending-cancel {
  min-height: 34px;
  padding: 7px 15px;
  border-radius: 999px;
  border: 1px solid #6b7fa3;
  background: transparent;
  color: #3d4a60;
  font: 700 12px var(--font-ui, Poppins, sans-serif);
  cursor: pointer;
}
.pf-pending-cancel:hover { background: #dde4ee; }

/* ===================================================================
   SUBSCRIPTION CHANGE CONFIRMATION PAGE  (Aug 2026)
   =================================================================== */
.sc-lead {
  margin: 0 0 14px;
  font: 400 13.5px/1.6 var(--font-ui, Poppins, sans-serif);
  color: #3c2f1e;
}

.sc-points {
  margin: 14px 0 18px;
  padding-left: 18px;
  font: 400 12.5px/1.65 var(--font-ui, Poppins, sans-serif);
  color: #4a3f2f;
}
.sc-points li { margin-bottom: 5px; }

.sc-actions { margin: 0; }

/* Current-plan card when renewal is due: the badge states the fact, the button
   offers the action. Stacked rather than side by side because the plan card is
   a book page and never wide enough for two pill buttons on a phone. */
.plan-cta-pair {
  display: flex;
  flex-direction: column;
  gap: 8px;
  align-items: stretch;
}

.plan-subscribe.plan-renew {
  background: var(--maroon, #7b3335);
  color: #fffdf7;
  border-color: var(--maroon, #7b3335);
}

/* Grace-period explanation on Profile. Maroon like the expired bar, so the two
   read as one system, but laid out as a list because it answers four separate
   questions and a paragraph would bury the reassuring one. */
/* ===================================================================
   THE EXPIRED-SUBSCRIPTION CARD  (Aug 2026, rebuilt)

   This was a bold line with four bullets under it — a paragraph wearing a
   border. Every part carried the same weight, so the one fact the customer
   needs (how long have I got, or is it already gone) read at the same volume
   as the footnote about deletion, and the block had no shape of its own.

   It is now a status card with four deliberate levels:

     chip      the state in one word, colour-coded, read first
     headline  the consequence, large enough to be the answer
     sub       one sentence of what to do about it
     facts     the dates, as a definition grid — a date is data, not prose
     notes     what stays true, demoted to the bottom

   TWO STATES, ONE COMPONENT. `.is-grace` is a warning: nothing has been lost
   yet. `.is-stopped` is the alert state, and takes the same maroon the
   site-wide bar and Profile's status text already use so the page does not
   invent a third red for the same condition.
   =================================================================== */
.pf-grace {
  margin: 14px 0 0;
  padding: 13px 15px 12px;
  background: #fdf7ee;
  border: 1px solid #e6d6b6;
  border-left: 3px solid #b8892f;
  border-radius: 8px;
}
.pf-grace.is-stopped {
  background: #f9eded;
  border-color: #e0c3c3;
  border-left-color: var(--maroon, #7b3335);
}


/* THE ANSWER, AT THE SIZE OF AN ANSWER. "6 days of full access left" is the
   whole reason somebody reads this block, so it is the largest thing in it. */
.pf-grace-head {
  margin: 0 0 5px;
  font: 700 clamp(15px, 1.6vw, 18px)/1.25 var(--font-display, serif);
  color: #3c2f1e;
}
.pf-grace.is-stopped .pf-grace-head { color: var(--maroon, #7b3335); }

.pf-grace-sub {
  margin: 0;
  font: 400 12.5px/1.5 var(--font-ui, Poppins, sans-serif);
  color: #5c4a33;
}

/* DATES AS DATA. A definition grid puts every label above its value in a
   consistent column, so the three facts scan in one pass instead of being
   read as three sentences. It wraps rather than scrolls; on a narrow page the
   columns become rows and nothing is truncated. */

/* TYPOGRAPHY FLOOR: read while deciding whether to renew. 12px, not below. */

/* ---- THE PAGE'S ACTION BUTTONS ARE SECONDARY, AND SHOULD LOOK IT ----
   Renew Subscription was a 190px-minimum pill with 11px/28px padding and a
   drop shadow, and Change Password sat under it at 22px padding. Both were
   sized like the primary action of a checkout on a page whose primary content
   is a form — they crowded the rows above and below them and, inside a book
   page that cannot grow, pushed content out of sight. Same vocabulary, one
   step down in scale. */
/* ---- THE BUTTON IS NOT PART OF THE BOX ----
   With 6px between them the Renew control read as the expired box's footer —
   an action belonging to that panel rather than to the subscription. The gap is
   now larger than the box's own internal spacing, which is what makes two
   things read as separate rather than nested. */
.pf-grace + .pf-renew,
.pf-grace ~ .pf-renew { margin-top: 20px; }

/* ---- TWO ACTIONS, ONE ROW ----
   Stacked and individually centred, these spent two rows of a book page that
   cannot grow. Side by side they spend one. `flex-wrap: wrap` means the pair
   drops to two lines only where the row genuinely will not fit, so the
   single-column case is the exception rather than the default. */
.pf-actions-row {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 8px;
  margin: 10px 0 0;
}

.pf-renew,
.pf-change-pw {
  /* The 44px floor applies at EVERY width, not only on touch. These sit beside
     each other now, so an unequal pair reads as a mistake — and a 38px control
     next to a 44px one is exactly that. Flex centring on both axes, for the
     same reason the Verify button needed it (A10l §6): `min-height` on a block
     box adds the space below the label instead of around it. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 0;
  min-height: 44px;
  border-radius: 999px;
  margin: 0;
  box-sizing: border-box;
}
/* SIZED SO THE PAIR ACTUALLY FITS. Measured with the real labels — the widest
   is "Change Subscription" — the pair needed 340px and the book page offers
   303px at 390 and 338px at 430, so it wrapped on the two most common phone
   widths. Trimming the horizontal padding and settling both at the 12px
   typography floor brings the pair to ~290px, which fits every width down to
   360. At 320 it still wraps, and that is the right answer: "where space
   allows" is a condition, not a target.

   `white-space: nowrap` because a wrapped label inside a pill turns a 44px
   control into a 60px one and breaks the pair's alignment — measured at 320
   before this was added. */
.pf-renew {
  padding: 9px 16px;
  font-size: 12px;
  white-space: nowrap;
  box-shadow: none;
}
.pf-change-pw {
  padding: 8px 14px;
  font-size: 12px;
  white-space: nowrap;
}

/* Subscription status value on Profile, coloured by state. The label is a
   fact, not an action, so it is tinted text rather than a pill — a pill would
   read as something to press. */
.pf-status { font-weight: 700; }
.pf-status-ok   { color: #4f5b33; }
.pf-status-warn { color: #8a6d1f; }
.pf-status-bad  { color: var(--maroon, #7b3335); }

/* ===================================================================
   CURRENT-PLAN RIBBON  (Aug 2026)

   A ribbon, not a button. The visual language has to make the difference
   obvious at a glance: a badge states what you have, a button offers what you
   can do. Two pill buttons side by side — "Subscribed" next to "Renew Now" —
   read as two choices, which is exactly the confusion this replaces.

   Gold on parchment, uppercase and letter-spaced: the site's own furniture, and
   nothing like the maroon of an action.
   =================================================================== */
/* ===================================================================
   PLAN CARDS — PRESENTATION PASS  (Aug 2026)

   Presentation only. Not one price, threshold or destination is touched here;
   every rule below changes how the existing markup looks.

   The cards read as an unstyled table on a page: price, a blurb, a list of
   rows with hairlines, a button. Nothing said which plan was better than which,
   nothing separated the price from the contents, and the current plan looked
   the same as the rest.

   Four changes, each with a reason:
     1. the price gets a plate of its own, so it stops being the first row
     2. the module rows get a zebra ground instead of ruled lines, which reads
        as a list rather than a form
     3. included / paid modules are distinguishable at a glance
     4. the current plan's card is tinted, so the customer's own plan is
        recognisable before any text is read
   =================================================================== */

/* ---- 1. THE PRICE, ON ITS OWN GROUND ---- */
.plan-hero {
  padding: 10px 8px 9px;
  margin: 0 0 10px;
  border: 0;
  border-radius: 8px;
  background: linear-gradient(180deg, rgba(255,253,247,.95) 0%, rgba(246,236,214,.9) 100%);
  box-shadow: inset 0 0 0 1px rgba(150, 120, 70, .26);
}
.plan-pay-big .pl-figure {
  /* The figure was already the biggest thing on the card; giving it the plate
     lets it come down slightly and still dominate, which buys back a row. */
  font-size: clamp(23px, 2.7vw, 34px);
}
.plan-was {
  margin: 4px 0 0;
  font-size: 11.5px;
  opacity: .72;
}

/* ---- 2. A CONTAINED PANEL, NOT A RULED TABLE ----
   Zebra banding was the previous attempt and it was still a spreadsheet: rows
   the full width of the page, each one a horizontal band, nothing holding them
   together. The list is now a PANEL with its own ground and border, and the
   rows inside it are entries rather than table rows — no horizontal rules at
   all, the separation carried by spacing and by a soft ground on each entry.

   IT HAS TO SCALE. Some plans carry four modules and some carry a dozen, so
   the panel scrolls inside itself rather than growing: `.plan-page` is
   `overflow: hidden`, so a table that grows past the card does not become a
   longer card, it becomes an invisible one. Scrolling here keeps every module
   reachable at any plan size, which is what "accommodate plans with many
   modules" requires. */
.plan-table {
  gap: 3px;
  padding: 8px;
  border-radius: 9px;
  background: rgba(255, 253, 247, .62);
  box-shadow: inset 0 0 0 1px rgba(150, 120, 70, .22);
  overflow-y: auto;
  min-height: 0;
  /* Thin, and only where the platform offers it — a scrollbar the width of the
     old rules would put a vertical line back in place of the horizontal ones. */
}

.plan-row {
  border-bottom: 0;
  border-radius: 6px;
  padding: clamp(4px, .6vh, 7px) 9px;
  background: rgba(255, 253, 247, .9);
  box-shadow: 0 1px 0 rgba(150, 120, 70, .1);
}
.plan-row .pl-price { color: #6d5327; }

/* A GROUP HEADING IS A LABEL, NOT AN ENTRY. It gets no card of its own, so the
   entries beneath it read as belonging to it rather than as siblings of it. */
.plan-row.plan-group {
  background: none;
  box-shadow: none;
  padding-left: 2px;
}
/* "included" is a different KIND of answer from "$25", so it looks different
   rather than merely smaller. */
.plan-row .pl-price i {
  /* 11px, which is what this rule already used. Shrinking it to 10.5 would
     have added a NEW entry to A7b's itemised typography-floor list for a
     purely decorative gain — the uppercase treatment and the colour already
     make "included" read as a different kind of answer from "$25". */
  font-size: 11px;
  letter-spacing: .04em;
  text-transform: uppercase;
  opacity: .75;
  color: #5f6b3d;
}
.plan-row.plan-group {
  margin-top: 8px;
  padding-bottom: 1px;
}
.plan-row.plan-group:first-child { margin-top: 0; }
.plan-row.plan-group .pl-name {
  opacity: .72;
  color: #8a6d1f;
  font-weight: 700;
}

.plan-blurb {
  margin: 0 0 9px;
  font-size: 12.5px;
  opacity: .8;
}

/* ---- 3. THE ACTION ---- */
.plan-subscribe {
  margin: 10px auto 2px;
  padding: 9px 26px;
  border-radius: 999px;
  font-size: 12.5px;
  letter-spacing: .02em;
  box-shadow: 0 2px 7px rgba(123, 51, 53, .22);
}
/* The inert current-plan state is a statement, so it is quiet: no fill, no
   shadow, nothing that invites a press. */
.plan-subscribe.is-current {
  box-shadow: none;
}

/* ---- 4. THE CUSTOMER'S OWN PLAN IS RECOGNISABLE ----
   `:has()` keeps this a pure CSS consequence of the stamp already being in the
   markup, so no PHP had to learn about it. Where :has() is unsupported the
   card simply looks like the others and the stamp still says which it is. */
.plan-page:has(.plan-ribbon) {
  background:
    linear-gradient(180deg, rgba(216, 180, 95, .13) 0%, rgba(216, 180, 95, 0) 190px);
}
.plan-page:has(.plan-ribbon.is-expired) {
  background:
    linear-gradient(180deg, rgba(164, 66, 63, .12) 0%, rgba(164, 66, 63, 0) 190px);
}

/* ===================================================================
   À LA CARTE — SELECTABLE TILES, NOT A CHECKLIST  (Aug 2026)

   Selection and pricing logic untouched: these are the same <label> and
   <input type="checkbox"> elements, and the form is still a GET that works
   with nothing running.

   The list read as a form with thirteen ruled rows and a 14px checkbox at the
   start of each. Two problems, both presentational: it was dull, and the
   checkbox was the smallest thing in a row that is entirely about being
   picked. The row is now the target and it shows its own state — the checkbox
   stays in the markup and stays reachable, but the TILE is what reads as
   selected.
   =================================================================== */
/* ---- THE LIST SCROLLS INSTEAD OF OVERFLOWING ----
   THIS IS THE BUTTON OVERLAP. `.ac-inline` is a flex column at `height: 100%`
   and the list is its `flex: 1 1 auto; min-height: 0` child — but with
   `overflow: visible`. A flex child that may not grow and may not scroll does
   the only thing left: it paints outside its box, straight over the submit
   button below it. The button was not mispositioned; the list was on top of it.

   `overflow-y: auto` gives the list somewhere to put the excess, which fixes
   the overlap and the scrolling complaint at once — the page stops growing, so
   there is nothing to scroll past. */
.ac-inline-list {
  gap: 6px 10px;
  overflow-y: auto;
  padding: 8px;
  border-radius: 9px;
  background: rgba(255, 253, 247, .55);
  box-shadow: inset 0 0 0 1px rgba(150, 120, 70, .2);
}

.ac-inline-item {
  gap: 8px;
  /* TOP, NOT CENTRE. A name that wraps to two lines would otherwise drag its
     checkbox and price to the middle of the tile and out of line with every
     unwrapped neighbour. */
  align-items: flex-start;
  padding: 7px 9px;
  border-bottom: 0;
  border: 1px solid rgba(150, 120, 70, .3);
  border-radius: 8px;
  background: rgba(255, 253, 247, .85);
  transition: background .12s, border-color .12s, box-shadow .12s;
}

/* ---- EVERY NAME IS READABLE IN FULL ----
   The names were `white-space: nowrap` with `text-overflow: ellipsis`, so
   "Daily Quote Notifications" became "Daily Quote Notif…" — the customer was
   asked to buy something they could not read the name of. Tiles wrap instead;
   a tile is not a table cell and has no column to keep. */
.ac-inline-item .pl-name {
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
  line-height: 1.3;
  overflow-wrap: anywhere;
}
/* The price stays on the first line, aligned with the start of the name. */
.ac-inline-item .pl-price { align-self: flex-start; padding-top: 1px; }
/* The control is the tile, so the checkbox no longer has to be the target;
   it stays visible and reachable, and stops being the smallest thing in a row
   that is entirely about being picked. */
.ac-inline-item input { margin-top: 1px; }

/* ---- A TILE IS A TOUCH TARGET ----
   Measured at 36px on every touch width — below the 44px floor, on the one
   control this page exists for. The tile IS the control now (the whole label
   toggles the checkbox), so the floor applies to the tile rather than to the
   14px box inside it. Applied only below the reader breakpoint, where the
   pointer is a finger; above it a compact row is correct and adding 8px to
   every entry would cost a module off the visible list. */
@media (max-width: 860px) {
  .ac-inline-item {
    min-height: 44px;
    align-items: center;
    padding: 8px 10px;
  }
  /* Centring the tile's contents is right for a single-line name, but a name
     that wraps must still line its price up with the first line. */
  .ac-inline-item .pl-price { align-self: center; padding-top: 0; }
}
@media (hover: hover) {
  .ac-inline-item:hover { border-color: #b8892f; background: #fffdf7; }
}

/* THE SELECTED STATE IS ON THE TILE. `:has()` again — the input already
   carries the state, so nothing needs to duplicate it in a class. Without
   :has() support the native checkbox still shows the tick, which is why the
   input is never hidden. */
.ac-inline-item:has(input:checked) {
  background: rgba(216, 180, 95, .2);
  border-color: #b8892f;
  box-shadow: 0 1px 4px rgba(150, 120, 70, .18);
}
.ac-inline-item:has(input:focus-visible) {
  outline: 2px solid var(--maroon, #7b3335);
  outline-offset: 1px;
}

.ac-inline-item .pl-price {
  font-size: 11px;
  color: #6d5327;
}
/* ---- CATEGORY LABELS DO NOT BREAK MID-NAME ----
   "Quotes Collection" was wrapping to two lines inside a grid cell sized for
   one, so a two-word category read as two fragments stacked on top of each
   other. The label spans both columns (`grid-column: 1/-1` above), so the
   available width was never the problem — the 10px uppercase type with .08em
   tracking simply measures wider than it looks, and the group heading was
   inheriting the ITEM's tight line-height.

   Given its own line-height and told not to break inside a word, a two-word
   category stays on one line at every supported width; a genuinely long one
   wraps at the SPACE, which is a legible break rather than a broken word. */
.ac-inline-group {
  margin: 8px 0 2px;
  color: #8a6d1f;
  font-weight: 700;
  opacity: .8;
  line-height: 1.45;
  white-space: nowrap;
  overflow-wrap: normal;
  word-break: keep-all;
}
/* Below the two-column collapse the cell is the full page width, so a long
   category can afford to wrap — but only between words. */
@media (max-width: 620px) {
  .ac-inline-group { white-space: normal; }
}

/* ---- THE LAST PAGE ---- */
/* "The End" is now the whole message, so it carries the weight the imprint
   and the strapline used to share. */
.plan-end-imprint {
  margin: 0 0 14px;
  font-size: clamp(19px, 2.2vw, 26px);
  letter-spacing: .06em;
}

/* ================= A FOLDED CORNER STAMP =================
   Three attempts, and the reason the first two were wrong is worth keeping:

     1. `display: block` full-width above the action — a gold slab that added a
        row to a card which is `height: 100%; overflow: hidden`, so every pixel
        it took was a row of the plan's own contents pushed out of sight.
     2. A small rectangle pinned to the corner — still a box, still reading as
        a label sitting beside the price rather than a mark ON the card.

   This is the third: a band folded diagonally across the corner, the way a
   stamp or a seal sits on paper. It is `position: absolute`, so it costs
   ZERO height and cannot add a row. It is `pointer-events: none`, so it can
   never intercept a click meant for the card.

   HOW THE FOLD IS MADE. The band is wider than the corner it crosses and is
   rotated 45deg; `.plan-page` is `overflow: hidden`, so the two ends are
   clipped by the card's own edge. That clipping IS the fold — no extra
   wrapper, no clip-path, and it degrades to a plain rotated band anywhere the
   transform is unsupported.

   WHY TOP-LEFT AND NOT TOP-RIGHT. The price is centred in `.plan-hero`, so
   either corner is geometrically equivalent — but the eye reads the card left
   to right, and a status that is read BEFORE the price is a status, while one
   read after it looks like a footnote about the price. Measured for overlap
   against the price, the blurb, the table and the action at every breakpoint;
   see tools/plans_layout.py. */
.plan-page { position: relative; }

.plan-ribbon {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 3;
  width: 146px;
  padding: 3px 0;
  /* ============ THE ARITHMETIC, BECAUSE GUESSING GOT IT WRONG ============
     The band's ends hang off the corner and are clipped by .plan-page's
     overflow:hidden — that overhang IS the fold. But the TEXT is centred in the
     band, so the visible chord has to be longer than the text or the first and
     last letters are clipped with the ends. They were: "SUBSCRIBED" rendered as
     a fragment at every width.

     For a 45deg band across the top-left corner, with the band's centre at
     (cx, cy) from that corner, the chord inside the card is sqrt(2)*(cx + cy).
     Previously the centre landed at (29, 27), giving 79px of visible band for
     ~95px of text — 16px short, which is exactly what was being cut.

     Now: the element is 146px wide, so its own centre is at (73, 10); the
     translate below puts it at (53, 39), giving sqrt(2)*92 = 130px of chord for
     ~82px of text (letter-spacing reduced from 1.1px to .6px). That is 48px of
     margin, so the word stays whole even if the type metrics shift.

     Moving the band inward costs reach across the card, which is why the price
     clearance is re-measured rather than assumed — see tools/plans_layout.py,
     which now checks BOTH that the stamp clears the content and that the stamp
     itself is fully inside the card. */
  transform: translate(-20px, 29px) rotate(-45deg);
  transform-origin: center;
  background: linear-gradient(180deg, #d8b45f 0%, #c9a24e 100%);
  box-shadow: 0 1px 4px rgba(60, 36, 14, .3);
  text-align: center;
  pointer-events: none;
}

/* EXPIRED IS NOT A DECORATION. A lapsed subscription is the one thing on this
   page the customer has to act on, so it takes the alert colour the site-wide
   bar and Profile already use for the same state rather than inventing a third
   red. Gold would have read as a badge of honour for having lapsed. */
.plan-ribbon.is-expired {
  background: linear-gradient(180deg, #a4423f 0%, #8d3335 100%);
  box-shadow: 0 1px 4px rgba(90, 26, 28, .34);
}

.plan-ribbon-text {
  /* INLINE-BLOCK, so the box is the WORD and not the band.

     As a block it filled the band's full width, which meant nothing could tell
     — in CSS or in a measurement — where the glyphs actually ended. The first
     check written for this reported the band's ends as clipped text and the
     fix chased the wrong number. Shrink-wrapped to the glyphs, the box IS the
     word: the stylesheet centres it with `text-align`, and a probe that asks
     for its corners gets the corners of the word. */
  display: inline-block;
  /* 11px, UNCHANGED. Dropping to 9.5px would re-introduce exactly the label
     size A1 records as the bottom-navigation regression, and the typography
     floor findings in A7b are a fixed, itemised list — this change must not
     add to it. */
  font: 700 11px/1.25 var(--font-ui, Poppins, sans-serif);
  /* .6px, down from 1.1px: at 10 characters that is 5px of width bought back
     for nothing, and the word still reads as a stamp. The SIZE stays 11px — see
     the A7b note above. */
  letter-spacing: .6px;
  color: #3c2f1e;
  text-transform: uppercase;
  white-space: nowrap;
}
.plan-ribbon.is-expired .plan-ribbon-text { color: #fff4ee; }

/* The band's diagonal is a fixed pixel length, so on a narrow card it would
   reach further across the page than it does on a wide one. Shrinking it below
   the two-page breakpoint keeps the same proportion of the corner covered. */
/* MEASURED, NOT ESTIMATED. On a narrow card the price is wide relative to the
   page while the fold's diagonal is a fixed pixel length, so the band reached
   69px in and crossed the centred figure — confirmed at 430, 390 and 320 by
   tools/plans_layout.py, and only there; every width from 768 up was already
   clear. Two changes together, because neither alone was enough: the fold gets
   shorter, and the price plate starts below the corner it has to clear. */
@media (max-width: 620px) {
  /* Same arithmetic on a narrower card: centre at (65, 33) gives sqrt(2)*98 =
     138px of chord. The band is not shrunk here — shrinking it was what clipped
     the text in the first place — only pulled slightly closer to the corner so
     it reaches less far across a narrow page. */
  /* THE BAND MUST SIT SQUARE TO THE CORNER, and on a phone it did not.
     A 45deg band reads as sitting ON a corner only when its centre is as far
     down the left edge as it is along the top one. Desktop is close: centre
     (53, 44.5), 8.5px out. This block put it at (65, 38.5) — 26.5px out, so
     the band rode up the top edge and met the card's left edge much lower than
     it met the top. That is the reported "sits slightly too high and does not
     align properly with the card", and it is why only mobile showed it.

     The fix redistributes the centre rather than moving the band outward, and
     it is the SUM that had to give: cx + cy sets how far the band reaches
     across the card, and the first attempt held the sum at 103.5 while
     squaring the offset — which put the band 9px lower and made it clash with
     .plan-blurb at 320px and 360px. tools/plans_layout.py caught that, which
     is the whole reason the stamp is measured rather than eyeballed.

     Settled at centre (49, 40.5): the same 8.5px offset desktop has and the
     operator has approved, with the sum pulled in to 89.5. That still leaves
     sqrt(2) * 89.5 = 126px of visible band for 62px of text — 64px of margin,
     so the word cannot clip — and the band clears every element on the card at
     all eleven measured widths. Element 130x31, so its own centre is
     (65, 15.5), hence the translate below. Desktop is untouched. */
  .plan-ribbon {
    width: 130px;
    transform: translate(-16px, 25px) rotate(-45deg);
  }

  /* ---- THE PRICE PLATE GIVES ITS HEIGHT BACK TO THE MODULES ----
     On a 320x568 phone the reader's page is about 418px, and the plate was
     taking 99px of it — nearly a quarter — leaving 201px for the module list.
     That is the reported cramping: the modules were not clipped by a bug, they
     were simply not given the room.

     Three small reductions rather than one large one, so the price still reads
     as the card's headline: the stamp clearance comes from the plate's own top
     padding instead of an extra 24px on top of it, the figure comes down a
     step, and the "was" line loses its separate margin. Measured after: the
     plate is ~30px shorter and the module list gains every pixel of it, while
     tools/plans_layout.py confirms the stamp still clears the price. */
  /* THE TOP PADDING IS NOT NEGOTIABLE, so the height comes from below it.
     The stamp needs the top-left corner — the word is ~82px long, which fixes
     the diagonal it must occupy — and the price is CENTRED, so the plate's
     first line has to start below the stamp's reach or the two collide. Cutting
     the top padding to 17px did exactly that: the stamp landed on "$312 before
     your 20% discount" at 320px.

     So every other measurement in the block gives instead, and together they
     return more than the padding would have: a smaller figure, a tighter "was"
     line, less plate padding below, a smaller gap to the blurb, and a blurb
     that stops taking a line and a half on a phone. */
  .plan-hero {
    padding: 22px 8px 6px;
    margin-bottom: 6px;
  }
  .plan-pay-big .pl-figure { font-size: clamp(20px, 6vw, 26px); }
  .plan-pay-big .pl-unit { font-size: 11px; }
  .plan-was { margin-top: 1px; font-size: 10.5px; }
  .plan-blurb { margin-bottom: 6px; font-size: 11.5px; line-height: 1.35; }
  .plan-table { padding: 6px; }
  .plan-row { padding: 4px 8px; }
}

/* The narrow phones are the only widths where the price still met the fold
   after the reduction above — the figure is 23px there against a card barely
   wider than the text. A little more clearance on the phone widths, rather
   than shrinking the price everywhere.

   430, NOT 380 OR 390. Both earlier attempts were rejected by gate 36, and
   both times because the number was picked to fit the measurement instead of
   from the scale. The standard scale is {360, 430, 640, 760, 860, 1024, 1180};
   430 is the "large phone" step, is already used elsewhere in this file, and
   as a max-width it covers 430, 390, 375 and 320 in one rule — which is every
   width the measurement flagged. */
@media (max-width: 430px) {
  .plan-hero { padding-top: 32px; }
}

/* The inert "Subscribed" control below the stamp, shown outside the renewal
   window. Quiet on purpose — it is a statement, and the stamp has already
   made it. */
.plan-cta-pair { display: block; }

/* ===================================================================
   PAYMENT PAGE — TWO PAGES, TWO QUESTIONS  (Aug 2026, corrected)

   Left: what am I buying and what happens. Right: how do I pay.

   THE MARKUP NOW MATCHES THIS COMMENT. It did not. `.pay-context-mobile` was
   described here and styled below, but nothing in payment.php ever carried the
   class: the context lived only in the RIGHT page, where the card-form panel
   (position: absolute; inset: 0) covered it. So a desktop customer saw a logo
   on the left and a card form on the right, and the summary of what they were
   about to pay for was never on screen. The stylesheet documented one design
   and the page implemented another.

   The context is now emitted into BOTH pages and exactly one copy is visible
   at any width. 860px is the breakpoint reader.css already uses to drop the
   left page; it is on gate 36's grandfathered list.
   =================================================================== */

/* ---- LEFT PAGE: the mark, then the summary ---- */
.pay-left {
  display: flex;
  flex-direction: column;
}

/* A MARK, NOT A PLATE. .sb-logo-plate centres itself over the whole page and is
   `position: absolute; inset: 0`, so it would cover the summary completely.
   This is a logo at the top of a document, and deliberately smaller than the
   plate's clamp(104px, 11vw, 176px). */
/* SMALL, AND OUT OF THE WAY. The mark was a centred 78px block with an 18px
   rule under it and another 18px below that — roughly 120px of masthead before
   the summary started, on a page whose left half then had to be scrolled to
   read. A logo on a receipt is an identifier, not a header: it goes in the
   upper-left at a size you notice without reading, and the content starts at
   the top of the page rather than a third of the way down. */
.pay-left-mark {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 10px;
}
.pay-left-mark img {
  width: clamp(30px, 2.6vw, 40px);
  height: auto;
  display: block;
  filter: drop-shadow(0 2px 5px rgba(60, 36, 14, .2));
}
.pay-left-mark img.img-missing { display: none; }


/* ---- THE LEFT PAGE READS WITHOUT SCROLLING ----
   Every gap on this page was tuned for a page holding a logo and nothing else.
   Now that it holds the whole transaction, the same values push the last rows
   below the fold. These are tightened only for the payment page's left half —
   the shared components keep their own rhythm everywhere else. */
.pay-left .pay-context-title { margin: 0 0 2px; }
.pay-left .sb-title-rule { margin-bottom: 10px; }
.pay-left .pay-row { padding: 5px 0; }
.pay-left .pay-explain { margin-top: 10px; padding-top: 8px; }
.pay-left .pay-explain p { margin: 0 0 5px; }
.pay-left .pay-explain p:last-child { margin-bottom: 0; }
.pay-left .pay-explain-head { margin: 0 0 5px; }
.pay-left .sb-lede { margin: 0 0 8px; }

/* ================= THE PANEL'S DEFAULT COMES FROM HERE =================
   Not from the `hidden` attribute, and not from JavaScript. CSS applies before
   the first paint, so there is no frame in which the card form is missing —
   which is what the "brief blank state before the card form appears" was.

   Above the breakpoint the panel IS the right-hand page and is always shown.
   Below it the panel covers the subscription details and only opens on
   "Pay Now". `.is-open` is a PHONE state; payment-flow.js clears it when the
   layout goes wide so it cannot leak across a resize. */
@media (max-width: 860px) {
  .page-payment .pay-form-panel { display: none; }
  .page-payment .pay-form-panel.is-open { display: block; }
}

/* ---- THE CLOSE BUTTON IS A PHONE CONTROL ----
   On desktop both halves are already on screen, so there is nothing to close:
   pressing × emptied the right-hand page and left no way back. The button is
   removed here, and payment-flow.js ALSO refuses to close at this width — CSS
   visibility is presentation, not a safety mechanism, and Escape does not go
   through the button. */
@media (min-width: 861px) {
  .page-payment .pay-form-close { display: none; }
}

/* ---- WHICH COPY IS VISIBLE ---- */
/* Desktop: the left page carries the context, so the right page's copy is off
   and the panel covering the right page leaves payment, and nothing else. */
.pay-context-mobile { display: none; }

.pay-context-title {
  margin-bottom: 2px;
}

.pay-form-lede {
  margin: 0 0 12px;
}

@media (max-width: 860px) {
  /* THE LEFT PAGE GOES, AND ITS COPY GOES WITH IT.

     auth.css re-shows .page-left for .page-auth-book pages at this width,
     because auth pages carry real content there. Payment now carries real
     content there too — but it is a DUPLICATE of what the right page shows, so
     letting that rule apply would print the summary twice, stacked. Scoped to
     the payment page, and matching auth.css's specificity (0,3,0): mw.css loads
     last so it wins the tie, which is this project's documented cascade rule
     rather than an !important. */
  .page-payment .static-book .page-left { display: none; }

  /* The context travels with the form instead, stacked above it, which keeps
     the reading order the two pages have on a wider screen. */
  .pay-context-mobile { display: block; }
}

/* ===================================================================
   PAYMENT — "PAY NOW" OPENER  (Aug 2026, resized)

   The card form lives in the $bookRightPanelHtml slot, the same sub-screen
   mechanism Profile uses for Edit Profile. Two shapes from one markup:

     two-page (>=861px)  both pages are on screen, so the panel is simply the
                         right half and is always open. The opener would be a
                         button that reveals something already visible, so it
                         is hidden.
     one-page (<=860px)  the left page is dropped, the context fills the
                         screen, and the panel covers it on demand. The opener
                         sits at the TOP so a customer who has decided to pay
                         does not scroll past the explanation to find it.
   =================================================================== */
.pay-open { display: none; }

@media (max-width: 860px) {
  .pay-open { display: inline-flex; }

  /* ================= SIZE, SPACING AND ALIGNMENT =================
     This was `width: 100%`, maroon and bold — a slab the full width of the book
     page, directly under the title, heavier than any other control on the site
     and out of scale with the summary beneath it.

     It is now the pill geometry every other primary action already uses, sized
     to its own label rather than to the page. It keeps the 44px tap-target
     floor, which is the one thing the full-width version had right.

     ALIGNMENT. .pf-head is a flex row on wider screens and was switched to
     `display: block` here, which left the button as a bare inline-flex box on
     its own line with nothing deciding where it sat — it read as misaligned
     against the rule above it and the summary below. The head is now a column
     with a defined gap, so the button shares the page body's left edge with
     every other element. */
  .page-payment .pf-head {
    display: flex;
    flex-direction: column;
    /* THE OVERLAP, AND IT IS NOT A PHONE QUIRK.
       .pf-head carries `flex-wrap: wrap` from its base rule, for the desktop
       row where the button drops below a long title. Switching the direction
       to `column` while leaving wrap on turns a bounded box into a MULTI-COLUMN
       flex container: once the items no longer fit the height, they wrap
       sideways into a second column and land on top of the content beside
       them. That is the reported overlap. A column that must stay one column
       has to say so. */
    flex-wrap: nowrap;
    align-items: stretch;
    gap: 12px;
  }

  .page-payment .pf-head .pay-open {
    /* THE LABEL SAT AT THE TOP OF THE BUTTON, and `justify-content` could not
       fix it: that centres the MAIN axis, which on a row-direction flex box is
       the horizontal one. Nothing was centring the cross axis, so the anonymous
       text item stretched to the full 44px and `line-height: 1` drew the glyphs
       against its top edge. `align-items: center` is the missing declaration,
       and `display: inline-flex` is restated here so the rule does not depend
       on a `.pay-open` declaration further up to make the box a flex container
       at all. */
    display: inline-flex;
    align-items: center;
    justify-content: center;
    align-self: flex-start;
    width: auto;
    min-height: 44px;
    /* Vertical padding as well as horizontal: with `min-height` alone the box
       had no intrinsic height of its own, so it depended entirely on the
       min-height to clear the tap floor. */
    padding: 11px 26px;
    border-radius: 999px;
    background: var(--maroon, #7b3335);
    border: 1px solid var(--maroon, #7b3335);
    color: #fffdf7;
    font: 700 14px/1 var(--font-ui, Poppins, sans-serif);
    letter-spacing: .3px;
    margin: 0;
    box-sizing: border-box;
  }
}

/* ===================================================================
   AUTH — THE VERIFY BUTTON

   THE FIRST ATTEMPT HERE MADE IT WORSE, and the reason is worth keeping.
   `.sb-form .sb-submit` in auth.css (specificity 0,2,0) already sizes this
   control: `display: block; width: auto; padding: 8px 26px; min-width: 140px`.
   A `.sb-submit-inline` rule is 0,1,0 and loses to it — so of everything that
   rule declared, only `min-height: 44px` took effect, because auth.css does not
   set that property and there was nothing to lose to.

   The result was the worst of both: a BLOCK box stretched to 44px with its
   content still laid out at the top, so the label sat 6.5px above centre where
   before it had been 2.5px off. Adding `line-height: 1` on top of that was a
   guess at the symptom and moved it further out.

   Two things were actually needed and neither is a size:
     1. WIN THE CASCADE. `.sb-form .sb-submit.sb-submit-inline` is 0,3,0, so
        the display mode this rule sets is the one that applies.
     2. CENTRE ON BOTH AXES. `min-height` on a block box adds space BELOW the
        content; only a flex box distributes it around the content. This is the
        same fault the Pay Now button had (A10k §7) — `text-align` and padding
        answer the horizontal question and nothing was answering the vertical
        one.

   Measured after the change rather than assumed: see the figures in A10l.
   =================================================================== */
.sb-form .sb-submit.sb-submit-inline {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: auto;
  min-width: 148px;
  min-height: 44px;          /* the touch floor */
  padding: 10px 26px;
  margin: 8px auto 0;        /* auto margins centre it in the flex column */
  box-sizing: border-box;
  text-align: center;
}

/* ===================================================================
   FORMS IN A BOOK PAGE — TWO COLUMNS WHERE THERE IS ROOM

   A book page is a fixed box with `overflow: hidden`, so a form taller than
   the page does not scroll into view, it disappears. Six full-width fields on
   Contact Us put the Send button below the fold; pairing the short ones halves
   the height without shrinking a single control.

   THE COLLAPSE POINT IS MEASURED, NOT GUESSED. 620px is the width at which a
   half of the column stops holding a usable text input beside its label in
   this type size — it is already the plans book's own collapse point and is on
   gate 36's grandfathered list, so no new threshold is introduced. Above it,
   related fields stay side by side on a phone in landscape exactly as they do
   on a laptop; the brief asks for that, and stacking everything unconditionally
   is what it asks us not to do.
   =================================================================== */
.sb-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px 14px;
  align-items: start;
}
/* Message spans both columns: it is the content, not a detail beside another. */
.sb-grid .sb-field-wide { grid-column: 1 / -1; }

/* min-width:0 on the cells, or a long placeholder sets the column width and
   the grid overflows the page instead of the text wrapping inside it. */
.sb-grid > .sb-field { min-width: 0; }
.sb-grid > .sb-field > input,
.sb-grid > .sb-field > select,
.sb-grid > .sb-field > textarea { width: 100%; box-sizing: border-box; }

/* The select is styled as the inputs are, so Subject does not read as a
   different KIND of field from the ones beside it. */
.sb-grid > .sb-field > select {
  min-height: 44px;                 /* the touch floor, same as the inputs */
  font: inherit;
  color: inherit;
}

/* ---- THE PAIRS HOLD ON A PHONE TOO ----
   620px was inherited from the plans book's own collapse point, and it was the
   wrong number for THIS component: it stacked every field on every phone,
   which is exactly what the brief says not to do. A 390px screen has roughly
   350px of usable page — two 170px halves — and a text input is perfectly
   usable at that width. The label sits ABOVE its control here, so a half column
   does not have to hold a label and a field side by side.

   The real limit is where a half column stops holding a legible placeholder.
   Measured at 360 (the small-phone step, and the narrowest supported width
   where two columns still work) — below that the columns are under 150px and
   the placeholders truncate, so the grid collapses. 360 is on the standard
   scale; 620 is grandfathered but was never right for a form. */
@media (max-width: 360px) {
  .sb-grid { grid-template-columns: 1fr; }
}

/* The sentence that answers "is this really what I pay?", inside the summary
   box directly under the total. Below the box it would read as a footnote. */
.pay-total-note {
  margin: 8px 0 0;
  padding-top: 8px;
  border-top: 1px solid rgba(150, 120, 70, .22);
  font-size: 12px;
  line-height: 1.45;
  color: #5c4a33;
}

/* ===================================================================
   SHELF SPACING WHERE THERE IS NO "ADD" BUTTON

   Reported as books touching the heading on invited-participant pages, and
   correct on the owner's. The two views share one stylesheet, so the
   difference had to be in the MARKUP — and it is: every library group is

       .library-heading   (margin-bottom: 14px)
       .cd-create-wrap    (margin-top: 14px)   <-- owner only
       .shelf

   `.cd-create-wrap` is rendered only when `canCreate` is true. An invited
   participant cannot create, so the paragraph is absent and its 14px of
   margin goes with it — the shelf slides up under the heading. Nobody removed
   the gap; the gap was a side effect of a button that guests never see.

   Giving the SHELF its own top margin makes the spacing a property of the
   shelf rather than an accident of what happens to sit above it. Where the
   button IS present, margins collapse against its own 14px rather than
   stacking, so the owner view is unchanged — which is the requirement.
   =================================================================== */
.library-group .shelf { margin-top: 14px; }

/* The memory library already declared 12px for this; it now says the same
   thing as everything else rather than a slightly different one. */
.page-memory-library .library-group .shelf { margin-top: 14px; }

/* ===================================================================
   CURRENT PARTICIPANTS — A COMPACT ROW, NOT A CARD

   Each participant occupied four stacked lines — name, email, status, actions
   — inside a bordered box, so a book with six people needed a great deal of
   scrolling to see who was in it. The panel is a management list: the owner is
   there to scan names and act on one of them, not to read six addresses.

   The same DOM, laid out as a two-column grid: identity on the left, actions
   on the right, status inline under the name. Nothing was removed — the email
   is still there, one size down and muted, because it is how two people with
   the same name are told apart.

   `min-width: 0` on the item is load-bearing. The row is a flex item of
   .mm-list, whose default `min-width: auto` lets a long unbroken address push
   the row wider than the panel; the status line's ellipsis can only do its job
   once the row is allowed to be narrower than its content.
   =================================================================== */
.mm-item {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas:
    "name    actions"
    "status  actions";
  align-items: center;
  column-gap: 10px;
  row-gap: 1px;
  min-width: 0;
  padding: 8px 10px;
}
.mm-name   { grid-area: name; font-size: 12.5px; line-height: 1.25; }
/* FLEX-WRAP WAS DEFEATING THE ELLIPSIS. The status line inherits
   `flex-wrap: wrap` from the base .mm-status rule, so a long address took a
   second line instead of truncating — the line measured 42px against a 20px
   single line, and that doubling was most of the row height on a phone.
   nowrap lets the ::after below shrink and ellipsise, which is what it was
   always styled to do. */
.mm-status { grid-area: status; gap: 6px; flex-wrap: nowrap; min-width: 0; }
/* The badge next to it reads Owner / Accepted / Pending, so "Status:" labels
   something already self-describing and costs a fifth of the line to say it.
   The element stays in the DOM; only this compact row hides it. */
.mm-status-label { display: none; }
.mm-status .cp-badge, .mm-memory { flex: 0 0 auto; }
.mm-actions {
  grid-area: actions;
  align-self: center;
  flex-wrap: wrap;
  justify-content: flex-end;
}

/* The email moves onto the status line as a quiet identifier rather than
   taking a row of its own. */
.mm-email {
  grid-area: status;
  display: none;
}
.mm-status::after {
  content: attr(data-email);
  flex: 1 1 auto;
  font-size: 10.5px;
  color: #9a8763;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

/* Narrow pages: the NAME takes the full width and the actions come up beside
   the status badge, rather than everything stacking into a third row.

   Stacking was the first attempt and it measured badly: a 44px full-width
   actions row of its own put every participant at 113.6px on a 390px phone, so
   four people no longer fitted on the panel. Sharing row two with the badge
   brings that to ~78px while the buttons keep their 44px target.

   The address comes off the line here because there is no room for it beside
   two buttons — 209px of content at 320px, of which the buttons need 135. The
   name identifies the row, and the full address is still in the DOM on
   .mm-email for anything that reads the list. */
@media (max-width: 430px) {
  .mm-item {
    grid-template-areas:
      "name    name"
      "status  actions";
    row-gap: 4px;
  }
  .mm-actions { justify-content: flex-end; }
  .mm-status::after { content: none; }
}

/* =====================================================================
 * ONE SCROLLBAR FOR THE WHOLE SITE
 *
 * Six surfaces had grown six treatments: 6px or 7px wide, radius 3 or 4, and
 * four different thumb colours — rgba(120,86,44,.35), rgba(120,86,44,.28),
 * rgba(150,120,70,.4) and #d7c08a — with only some of them declaring the
 * Firefox properties at all, so Firefox fell back to the platform default on
 * the rest.
 *
 * The Poems Collection treatment is the one that was actually designed for the
 * book: 6px, a gold-brown thumb on a transparent track, and a warmer thumb on
 * hover. It is declared once here and every custom-scroll surface opts in, so
 * a new panel gets the right bar by joining the list rather than by inventing
 * another variant.
 *
 * The reader toolbar is deliberately NOT in this list: it hides its scrollbar
 * entirely (reader.css), which is correct for a horizontal control strip.
 * ===================================================================== */
.poem-body,
.page-body.page-scroll,
.page-guest-book .page-right .sb-body,
.plan-table,
.ac-inline-list,
.sb-scroll,
.ov-panel,
.sh-access-screen,
.mm-list {
  scrollbar-width: thin;
  scrollbar-color: #d7c08a transparent;
}
.poem-body::-webkit-scrollbar,
.page-body.page-scroll::-webkit-scrollbar,
.page-guest-book .page-right .sb-body::-webkit-scrollbar,
.plan-table::-webkit-scrollbar,
.ac-inline-list::-webkit-scrollbar,
.sb-scroll::-webkit-scrollbar,
.ov-panel::-webkit-scrollbar,
.sh-access-screen::-webkit-scrollbar,
.mm-list::-webkit-scrollbar { width: 6px; height: 6px; }
.poem-body::-webkit-scrollbar-track,
.page-body.page-scroll::-webkit-scrollbar-track,
.page-guest-book .page-right .sb-body::-webkit-scrollbar-track,
.plan-table::-webkit-scrollbar-track,
.ac-inline-list::-webkit-scrollbar-track,
.sb-scroll::-webkit-scrollbar-track,
.ov-panel::-webkit-scrollbar-track,
.sh-access-screen::-webkit-scrollbar-track,
.mm-list::-webkit-scrollbar-track { background: transparent; }
.poem-body::-webkit-scrollbar-thumb,
.page-body.page-scroll::-webkit-scrollbar-thumb,
.page-guest-book .page-right .sb-body::-webkit-scrollbar-thumb,
.plan-table::-webkit-scrollbar-thumb,
.ac-inline-list::-webkit-scrollbar-thumb,
.sb-scroll::-webkit-scrollbar-thumb,
.ov-panel::-webkit-scrollbar-thumb,
.sh-access-screen::-webkit-scrollbar-thumb,
.mm-list::-webkit-scrollbar-thumb { background: #d7c08a; border-radius: 3px; }
.poem-body::-webkit-scrollbar-thumb:hover,
.page-body.page-scroll::-webkit-scrollbar-thumb:hover,
.page-guest-book .page-right .sb-body::-webkit-scrollbar-thumb:hover,
.plan-table::-webkit-scrollbar-thumb:hover,
.ac-inline-list::-webkit-scrollbar-thumb:hover,
.sb-scroll::-webkit-scrollbar-thumb:hover,
.ov-panel::-webkit-scrollbar-thumb:hover,
.sh-access-screen::-webkit-scrollbar-thumb:hover,
.mm-list::-webkit-scrollbar-thumb:hover { background: #c9a24e; }

/* ================= FILTERS =================
   DESKTOP: a two-column grid — all seven presets visible at once, discoverable
   without scrolling, which is what the column height is for.
   MOBILE: one snapping row (below). */
.ie-filters {
  display: grid; gap: 6px;
  /* Two columns is the phone-width fallback; the desktop block above sets
     three. Declared without a column count here so that block — which comes
     EARLIER in this file — is not overridden by this one at desktop widths. */
  grid-template-columns: repeat(auto-fill, minmax(88px, 1fr));
}
.ie-filters .ie-chip {
  width: 100%; padding: 0; overflow: hidden;
  display: flex; flex-direction: column; align-items: stretch; gap: 0;
  border-radius: 10px; min-height: 0;
  font-weight: 600; font-size: 11px; letter-spacing: .2px;
}
.ie-filter-thumb {
  display: block; width: 100%; aspect-ratio: 4 / 3;
  background: #cdbfa6 center / cover no-repeat;
}
.ie-filter-cap {
  display: block; padding: 6px 4px; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
}
.ie-filters .ie-chip.is-active {
  box-shadow: 0 0 0 2px var(--maroon, #7b3335);
}
.ie-filters .ie-chip.is-active .ie-filter-cap { color: #fff; }
.ie-filters .ie-chip.is-active {
  background: var(--maroon, #7b3335); border-color: var(--maroon, #7b3335);
  color: #fff; box-shadow: 0 2px 8px -2px rgba(123, 51, 53, .55);
}
/* ================= ADJUST =================
   DESKTOP shows all six rows at once. There is 700-900px of column and hiding
   five of six behind a chip strip was the "tiny scrolling box" — the strip is a
   phone affordance and is hidden here.
   MOBILE shows the strip and only the selected row. Same markup, same sliders,
   same state; only which rows are visible changes. */
.ie-adj-rows { display: flex; flex-direction: column; gap: 2px; }
.ie-adj-row {
  padding: 7px 9px; border-radius: 10px;
  border: 1px solid transparent;
  transition: background .14s, border-color .14s;
}
.ie-adj-row:hover { background: rgba(120, 86, 44, .06); }
/* A moved adjustment is legible at a glance without selecting each in turn. */
.ie-adj-row.is-set { background: rgba(201, 162, 78, .13); border-color: rgba(201, 162, 78, .4); }
.ie-adj-head {
  display: flex; align-items: center; gap: 8px; margin-bottom: 1px;
}
.ie-adj-name {
  flex: 1 1 auto; min-width: 0; margin: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ie-adj-val {
  position: static; flex: 0 0 auto; margin: 0;
  min-width: 34px; text-align: right;
  font: 700 12.5px/1 var(--font-ui, Poppins, sans-serif);
  color: var(--maroon, #7b3335); font-variant-numeric: tabular-nums;
}
.ie-adj-row.is-set .ie-adj-val { color: #8a6a1e; }
/* Per-adjustment reset, beside the value it resets. The only route back to
   neutral used to be "Reset adjustments", which threw away all six. */
.ie-adj-rst {
  flex: 0 0 auto; width: 22px; height: 22px; padding: 0;
  display: grid; place-items: center; cursor: pointer;
  border: 0; border-radius: 7px; background: transparent; color: #a1907a;
  opacity: 0; transition: opacity .14s, background .14s, color .14s;
}
.ie-adj-rst svg { width: 13px; height: 13px; fill: currentColor; }
.ie-adj-row.is-set .ie-adj-rst,
.ie-adj-row:hover .ie-adj-rst,
.ie-adj-rst:focus-visible { opacity: 1; }
.ie-adj-rst:hover { background: rgba(123, 51, 53, .12); color: var(--maroon, #7b3335); }

/* The chip strip is the PHONE selector. */
.ie-adj-strip { display: none; }
.ie-adj-chip {
  flex: 0 0 auto; scroll-snap-align: start; position: relative;
  min-height: 34px; padding: 7px 12px; cursor: pointer;
  border: 1px solid rgba(120, 86, 44, .3); border-radius: 999px;
  background: #fffdf8; color: #6f5c44;
  font: 600 11.5px/1 var(--font-ui, Poppins, sans-serif); white-space: nowrap;
  transition: background .14s, color .14s, border-color .14s;
}
.ie-adj-chip.is-active {
  background: var(--maroon, #7b3335); border-color: var(--maroon, #7b3335); color: #fff;
}
.ie-adj-chip.is-set::after {
  content: ''; position: absolute; top: 5px; right: 7px;
  width: 5px; height: 5px; border-radius: 50%; background: var(--gold, #c9a24e);
}
.ie-adj-chip.is-active.is-set::after { background: #ffd982; }

.ie-hint {
  margin: 4px 0 0; font-size: 10.5px; line-height: 1.35; color: #8a7658;
}
.ie-fit-hint { margin: 0 0 6px; }
.ie-pane .ie-field { display: grid; gap: 4px; margin-bottom: 10px; }
.ie-pane .ie-lab {
  font: 700 10px/1 var(--font-ui, Poppins, sans-serif);
  letter-spacing: 1px; text-transform: uppercase; color: #8a7454;
}
.ie-slider { width: 100%; }
/* ---- The action bar: Discard and Save separated from the tools above ---- */
.ie-bar {
  margin-top: 0; padding-top: 10px; flex: 0 0 auto;
  border-top: 1px solid rgba(120, 86, 44, .2);
}
.ie-bar .ie-mini { min-height: 38px; }
.ie-bar .ov-primary {
  font-weight: 700; letter-spacing: .2px;
  box-shadow: 0 4px 14px -6px rgba(123, 51, 53, .8);
}
/* The dock's width is set once, in the layout rule above. This late override
   used to widen it back to 340px and beat that rule by position. */
@media (min-width: 861px) {
  .ie-dock { flex: 0 0 300px; }
}
/* TOUCH FLOOR: 40px. Compact is the goal, but a thumb is not a mouse — the
   pointer sizes above are deliberately smaller and are re-raised here. */
@media (hover: none), (max-width: 860px) {
  /* SCOPED TO THE EDITOR PANEL. `.ie-mini` is also the class the compact
     "Image Editor" action under an entry's media chip uses, and a 40px floor
     there made that small inline action as tall as a form field — the opposite
     of the compact arrangement it is meant to be. */
  /* Icons are 34px for a mouse and 40px for a thumb. Undo, Redo, Save and
     Close are the four most important controls in the editor; making them
     mouse-sized on a phone would be trading a real mis-tap for a few pixels. */
  .ie-panel .ie-icon, .ie-panel .ie-zoomview { width: 40px; height: 40px; }
  .ie-panel .ie-chip, .ie-panel .ie-adj-chip { min-height: 40px; padding: 9px 13px; }
  .ie-panel .ie-mini, .ie-panel .ie-bar .ie-mini { min-height: 40px; }
  .ie-panel .ie-pane .ie-btn { min-height: 44px; }
}

/* ---- MOBILE: the dock is a bottom SHEET, the image keeps the screen ----
   Redesigned rather than shrunk. The image is the top zone and takes every
   pixel the sheet does not need; the sheet is capped and scrolls internally, so
   nothing is ever clipped and the page never scrolls sideways. */
@media (max-width: 860px) {
  .ie-panel.ie-spread { padding: 10px 10px 0; gap: 0; }
  .ie-head { margin-bottom: 8px; padding-bottom: 8px; }
  .ie-body { gap: 10px; }
  /* The picture gets the larger share, always. */
  .ie-workspace { flex: 1 1 0; min-height: 0; }
  .ie-dock {
    margin: 0 -10px; border-radius: 18px 18px 0 0;
    padding: 10px 12px calc(12px + env(safe-area-inset-bottom, 0px));
  }
  .ie-tabs { gap: 3px; padding: 3px; }
  .ie-tab { min-height: 40px; font-size: 10px; }

  /* ---- PHONE: horizontal strips, one row, deliberately spaced ----
     The scrollbar used to sit hard against the chips. It is hidden entirely
     here (these strips are finger-scrolled, and a 4px bar under a 40px chip is
     decoration nobody uses) and the row keeps real breathing room below it.
     A right-edge fade signals there is more without occupying a row. */
  .ie-filters, .ie-adj-strip {
    display: flex; flex-wrap: nowrap; gap: 7px;
    overflow-x: auto; overflow-y: hidden;
    scroll-snap-type: x proximity; -webkit-overflow-scrolling: touch;
    padding: 2px 18px 2px 2px; margin: 0 -2px 2px;
    scrollbar-width: none; -ms-overflow-style: none;
    -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 24px), transparent 100%);
            mask-image: linear-gradient(90deg, #000 calc(100% - 24px), transparent 100%);
  }
  .ie-filters::-webkit-scrollbar, .ie-adj-strip::-webkit-scrollbar { display: none; }
  .ie-filters .ie-chip { flex: 0 0 84px; width: 84px; scroll-snap-align: start; }
  /* The strip IS the selector on a phone, so only the chosen row is shown. */
  .ie-adj-strip { display: flex; }
  .ie-adj-row { display: none; padding: 8px 2px 2px; background: none; border: 0; }
  .ie-adj-row.is-current { display: block; }
  .ie-adj-row.is-set { background: none; border-color: transparent; }
  .ie-adj-rst { opacity: 1; }
  .ie-pane .ie-actions { margin-top: 10px; padding-top: 10px;
                         border-top: 1px solid rgba(120, 86, 44, .16); }

  /* ============ TRANSFORM ON A PHONE: ALL OF IT, WITHOUT SCROLLING ============
     MEASURED, at 390x844, before this block:
         .ie-panes  clientHeight 164   content scrollHeight 216
         zoom field   y 668 -> 708     (label row above its own track)
         fit hint     y 727 -> 755     (two lines)
         fit row      y 770 -> 810
         actions      y 829 -> 884     <- bottom 884 in an 844px viewport
     Rotate left, Rotate right, Flip and Recentre were below the fold at every
     phone width: 52px over at 390, 82px at 360, 184px at 320.

     The pane was not too big — it was FOUR STACKED BLOCKS in a budget that
     affords three, and the budget is fixed on purpose: `.ie-dock` is
     height:30vh so that switching tabs cannot resize the picture. Growing the
     sheet or shrinking the image would trade this defect for that one, so
     neither is touched here. Only the Transform pane's own vertical stack is
     rearranged, and only below 861px.

       1  Zoom becomes ONE row      label | track | value   (was label above
          the track, two rows)                                   ~-14px
       2  the two-line hint is hidden, its wording moved onto the chips  -34px
       3  the four actions stay one icon row and lose a little chrome    -10px

     Nothing is removed: all six controls are present, all still ≥40px. */
  /* 5px, not 6. At 360x740 the pane is 134px and the stack measured 135 — one
     pixel over is still a scrollbar, and a scrollbar is the whole defect. Two
     gaps at 5px plus the trimmed action rule below bring it to 132. */
  .ie-pane[data-pane="transform"] { gap: 5px; }

  /* One row: the label and the readout sit either side of the track instead of
     above it. Both are already short words, and the track is the target. */
  .ie-pane[data-pane="transform"] .ie-field {
    display: grid; grid-template-columns: auto 1fr auto;
    align-items: center; column-gap: 9px; gap: 0;
    margin-bottom: 0;
  }
  .ie-pane[data-pane="transform"] .ie-lab { margin: 0; white-space: nowrap; }
  /* The readout is positioned absolutely in the desktop layout (it rides the
     label row); in a single row it is simply the third column. */
  .ie-pane[data-pane="transform"] .ie-val {
    position: static; min-width: 38px; text-align: right;
  }
  .ie-pane[data-pane="transform"] .ie-slider { width: 100%; margin: 0; }

  /* Hidden, not deleted — see the template: the chips carry the wording. */
  .ie-fit-hint { display: none; }

  .ie-pane[data-pane="transform"] .ie-fit-row { margin-bottom: 0; gap: 6px; }

  /* Four across, icon over label. 40px is the floor and it is kept: at 320px
     the pane is 281px wide, so each button is still ~66px. */
  .ie-pane[data-pane="transform"] .ie-actions {
    grid-template-columns: repeat(4, 1fr);
    gap: 5px; margin-top: 5px; padding-top: 5px;
  }
  .ie-pane[data-pane="transform"] .ie-btn {
    flex-direction: column; gap: 2px;
    font-size: 9.5px; line-height: 1.1; padding: 5px 2px;
    min-height: 44px; text-align: center;
  }
  .ie-pane[data-pane="transform"] .ie-btn svg { width: 15px; height: 15px; }
  /* ---- NO BOTTOM BAR ON A PHONE ----
     Even at one row it cost ~53px of a sheet that has little to spare, for
     three actions pressed once each. Save moved to the header beside Undo and
     Redo; Close is already there; Discard IS Close on a phone (both call
     doCancel), so nothing is lost. Reset all moves into the panes' own reset
     controls, which are contextual anyway.
     NOTHING IS REMOVED FROM THE DOM — the bar is hidden, so the desktop layout
     and every handler stay exactly as they are. */
  .ie-bar { display: none; }
  .ie-head-tools .ie-save-top { display: inline-flex; }
  .ie-save-top { background: var(--maroon, #7b3335); border-color: var(--maroon, #7b3335); color: #fff; }
  .ie-save-top:hover:not(:disabled) { background: #94413f; border-color: #94413f; color: #fff; }
  .ie-save-top:disabled { opacity: .5; }
  .ie-bar-unused {
    grid-template-columns: 44px 1fr 44px;
    gap: 8px; padding-top: 8px;
  }
  /* grid-row: 1 ON ALL THREE. Placement is sparse by default, so the cursor
     had already passed column 2 by the time Save was placed (it comes last in
     the DOM) and it dropped to a second row — which is the two-row bar this
     rule exists to remove. Naming the row makes DOM order irrelevant. */
  .ie-bar .ov-primary { grid-column: 2; grid-row: 1; order: 0; margin: 0; min-height: 44px; padding: 10px 14px; }
  .ie-bar .ie-mini {
    width: 44px; min-width: 44px; height: 44px; min-height: 44px; padding: 0;
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 0;                     /* the word is replaced by the glyph */
    border-radius: 12px;
  }
  .ie-bar .ie-mini::before {
    font-size: 17px; line-height: 1; font-weight: 700; color: #6f5c44;
  }
  .ie-bar .ie-reset-all { grid-column: 1; grid-row: 1; }
  .ie-bar .ie-reset-all::before { content: '\21BA'; }   /* reset: open circle arrow */
  .ie-bar .ie-cancel { grid-column: 3; grid-row: 1; }
  .ie-bar .ie-cancel::before { content: '\2715'; color: #9a3b3b; }  /* discard */
}
/* 430px and below: the four tabs must still fit one row without wrapping. */
@media (max-width: 430px) {
  .ie-panel.ie-spread { padding: 8px 8px 0; }
  .ie-stage { padding: 8px; border-radius: 10px; }
  .ie-dock { margin: 0 -8px; }
  .ie-tab { font-size: 10px; }
  .ie-tab span { font-size: 10px; }
}
/* 360px and below: labels shrink before anything is allowed to clip. */
@media (max-width: 360px) {
  .ie-panel.ie-spread .ie-head .ov-title { font-size: 13.5px; }
  /* 40px is the floor even here: these are the undo/redo/close targets. */
  .ie-icon { width: 40px; height: 40px; }
  .ie-tabs { gap: 2px; padding: 3px; }
  .ie-tab { font-size: 9.5px; letter-spacing: 0; }
  .ie-tab span { font-size: 9.5px; }
  .ie-tab svg { width: 14px; height: 14px; }
  .ie-chip, .ie-adj-chip { padding: 7px 10px; font-size: 11px; }
}

/* =====================================================================
 * OPENING A BOOK, AND RETURNING IT
 *
 * Reported: the book paints and then absorbs clicks until initialisation
 * finishes, so it reads as broken; and Return Book removed the reader with no
 * transition at all, so the book did not go anywhere, it simply stopped
 * existing.
 *
 * Both are handled in the book's own visual language — the parchment, the
 * maroon, the shadow the book already casts — rather than with a new spinner
 * style borrowed from elsewhere. Nothing here changes the book's size,
 * proportions or layout: the veil sits over the stage and the return is a
 * scale and a fade.
 * ===================================================================== */

.book-stage { position: relative; }

/* ---- 1. still opening ----
   ============ WHAT WAS WRONG WITH THE PREVIOUS TREATMENT ============
   The veil was a near-opaque radial wash — rgba(253,250,241,.72) to
   rgba(233,223,201,.86) — over `inset: -12px` of the whole stage, with a 34px
   spinning ring on top. At those alpha values the book underneath was gone,
   not softened, so opening any book began with the artwork being replaced by a
   flat cream panel and a loading spinner. It announced a wait rather than
   covering one, and on a fast load it flashed all of that up and took it away
   again inside a couple of hundred milliseconds.

   Three changes, none of which touch WHEN the loader is removed:

   1. The veil is now a light wash the book reads through. It still exists to
      catch clicks — that is its real job — but it no longer hides the thing
      the reader came to look at.
   2. The spinner is smaller, thinner and quieter, and sits low over the book
      rather than dead centre across it.
   3. Both FADE IN over 240ms after a 220ms delay. That is not a delay before
      the book becomes usable — readiness is still `state.paginated`, and the
      class is still removed the moment it is true. It means a reader that
      initialises quickly never paints a loader at all, which is the case that
      produced the flash. A slow one gets the indicator it needs. */
body.reader-booting .book-stage::before {
  content: '';
  position: absolute; inset: 0;
  z-index: 40;
  /* NO WASH AT ALL. This was a near-opaque radial gradient, then a light
     tint; both distorted the book to say something a small spinner says
     better. The element remains ONLY to catch clicks on a reader that has
     not initialised — that was always its real job — so it is completely
     transparent and the book is shown undimmed while it loads. */
  background: transparent;
  /* THE VEIL IS THE INTERACTION BLOCK. Clicks were being silently swallowed by
     an uninitialised reader; now they land on something that says why. */
  pointer-events: auto;
  opacity: 0;
  animation: mw-boot-in .16s ease-out .06s forwards,
             mw-boot-give-up 0s linear 8s forwards;
}
body.reader-booting .book-stage::after {
  content: '';
  position: absolute; z-index: 41;
  /* RAISED WITH `top`, NOT WITH A TRANSFORM. mw-boot-spin animates
     `transform`, and an animated property overrides the element's own
     `transform` for the whole run — a translate written here would be
     discarded the moment the spin started, and would also be lost entirely
     under prefers-reduced-motion, where the spin is removed. `top` is
     untouched by either. */
  top: 34%; left: 50%;
  /* A ring ON the book, so the wait is attached to the thing being waited for.
     RAISED AND ENLARGED (Aug 2026). Reported as "quite in the centre and not
     properly visible": at 20px dead centre it sat on the binding shadow, which
     is the darkest, busiest band of the whole spread, and a 2px hairline in
     18%-opacity maroon had almost nothing to read against there.

     28px rather than 20 — noticeable without becoming a page element in its
     own right — and moved from just below centre to 34% of the stage height,
     which clears the binding shadow and puts it in the upper third where the
     eye already is. The ring is thicker (3px) and both its track and its head
     are stronger, so the rotation is legible at a glance instead of inferred.

     The plate behind it is now fully opaque and slightly larger than the ring,
     so the spinner never has to compete with whatever artwork is underneath —
     that was the other half of "not properly visible". Timing is untouched:
     still the 220ms delay and 240ms fade, so a fast reader still paints no
     loader at all. */
  width: 28px; height: 28px;
  margin: 0 0 0 -14px;
  border-radius: 50%;
  border: 3px solid rgba(123, 51, 53, .22);
  border-top-color: rgba(123, 51, 53, .92);
  background: rgba(253, 250, 243, 1);
  box-shadow: 0 2px 10px rgba(40, 26, 12, .28), 0 0 0 6px rgba(253, 250, 243, .92);
  opacity: 0;
  /* ============ 220ms OF NOTHING, MEASURED AND REMOVED ============
     This was `.24s ease-out .22s`. Sampled in Chromium against the real
     stylesheet, the ring was at opacity 0 for the first ~300ms after the page
     painted and did not reach full strength until ~570ms — so a reader that
     was slow to initialise showed a blurred room and no indicator for over
     half a second, which is exactly the reported "blurred background with no
     sign anything is happening".

     The delay was introduced so a reader that initialises instantly never
     paints a loader at all, and that intent is kept — 60ms still swallows a
     genuinely immediate paginate — but it is no longer long enough to read as
     a void. Removal is still driven by `state.paginated`, not by any timer
     here, and the 8s CSS safety is untouched. */
  animation: mw-boot-spin .9s linear infinite,
             mw-boot-in .16s ease-out .06s forwards,
             mw-boot-give-up 0s linear 8s forwards;
}
@keyframes mw-boot-spin { to { transform: rotate(360deg); } }
/* Fades the loader IN. Kept separate from mw-boot-spin so the two can run on
   the same element without one resetting the other's transform. */
@keyframes mw-boot-in { to { opacity: 1; } }
/* SAFETY, NOT STYLE. If reader.js never runs, nothing else would ever remove
   the veil — so the stylesheet retires it on its own at the same 8s ceiling
   the script uses. A page that cannot initialise must still be usable. */
@keyframes mw-boot-give-up { to { opacity: 0; visibility: hidden; pointer-events: none; } }

@media (prefers-reduced-motion: reduce) {
  /* NO SPIN, BUT STILL AN INDICATOR. The base rules now start both pseudo-
     elements at `opacity: 0` and fade them in, so simply dropping to the
     give-up animation here would leave a reduced-motion reader with no loading
     indication at all — they would see a book that appears finished and does
     not respond. Opacity is set outright instead, and only the rotation and
     the fade are removed. */
  body.reader-booting .book-stage::before {
    opacity: 1;
    animation: mw-boot-give-up 0s linear 8s forwards;
  }
  body.reader-booting .book-stage::after {
    opacity: 1;
    animation: mw-boot-give-up 0s linear 8s forwards;
  }
}

/* ---- 1b. leaving via Return Book: hide the spread swap ----
   closeBook() starts with `fb.goTo(0, false)`, an instant move to the first
   spread. reader.css hides `.page-left` during `.closing` (the cover's back
   face replaces it) but holds `.page-right` at `opacity: 1` for the whole
   1.05s cover sweep — so a reader on page 11 watched the right page switch to
   the title page and sit there, legible, for over a second. That is the jump.

   The BODY fades, not the page: the parchment stays, so the cover still sweeps
   over a page rather than over the bare maroon board — the effect the turn
   work spent a round removing. Applied only by the Return Book handler, so
   drag-to-close and close-from-the-back are unchanged; they have no reason to
   blank content the reader is still looking at. */
.book-stage.is-leaving .flipbook.closing .page-right .page-body {
  opacity: 0;
  transition: opacity .2s ease-out;
}
@media (prefers-reduced-motion: reduce) {
  .book-stage.is-leaving .flipbook.closing .page-right .page-body {
    transition: none;
  }
}

/* ---- 2. going back to the shelf ----
   NO CLASS, NO RULE. The `.is-returning` scale-and-fade that sat here is gone.
   The return is the book's own closing animation followed by the shelf page's
   reverse cover flight; a third movement between them added nothing and, by
   fading the stage out before the page changed, produced the empty frame it
   was meant to avoid. */

/* =====================================================================
 * THE SHELF COLUMN MUST NOT LEAVE THE SCREEN
 *
 * Reported with a photograph: on a collection page the fifth book on a shelf
 * is cut in half by the right edge of the window.
 *
 * MEASURED, not guessed: on couple_diary.php at 1440 the `.shelf-wall-col`
 * grid track ends at x=1456 against a 1440 viewport — and it does so with only
 * FOUR books on the shelf, so it is the column that overflows, not the books.
 * `document.scrollWidth` equals the body width, which is why no overflow audit
 * has ever reported it: `overflow-x: hidden` swallows the excess and the books
 * past the edge are simply clipped away.
 *
 * This is the standard CSS grid blow-out. A grid item's `min-width` defaults to
 * `auto` (= min-content), so a track can refuse to shrink to its declared share
 * — here 1fr of `22.5% 44.5% 1fr` — and push the row wider than the grid.
 *
 * main.css ALREADY carries the remedy and its own explanation of exactly this
 * failure ("the standard grid blow-out and `min-width: 0` is its standard
 * remedy"), but scoped inside a mobile media query. The same blow-out happens
 * at every width; only the phone case had been noticed. Applying it unscoped
 * is the one-line completion of a fix that was already half made.
 *
 * Nothing about book size, spacing or the number of shelves changes: the
 * column simply stops exceeding the space it was given.
 * ===================================================================== */
.page-home .room-col { min-width: 0; }

/* =====================================================================
   A PANEL THAT IS SAVING IS NOT A PANEL YOU CAN USE.

   The behaviour is enforced in JavaScript — reader-util.js's lockPanel()
   disables every control and closeOverlays() refuses to dismiss — because a
   visual treatment alone can be clicked straight through. This is the part
   the user can SEE, so "wait" is legible rather than something they discover
   by pressing things that no longer answer.

   Deliberately restrained: a slight fade and a progress cursor. The panel
   must stay READABLE while it saves — the entry being written is on it, and
   dimming it into illegibility would read as an error state rather than as
   work in progress.

   THE SAVE BUTTON IS EXCLUDED FROM THE FADE. It carries the spinner and the
   "Saving Entry…" label, which is the one thing that must not recede, and
   `pointer-events` is left alone on it because it is already `disabled`.

   Appended at the END of mw.css, which is where a new rule belongs: nothing
   already in the file can be inverted by a rule added after all of it
   (tools/css_inversions.py, gate 35).
   ===================================================================== */
.page-overlay.is-saving { cursor: progress; }

.page-overlay.is-saving .ov-form,
.page-overlay.is-saving .overlay-close {
  /* The controls inside are `disabled`, which already refuses the pointer.
     This closes the gap for the things that carry no disabled state at all —
     a <label class="ov-drop"> that opens a file dialog, and the decorative
     rows around it. */
  pointer-events: none;
}
.page-overlay.is-saving .overlay-close { opacity: .55; }

/* THE FADE IS ON THE FORM'S CHILDREN, NOT ON THE FORM.
   `opacity` establishes a stacking context, so a child cannot be more opaque
   than its parent: fading `.ov-form` to .55 and then setting the save button
   back to 1 does nothing at all — the button renders at .55 with the rest.
   Every save control in reader.php carries `.ov-primary`, and the progress
   bar `.ov-progress`, so both can simply be left out of the selector. */
.page-overlay.is-saving .ov-form > *:not(.ov-primary):not(.ov-progress) {
  opacity: .55;
}

/* =====================================================================
   TITLE PAGE — THE TOP BLOCK, TIGHTENED SO THE SUMMARY FITS ON IT

   MEASURED BEFORE ANYTHING WAS CHANGED, at 1440x900 on an In Memory Of
   title page carrying a name, two dates and a two-line summary:

     flourish  top 36    h 49.8      .title-page gap: 24px x 5 = 120px of
     h1        top 109.8 h 76.8      pure gap, plus .tp-dates' own 16px
     dates     top 230.6 h 145.3     margin and .tp-summary's own 14px on
     summary   top 405.9 h 99        TOP of it
     rule      top 540.9
     imprint   top 564.9 -> 585.7    against a 592px page: OVERFLOWING

   So the summary did not "sit far down the page" by accident — the page had
   already spent 120px on gaps before it got there, and the last element was
   past the leaf. The same page WITHOUT a summary ended at 452.7 and looked
   fine, which is why this only became visible once summaries existed.

   WHAT THIS CHANGES: spacing only. No element is moved, renamed, removed or
   restyled; the order, the fonts, the colours and the rule are exactly as
   approved. The flourish comes down to a size proportional to the title it
   sits above rather than a 30px glyph in a 49.8px box.

   NOTHING IS NEEDED FOR THE EMPTY CASE. pagination.js omits `.tp-summary`
   from the DOM entirely when the book has none, so a flex `gap` cannot leave
   a hole where it would have been — the page closes up by itself.

   Appended at the END of mw.css, which is where a new rule belongs: nothing
   already in the file can be inverted by a rule added after all of it
   (tools/css_inversions.py, gate 35).
   ===================================================================== */
.title-page {
  gap: 12px;
  padding-top: clamp(14px, 2.6vh, 30px);
}

/* Proportional to the title beneath it, not a separate ornament competing
   with it. `line-height: 1` is what removes the 20px of box the glyph was
   carrying above and below itself. */
.title-page .tp-flourish {
  font-size: 22px;
  line-height: 1;
}

/* The title and the flourish read as ONE block: the gap above the title is
   the flex gap, and this removes the extra margin under it so the name and
   dates follow immediately. */
.title-page h1 { margin-bottom: 0; }

/* Name and dates: a tight group near the top, which is what makes them read
   as facts about the book rather than as three separate lines. The 16px
   top margin was doubling the flex gap. */
.tp-dates { margin: 2px 0 0; }
.tp-dates p { margin: 4px 0; }

/* The summary is deliberately the LAST thing before the rule, and it is
   allowed a slightly wider column than the 34ch it had — at 34ch a two-line
   summary became three lines and pushed the imprint off the leaf. */
.tp-summary {
  margin: 2px auto 0;
  max-width: 38ch;
}

/* =====================================================================
   TITLE PAGE — THE SUMMARY, LEGIBLE

   MEASURED: the summary was #7b6750 on the page's #f6efe2, which is
   **4.71:1** — under WCAG AA for body text (4.5:1 is the floor, and this is
   set at 15px, so it only just clears it and reads as washed out next to the
   name and dates beside it at #34281c, 12.52:1). It looked like a caption
   somebody had greyed out rather than a sentence the owner wrote.

   It now uses `--page-ink`, the reader's own approved dark text colour — the
   same one `.tp-dates` and the entry text already use, so this introduces no
   new colour and follows the night theme for free.

   THE LABEL IS SUBORDINATE, AND STAYS SUBORDINATE. Same uppercase
   letter-spaced treatment as `.tp-dates span` ("Name", "Date of Birth"),
   which is the page's existing vocabulary for "this is what the thing below
   is". It is a label, not a heading: it must never compete with the title.

   HIERARCHY UNCHANGED: title (clamp 22-32) > dates (clamp 14-17) >
   summary (15) > label (10.5). Only the COLOUR and the label are new; the
   size, the family and the 38ch column are as they were.

   The block is emitted as one node by pagination.js and omitted entirely when
   there is no summary, so the label cannot appear alone and the flex gap
   cannot leave a hole where the block would have been.
   ===================================================================== */
.tp-summary-block {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 5px;
  margin: 2px auto 0;
  max-width: 38ch;
}

.tp-summary-label {
  font-family: var(--font-ui);
  font-size: 10.5px;
  letter-spacing: .11em;
  text-transform: uppercase;
  font-weight: 700;
  color: #9a8763;                 /* exactly .tp-dates span */
}

.tp-summary-block .tp-summary {
  margin: 0;
  color: var(--page-ink, #34281c);
  font-size: 15px;
  line-height: 1.6;
  max-width: none;                /* the block owns the column now */
}
