/* Daily Agenda visual language: warm ivory ground, cards, colored platform
   borders. Phone-first — everything below is sized for a thumb. */

:root {
  --ivory:      #FBF7EF;
  --card:       #FFFFFF;
  --ink:        #2B2622;
  --ink-soft:   #6B625A;
  --line:       #E8DFD1;
  --fb:         #3B6FD4;
  --ig:         #C9438F;
  /* Darkened from #B4731A, which read at 3.5:1 on its own --warn-bg — under
     the 4.5 a paragraph needs, and this is the colour every "saved, not yet
     enforced" note and every ⚠ line is written in. */
  --warn:       #8A5510;
  --warn-bg:    #FDF3E2;
  --danger:     #A8341F;
  /* THE HOUSE BURGUNDY, named. It is the colour a form's button is born
     wearing and the colour the brand is, and it was a hex typed out wherever
     it was wanted. `--danger` is a rust and is about danger; this one is
     about us. */
  --burgundy:   #8C1D2A;
  --yt:         #CC0000;   /* YouTube's own red, not the app's rust */
  --go:         #2E6B4F;
  --go-deep:    #235640;
  --go-press:   #24553F;
  /* BRIGHT ORANGE, for the one button that has to be told apart from the
     primary rather than ranked below it. "+ New Special Form" sits an inch
     from "+ New form" and they make two different things; in outline it read
     as the quieter of the two, which is not what it is.

     #CC4A02 carries white at 4.62:1 — over the 4.5 ordinary text needs, so it
     is readable as well as loud. A brighter orange than this one does not:
     #E8590C is 3.58:1 and #FF8A3D cannot take white at all. */
  --orange:     #CC4A02;
  --orange-press: #A93C02;
  --radius:     14px;
  --shadow:     0 1px 2px rgba(43,38,34,.06), 0 4px 14px rgba(43,38,34,.05);

  /* GAP — the one spacing token.
   *
   * `--gap` is the distance between two adjacent content cards, and
   * `--gap-outer` is the distance from a card to the left or right edge of the
   * content area. The rule that keeps this looking like one application, and
   * the one that kept getting lost:
   *
   *     --gap-outer >= --gap, always, on every page and in every section.
   *
   * Outer spacing narrower than the gap between cards is what makes a page
   * look like it is spilling out of its panel, and it is the failure that has
   * come back three times — most recently because eight pages use a `.wrap`
   * container that had no padding defined at all, so their cards sat flush
   * against the edges while `.cards` pages looked right.
   *
   * `tests/test_spacing.py` asserts the inequality against the stylesheets, so
   * a future edit that narrows the outer padding fails a test rather than
   * quietly shipping.
   *
   * The Numbers page is the reference for "correct": 12px both on a phone,
   * 20px outer against a 12px gap on the desktop. Everything else matches it.
   */
  --gap:        12px;
  --gap-outer:  12px;   /* phone; the desktop block below widens it to 20px */

  /* The older names, kept because three pages' worth of rules use them. They
   * are the same measurements, said the same way.
   *
   *   --space      between things in a group (fields, chips, rows)
   *   --space-lg   between groups (cards, sections)
   *   --space-sm   inside a thing (a chip's padding, a label's gap)
   */
  --space:      var(--gap);
  --space-lg:   20px;
  --space-sm:   6px;
}

* { box-sizing: border-box; }

/* The HTML `hidden` attribute is only `display: none` in the UA stylesheet, so
   ANY author rule setting display — every `display: flex` below — silently
   beats it. That is why the bulk sheet could not be closed: it was never being
   opened, it was permanently displayed, and toggling `hidden` did nothing.
   This rule has to stay above the component styles. */
[hidden] { display: none !important; }

html, body {
  margin: 0;
  background: var(--ivory);
  color: var(--ink);
  font: 16px/1.5 ui-sans-serif, -apple-system, "Segoe UI", Roboto, sans-serif;
  -webkit-text-size-adjust: 100%;
}

/* ---------- top bar ---------- */
.topbar {
  position: sticky; top: 0; z-index: 20;
  background: rgba(251,247,239,.94);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--line);
  padding: max(10px, env(safe-area-inset-top)) 14px 8px;
}
.counters { font-size: 15px; display: flex; gap: 6px; align-items: baseline; flex-wrap: wrap; }
.counters b { font-size: 21px; font-variant-numeric: tabular-nums; }
.count-todo b { color: var(--ink); }
.count-done b { color: var(--go); }
.count-flag b { color: var(--warn); }
/* The lane the app answers itself. Not a warning colour: nothing is
   wrong with these, and the one number on this strip that wants the
   eye is the one that says how much is Tom's. */
.count-auto b { color: var(--ink-soft); }
.dot { color: var(--ink-soft); }
.split { font-size: 12.5px; color: var(--ink-soft); margin-top: 2px; min-height: 18px; }

/* A row of chips is a row of things to press, and 7px apart they are one
   thing with lines through it. */
.filters { display: flex; gap: var(--gap, 15px); margin-top: 9px;
           overflow-x: auto; padding-bottom: 2px; }
.filters::-webkit-scrollbar { display: none; }

.chip {
  flex: 0 0 auto;
  border: 1px solid var(--line);
  background: var(--card);
  color: var(--ink-soft);
  border-radius: 999px;
  padding: 7px 13px;
  font-size: 13.5px;
  font-family: inherit;
  cursor: pointer;
}
.chip-on { background: var(--ink); color: var(--ivory); border-color: var(--ink); }
.chip-refresh { font-size: 16px; padding: 6px 13px; }

/* ---------- cards ---------- */
/* The two content containers. `.cards` stacks its children with a flex gap;
   `.wrap` holds sections that carry their own bottom margins. They differ in
   how they space their children and must never differ in how far those
   children sit from the edge — which is exactly what had drifted. */
.cards {
  padding: var(--gap-outer) var(--gap-outer) 120px;
  display: flex; flex-direction: column; gap: var(--gap);
}
.wrap {
  padding: var(--gap-outer) var(--gap-outer) 120px;
}
.empty { text-align: center; color: var(--ink-soft); padding: 44px 12px; }

.card {
  background: var(--card);
  border: 1px solid var(--line);
  border-left: 4px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 13px 14px 12px;
  /* So the corner ✕ has a corner to sit in. */
  position: relative;
}
/* Every platform gets its own edge. Only two had one, so a YouTube card and a
   Facebook card were the same card at a glance — and a blog comment looked
   like neither. */
.card.platform-facebook  { border-left-color: var(--fb); }
.card.platform-instagram { border-left-color: var(--ig); }
.card.platform-hyvor     { border-left-color: var(--go); }
.card.platform-youtube   { border-left-color: var(--yt); }
.card.platform-tiktok    { border-left-color: var(--ink); }
.card.is-flagged { background: var(--warn-bg); }
.card.is-sending { opacity: .55; }
.card.is-gone { display: none; }

.card-head {
  display: flex; align-items: center; margin-bottom: 7px;
  /* Tight along the row, where these read as one line of detail — and the
     house 12 BETWEEN rows, because once this wraps they are rows, and the
     age pill on one of them was sitting 9px off the like button under it. */
  column-gap: 8px; row-gap: var(--gap-tight-min, 12px);
  /* A card can carry a badge, a name, an age, a like, "waiting since Aug 12"
     and "↻ redrafted" all at once, which is wider than a phone. It used to
     run off the right edge; now it wraps. */
  flex-wrap: wrap;
  padding-right: 22px;     /* room for the ✕ in the corner */
}
.badge {
  font-size: 10.5px; font-weight: 700; letter-spacing: .5px; text-transform: uppercase;
  padding: 3px 7px; border-radius: 5px; color: #fff;
}
.badge.facebook  { background: var(--fb); }
.badge.instagram { background: var(--ig); }
.badge.hyvor     { background: var(--go); }
.badge.youtube   { background: var(--yt); }
.badge.tiktok    { background: var(--ink); }
.author {
  font-weight: 600; font-size: 14.5px; flex: 1 1 auto;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  /* Not `min-width: 0`. The head wraps now, and with a red age tag and two
     others beside it this box was being squeezed to a few pixels — which cut
     the author's avatar in half, because the avatar lives inside it and the
     box clips. Below this it wraps to its own line instead, which is the
     right answer for a name. */
  min-width: 108px;
}
.age { font-size: 12px; color: var(--ink-soft); flex: 0 0 auto; }
.like { border: none; background: none; font-size: 19px; cursor: pointer;
        color: var(--ink-soft); padding: 0 2px; line-height: 1; }
/* A thumb, not a heart: the Graph API lets a page add a plain Like and no
   other reaction, so a heart would promise something Meta cannot do. */
.like { filter: grayscale(1) opacity(.45); transition: filter .15s ease; }
.like.liked { filter: none; }

.comment { margin: 0 0 10px; font-size: 15.5px; white-space: pre-wrap; word-break: break-word; }
.attachment { font-size: 12.5px; color: var(--ink-soft); margin-bottom: 8px; }
.attachment img { max-width: 140px; border-radius: 8px; display: block; margin-top: 5px; }

.person {
  font-size: 12.5px; color: var(--ink-soft); background: #FAF6EE;
  border: 1px solid var(--line); border-radius: 9px; padding: 7px 9px; margin-bottom: 9px;
}
.person b { color: var(--ink); }

.flags { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 7px; }
.flag {
  font-size: 11.5px; font-weight: 600; padding: 3px 8px; border-radius: 999px;
  background: var(--warn-bg); color: var(--warn); border: 1px solid #EBD5AC;
}
/* A held draft is not a fault, so it is amber rather than red. Red says
   "something broke"; this says "you need to look at this one". */
.flag.danger { background: var(--warn-bg); color: var(--warn); border-color: #EBD5AC; }
.flag.plain  { background: #F2EFE8; color: var(--ink-soft); border-color: var(--line); }

.draft {
  width: 100%; font: inherit; font-size: 15.5px; color: var(--ink);
  background: #FCFAF6; border: 1px solid var(--line); border-radius: 10px;
  padding: 10px 11px; resize: vertical; min-height: 84px;
}
.draft:focus { outline: 2px solid var(--go); outline-offset: -1px; background: #fff; }
.draft.edited { background: #FFFDF5; border-color: #D9C89E; }

.notes { font-size: 12.5px; color: var(--ink-soft); margin-top: 6px; }
/* Why a regenerate is wanted, offered rather than hidden behind a first tap.
   Quieter than the buttons below them: they are a qualifier on Regenerate,
   not a fourth and fifth action. */
.reasons { display: flex; gap: var(--gap, 15px); flex-wrap: wrap;
           margin-top: 8px; }
.reasons .chip { font-size: 12px; padding: 12px; color: var(--ink-soft); }
.reasons .chip:hover { color: var(--ink); border-color: var(--ink-soft); }

.actions {
  display: flex; gap: var(--gap, 15px); margin-top: 10px;
  align-items: stretch;
  /* On a platform the app can read but not post to, the row carries a
     sentence saying why as well as a button, and a sentence has a
     min-content width a phone does not have. It wraps rather than running
     off the card. The ordinary three-button row still fits one line. */
  flex-wrap: wrap;
}

/* Three buttons, and they have to fit a 360px phone with the card's padding
   taken off — about 328px of room. `min-width: 0` on all of them is what lets
   the row shrink instead of running off the right edge, which is what it was
   doing with four. */
.actions .btn { min-width: 0; }

@media (max-width: 460px) {
  .actions { gap: 6px; }
  .actions .btn {
    font-size: 13.5px; padding: 11px 8px; white-space: nowrap;
  }
  /* All three share the row and shrink together. `flex: 1 1 44%` on Approve
     looked reasonable and was the whole bug: a percentage basis is what the
     browser adds up when deciding whether the line fits, so 44% + Regenerate
     + Write my own came to more than the row and the third one wrapped —
     at every phone width, on every card. A basis of `auto` is the button's
     own text, which is what actually has to fit. */
  /* May shrink, never grows. `flex-grow: 1` was the fault once Approve moved
     to the end of the row: the two secondary buttons expanded to fill the line
     and pushed the primary off the right edge of the card. Nothing grows here
     — the row is the three labels and the gaps between them. */
  .actions .btn { flex: 0 1 auto; }
  /* And the primary never shrinks either. It is the one button on the card
     that must always be readable, so the other two give way instead. */
  .actions .btn-approve { flex: 0 0 auto; }
}

/* A small phone. Measured rather than guessed: at 360px the three labels plus
   their padding came to 306px in a 303px row, so "Approve & send" lost its
   last letter. The first fix made them fit to the pixel — 333 in 333 — and
   then some cards wrapped anyway, because sub-pixel text rendering differs by
   a fraction between one string and the next. Fitting exactly is not fitting.
   These numbers leave about fifteen pixels of slack at 360. */
@media (max-width: 400px) {
  /* 6px, not 4. Two gaps at 4px saved eight pixels on a row the note above
     says has about fifteen to spare, and bought a pair of buttons close
     enough that a thumb lands between them. */
  .actions { gap: 6px; }
  .actions .btn { font-size: 12px; padding: 11px 5px; }
}

/* The short label. Measured: at 360px the three natural widths came to 325px
   in a 303px row, and at 430px to 381px in 373px — short by 22 and by 8. With
   "Approve" instead of "Approve & send" there is about twenty pixels of slack
   at the narrowest, which is slack rather than a coincidence. */
.lbl-narrow { display: none; }
@media (max-width: 560px) {
  .lbl-wide { display: none; }
  .lbl-narrow { display: inline; }
}

/* "Don't answer", as a close button in the corner. It was the fourth button
   in the action row and the row did not fit on a phone. Big enough to hit on
   purpose; it asks before it does anything, because it is also where a thumb
   lands by accident on the way down the page. */
.card-x {
  position: absolute; top: 6px; right: 6px; z-index: 2;
  border: none; background: none; cursor: pointer;
  color: var(--ink-soft); font-size: 15px; line-height: 1;
  padding: 7px 8px; border-radius: 8px;
}
.card-x:hover { background: var(--ivory); color: var(--danger); }
.btn {
  font: inherit; font-size: 14.5px; font-weight: 600;
  border-radius: var(--r-sm); padding: 11px 14px; cursor: pointer;
  border: 1px solid var(--line); background: var(--card); color: var(--ink);
  text-decoration: none; display: inline-flex; align-items: center; justify-content: center;
}
.btn:active { transform: translateY(1px); }
/* A button is the width of its label. `flex: 1` here was making every
   primary button in the app stretch to whatever row it happened to be in — a
   full-width green bar for "+ New giveaway", a three-line "+ Write a
   question" — and it was only ever right in one place: the card action row,
   where Approve IS the row and the other two are secondary. So it is stated
   there instead of everywhere. */
.btn-approve { background: var(--go); border-color: var(--go); color: #fff; }
.actions .btn-approve { flex: 1; }
.btn-approve:active { background: var(--go-press); }
.btn-regen { flex: 0 0 auto; }

/* The one outline-pill action button.
 *
 * "Add a task", "Add an idea", "Suggest some", "Add a partner", "+ New
 * giveaway" were each written as a `.chip` — a 7px filter pill — so an action
 * that opens a dialog looked like a filter, and every page's version was a
 * slightly different size. This is the same internal padding and type as the
 * green primary button, in outline, with the pill shape kept: the two read as
 * the same family, which is what makes the green one obviously the primary.
 *
 * `.chip` stays exactly as it was, for the dense things it was made for —
 * filters, tabs, pagination. An action and a filter should not look alike.
 */
.btn-quiet {
  border-radius: 999px;
  background: var(--card);
  color: var(--ink);
  border-color: var(--line);
  flex: 0 0 auto;
}
.btn-quiet:hover { border-color: var(--go); color: var(--go); }
.btn-quiet:active { background: var(--field, #FBF7F0); }

/* THE OTHER FILLED BUTTON, and there is only one place for it.
 *
 * `.btn-approve` is the primary: the thing this screen is mostly for. This is
 * for the rare case of a SECOND action that is not secondary — "+ New Special
 * Form" beside "+ New form" makes a different kind of thing, not a lesser one,
 * and in outline it read as the quieter of the two. Filled and orange it is
 * plainly the other one rather than the smaller one.
 *
 * Pill-shaped like `.btn-quiet`, because that is the shape the row already
 * has and the difference being made here is colour, not geometry. */
.btn-orange {
  border-radius: 999px;
  background: var(--orange); border-color: var(--orange); color: #fff;
  flex: 0 0 auto;
}
.btn-orange:hover { background: var(--orange-press);
                    border-color: var(--orange-press); }
.btn-orange:active { background: var(--orange-press); }

/* Nothing touching an edge, a rule or its neighbour. A row of these gets the
   gap token between them and clear air above, so they never sit flush against
   the card border or the list they act on. */
.btn-row {
  display: flex; flex-wrap: wrap; gap: var(--space);
  margin: var(--space) 0 0;
}
.btn-link { flex: 0 0 auto; color: var(--ink-soft); }
.btn[disabled] { opacity: .5; pointer-events: none; }

/* ---------- bulk bar ---------- */
.bulkbar {
  position: fixed; left: 12px; right: 12px;
  bottom: calc(12px + env(safe-area-inset-bottom));
  background: var(--ink); color: var(--ivory);
  border-radius: var(--radius); box-shadow: var(--shadow);
  padding: 11px 13px; display: flex; align-items: center; gap: 12px; z-index: 30;
  font-size: 14.5px;
}
.bulkbar .btn-approve { flex: 0 0 auto; padding: 9px 15px; }
/* "3 new" — what has arrived since the sheet was last opened. The count beside
   it is how many there are in total, which is always large and so stops being
   read; this is the one worth noticing, so it is the one that is coloured. */
.bulk-new {
  flex: 0 0 auto; font-size: 12px; font-weight: 700;
  padding: 2px 8px; border-radius: 999px;
  background: var(--go); color: #fff;
}

/* ---------- toasts (the undo window) ---------- */
.toasts {
  position: fixed; left: 12px; right: 12px;
  /* ABOVE the quick-reply bar, not over it. Both are pinned to the same
     corner and the toasts win on z-index, so an undo window used to hide the
     bar completely — which is what "the bar is missing" turned out to be. */
  bottom: calc(12px + var(--bulkbar-h, 0px) + env(safe-area-inset-bottom));
  display: flex; flex-direction: column-reverse; gap: 8px; z-index: 40;
  pointer-events: none;
}
.toast {
  pointer-events: auto;
  background: var(--ink); color: var(--ivory);
  border-radius: var(--radius); box-shadow: var(--shadow);
  padding: 11px 13px; display: flex; align-items: center; gap: 12px; font-size: 14px;
}
.toast.err { background: var(--danger); }
.toast span { flex: 1; }
.toast button {
  font: inherit; font-weight: 700; font-size: 14px;
  background: none; border: 1px solid rgba(251,247,239,.5); color: var(--ivory);
  border-radius: var(--r-sm); padding: 6px 12px; cursor: pointer;
}
.toast .bar { position: absolute; }

/* ---------- login ---------- */
/* The sign-in and lock screens.
 *
 * These pages have **no sidebar** — there is nothing to navigate to until you
 * are signed in — but they load the same stylesheet as the app, and the app
 * reserves a gutter for the rail with `body { padding-left }` at two
 * breakpoints. That gutter was still being reserved here, so the block was
 * centred inside a content box narrower than the window and sat off-centre
 * against the actual viewport.
 *
 * `body.auth` is element-plus-class, so it beats the bare `body` rules in the
 * media queries below without needing `!important` — and it beats them at
 * every breakpoint, which is the part that matters.
 */
body.auth {
  padding-left: 0;
  padding-right: 0;
  min-height: 100dvh;
  display: flex;
  align-items: flex-start;
  justify-content: center;
}
body.auth .login { margin: 16vh 0; }

.login {
  max-width: 340px; margin: 16vh auto; padding: 0 20px; text-align: center;
  width: 100%;
}
.login h1 { font-size: 21px; margin-bottom: 4px; }
.login p { color: var(--ink-soft); font-size: 14px; margin-top: 0; }
.login input {
  width: 100%; font: inherit; font-size: 16px; padding: 13px;
  border: 1px solid var(--line); border-radius: 10px; background: var(--card);
  margin-bottom: 10px;
}
.login .btn-approve { width: 100%; }
.login .error { color: var(--danger); font-size: 14px; }

/* ---------- post context strip ---------- */
.postctx {
  display: flex; align-items: center; gap: 9px;
  background: #FAF6EE; border: 1px solid var(--line); border-radius: 9px;
  padding: 6px 9px; margin-bottom: 9px;
}
.postthumb {
  width: 42px; height: 42px; object-fit: cover; border-radius: 6px;
  flex: 0 0 auto; background: var(--line);
}
.postcap {
  /* flex:1 + min-width:0 so the caption cannot be squeezed out by the
     thumbnail — it is the part that actually identifies the post. */
  flex: 1; min-width: 0;
  font-size: 12.5px; color: var(--ink-soft); line-height: 1.35;
  overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow-wrap: anywhere;
}

/* ---------- conversation history ---------- */
.convo {
  border-left: 2px solid var(--line); padding: 2px 0 2px 10px;
  margin: 0 0 10px; display: flex; flex-direction: column; gap: 5px;
}
.turn { font-size: 13px; line-height: 1.4; }
.turn .who { font-weight: 600; color: var(--ink-soft); }
.turn.tom .who { color: var(--go); }
.turn.tom { color: var(--ink); }
.turn.them { color: var(--ink-soft); }
.convo-note { font-size: 11.5px; color: var(--ink-soft); font-style: italic; }

/* ---------- regenerate spinner ---------- */
.spin {
  width: 13px; height: 13px; margin-right: 7px; flex: 0 0 auto;
  border: 2px solid var(--line); border-top-color: var(--ink-soft);
  border-radius: 50%; display: inline-block;
  animation: spin .7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.btn-regen.working { color: var(--ink-soft); }
.card.regenerating .draft { opacity: .45; }

/* ---------- bulk review sheet ---------- */
/* The sheet floats clear of the screen edges so all four corners are visible
   and identical. It used to sit flush at the bottom with only the top corners
   rounded; the safe-area padding now lives on the backdrop, not the box. */
.sheet {
  position: fixed; inset: 0; z-index: 50;
  background: rgba(43,38,34,.45); display: flex;
  align-items: flex-end; justify-content: center;
  padding: 0 max(8px, env(safe-area-inset-left)) calc(8px + env(safe-area-inset-bottom))
             max(8px, env(safe-area-inset-right));
}
.sheet-inner {
  background: var(--ivory); width: 100%; max-height: 88vh;
  /* `vh` on a phone is the tall viewport — the one with the browser's own bars
     hidden — so 88vh can be more than what is actually on the screen, and the
     bottom of the sheet ends up below the fold with the Send button on it.
     `dvh` is the viewport as it is right now. The `vh` line above stays as the
     fallback for anything that does not know `dvh`. */
  max-height: 88dvh;
  border-radius: var(--radius);
  display: flex; flex-direction: column;
  /* The footer's own background would square off the bottom corners without
     this — the clip is what makes the radius actually show. It is also what
     stops a long unbroken word widening the whole sheet. */
  overflow: hidden;
  min-width: 0;
}
.sheet-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 14px 14px 4px;
  flex: 0 0 auto;
}
.sheet-head h2 { font-size: 17px; margin: 0; }
.sheet-close {
  border: none; background: none; font-size: 19px; color: var(--ink-soft);
  cursor: pointer; padding: 4px 6px;
}
.sheet-note {
  font-size: 13px; color: var(--ink-soft); margin: 0; padding: 0 14px 10px;
  flex: 0 0 auto;
}
.sheet-status { flex: 0 0 auto; }
/* The one part that scrolls. `min-height: 0` is what lets it: without it a
   flex child refuses to shrink below its content, the column grows past the
   sheet, and the footer goes off the bottom of the screen — which is exactly
   what it was doing. */
.sheet-list {
  overflow-y: auto; overflow-x: hidden; padding: 0 14px;
  flex: 1 1 auto; min-height: 0;
}
/* Always there, at the bottom, whatever the list is doing. */
.sheet-foot {
  display: flex; gap: 8px; padding: 11px 14px 14px;
  border-top: 1px solid var(--line); background: var(--ivory);
  flex: 0 0 auto;
}
.sheet-foot .btn-approve { flex: 1 1 auto; min-width: 0; }
.sheet-foot .btn { min-width: 0; white-space: nowrap; }
@media (max-width: 400px) {
  .sheet-foot { gap: 6px; padding: 10px 10px 12px; }
  .sheet-foot .btn { font-size: 12.5px; padding: 11px 8px; }
}

.bulkitem {
  display: flex; gap: 10px; align-items: flex-start;
  padding: 10px 0; border-bottom: 1px solid var(--line);
  /* A row may never be wider than the sheet. Tom was pinch-zooming out to
     read it, which means something in here was setting the width — a long
     post title, a URL in a draft, a name with no spaces in it. */
  min-width: 0; max-width: 100%;
}
.bulkitem:last-child { border-bottom: none; }
.bulkitem input[type=checkbox] { width: 21px; height: 21px; flex: 0 0 auto; margin-top: 2px; accent-color: var(--go); }
.bulkitem .bi-body { flex: 1; min-width: 0; }
.bulkitem .bi-who { font-size: 12px; color: var(--ink-soft); margin-bottom: 3px;
                    display: flex; align-items: baseline; gap: 4px; flex-wrap: wrap; }
.bulkitem .bi-who b { color: var(--ink); }
.bulkitem .bi-post {
  color: var(--ink); background: #FAF6EE; border: 1px solid var(--line);
  border-radius: 999px; padding: 1px 8px; max-width: 100%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.bulkitem .bi-them {
  font-size: 13px; color: var(--ink-soft); margin-bottom: 3px;
  overflow-wrap: anywhere;
}
.bulkitem .bi-draft { font-size: 14px; overflow-wrap: anywhere; }
.bulkitem .bi-who { min-width: 0; }
/* The post title truncates. It is context, not the thing being reviewed, and
   it is the longest string in the row. */
.bulkitem .bi-post { flex: 0 1 auto; }
.bulkitem .bi-rewrite, .bulkitem .bi-rewrite input { max-width: 100%; min-width: 0; }
.bulkitem.off { opacity: .45; }

/* ---------- the sending pile ---------- */
.pile-head {
  font-size: 11px; letter-spacing: .5px; text-transform: uppercase;
  color: var(--ink-soft); margin: 4px 2px -4px;
}
/* ---------- a lane heading, and the line under it ----------
   The heading is a label and stays upper-case; the line under it is prose and
   must not be — eleven-pixel caps is unreadable at sentence length, which is
   what `.pile-head` would have made of it. The extra bottom margin is because
   this one is two lines tall and the pile heading's negative margin was set
   for one. */
.pile-lane { margin-bottom: 8px; }
.pile-note {
  display: block; text-transform: none; letter-spacing: 0;
  font-size: 12px; line-height: 1.35; margin-top: 3px;
  color: var(--ink-soft);
}
.sending-card.state-failed { border-left-color: var(--danger); background: #FDF2F0; }
.sending-card.state-sent   { border-left-color: var(--go); }
.sendstate {
  flex: 0 0 auto; font-size: 11.5px; font-weight: 600; padding: 3px 9px;
  border-radius: 999px; background: #F2EFE8; color: var(--ink-soft);
  display: inline-flex; align-items: center;
}
.sendstate.sending { background: var(--warn-bg); color: var(--warn); }
.sendstate.sent    { background: #F1F6F2; color: var(--go); }
.sendstate.failed  { background: #FBEAE6; color: var(--danger); }

/* ---------- answered view ---------- */
.card.answered-card { border-left-color: var(--go); }
.answered-reply {
  background: #F1F6F2; border: 1px solid #D6E4DA; border-radius: 9px;
  padding: 9px 10px; font-size: 14.5px; margin-top: 8px;
}
.answered-meta { font-size: 12px; color: var(--ink-soft); margin-top: 6px; }

/* ---------- security page ---------- */
.sec-h { font-size: 16px; margin: 0 0 8px; }
.sec-note { font-size: 13.5px; color: var(--ink-soft); margin: 0 0 12px; }
.sec-policy {
  font-size: 14.5px; background: #F1F6F2; border: 1px solid #D6E4DA;
  border-radius: 9px; padding: 10px 12px; margin: 0;
}
.sec-policy.weak { background: var(--warn-bg); border-color: #EBD5AC; color: var(--warn); }
.sec-warn { font-size: 13px; color: var(--danger); margin: 8px 0 0; }

/* The go-live checklist. Three states, and the mark carries the meaning as
   well as the colour — a red dot on its own is not a state anybody can read
   in daylight on a phone. */
.pf-row {
  display: flex; gap: 10px; align-items: flex-start; padding: 8px 0;
  border-bottom: 1px solid var(--line); font-size: 14px;
}
.pf-row:last-of-type { border-bottom: 0; }
.pf-mark {
  flex: 0 0 20px; text-align: center; font-weight: 700; line-height: 1.5;
}
.pf-ok .pf-mark { color: var(--go); }
.pf-warn .pf-mark { color: var(--warn); }
.pf-blocker .pf-mark { color: var(--danger); }
.pf-blocker b { color: var(--danger); }
.pf-row .set-note { margin: 4px 0 0; }
.sec-input {
  flex: 1; min-width: 0; font: inherit; font-size: 15px; padding: 10px 11px;
  border: 1px solid var(--line); border-radius: var(--r-sm); background: #FCFAF6;
}
/* A row that draws its own divider keeps its contents clear of it.
 *
 * 9px above and below a hairline is close enough that the chip on one row and
 * the line under it read as one object. The line is a thing to be spaced
 * from, the same as anything else. */
.sec-item {
  display: flex; align-items: center; gap: var(--space);
  padding: var(--gap, 15px) 0;
  border-bottom: 1px solid var(--line); font-size: 14px;
}
.sec-item:last-child { border-bottom: none; }
.sec-item .grow { flex: 1; min-width: 0; }
.sec-item .meta { font-size: 12px; color: var(--ink-soft); }
.sec-item button {
  font: inherit; font-size: 13px; border: 1px solid var(--line); background: var(--card);
  color: var(--danger); border-radius: var(--r-sm); padding: 6px 11px; cursor: pointer;
}
.sec-empty { font-size: 13.5px; color: var(--ink-soft); padding: 6px 0 12px; }
.sec-qr { background: #fff; border: 1px solid var(--line); border-radius: 10px;
          padding: 10px; width: 220px; max-width: 100%; margin: 4px 0 10px; }
.sec-qr svg { width: 100%; height: auto; display: block; }
.sec-secret {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px;
  background: #F2EFE8; border: 1px solid var(--line); border-radius: 8px;
  padding: 8px 10px; word-break: break-all; margin-bottom: 10px;
}
.sec-log { font-size: 12.5px; color: var(--ink-soft); }
.sec-log div { padding: 4px 0; border-bottom: 1px solid var(--line); }
.sec-log div:last-child { border-bottom: none; }
.sec-log .ok { color: var(--go); }
.sec-log .refused { color: var(--danger); }
.login .alt { margin: 14px 0 0; }
.login .or { font-size: 12.5px; color: var(--ink-soft); margin: 12px 0; }

/* ---------- people ---------- */
.people-search { flex: 1; min-width: 140px; font-size: 14px; padding: 7px 11px; }
.person-card {
  background: var(--card); border: 1px solid var(--line); border-left: 4px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow); padding: 12px 14px; cursor: pointer;
}
.person-card.pf-instagram { border-left-color: var(--ig); }
.person-card.pf-facebook  { border-left-color: var(--fb); }
.person-card.pf-both      { border-left-color: var(--go); }
.person-top { display: flex; align-items: center; gap: 11px; }
.person-namecol { flex: 1; min-width: 0; }
.person-name { display: block; font-weight: 600; font-size: 15.5px;
               overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.person-count { font-size: 12.5px; color: var(--ink-soft); font-variant-numeric: tabular-nums; }
.person-bullets { margin: 7px 0 0; padding: 0; list-style: none;
                  font-size: 13px; color: var(--ink-soft); }
.person-bullets li { padding: 1px 0; }
.person-bullets li b { color: var(--ink); font-weight: 600; }
.person-open { color: var(--warn); }
.person-badges { display: flex; gap: 5px; margin-top: 7px; flex-wrap: wrap; }
.pbadge { font-size: 10.5px; font-weight: 700; letter-spacing: .4px; text-transform: uppercase;
          padding: 2px 6px; border-radius: 4px; color: #fff; }
.pbadge.instagram { background: var(--ig); }
.pbadge.facebook  { background: var(--fb); }
.pbadge.unknown   { background: var(--ink-soft); }
.pbadge.todo      { background: var(--warn); }

/* ---------- the person card ---------- */
/* Read mode is a card, not a form: a big initial circle, the name and quick
   facts beside it, and everything Tom typed shown as text or chips. The input
   boxes only exist while EDITING. */
.p-head { align-items: flex-start; padding: 14px 14px 10px; gap: 10px; }
.p-hero { display: flex; align-items: center; gap: 13px; flex: 1; min-width: 0; }
.p-hero .avatar { box-shadow: 0 2px 8px rgba(43,38,34,.18); }
.p-hero-text { min-width: 0; flex: 1; }
.p-hero-text h2 { font-size: 19px; margin: 0; line-height: 1.2;
                  overflow: hidden; text-overflow: ellipsis; }
.p-sub { font-size: 13px; color: var(--ink-soft); margin-top: 1px; }
.p-facts { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 6px; }
.p-fact {
  font-size: 11.5px; padding: 2px 8px; border-radius: 999px;
  background: #F2EFE8; color: var(--ink-soft); white-space: nowrap;
}
.p-fact.todo { background: var(--warn-bg); color: var(--warn); font-weight: 600; }
.p-fact.pf-instagram { background: #FBEEF5; color: var(--ig); }
.p-fact.pf-facebook  { background: #EAF0FB; color: var(--fb); }

.p-rows { display: flex; flex-direction: column; gap: 9px; padding: 4px 0 2px; }
.p-row { display: flex; gap: 9px; align-items: baseline; }
.p-key {
  flex: 0 0 62px; font-size: 11px; letter-spacing: .5px; text-transform: uppercase;
  color: var(--ink-soft); padding-top: 3px;
}
.p-vals { flex: 1; min-width: 0; display: flex; flex-wrap: wrap; gap: 5px; }
.p-chip {
  font-size: 13px; padding: 3px 10px; border-radius: 999px;
  background: #FAF6EE; border: 1px solid var(--line); color: var(--ink);
}
.p-text { flex: 1; min-width: 0; font-size: 14.5px; line-height: 1.45; }
.p-thread {
  margin-top: 11px; font-size: 14px; line-height: 1.45;
  background: var(--warn-bg); border: 1px solid #EBD5AC;
  border-radius: var(--r-sm); padding: 9px 11px;
}
.p-thread b { display: block; font-size: 11px; letter-spacing: .5px;
              text-transform: uppercase; color: var(--warn); margin-bottom: 2px; }

/* The one field that stays a write-in box, because it is the one Tom adds to
   while he is reading. */
.p-notes { margin-top: 14px; }
.p-notes label { display: block; font-size: 11px; letter-spacing: .5px;
                 text-transform: uppercase; color: var(--ink-soft); margin-bottom: 4px; }
.p-notes textarea {
  width: 100%; font: inherit; font-size: 14.5px; padding: 10px 11px;
  border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--card); resize: vertical; min-height: 76px;
}
.p-note-save { margin-top: 7px; }

/* ---------- attachments ---------- */
.p-files { margin-top: 16px; }
.p-files h3 { font-size: 11px; letter-spacing: .5px; text-transform: uppercase;
              color: var(--ink-soft); margin: 0 0 7px; }
.p-files-empty { font-size: 13px; color: var(--ink-soft); margin: 0; font-style: italic; }
.p-file-grid { display: flex; flex-wrap: wrap; gap: 10px; }
.p-file { position: relative; width: 92px; }
.p-file a { display: block; line-height: 0; }
.p-file img {
  width: 92px; height: 92px; object-fit: cover;
  border-radius: var(--r-sm); border: 1px solid var(--line); background: #F2EFE8;
}
.p-file-doc {
  display: flex; align-items: center; justify-content: center;
  width: 92px; height: 92px; border-radius: var(--r-sm);
  border: 1px dashed var(--line); background: #FAF6EE;
  font-size: 12px; font-weight: 700; letter-spacing: .5px; color: var(--ink-soft);
}
.p-file-cap {
  display: block; font-size: 11.5px; color: var(--ink-soft); line-height: 1.3;
  margin-top: 4px; overflow: hidden; display: -webkit-box;
  -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow-wrap: anywhere;
}
.p-file-del {
  /* Inside the tile, not overhanging it: the sheet's scroll container clips
     anything that pokes out at the right-hand edge. */
  position: absolute; top: 4px; right: 4px; width: 24px; height: 24px;
  border-radius: 50%; border: 1px solid var(--line); background: var(--card);
  color: var(--danger); font-size: 12px; line-height: 1; cursor: pointer;
  box-shadow: var(--shadow);
}

.p-field { margin-bottom: 10px; }
.p-field label { display: block; font-size: 12.5px; color: var(--ink-soft); margin-bottom: 3px; }
.p-field input, .p-field textarea {
  width: 100%; font: inherit; font-size: 14.5px; padding: 9px 10px;
  border: 1px solid var(--line); border-radius: var(--r-sm); background: #FCFAF6; resize: vertical;
}
.p-field textarea { min-height: 68px; }
.p-hist { margin-top: 12px; }
.p-hist h3 { font-size: 13.5px; margin: 0 0 6px; color: var(--ink-soft); }
.p-turn { border-left: 2px solid var(--line); padding: 4px 0 6px 10px; margin-bottom: 6px;
          font-size: 13.5px; }
.p-turn .when { font-size: 11.5px; color: var(--ink-soft); }
.p-turn .yours { color: var(--go); margin-top: 3px; }
.p-turn.unanswered { border-left-color: var(--warn); }

/* ---------- stats ---------- */
.stat-grid { display: grid; grid-template-columns: 1fr auto; gap: 6px 12px; font-size: 14px; }
.stat-k { color: var(--ink-soft); }
.stat-v { font-variant-numeric: tabular-nums; font-weight: 600; text-align: right; }
.stat-v.good { color: var(--go); }
.stat-v.warn { color: var(--warn); }
.bars { margin-top: 12px; display: flex; flex-direction: column; gap: 3px; }
.bar-row { display: flex; align-items: center; gap: 8px; font-size: 12px; }
.bar-label { width: 42px; color: var(--ink-soft); flex: 0 0 auto; }
.bar-track { flex: 1; height: 12px; background: #F2EFE8; border-radius: 6px; overflow: hidden; }
.bar-fill { display: block; height: 100%; background: var(--go); border-radius: 6px; }
.bar-val { width: 54px; text-align: right; font-variant-numeric: tabular-nums;
           color: var(--ink-soft); flex: 0 0 auto; }

/* ---------- bulk sheet: per-item state ---------- */
.sheet-status {
  font-size: 12.5px; color: var(--ink-soft); margin: 0; padding: 0 14px 8px;
  font-variant-numeric: tabular-nums;
}
.bulk-group {
  font-size: 12px; font-weight: 700; letter-spacing: .4px; text-transform: uppercase;
  color: var(--ink-soft); padding: 12px 0 4px; position: sticky; top: 0;
  background: var(--ivory);
}
.bulk-group.fresh { color: var(--warn); }
.bulk-group.fresh::before { content: '● '; }
.bi-age { font-size: 11.5px; color: var(--ink-soft); margin-left: 6px; white-space: nowrap; }
.bi-state {
  flex: 0 0 auto; font-size: 12px; color: var(--ink-soft);
  display: inline-flex; align-items: center; gap: 5px; white-space: nowrap;
}
.bi-state.ok  { color: var(--go); font-weight: 600; }
.bi-state.bad { color: var(--danger); }
.bulkitem.status-sending { opacity: .75; }
.bulkitem.status-sent    { opacity: .55; }
.bulkitem.status-failed  { background: #FBEAE6; border-radius: 8px; padding-left: 6px; }
.bulkitem input[type=checkbox]:disabled { opacity: .4; }

/* ---------- manual name (Facebook, until App Review) ---------- */
.namefield {
  flex: 0 1 130px; min-width: 0; font: inherit; font-size: 12.5px;
  padding: 4px 8px; border: 1px dashed var(--line); border-radius: var(--r-sm);
  background: #FCFAF6; color: var(--ink);
}
.namefield:focus { outline: 2px solid var(--go); outline-offset: -1px;
                   border-style: solid; background: #fff; }
.author.named { color: var(--ink); }

/* ---------- desktop ---------- */
@media (min-width: 700px) {
  /* Desktop widens the outer padding past the gap rather than narrowing it —
     the inequality holds in both directions of the breakpoint. */
  :root { --gap-outer: 20px; }
  .cards, .wrap {
    max-width: 900px; margin: 0 auto;
    padding: 16px var(--gap-outer) 140px;
  }
  /* The one exception the spacing rule allows, and it is about width rather
     than padding: the Forms live preview shows a real page at a real device
     width, so clamping the page to a reading width would make the preview a
     lie. The outer padding is unchanged. */
  .wrap-wide { max-width: 1380px; }
  /* AND THE BUILDER ITSELF IS ALLOWED THE WHOLE DESK.
   *
   * 1380 is a reading width, and the two-column builder is not reading. A
   * form set to 1200px needs 1200 for the preview plus a column of controls
   * beside it — about 1600 — so inside a 1340px content box it never fitted,
   * and the layout did the only thing left: put the preview ABOVE the
   * editor. On a 4K screen that meant two thirds of the window empty and the
   * controls pushed a screen down, which is the case this was reported for.
   *
   * Only the tabs that HAVE a preview take this; the list, the submissions
   * and the settings are prose and stay at a reading width. */
  .wrap-builder { max-width: 1900px; }
  .topbar { padding-left: 20px; padding-right: 20px; }
  .postctx { padding: 9px 11px; gap: 13px; }
  .postthumb { width: 48px; height: 48px; border-radius: 8px; }
  /* One line, full width, ellipsis. A wrapping caption pushed the comment
     down the card and made every card a different height; a single line that
     shortens as the window narrows keeps the card rhythm and still says which
     post this is. */
  .postcap {
    font-size: 13.5px; display: block; -webkit-line-clamp: none;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    overflow-wrap: normal;
  }
  .card { padding: 16px 18px 14px; }
  .comment { font-size: 16px; }
  .draft { font-size: 16px; min-height: 92px; }
  /* The reason chips were only revealed by the first Regenerate tap, which on
     a big screen just looks like they are missing. There is room here. */
  .reasons { display: flex !important; }
  .reasons[hidden] { display: flex !important; }
  .bulkbar, .toasts { left: auto; right: 24px; width: min(420px, calc(100vw - 48px)); }
  .sheet { align-items: center; justify-content: center; }
  .sheet-inner { max-width: 720px; border-radius: var(--radius); max-height: 84vh; }
}

/* ---------- desktop drawer ---------- */
/* Mobile keeps the top filter bar as its navigation; a drawer there would eat
   thumb space for no gain, so this whole component starts hidden. */
.drawer { display: none; }

@media (min-width: 700px) {
  .drawer {
    display: block; position: fixed; left: 0; top: 0; bottom: 0; z-index: 60;
    width: 56px; transition: width .16s ease;
  }
  /* THE RAIL SCROLLS WHEN IT DOES NOT FIT, rather than quietly losing its
     last items.

     It was `overflow: hidden`, and the list of sections had grown to 845px.
     On a 1366x768 laptop that silently cut off Download log and Sign out; on
     a 700px window it cut off SETTINGS as well. Nothing said so — the items
     were in the DOM, correct and clipped — which is why it read as "the
     Settings icon has stopped showing" rather than as a rail that was too
     short. Adding System is what pushed it over the edge on a laptop, and
     the next section added would have taken Settings out on every screen.

     `overflow-y: auto` and no scrollbar drawn: a 56px rail with a scrollbar
     down it is worse than the problem. Same treatment `.subtabs` already
     gets, for the same reason — the row scrolls, the furniture does not show. */
  .drawer-inner {
    height: 100%; width: 100%;
    overflow-x: hidden; overflow-y: auto;
    scrollbar-width: none;
    background: var(--card); border-right: 1px solid var(--line);
    display: flex; flex-direction: column; padding: 10px 8px;
    box-shadow: 0 0 0 rgba(0,0,0,0);
    transition: box-shadow .16s ease;
  }
  .drawer-inner::-webkit-scrollbar { display: none; }
  /* Slides out on hover when collapsed, and stays out when pinned. */
  .drawer:hover, .drawer.pinned { width: 210px; }
  .drawer:hover .drawer-inner { box-shadow: var(--shadow); }

  .drawer-head { display: flex; align-items: center; gap: 9px; padding: 4px 6px 12px; }
  .drawer-logo { width: 28px; height: 28px; border-radius: 7px; flex: 0 0 auto; }
  .drawer-title { font-weight: 700; font-size: 15px; white-space: nowrap; }
  .drawer-pin {
    margin-left: auto; border: none; background: none; cursor: pointer;
    color: var(--ink-soft); font-size: 14px; padding: 2px 4px;
  }
  .drawer.pinned .drawer-pin { color: var(--go); transform: rotate(180deg); }

  .drawer-item {
    display: flex; align-items: center; gap: 12px; white-space: nowrap;
    padding: 10px 8px; border-radius: 9px; margin-bottom: 2px;
    color: var(--ink-soft); text-decoration: none; font-size: 14.5px;
    font-family: inherit; border: none; background: none; width: 100%;
    cursor: pointer; text-align: left;
  }
  .drawer-item:hover { background: #FAF6EE; color: var(--ink); }
  .drawer-item.active { background: #F1F6F2; color: var(--ink); font-weight: 600; }
  /* NOT ALLOWED TO SHRINK. In a column flex box that scrolls, the default
     `flex-shrink: 1` squashes every row a few pixels shorter instead of
     letting the rail scroll — thirteen sections quietly losing their padding
     until they are unreadable, which is a subtler version of the same fault. */
  .drawer-item { flex: 0 0 auto; }
  .di-icon { width: 22px; text-align: center; flex: 0 0 auto; font-size: 15px; }
  .di-label { opacity: 0; transition: opacity .12s ease; }
  .drawer:hover .di-label, .drawer.pinned .di-label { opacity: 1; }
  /* Pushes Download log and Sign out to the bottom when there is room, and
     collapses to nothing when there is not, rather than shoving them out of
     a short rail. */
  .drawer-spacer { flex: 1 1 0; min-height: 8px; }
  .drawer-form { margin: 0; flex: 0 0 auto; }

  /* Make room for the rail; pinning widens the gutter rather than covering
     the queue, so nothing is ever hidden behind the drawer. */
  body { padding-left: 56px; transition: padding-left .16s ease; }
  body.drawer-pinned { padding-left: 210px; }
  body.auth { padding-left: 0; }
}

/* On very wide screens the drawer is open by default — there is room for it
   and hunting for a hover target on a large monitor is a poor trade. */
@media (min-width: 1400px) {
  .drawer { width: 210px; }
  .drawer .di-label { opacity: 1; }
  body { padding-left: 210px; }
  body.auth { padding-left: 0; }
}

/* ---------- screening ---------- */
.chip-alert { border-color: #EBD5AC; color: var(--warn); }
.chip-alert.chip-on { background: var(--warn); border-color: var(--warn); color: #fff; }
.card.screened-scam, .card.screened-hate {
  border-left-color: var(--danger); background: #FDF6F4;
}
.card.screened-tom-only { border-left-color: var(--warn); background: var(--warn-bg); }
/* Scam and hate get no reply at all, so the draft box and the two buttons
   that would send one are not there. That is still right.

   Tom-only is NOT in this list any more. These cards get a best-effort draft
   now, and hiding the box left a card that said "a starting point only — read
   it before you send it" above nothing at all, with no way to send it either.
   The draft was in the DOM the whole time with `display: none` on it. */
.card.screened-scam .draft, .card.screened-hate .draft { display: none; }
.card.screened-scam .btn-approve, .card.screened-hate .btn-approve,
.card.screened-scam .btn-regen, .card.screened-hate .btn-regen,
.card.screened-scam .reasons, .card.screened-hate .reasons {
  display: none !important;
}
.btn-hide { color: var(--danger); border-color: #EFC9C0; }
.btn-dismiss { color: var(--ink-soft); }

/* ---------- home ---------- */
.home-card { padding: 16px 18px; }
.home-hero { text-align: center; background: linear-gradient(160deg,#FFFDF8,#F4EEE1); }
.home-big { font-size: 54px; font-weight: 800; line-height: 1; letter-spacing: -1px; }
.home-biglabel { color: var(--ink-soft); font-size: 14px; margin-top: 4px; }
.home-jumps { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; margin-top: 14px; }
.home-jump {
  text-decoration: none; font-size: 14px; font-weight: 600; padding: 10px 15px;
  border-radius: 999px; background: var(--card); border: 1px solid var(--line); color: var(--ink);
}
.home-jump.go { background: var(--go); border-color: var(--go); color: #fff; }
.home-jump.warn { background: var(--warn-bg); border-color: #EBD5AC; color: var(--warn); }
.home-jump.bad { background: #FBEAE6; border-color: #EFC9C0; color: var(--danger); }
.home-h { display: flex; align-items: baseline; justify-content: space-between;
          font-weight: 700; font-size: 15px; margin-bottom: 10px; }
.home-more { font-size: 13px; color: var(--go); text-decoration: none; font-weight: 600; }
.home-stats { display: grid; grid-template-columns: repeat(auto-fit,minmax(96px,1fr)); gap: 8px; }
/* The one-line agenda summary on Home. Warnings are red because the two that
   matter — publishing is live, a job failed — are things to act on today. */
.home-note { font-size: 14px; color: var(--ink-soft); margin: 4px 0 0; }
/* Top pages: paths can be long, and wrapping them turns a five-line list into
   a nine-line one. Truncated, with the full path on hover. */
.home-list { list-style: none; margin: 10px 0 0; padding: 0; }
.home-list li {
  font-size: 13.5px; color: var(--ink-soft); padding: 3px 0;
  border-top: 1px solid var(--line);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.home-warn { color: var(--danger); font-weight: 600; }
.home-stat { background: #FAF6EE; border: 1px solid var(--line); border-radius: var(--r-sm);
             padding: 10px 8px; text-align: center; }
.home-stat.go .hs-n { color: var(--go); }
.home-stat.warn .hs-n { color: var(--warn); }
.home-stat.bad .hs-n { color: var(--danger); }
.home-stat.fb .hs-n { color: var(--fb); }
.home-stat.ig .hs-n { color: var(--ig); }
.hs-n { font-size: 20px; font-weight: 700; font-variant-numeric: tabular-nums; }
.hs-l { font-size: 11.5px; color: var(--ink-soft); margin-top: 2px; }
.home-post, .home-person {
  display: flex; align-items: center; gap: 11px;
  padding: var(--gap, 15px) 0;
  border-bottom: 1px solid var(--line); text-decoration: none; color: inherit;
}
.home-post:last-child, .home-person:last-child { border-bottom: none; }
.home-post img { width: 44px; height: 44px; object-fit: cover; border-radius: var(--r-sm); flex: 0 0 auto; }
.hp-body { flex: 1; min-width: 0; }
.hp-cap { font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hp-meta { font-size: 12px; color: var(--ink-soft); }
.hp-badge { flex: 0 0 auto; background: var(--warn); color: #fff; font-size: 12.5px;
            font-weight: 700; border-radius: 999px; padding: 3px 9px; }
.hp-badge.soft { background: #F2EFE8; color: var(--ink-soft); }

/* ---------- log ---------- */
.log-search { flex: 1; min-width: 150px; font-size: 14px; padding: 7px 11px; }
.log-person { flex: 1; min-width: 110px; font-size: 14px; padding: 7px 11px; }
.log-date { font-size: 13px; padding: 6px 9px; flex: 0 1 140px; }
.log-draft { font-size: 13.5px; color: var(--ink-soft); background: #FAF6EE;
             border: 1px solid var(--line); border-radius: var(--r-sm);
             padding: 8px 10px; margin-top: 8px; }
.log-draft b { color: var(--ink); }
/* The infinite-scroll sentinel. It has to be tall enough to be observable and
   quiet enough not to look like a card. */
.log-more { min-height: 40px; padding: 10px 20px 90px; text-align: center;
            font-size: 12.5px; color: var(--ink-soft); }
.filter-label { font-size: 12px; color: var(--ink-soft); align-self: center;
                flex: 0 0 auto; text-transform: uppercase; letter-spacing: .4px; }
.author { display: inline-flex; align-items: center; gap: 7px; }
.spark { margin-top: 12px; }
.spark svg { width: 100%; height: 64px; display: block; }
.spark-cap { font-size: 12.5px; color: var(--ink-soft); margin-top: 2px; }
.spark-cap b { color: var(--ink); font-variant-numeric: tabular-nums; }
.bar-fill { transition: width .3s ease; }

/* The "new above you" pill. Announcing new work is right; scrolling him to it
   mid-sentence is not. */
.newpill {
  position: fixed; left: 50%; transform: translateX(-50%);
  top: calc(10px + env(safe-area-inset-top));
  z-index: 45; border: none; cursor: pointer;
  background: var(--ink); color: var(--ivory);
  border-radius: 999px; padding: 8px 16px;
  font: inherit; font-size: 13.5px; font-weight: 600;
  box-shadow: var(--shadow);
}

/* What a person has asked for, and the last resort. Kept visually quiet:
   these are not everyday controls and should not look like them. */
.p-consent { margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--line); }
.p-consent h3 { font-size: 14px; margin: 0 0 10px; color: var(--ink-soft); }
.p-consent-row {
  display: flex; gap: 10px; align-items: flex-start;
  padding: 8px 0; cursor: pointer;
}
.p-consent-row input { margin-top: 3px; flex: 0 0 auto; }
.p-consent-row b { display: block; font-size: 14.5px; font-weight: 600; }
.p-consent-row small { display: block; font-size: 12.5px; color: var(--ink-soft);
                       line-height: 1.4; margin-top: 1px; }
.p-danger { margin-top: 12px; }
.gv-notyet {
  border-left: 3px solid var(--warn, #B4731A); padding-left: 10px;
  margin-top: 6px;
}
.gv-notyet b { color: var(--warn, #B4731A); }

/* The preview page wears the giveaway's own background, so the cards inside it
   have to look like cards on it — cream, as they are on the real page. */
.gv-preview-page > h2, .gv-preview-page > h3,
.gv-preview-page > .gv-preview-desc, .gv-preview-page > .gv-preview-prize {
  color: #f3ede3; text-shadow: 0 1px 3px rgba(0,0,0,.5);
}
.gv-preview-page.is-light > h2, .gv-preview-page.is-light > h3,
.gv-preview-page.is-light > .gv-preview-desc,
.gv-preview-page.is-light > .gv-preview-prize {
  color: #2b1114; text-shadow: none;
}
.gv-preview-media {
  width: 100%; border-radius: 10px; display: block; margin-bottom: 10px;
  max-height: 160px; object-fit: cover;
}

/* The background chooser's swatch: the real gradient with a real card on it,
   because "dark radial burgundy" means nothing until you see it. */
.gv-bg-preview {
  margin-top: 10px; border-radius: 12px; min-height: 132px;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--line);
}
.gv-bg-card {
  background: #f3ede3; color: #2b1114; border-radius: 10px;
  padding: 12px 16px; font-size: 13.5px; font-weight: 600;
  box-shadow: 0 6px 18px rgba(0,0,0,.35);
}
.gv-colour { height: 44px; padding: 4px; cursor: pointer; }

.chip-danger { border-color: var(--stop, #B3261E); color: var(--stop, #B3261E); }
.chip-danger:active { background: var(--stop, #B3261E); color: #fff; }

/* Search. One box, above the filters — it searches everything, so it does not
   belong inside a row of things that narrow one view. */
.searchbar { display: flex; gap: 8px; align-items: center; margin: 8px 0 2px; }
.search-input {
  flex: 1 1 auto; min-width: 0; font: inherit; font-size: 16px;
  padding: 9px 12px; border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--card); color: var(--ink);
}
.search-input:focus { outline: 2px solid var(--go); outline-offset: 1px; }
.s-head { font-size: 13px; color: var(--ink-soft); margin: 10px 2px 6px; }
.s-hit {
  background: var(--card); border-radius: var(--r); box-shadow: var(--shadow);
  padding: 12px 14px; margin-bottom: 8px;
}
.s-hit .who { font-size: 12.5px; color: var(--ink-soft); margin-bottom: 4px; }
.s-hit .who b { color: var(--ink); }
.s-hit .body { font-size: 15px; line-height: 1.45; overflow-wrap: anywhere; }
.s-hit .body mark { background: #FBE7A2; color: inherit; border-radius: 3px;
                    padding: 0 2px; }
.s-hit.mine { border-left: 3px solid var(--go); }
.s-hit .s-links { margin-top: 8px; display: flex; gap: 8px; flex-wrap: wrap; }

/* ---- the last seven days of spend ------------------------------------
 *
 * A column per day, anchored to a baseline, with a three-line scale behind
 * them. The plot has a left gutter for the scale's figures and the bars are
 * inset by it, so a tick and a bar never sit on top of each other.
 *
 * The bars are separated by a gap in the surface rather than by a border
 * around each one: a hairline outline on a coloured block reads as a box with
 * something in it, and what is wanted is the block. */
.wk { margin-top: 6px; }
/* The week either side. A row above the plot rather than arrows floating on
   it: an arrow sitting over the bars is a thing to click ON the chart, and
   these move WHICH chart. The caption between them carries the month, which
   the bars' own day-of-month labels cannot once the range has paged away from
   the week the reader is standing in. */
.wk-head {
  display: flex; align-items: center; justify-content: center; gap: 10px;
  margin: 2px 0 -8px;
}
.wk-range {
  font-size: 12.5px; color: var(--ink-soft); font-variant-numeric: tabular-nums;
  min-width: 11ch; text-align: center;
}
.wk-arrow {
  flex: none; width: 26px; height: 26px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--line); border-radius: 7px;
  background: var(--card); color: var(--ink);
  font-size: 15px; line-height: 1; cursor: pointer;
}
.wk-arrow:hover:not(:disabled) { background: var(--surface); }
/* Dead, not gone. An arrow that vanishes at the end of the record takes the
   symmetry of the row with it and moves the caption sideways on every page. */
.wk-arrow:disabled { opacity: .35; cursor: default; }
/* The room under the plot belongs to the PLOT, not to the note below it.
   `.wk-day` hangs outside the box at `top: 100%`, and the clearance used to
   come from `.wk-note`'s top margin — which was fine while the graph always
   contained today and therefore always had a note. The moment the arrows
   could page to a week that does not, the note went away and took the
   clearance with it, and the weekday labels landed on the figures below. */
.wk-plot {
  position: relative; height: 132px; margin: 26px 0 40px;
  padding-left: 48px;
}
/* Solid hairlines, one shade off the surface. Never dashed — a dashed rule
   reads as a threshold or a projection, and this is neither. */
.wk-rule {
  position: absolute; left: 48px; right: 0; height: 0;
  border-top: 1px solid var(--line); pointer-events: none;
}
.wk-base { border-top-color: var(--ink-soft); opacity: .45; }
.wk-tick {
  position: absolute; right: 100%; top: -0.62em; padding-right: 8px;
  font-size: 11px; color: var(--ink-soft); font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.wk-bars {
  position: absolute; inset: 0 0 0 48px;
  display: flex; align-items: flex-end; gap: 2px;
}
.wk-col {
  flex: 1 1 0; min-width: 0; height: 100%;
  display: flex; flex-direction: column; justify-content: flex-end;
  align-items: center; position: relative;
}
.wk-val {
  position: absolute; bottom: 100%; left: 0; right: 0; margin-bottom: 4px;
  text-align: center; font-size: 11.5px; font-weight: 600; color: var(--ink);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.wk-val-off { display: none; }
/* Capped, not full-bleed. A column-wide block is a wall of colour: it reads
   loud, it crowds its neighbour, and the white either side of a narrower bar
   is what makes seven of them scan as a series rather than a fence. */
.wk-track {
  display: flex; align-items: flex-end; justify-content: center;
  width: 100%; max-width: 46px; height: 100%;
}
.wk-track i {
  display: block; width: 100%; background: var(--go);
  /* Rounded at the data end only. A bar rounded at the baseline too floats
     off it, and the baseline is the one place the bar must be exact. */
  border-radius: 4px 4px 0 0;
}
/* Today is a day still being filled in, not a small day. Lighter, and said in
   words under the graph as well — the difference must not be carried by the
   colour alone.

   Lighter than THE BAR COLOUR, whatever the bar colour is. This was a typed
   #8FBFA8 — a tint of the Classic green — so under Branded the six finished
   days turned red-orange and today stayed sage, which reads as a different
   series rather than as the same series unfinished.

   Mixed towards `--card` rather than towards `transparent`: the plot draws
   three gridlines behind the bars, and a bar with alpha in it would have the
   rules showing through today's column and no other. */
.wk-today .wk-track i {
  background: color-mix(in srgb, var(--go) 50%, var(--card));
}
.wk-col:hover .wk-track i { filter: brightness(.92); }
.wk-day {
  position: absolute; top: 100%; left: 0; right: 0; margin-top: 7px;
  text-align: center; line-height: 1.25;
}
.wk-day b {
  display: block; font-size: 11.5px; font-weight: 600; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.wk-day small {
  display: block; font-size: 10.5px; color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}
/* Clear of the day labels, which hang below the plot rather than inside it —
   the plot reserves that room itself now, and this margin collapses with it
   to the larger of the two, so the gap the reader sees is the one that was
   always here. */
.wk-note {
  margin: 46px 0 0; font-size: 12px; color: var(--ink-soft);
}
@media (max-width: 520px) {
  .wk-plot { height: 108px; padding-left: 40px; }
  .wk-rule, .wk-bars { left: 40px; }
  .wk-bars { inset: 0 0 0 40px; }
  .wk-track { max-width: 30px; }
  .wk-tick { font-size: 10px; padding-right: 6px; }
  .wk-val { font-size: 10.5px; }
  .wk-day b { font-size: 10.5px; }
  .wk-day small { font-size: 10px; }
}

/* Spend per model — a name, a bar, a number. */
.money-models { margin-top: 14px; display: flex; flex-direction: column; gap: 7px; }
.money-row { display: flex; gap: 10px; align-items: center; font-size: 13.5px; }
.money-name { flex: 0 0 auto; min-width: 128px; overflow-wrap: anywhere; }
.money-bar { flex: 1 1 auto; height: 8px; background: var(--line);
             border-radius: 999px; overflow: hidden; }
.money-bar i { display: block; height: 100%; border-radius: 999px; }
.money-amt { flex: 0 0 auto; color: var(--ink-soft); font-variant-numeric: tabular-nums; }
.sec-warn-inline { color: var(--warn); font-weight: 600; font-size: 13px; }
@media (max-width: 520px) {
  .money-name { min-width: 92px; font-size: 12.5px; }
  .money-amt { font-size: 12px; }
}

/* In-app dialogs. Nothing here calls prompt() or confirm(). */
.dlg-back {
  position: fixed; inset: 0; z-index: 90;
  background: rgba(20, 16, 14, .48);
  display: flex; align-items: center; justify-content: center;
  padding: max(16px, env(safe-area-inset-top)) 16px max(16px, env(safe-area-inset-bottom));
  animation: dlg-fade .12s ease-out;
}
@keyframes dlg-fade { from { opacity: 0; } to { opacity: 1; } }
body.dlg-open { overflow: hidden; }
.dlg {
  background: var(--card); color: var(--ink);
  border-radius: var(--r-lg, 18px); box-shadow: 0 18px 50px rgba(0,0,0,.28);
  width: 100%; max-width: 420px; padding: 22px 22px 18px;
  max-height: 100%; overflow-y: auto;
}
/* A dialog holding a whole editable post is a different object from one asking
   a yes/no question, and 420px was sized for the second. Twice the width on a
   desktop or tablet; the phone rule below overrides it, and it has to come
   after this one to do that. */
.dlg-wide { max-width: 860px; }

.dlg-title { font-family: var(--heading-font); font-size: 19px; margin: 0 0 6px; }
/* pre-wrap because some bodies are a list — the erasure dialog itemises every
   row it is about to delete, and collapsing that into one paragraph would lose
   the only thing that makes it readable. */
.dlg-body {
  font-size: 14.5px; color: var(--ink-soft); line-height: 1.5; margin: 0 0 14px;
  white-space: pre-wrap;
}
.dlg-form { display: flex; flex-direction: column; gap: 12px; }
.dlg-field { display: flex; flex-direction: column; gap: 5px; }
.dlg-field label { font-size: 13px; color: var(--ink-soft); }
.dlg-input {
  font: inherit; font-size: 16px; width: 100%; box-sizing: border-box;
  padding: 11px 12px; border: 1px solid var(--line);
  border-radius: var(--r-sm); background: var(--field, #FBF7F0); color: var(--ink);
}
.dlg-input:focus { outline: 2px solid var(--go); outline-offset: 1px; border-color: var(--go); }
/* A select needs its own arrow room and must not inherit a text field's
   monospace-ish measure. */
.dlg-select { appearance: auto; padding-right: 8px; cursor: pointer; }
.dlg-hint { font-size: 12.5px; color: var(--ink-soft); }
.dlg-err { font-size: 13px; color: var(--stop, #B3261E); margin: 0; min-height: 0; }
.dlg-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 2px; }
.dlg-cancel { background: none; }
.dlg-danger { background: var(--stop, #B3261E); border-color: var(--stop, #B3261E); color: #fff; }
@media (max-width: 480px) {
  .dlg-back { align-items: flex-end; }
  .dlg, .dlg-wide {
    max-width: none; border-radius: var(--r-lg, 18px) var(--r-lg, 18px) 0 0;
  }
  .dlg-actions { flex-direction: column-reverse; }
  .dlg-actions .btn { width: 100%; }
}

/* The overview's progress rings. Two circles: a track and an arc. A bar and a
   number would take the same space and say less at a glance — but the numbers
   are underneath regardless, because the ring is the summary, not the source. */
.home-rings {
  display: grid; gap: var(--space);
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
}
.home-ring { text-decoration: none; color: var(--ink); text-align: center; }
.ring-wrap { position: relative; width: 64px; height: 64px; margin: 0 auto; }
.ring-svg { width: 64px; height: 64px; transform: rotate(-90deg); }
.ring-track { fill: none; stroke: var(--line); stroke-width: 7; }
.ring-arc {
  fill: none; stroke: var(--go); stroke-width: 7; stroke-linecap: round;
  transition: stroke-dashoffset .4s ease;
}
.home-ring-alert .ring-arc { stroke: var(--danger); }
.ring-inner {
  position: absolute; inset: 0; display: flex; align-items: center;
  justify-content: center; font-size: 14px; font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.ring-label { font-size: 13.5px; font-weight: 650; margin-top: 6px; }
.ring-sub { font-size: 12px; color: var(--ink-soft); }

/* The rulebook. A rule is a label, its editor, what it does and where it
   bites — in that order, because "where it bites" is the part that decides
   whether an edit is safe and it is the part nobody can see from a value. */
.rule { padding: var(--space) 0; border-top: 1px solid var(--line); }
.rule:first-of-type { border-top: 0; }
.rule-head { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
.rule-label { font-weight: 650; }
.rule-changed {
  font-size: 11px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .04em; color: var(--warn); border: 1px solid var(--warn);
  border-radius: 999px; padding: 1px 7px;
}
.rule-edit { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.rule-num { max-width: 160px; }

/* ---------- Tom's voice rules ----------
 *
 * A rule is a SENTENCE here, not a label with a control beside it, so the row
 * has to let it wrap and put the buttons after it rather than squeezing it
 * into whatever is left of one line. */
.voice-rule .rule-head { align-items: center; flex-wrap: wrap; }
/* THE WHOLE LINE, ALWAYS. Left to fill the row, a short rule keeps its buttons
   beside it and a long one wraps them underneath — so Edit was in a different
   place on almost every row, and "Never use hashtags." sat with a hand's width
   of gap before its buttons. A sentence takes the line; the buttons take the
   next one. */
.voice-rule .rule-label {
  flex: 1 0 100%; font-weight: 400; line-height: 1.45;
  margin-bottom: 4px;
}
/* Switched off, and saying so without pretending to be gone: the words stay
   readable, because somebody has to be able to read the rule they turned off
   in order to decide to turn it back on. */
.voice-rule.is-off .rule-label { opacity: .55; text-decoration: line-through; }
.voice-kind {
  font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  font-size: 11.5px; color: var(--ink-soft);
  margin-top: var(--space); padding-top: var(--space);
  border-top: 1px solid var(--line);
}
.voice-add { display: grid; gap: var(--space-sm); }
.voice-add .chip { justify-self: start; }
.rule-day { min-width: 46px; }
.rule-rows { display: flex; flex-direction: column; gap: 4px; width: 100%; }
.rule-row {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: 3px 0;
}
.rule-bad { font-family: var(--mono, ui-monospace, monospace); font-size: 13px; }
.rule-arrow { color: var(--ink-soft); }
.rule-good { font-weight: 650; }
.rule-why, .rule-kind { font-size: 12.5px; color: var(--ink-soft); }
.rule-kind { min-width: 130px; }
.rule-bites { font-style: italic; opacity: .85; }
/* A quieter chip, for the actions beside a rule that are not the point of the
   row — remove, restore, flip the severity. */
.chip-soft { color: var(--ink-soft); font-size: 12px; padding: 3px 9px; }
/* A profile that has been turned off still shows — a calendar with holes in it
   is harder to trust than one that says which days are switched off. */
.season-off { opacity: .55; }

/* The media page. A thumbnail is a reference to Drive's own thumbnail — this
   app has never held the file and does not hold the preview either. */
.media-thumb {
  width: 48px; height: 48px; object-fit: cover; border-radius: 8px;
  border: 1px solid var(--line); flex: none;
}
.paste-block {
  white-space: pre-wrap; font-size: 12.5px; line-height: 1.5;
  background: var(--ivory); border: 1px solid var(--line); border-radius: 10px;
  padding: 10px 12px; margin: var(--space-sm) 0; overflow-x: auto;
}

/* ---------- the dashboard: squares, and nothing else ---------- */
/* Every section used to be drawn inline, which put the whole idea bank on one
   screen and made nothing findable. A widget is enough to decide whether to
   open something and no more: a headline number, three counts, the newest
   item, and a way in. */
.w-grid {
  display: grid; gap: var(--gap);
  grid-template-columns: 1fr;                     /* phone: one per row */
  /* Clear of the app bar. With no header card above it the grid would
     otherwise start hard against the top of the content area. */
  margin-top: var(--space-sm);
}
@media (min-width: 640px) {
  .w-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 1000px) {
  .w-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

.w-tile {
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow);
  padding: var(--space); display: flex; flex-direction: column;
  /* The gap the squares were missing: the number, the counts and the button
     were touching, which made a tile read as one block of text. */
  gap: var(--space);
  /* Square-ish: tall enough to hold its numbers without the row going ragged
     when one widget has a long label. */
  min-height: 210px;
}
/* One ground for every square. `.w-sourced` used to tint some of them a
   different cream, so a grid of eighteen tiles had two backgrounds in it for a
   reason nobody could see from looking — where the number came from is not a
   thing the reader is asking. It is a border and a word instead. */
.w-alert { border-color: var(--warn); }
.w-sourced { border-style: dashed; }
.w-top { display: flex; align-items: center; gap: var(--space-sm); }
.w-icon { font-size: 17px; }
.w-label { font-weight: 650; font-size: 14.5px; }
.w-big { display: flex; align-items: baseline; gap: 6px; }
.w-big b { font-size: 30px; line-height: 1; font-variant-numeric: tabular-nums; }
.w-unit { font-size: 12.5px; color: var(--ink-soft); }
/* Seven days of traffic as a shape. No axis: at 30px tall the numbers would
   be unreadable, and the question a square answers is "up or down". */
.w-spark-box { padding: 2px 0; }
.w-spark { display: block; width: 100%; height: 30px; overflow: visible; }
.w-spark polyline {
  fill: none; stroke: var(--go); stroke-width: 1.6;
  vector-effect: non-scaling-stroke; stroke-linejoin: round;
  stroke-linecap: round;
}

.w-lines { display: flex; flex-direction: column; gap: 2px; }
.w-line {
  display: flex; align-items: baseline; gap: 10px;
  font-size: 12.5px; color: var(--ink-soft);
  border-top: 1px solid var(--line); padding-top: 4px;
}
/* The label takes the slack so the number sits on the right edge. Without
   this "Approved" and its number were touching — "Approved0" — which is the
   spacing complaint, and it reads as a word with a digit stuck on it. */
.w-line > .grow { flex: 1 1 auto; min-width: 0; }
.w-line b { color: var(--ink); font-variant-numeric: tabular-nums; }
.w-latest {
  margin: 0; font-size: 12.5px; color: var(--ink-soft); flex: 1 1 auto;
  overflow: hidden; display: -webkit-box; -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
}
/* A real button, not a link pretending to be one. It was a bare green phrase
   with an arrow, which read as a caption and gave nothing to aim at on a
   tablet. Same shape as every other action button in the app, and the tinted
   ground is what separates it from the numbers above it. */
.w-more {
  margin-top: auto; align-self: stretch; justify-content: center;
  background: var(--warn-bg, #FBF2DE); border-color: var(--line);
  color: var(--ink); font-size: 13.5px;
}
.w-more:hover { background: var(--go); border-color: var(--go); color: #fff; }

/* ---------- a section page ---------- */
.sec-top {
  display: flex; align-items: center; gap: var(--space-sm); flex-wrap: wrap;
}
.sec-top h3 { margin: 0; }

.sec-tabs {
  display: flex; gap: var(--space-sm); flex-wrap: wrap;
  margin: var(--space) 0;
}
.sec-tab {
  display: inline-flex; align-items: center; gap: 6px;
  font: inherit; font-size: 13px; padding: 7px var(--space);
  border: 1px solid var(--line); border-radius: 999px;
  background: var(--card); color: var(--ink); cursor: pointer;
}
.sec-tab-on { background: var(--ink); color: var(--ivory); border-color: var(--ink); }
.sec-tab-n { font-variant-numeric: tabular-nums; opacity: .7; font-size: 12px; }
/* A tab is a drop target: on a phone it is the only one there is. */
.sec-tab-over { border-color: var(--go); border-style: dashed; }

/* Two panels side by side on a desktop or tablet — the tab you are in, and one
   you are moving things to. Dragging across is a change of state; dragging
   within is a reorder. A phone gets one column and moves things by a button,
   because a finger cannot hold two panels at once. */
.sec-panels { display: grid; gap: var(--gap); grid-template-columns: 1fr; }
@media (min-width: 900px) {
  .sec-panels { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); }
  .sec-panel-target { position: sticky; top: var(--gap-outer); }
}
@media (max-width: 899px) {
  /* No second panel to drag to, so it is not shown at all rather than sitting
     there as a target nothing can reach. */
  .sec-panel-target { display: none; }
}
.sec-panel-head {
  display: flex; align-items: center; gap: var(--space); flex-wrap: wrap;
  margin-bottom: var(--space-sm);
}
.sec-panel-head h4 {
  margin: 0; font-size: 14px; font-weight: 700; letter-spacing: .02em;
  text-transform: uppercase; color: var(--ink-soft);
}
.sec-dropzone {
  border: 2px dashed var(--line); border-radius: var(--radius);
  padding: var(--space-lg) var(--space); text-align: center;
  color: var(--ink-soft); font-size: 13px; margin-top: var(--space);
}
.sec-dropzone.is-over { border-color: var(--go); color: var(--go); }

/* ---------- the idea tiles ---------- */
/* An idea is a piece of writing. A row of text with two buttons on the end
   tells you nothing about what it says, so each tile carries the opening of
   the post itself. */
.idea-find { max-width: 220px; padding: 6px 10px; font-size: 13px; }
.idea-empty { font-style: italic; }

.idea-grid {
  display: grid; gap: var(--gap);
  grid-template-columns: 1fr;
}
@media (min-width: 640px) {
  .idea-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 1300px) {
  .idea-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

.idea-tile {
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow);
  padding: var(--space); display: flex; flex-direction: column;
  gap: var(--space-sm);
  cursor: pointer; text-align: left; min-height: 160px;
  transition: transform .12s ease, box-shadow .12s ease;
}
.idea-tile:hover, .idea-tile:focus-visible {
  transform: translateY(-1px); box-shadow: 0 3px 12px rgba(0, 0, 0, .10);
  outline: none; border-color: var(--go);
}
.idea-tile.is-dragging { opacity: .45; transform: none; }
.idea-tile.is-working { border-style: dashed; }

.idea-tile-top { display: flex; align-items: center; gap: 6px; }
.idea-grip {
  cursor: grab; color: var(--ink-soft); font-size: 15px; line-height: 1;
  padding: 2px 4px; margin: -2px 0 -2px -4px;
  touch-action: none;
}
.idea-grip:active { cursor: grabbing; }
.idea-src {
  font-size: 10.5px; letter-spacing: .06em; text-transform: uppercase;
  color: var(--ink-soft); font-weight: 700;
}
.idea-flag {
  font-size: 10.5px; font-weight: 700; color: var(--warn);
  border: 1px solid var(--warn); border-radius: 999px; padding: 1px 7px;
}
/* A note is Tom's own and never goes in the post, so the tile says one exists
   without showing it — a private note on a public-looking card is a note that
   ends up read out. */
.idea-note-dot { color: var(--ink-soft); font-size: 13px; }
.idea-title {
  margin: 0; font-size: 14.5px; line-height: 1.3; font-weight: 650;
  overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}
.idea-preview {
  margin: 0; font-size: 12.5px; line-height: 1.45; color: var(--ink-soft);
  flex: 1 1 auto; overflow: hidden; display: -webkit-box;
  -webkit-line-clamp: 3; -webkit-box-orient: vertical; white-space: pre-wrap;
}
.idea-preview-empty { font-style: italic; opacity: .7; }
.idea-move { align-self: flex-start; }

/* A badge that says "checked" whatever happened is decoration. These say which
   check ran and what it found, so a row of green means something. */
.idea-badges { display: flex; flex-wrap: wrap; gap: 4px; }
.idea-badge {
  font-size: 10.5px; font-weight: 650; border-radius: 999px;
  padding: 1px 7px; border: 1px solid var(--line); color: var(--ink-soft);
}
.idea-badge-passed { border-color: var(--go); color: var(--go); }
.idea-badge-fixed  { border-color: var(--ink); color: var(--ink); }
.idea-badge-flagged { border-color: var(--warn); color: var(--warn);
                      background: var(--warn-bg, transparent); }
.idea-badge-working {
  border-color: var(--ink-soft); color: var(--ink-soft);
  animation: idea-pulse 1.1s ease-in-out infinite;
}
@keyframes idea-pulse { 50% { opacity: .35; } }

.idea-pager {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-sm);
  margin-top: var(--space);
}
.idea-per { max-width: 140px; padding: 5px 8px; font-size: 12.5px; }
.idea-gap { color: var(--ink-soft); padding: 0 2px; }
.idea-pagenote { flex-basis: 100%; margin: 4px 0 0; font-size: 11.5px; }

/* ---------- the detail view ---------- */
.idea-detail { display: flex; flex-direction: column; gap: var(--space);
               text-align: left; }
.idea-field { display: flex; flex-direction: column; gap: 4px; }
.idea-field-label {
  font-size: 12px; font-weight: 650; color: var(--ink-soft);
}
/* Tom's own notes. Tinted so the difference between "this goes out" and "this
   never does" is visible at a glance rather than only in the label. */
.idea-private { border-left: 3px solid var(--line); padding-left: var(--space); }
.idea-review {
  border: 1px solid var(--warn); border-radius: var(--r-sm, 10px);
  padding: 10px 12px; background: var(--warn-bg, transparent);
}
.idea-review-row { margin-top: 6px; font-size: 13px; }
.idea-review-row code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  background: var(--field, #FBF7F0); padding: 1px 5px; border-radius: 4px;
}

/* Which platforms a giveaway counts. A picker rather than a comma-separated
   box: a trailing space or a misspelling counted nothing, and the only symptom
   was a giveaway with fewer entries than it should have had. */
.gw-platform {
  display: flex; gap: 8px; align-items: flex-start;
  padding: 7px 0; border-bottom: 1px solid var(--line); cursor: pointer;
}
.gw-platform:last-of-type { border-bottom: 0; }
.gw-platform input[type="checkbox"] { flex: none; margin-top: 3px; }
.gw-unidentified { color: var(--warn); }

/* Stacked actions, full width, each label on one line.
   Side by side, "Finalise — write the rest and make the post" wrapped to three
   lines and the buttons became different heights, which made the important one
   hard to pick out. Order is deliberate: the way out at the top, the thing you
   came to do at the bottom, nearest the thumb. */
.dlg-actions.dlg-stack {
  flex-direction: column-reverse;
  align-items: stretch;
  gap: var(--space-sm);
  margin-top: var(--space);
}
.dlg-actions.dlg-stack .btn {
  width: 100%;
  white-space: nowrap;
  padding: 11px var(--space);
}
/* The two that carry consequences say so in colour: green for the one that
   produces the post, red for the one that puts it away. */
.dlg-actions.dlg-stack .btn-approve { background: var(--go); border-color: var(--go); }
.dlg-actions.dlg-stack .dlg-danger { background: var(--stop, #B3261E);
                                     border-color: var(--stop, #B3261E); }

/* The signature logo, as it will appear under Tom's name in an email. */
.sig-logo {
  display: block; max-width: 180px; height: auto; margin: var(--space) 0;
  border: 1px solid var(--line); border-radius: 8px; padding: var(--space-sm);
  background: #fff;
}
/* Choose / Replace / Remove, under the picture they act on. */
.sig-logo-pick { display: flex; gap: var(--space-sm); flex-wrap: wrap;
                 margin-top: var(--space); }

/* A button that is working says so and stops taking presses. */
.btn.is-working { opacity: .75; cursor: progress; }
.p-draft { white-space: pre-wrap; margin: var(--space) 0; }

/* The day template: what runs, when, on which days. */
.dt-slot { padding: var(--space) 0; }
.dt-grid {
  display: grid; gap: var(--space); margin: var(--space-sm) 0;
  grid-template-columns: 1fr;
}
@media (min-width: 700px) {
  .dt-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
.dt-field { display: flex; flex-direction: column; gap: 4px; }
.dt-field label { font-size: 12px; color: var(--ink-soft); }
.dt-days { display: flex; flex-wrap: wrap; gap: var(--space-sm);
           align-items: center; }
.dt-day { min-width: 42px; }
.dt-count { font-variant-numeric: tabular-nums; min-width: 74px; }
/* The Rulebook asks for nine to eleven items a day. A template that quietly
   drops to six is the failure §0 of the Rulebook is a post-mortem of, so the
   number says when it is outside that. */
.dt-count-off { color: var(--warn); font-weight: 700; }

/* A like is not a reply, and on Facebook they look the same from outside — a
   thumb reads as "dealt with". Saying it in words is what stops a like being
   mistaken for a sent reply. */
.like-state {
  font-size: 11.5px; color: var(--warn); font-weight: 650;
  white-space: nowrap;
}

/* How long somebody has been waiting on us, not how old the comment is.
   Quiet until it is genuinely late, then warm — this is a nudge to clear the
   oldest first, not an alarm. */
.waiting-tag {
  font-size: 11.5px; color: var(--ink-soft); font-weight: 600;
  white-space: nowrap; padding: 1px 7px; border-radius: 999px;
  background: var(--ivory); border: 1px solid var(--line);
}
.waiting-tag.waiting-old {
  color: var(--warn); background: var(--warn-bg); border-color: var(--warn);
}

/* A platform the app can read but not post to. The reason replaces the
   Approve button rather than sitting beside it: a button that looks like it
   sent something is worse than no button. */
.no-reply {
  margin: 0; font-size: 13px; color: var(--ink-soft); font-style: italic;
  padding: var(--space-sm) 0;
  /* Its own line. It is a sentence, and a sentence beside a button is a row
     as wide as the sentence. */
  flex: 1 1 100%;
}

/* ---------- Delivery ----------
 *
 * One screen answering "did what I approved actually go?". Deliberately plain:
 * every row is a person and a sentence, because the failure this page exists
 * for was a row that looked like nothing at all.
 */
.plain-list { list-style: none; margin: 0; padding: 0; }
.delivery-row {
  padding: var(--space-sm) 0;
  border-top: 1px solid var(--line);
}
.delivery-row:first-child { border-top: 0; }
.delivery-top {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  font-size: 14px;
}
.delivery-said {
  margin: 3px 0 0; font-size: 13.5px; color: var(--ink-soft);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.row-actions {
  display: flex; gap: var(--space-sm); flex-wrap: wrap;
  align-items: center;
  margin-top: var(--space);
}

/* Out of the way and out of the layout. Used for the real <input type=file>
   behind a button that looks like the rest of the app — it was set on one and
   nothing defined it, so the browser's grey "Choose Files" sat in the middle
   of the media page. */
.hidden { display: none !important; }
.muted { font-size: 13.5px; color: var(--ink-soft); margin: 0 0 var(--space-sm); }

/* ---------- delivery in the header, and pacing ---------- */

.count-delivery { color: var(--ink-soft); text-decoration: none; }
.count-delivery:hover { text-decoration: underline; }
.count-delivery.count-bad { color: var(--danger); font-weight: 650; }

/* Progress, not a spinner. A spinner says "wait"; a bar with a denominator
   says how long for, which is the difference between pacing and a hung queue.
   Nothing here blocks the page — answering carries on while it drains. */
.pacing {
  display: flex; align-items: center; gap: var(--space-sm);
  padding: var(--space-sm) 0 0;
  font-size: 13px; color: var(--ink-soft);
}
.pacing-bar {
  flex: 0 0 120px; height: 5px; border-radius: 999px;
  background: var(--line); overflow: hidden;
}
.pacing-bar span {
  display: block; height: 100%; width: 2%;
  background: var(--go); border-radius: 999px;
  transition: width .4s ease;
}
.pacing-text { flex: 1 1 auto; min-width: 0; }
@media (max-width: 560px) {
  /* On a phone the reassurance is the count, not the sentence about rate
     limits. */
  .pacing-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
}

/* ---------- the post's event is over ----------
 *
 * Loud on purpose. A hundred and twenty-one of two hundred cards sat on posts
 * whose draw had already been made, and every draft that talked about it as
 * upcoming read perfectly well. The badge is a fact about the POST, so it
 * appears even when the text looks fine — which is exactly the case nobody
 * catches by reading.
 */
/* AMBER, NOT RED, AND THE DIFFERENCE IS WHAT RED MEANS.
 *
 * This was `--danger`, the same solid red as a refusal, and it is on a card
 * whenever the POST's event has been and gone — which was a hundred and
 * twenty-one of two hundred, including comments that arrived ten minutes ago.
 * Red on a comment that is not late, on most of the queue, teaches everybody
 * to stop reading red; and then the one that means "this person has been
 * waiting five days" does not get read either.
 *
 * So red is reserved for lateness — the age pill, the Need response chip, the
 * waiting tag past its window — and this is a warning, which is what it
 * actually is: not "you are late", but "the thing this post announced is over,
 * so mind the tense". Solid amber rather than the outlined amber of the
 * overdue tag, because the two must not be read as the same thing. White on
 * --warn is 6.1:1. */
.past-event-tag {
  font-size: 11.5px; font-weight: 700; white-space: nowrap;
  padding: 2px 8px; border-radius: 999px;
  color: #fff; background: var(--warn);
}

/* The draft changed after Tom last looked at it. Quiet — it is a note, not a
   warning; the warning is the badge above. */
.redrafted-tag {
  font-size: 11.5px; font-weight: 650; white-space: nowrap;
  padding: 1px 7px; border-radius: 999px;
  color: var(--go); background: var(--ivory); border: 1px solid var(--go);
}

/* ---------- one undo toast for a burst ----------
 *
 * Nine approvals used to leave nine toasts climbing the screen, and on a phone
 * they covered the Approve button of the card underneath — the faster Tom
 * worked, the more the app got in his way. One toast, counting.
 */
.toast-burst { display: flex; align-items: center; gap: 10px; }
.toast-burst .burst-label { flex: 1 1 auto; }
.toast-burst button {
  flex: 0 0 auto; font-size: 13.5px; font-weight: 650;
  border-radius: 999px; padding: 5px 11px; cursor: pointer;
}
.toast-burst .burst-all { opacity: .85; }

/* Setting a comment aside, and the way back to it. */
.skipped-card .msg { color: var(--ink-soft); }
.btn-skip { flex: 0 0 auto; }

/* ---------- rewrite with a note ----------
 *
 * Closed it costs one line; open it is a field and a button. Regenerate asks
 * for another take on the same brief — this is for saying what is actually
 * wrong with the draft in front of you, which is usually faster and always
 * more likely to work.
 */
.rewrite { margin-top: var(--space-sm); }
.rewrite > summary {
  cursor: pointer; font-size: 13px; color: var(--ink-soft);
  list-style: none; padding: 2px 0; user-select: none;
}
.rewrite > summary::-webkit-details-marker { display: none; }
.rewrite > summary::before { content: '▾ '; }
.rewrite[open] > summary::before { content: '▴ '; }
.rewrite > summary:hover { color: var(--ink); }
.rewrite-row {
  /* Eight, not the six-pixel token: this row wraps on a phone, so the token
     became the space between one line of buttons and the next, and six
     pixels between two stacked buttons is a thumb-width of ambiguity. */
  display: flex; gap: 8px; flex-wrap: wrap;
  /* Clear of whatever is above it. This row sits inside the actions row, so
     its top margin lands next to buttons rather than next to text — six
     pixels left the two touching on a phone, and ten still measured five
     once the flex gap above it had had its say. */
  margin-top: 12px;
}
.rewrite-note {
  flex: 1 1 220px; min-width: 0; font: inherit; font-size: 14px;
  padding: 8px 10px; border: 1px solid var(--line); border-radius: 10px;
  background: var(--card); color: var(--ink);
}
.btn-rewrite { flex: 0 0 auto; }

/* ---------- corrections ----------
 *
 * Before and after, side by side in the reading order they happened in. The
 * "after" is what the drafting model learns from, so it is the one with the
 * weight on it.
 */
.corr { display: flex; flex-direction: column; gap: 4px; }
.corr-permanent { border-left: 3px solid var(--go); }
.corr-said { margin: 4px 0 2px; font-size: 13.5px; color: var(--ink-soft); }
.corr-before {
  margin: 0; font-size: 14px; color: var(--ink-soft);
  text-decoration: line-through; text-decoration-color: var(--line);
}
.corr-why { margin: 2px 0; font-size: 13.5px; color: var(--warn); font-weight: 600; }
.corr-after { margin: 0; font-size: 14.5px; color: var(--ink); font-weight: 550; }
.corr-pattern { padding: var(--space-sm) 0; border-top: 1px solid var(--line); }
.corr-pattern:first-of-type { border-top: 0; }

/* ---------- who is talking ----------
 *
 * A thread with three people in it used to render as one column of grey, and a
 * draft answering the wrong one of them looked exactly like a draft answering
 * the right one. Four colours is enough — a thread with a fifth participant is
 * rare, and the fifth simply reuses the first.
 *
 * Green is not among them: `.turn.tom .who` is green, and green has to mean
 * Tom and only Tom for the colours to be worth anything at all.
 */
.turn.who-0 .who { color: var(--fb); }
.turn.who-1 .who { color: var(--ig); }
.turn.who-2 .who { color: var(--warn); }
.turn.who-3 .who { color: var(--danger); }

.convo-joined {
  font-size: 12.5px; font-weight: 650; color: var(--go);
  padding-top: 4px;
}

/* Which message the draft answers. Only shown when it could be ambiguous —
   a permanent "Replying to Susan" on a card that only ever had Susan in it is
   a label nobody reads. */
.replying-to {
  font-size: 12.5px; color: var(--ink-soft); font-weight: 600;
  padding: 2px 0 4px;
}

/* ---------- a conversation as one card ----------
 *
 * A parent comment and its replies are one conversation. Two different
 * top-level comments under the same post are not, and stay separate — merging
 * a whole post's comments into one card is the failure at the other extreme.
 *
 * The frame is deliberately light. Each reply inside keeps its own draft box
 * and its own buttons; this only says "these belong together".
 */
.thread {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--ivory) 60%, transparent);
  /* Twelve, like everything else that holds cards. `--space-sm` is 6px and
     is for the inside of a chip; a card sitting 6px from the edge of the box
     around it reads as a card that has been squeezed in. The gap between the
     cards in it is the same number for the same reason. */
  padding: var(--gap-tight-min, 12px);
  display: flex; flex-direction: column; gap: var(--gap-tight-min, 12px);
}
.thread-head {
  display: flex; align-items: center; gap: var(--space-sm);
  font-size: 12.5px; font-weight: 650; color: var(--ink-soft);
  padding: 2px 4px;
}
.thread-head > span { flex: 1 1 auto; }
.thread-all { flex: 0 0 auto; }
.thread .card { margin: 0; }

/* ---------- "where things live" ----------
 *
 * Post types and Seasonal profiles were both built, both working, and both
 * unfindable — a chip on a nav bar is not a description, and Tom asked twice
 * for features that already existed. A sentence each, on every settings
 * screen, that clicks through.
 */
/* Each row here is a bordered button. Two bordered buttons 6px apart read as
   one box with a line through it, and they are two things to press. */
.where-list { display: grid; gap: var(--gap, 15px); }
@media (min-width: 720px) {
  .where-list { grid-template-columns: 1fr 1fr; }
}
.where-row {
  display: flex; flex-direction: column; gap: 2px; text-align: left;
  /* Its own border is a line, and its words keep 12px clear of it. */
  padding: var(--gap-tight-min, 12px) 12px;
  border: 1px solid var(--line); border-radius: 10px;
  background: var(--card); color: var(--ink); cursor: pointer; font: inherit;
}
.where-row:hover { border-color: var(--go); }
.where-row.where-on { border-color: var(--go); background: var(--ivory); }
.where-row b { font-size: 14px; }
.where-row span { font-size: 13px; color: var(--ink-soft); line-height: 1.35; }

/* The rewrite pull-down inside the review sheet. Tighter than the card's — a
   sheet of forty rows cannot afford the card's breathing room — and marked
   once used, because a row whose text changed under you deserves a second
   read before it goes. */
.bi-rewrite { margin-top: 4px; }
.bi-rewrite > summary { font-size: 12px; }
.bi-rewrite .rewrite-note { font-size: 13px; padding: 6px 8px; }
.bi-rewritten .bi-draft {
  border-left: 3px solid var(--go); padding-left: 8px;
}

/* A run of chips built by script, sitting inside a .filters row exactly as the
   hand-written ones did. `display: contents` is the point: the wrapper carries
   no layout of its own, so the chips are laid out by the flex row above it and
   wrap like every other chip. */
.chip-row { display: contents; }

/* ---------- writing the answer yourself ----------
 *
 * A "For you" card has no draft — the screener holds those back on purpose,
 * because they are the ones where a wrong answer costs something. The card
 * used to offer "Rewrite with a note" and nothing else, which is a control
 * asking you to rewrite a draft that does not exist. Now the box is the point
 * and the helpers sit underneath it.
 */
.mine-head {
  display: flex; align-items: baseline; gap: var(--space-sm);
  flex-wrap: wrap; padding: 2px 0 4px;
}
.mine-label { font-size: 13px; font-weight: 700; color: var(--go); }
.mine-note { font-size: 12.5px; color: var(--ink-soft); flex: 1 1 200px; }
.mine-actions {
  display: flex; align-items: center; gap: var(--space-sm);
  flex-wrap: wrap; margin-top: var(--space-sm);
}
/* What the polish actually changed, said back to him. A polish that reports
   nothing looks exactly like one that quietly rewrote the meaning. */
.polish-note {
  font-size: 12.5px; color: var(--ink-soft); flex: 1 1 180px; min-width: 0;
}
.btn-mine, .btn-polish { flex: 0 0 auto; }

/* A refresh that failed, said in a line above the list rather than instead of
   it. One dropped connection used to replace everything Tom was working
   through with a sentence. */
.loadfail {
  margin: 0 var(--gap-outer) var(--space-sm);
  padding: 8px 11px; border-radius: 10px;
  background: var(--warn-bg); border: 1px solid var(--warn);
  color: var(--warn); font-size: 13.5px; font-weight: 600;
}

/* ---------- the status row, on one line ----------
 *
 * Seven statuses plus refresh plus "Show more" have to fit across a desktop or
 * a tablet without wrapping, so the chips here are tightened rather than the
 * row being allowed to become two. Below that width the row scrolls sideways,
 * which is how every other filter row on the site already behaves — a wrapped
 * toolbar pushes the first card off the screen, and the first card is the
 * work.
 */
/* ---------- the Inbox header ----------
 *
 * Three parts: one row of status chips with the search box on the end of it,
 * a pair of toggles under that, and everything else collapsed behind them.
 *
 * On a phone the first part goes inside the panel as well, so the header is
 * two buttons and the comments start near the top of the screen — which was
 * the complaint: the options were eating the top third of the phone.
 */
/* One collapsed bar: More, what you are looking at, and the lock.
   The same at every width — the phone and the desk used to be different
   shapes with different things reachable from each. */
.hdr-toggles {
  display: flex; gap: var(--gap-tight-min, 12px); align-items: center;
  margin-top: 9px; min-width: 0;
}
.hdr-toggles .chip { flex: 0 0 auto; }
.chip-more { display: inline-flex; align-items: center; gap: 7px; }
.chip-more.is-open { background: var(--ink); border-color: var(--ink);
                     color: var(--ivory); font-weight: 600; }
/* Something other than the status is narrowing the list. Marked on the button
   because on a phone the words for it do not fit, and a filter you cannot see
   is an Inbox that looks broken. */
.chip-more.has-filters { border-color: var(--go); }
.chip-more.has-filters::after {
  content: ''; width: 7px; height: 7px; border-radius: 999px;
  background: var(--go); flex: 0 0 auto;
}
.chip-more.is-open.has-filters::after { background: var(--ivory); }

/* What is being looked at, while the panel is shut. Elastic, and the first
   thing to be trimmed: the button and the lock keep their room. */
.hdr-active {
  flex: 1 1 auto; min-width: 0; display: flex; align-items: baseline;
  gap: 7px; font-size: 13.5px; color: var(--ink-soft);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.hdr-active-status { color: var(--ink); font-weight: 650; }
.hdr-active-n {
  flex: 0 0 auto; padding: 1px 8px; border-radius: 999px;
  background: var(--ivory); border: 1px solid var(--line);
  font-size: 12px; font-weight: 650; color: var(--ink);
}
.hdr-active-more { overflow: hidden; text-overflow: ellipsis; }
@media (max-width: 560px) {
  /* Truncated to "· …" it says nothing and looks like a fault. The dot on the
     More button carries the same news in the room available. */
  .hdr-active-more { display: none; }
}

/* Keep the panel open while you scroll. Same control, same meaning, as the
   giveaway builder's header lock. */
.hdr-lock { margin-left: auto; line-height: 1; }

#more-filters {
  display: flex; flex-direction: column; gap: var(--gap-tight-min, 12px);
  margin-top: var(--gap-tight-min, 12px);
}
#more-filters .searchbar { margin: 0; }
#more-filters .filters { margin-top: 0; }

/* ---------- the Reviews surface's own two rows ----------
 *
 * ROW 1 IS SOURCE THEN SEARCH, side by side. On Comments there are three
 * pull-downs and the search box is the widest thing in the panel, so it gets
 * a line of its own above them. On Reviews there is one pull-down, it is
 * short, and leaving the search box on a line by itself would be a panel two
 * rows tall before the tabs even start.
 *
 * Done with `order` rather than by moving the markup: the search box is the
 * Inbox's one search box, with its scope chips and its listeners, and the
 * version of this that reparented it lost the chips on every repaint.
 */
#more-filters.mf-reviews {
  flex-flow: row wrap;
  align-items: flex-start;
}
#more-filters.mf-reviews > .mf-drops {
  order: 1;
  flex: 0 1 260px;
}
#more-filters.mf-reviews > .searchbar {
  order: 2;
  flex: 1 1 240px;
  min-width: 0;
}
#more-filters.mf-reviews > .searchbar .search-input { min-width: 0; }
/* The scope chips belong to the search box and follow it onto its own line. */
#more-filters.mf-reviews > .filters { order: 3; flex: 1 1 100%; }
#more-filters.mf-reviews > #rv-tabrow { order: 4; flex: 1 1 100%; }

/* ROW 2: THE SECTIONS, AS TABS.
 *
 * This is the whole of the navigation into Dashboard, the reviews list, Add
 * Review, Settings, Preview & Appearance and the Embed test — there is no
 * button beside it and no second door. So it scrolls sideways rather than
 * wrapping into a block: a row that reflows to four lines on a phone stops
 * reading as "these are the sections" and starts reading as a menu, and the
 * one thing this row must never do is look like something you could have
 * missed. Same treatment, and the same reasoning, as the giveaway hub's step
 * row.
 */
.rv-tabrow {
  display: flex;
  gap: 6px;
  align-items: center;
  overflow-x: auto;
  scrollbar-width: none;
  padding-bottom: 2px;
  border-top: 1px solid var(--line);
  padding-top: var(--gap-tight-min, 12px);
}
.rv-tabrow::-webkit-scrollbar { display: none; }

.rv-tab {
  flex: 0 0 auto;
  min-height: 44px;
  padding: 0 14px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--card);
  color: var(--ink-soft);
  font: inherit;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
}
.rv-tab:hover { border-color: var(--ink-soft); color: var(--ink); }
.rv-tab-on {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--page-bg);
}
.rv-tab-on:hover { color: var(--page-bg); }
.rv-tab:focus-visible { outline: 2px solid var(--go); outline-offset: 2px; }

/* Refresh is a thing you DO, not a place you go. Pushed to the far end with a
   rule before it so it cannot be read as the seventh section. */
.rv-tab-do {
  margin-left: auto;
  border-style: dashed;
}
@media (max-width: 700px) {
  /* The row scrolls at this width, and an auto margin inside a scrolling flex
     row puts Refresh a screen and a half to the right of the last tab. */
  .rv-tab-do { margin-left: 0; }
}

/* The panel's own sections, drawn into the Inbox's card column. `.ra-body`
   inside brings its own spacing; this only has to stop the column's card
   rules reaching in. */
.inbox-ra { display: block; min-width: 0; }

/* The three counters, on the collapsed bar. Tabular figures so the row does
   not twitch sideways as a number goes from 9 to 10. */
.hdr-count { flex: 0 0 auto; }
.hdr-count b {
  color: var(--ink); font-weight: 650;
  font-variant-numeric: tabular-nums;
}
@media (max-width: 560px) {
  /* Two, not three. `.hdr-active` clips rather than wraps — it has to, or the
     header grows a line every time a filter is added — and the third counter
     came out as "Answe" with no ellipsis, which reads as a fault rather than
     as a number. The same trim `.hdr-active-more` takes at the same width. */
  .hdr-count--done { display: none; }
}
/* Its chips are all hidden, so the label would be alone. */
.row-empty { display: none; }

/* ---------- the three pull-downs ---------- */
/*
 * One column on a phone, three across on a desk. Not a grid of equal columns:
 * a status name is short and a list of platforms is not, so they take what
 * they need.
 */
.mf-drops {
  display: flex; flex-direction: column; gap: var(--gap-tight-min, 12px);
}
.mf-drop {
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--card); overflow: hidden; min-width: 0;
}
.mf-drop.is-open { border-color: var(--ink-soft); }
.mf-head {
  width: 100%; display: flex; align-items: center; gap: 10px;
  padding: 10px 13px; background: none; border: 0; cursor: pointer;
  font: inherit; text-align: left; color: inherit;
}
.mf-head:hover { background: var(--ivory); }
.mf-label {
  flex: 0 0 auto; font-size: 11px; text-transform: uppercase;
  letter-spacing: .05em; color: var(--ink-soft); font-weight: 700;
}
.mf-summary {
  flex: 1 1 auto; min-width: 0; font-size: 14px; font-weight: 600;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.mf-body {
  border-top: 1px solid var(--line); padding: 6px;
  display: flex; flex-direction: column; gap: 2px;
  max-height: min(46vh, 380px); overflow: auto;
}
.mf-opt {
  display: flex; align-items: center; gap: 9px;
  padding: 8px 9px; border: 0; border-radius: 8px; background: none;
  font: inherit; font-size: 14.5px; text-align: left; color: inherit;
  cursor: pointer; width: 100%;
}
.mf-opt:hover { background: var(--ivory); }
.mf-opt.is-on { background: var(--ivory); font-weight: 650; }
/* A fixed slot for the tick, so the names line up whether one is ticked or
   not — a list that shifts sideways as you choose is a list you cannot read
   down. */
.mf-mark {
  flex: 0 0 auto; width: 16px; text-align: center; color: var(--go);
  font-weight: 700;
}
.mf-opt-name { flex: 1 1 auto; min-width: 0; }
.mf-count {
  flex: 0 0 auto; font-size: 12.5px; color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}
.mf-opt.is-alert .mf-count { color: var(--danger); font-weight: 700; }

.mf-dates {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
  padding: 10px 9px 6px; margin-top: 4px;
  border-top: 1px solid var(--line);
}
.mf-dates-lead { flex: 0 0 100%; font-size: 11px; text-transform: uppercase;
                 letter-spacing: .05em; color: var(--ink-soft);
                 font-weight: 700; }
.mf-dates .log-date { flex: 1 1 120px; min-width: 0; }

@media (min-width: 700px) {
  /* Three across once there is room for them, which is where the pull-downs
     stop being a list and start being a bar. */
  .mf-drops { flex-direction: row; align-items: flex-start; }
  .mf-drop { flex: 1 1 0; }
  /* Open, it needs to sit over the comments rather than push them down — the
     panel is a thing you use and leave.
     `overflow: visible` is not decoration: the closed box hides its overflow
     to keep its rounded corners, and that same rule clipped the open list to
     nothing — the options were built, positioned, and invisible. */
  .mf-drop.is-open { position: relative; overflow: visible; }
  /* SQUARE ACROSS THE JOIN, so the two boxes read as one.
   *
   * The head keeps its rounded corners when it is open, and the panel below
   * is square across the top — so each side of the box curved inward and
   * stopped, and the panel's own side lines started a couple of pixels
   * further out with a notch of page showing between them. Nothing was
   * actually apart, which is why it was hard to name: the lines were not
   * touching the box above.
   *
   * A pull-down is one shape while it is open. Rounded at the top of the
   * head, rounded at the bottom of the list, and straight down both sides in
   * between. */
  .mf-drop.is-open {
    border-bottom-left-radius: 0; border-bottom-right-radius: 0;
  }
  .mf-drop.is-open .mf-body {
    position: absolute; left: -1px; right: -1px; top: 100%; z-index: 25;
    background: var(--card); border: 1px solid var(--ink-soft);
    /* A hairline where the summary ends and the list begins, in the panel's
       own border rather than left to the head's — the panel is drawn over
       the head's bottom edge, so that one is never seen. */
    border-top: 1px solid var(--line);
    border-radius: 0 0 var(--radius) var(--radius);
    box-shadow: var(--shadow);
  }
}

/* The card Back should bring you home to. */
.card-open {
  border-color: var(--go);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--go) 22%, transparent),
              var(--shadow);
}

/* The comment text is the handle for opening a card. Marked with a cursor
   rather than styled as a link: it is prose somebody wrote, and dressing it up
   as a control would make it harder to read, which is the one thing it is
   there for. */
.comment-openable { cursor: pointer; }

/* Two names app.js sets that nothing defined. `.burst-undo` was styled only
   through `.toast-burst button`, which works until somebody moves it. */
.burst-undo { border: 0; background: var(--go); color: #fff; }
.regen-label { display: inline; }


/* ---------- M6: what the Inbox chips and cards say now ------------------ */

/* "Need response" goes red when something under it is late. Overdue comments
   are in this list rather than a list of their own — answering one in two
   places was leaving it in the other — so the chip is what carries the
   warning. */
.chip.chip-late {
  border-color: var(--danger); color: var(--danger); font-weight: 700;
}
.chip.chip-late.chip-on {
  background: var(--danger); border-color: var(--danger); color: #fff;
}

/* Nothing to say. A chip permanently reading 0 is one you stop seeing, and
   then it is not there for the day it says 3. */
.chip.chip-quiet { color: var(--ink-soft); opacity: .6; }
.chip.chip-quiet.chip-on { opacity: 1; }

/* The age, once it is late. A sentence rather than "5d": a red badge with a
   number on it is a colour, and the words are what read as overdue. */
.age.age-late {
  color: var(--danger); font-weight: 700;
  background: #FDECEA; border: 1px solid #F7D5D1;
  border-radius: 999px; padding: 1px 8px;
}

/* The Tom-only flag is a warning, not a danger: it means read this before you
   send it, which is not the same as scam or abuse. */
.flag.warnflag {
  background: var(--warn-bg); color: var(--warn); border-color: #F2E0C2;
}

/* Three numbers over a list — today, this month, ever. It replaces the
   "JUST SENT · 67" strip, which counted one run rather than answering the
   question anyone actually has about a list of sent replies. */
.countstrip {
  display: flex; flex-wrap: wrap; gap: 6px 18px;
  padding: 10px 13px; margin-bottom: var(--space);
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow);
  font-size: 13px; color: var(--ink-soft);
}
.countstrip-one b {
  color: var(--ink); font-size: 15px; font-variant-numeric: tabular-nums;
}
/* `.countstrip-admin` is gone with the button it held. It was the way into
   the Reviews admin panel from the answering wall, back when the panel was a
   page 1,042px of header away; the sections are a tab row on this screen now
   and a link off the screen to reach them would be the long way round. */

/* ---------- P5: the dashboard's order, and its notes ------------------- */

.w-orderbar {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin: var(--space-sm) 0 0;
}
.w-orderbar-label {
  font-size: 11px; text-transform: uppercase; letter-spacing: .5px;
  color: var(--ink-soft); font-weight: 600;
}
.w-orderbar-hint { font-size: 12px; color: var(--ink-soft); margin-left: auto; }

.w-grip {
  flex: 0 0 auto; color: var(--ink-soft); font-size: 14px; line-height: 1;
  cursor: grab; touch-action: none; user-select: none; padding: 2px 1px;
}
.w-grip:active { cursor: grabbing; }
.w-tile.is-dragging {
  opacity: .65; box-shadow: 0 8px 22px rgba(43, 38, 34, .18);
  border-color: var(--go);
}

/* The private note, at the bottom of the page. Same component on the Agenda
   and on Materials — one box, three places. */
.w-notes { margin-top: var(--space); }
.w-notes textarea { width: 100%; }
.w-notes-state {
  display: block; font-size: 12px; color: var(--ink-soft); margin-top: 4px;
  min-height: 16px;
}


/* ---------- M7: the thumb as a toggle, ⋯, and platform identity -------- */

/* Liked, and it shows. It used to be a control with no state on it, so
   "have I already liked this?" could only be answered by looking at Facebook.
   Tapping it again takes the like back — on the platform too. */
.like.liked {
  color: var(--fb); filter: none; transform: scale(1.06);
}
.like[aria-pressed="true"] { opacity: 1; }
.like:disabled { opacity: .4; }

/* ⋯ beside the ✕, same size and weight, so the corner reads as one pair of
   controls rather than two afterthoughts. */
/* Far enough from the ✕ to be a separate target.

   It was at right:34px, which put its edge within a pixel of the ✕ beside it:
   two controls touching in the corner of every card on the page, one of them
   the one that removes the card. Measured rather than guessed: the ✕ is 29px
   wide sitting 6px from the edge, so 45px leaves ten pixels between them —
   headroom rather than the bare minimum, because the glyph widths move a
   little between fonts and this is not worth measuring twice. */
.card-more {
  position: absolute; top: 6px; right: 45px; z-index: 2;
  border: none; background: none; cursor: pointer;
  color: var(--ink-soft); font-size: 17px; line-height: 1;
  padding: 4px 8px; border-radius: 8px;
}
.card-more:hover { background: var(--ivory); color: var(--ink); }
.card-head { padding-right: 78px; }   /* room for both, and the gap */

/* Approve, waiting on a permission rather than broken. Not hidden: the draft
   can be written today and the button says when it will go. */
.btn.btn-waiting {
  background: var(--ivory); border-color: var(--line); color: var(--ink-soft);
  cursor: not-allowed;
}

/* A filter chip carries its platform's colour, so "YouTube" reads the same in
   the filter row as it does on a card. */
.chip-plat-facebook.chip-on  { background: var(--fb); border-color: var(--fb); }
.chip-plat-instagram.chip-on { background: var(--ig); border-color: var(--ig); }
.chip-plat-hyvor.chip-on     { background: var(--go); border-color: var(--go); }
.chip-plat-youtube.chip-on   { background: var(--yt); border-color: var(--yt); }
.chip-plat-tiktok.chip-on    { background: var(--ink); border-color: var(--ink); }
.chip-plat.chip-on { color: #fff; }

/* The other end of the list. New arrivals go on the BOTTOM, never above what
   Tom is reading, so this is how he finds out they are there. Sits above the
   quick-replies bar rather than on top of it. */
.newpill-below {
  top: auto;
  bottom: calc(88px + env(safe-area-inset-bottom));
  background: var(--go); color: #fff;
}

/* The sender is away rather than slow. Amber, because it is a wait and not a
   fault — nothing is lost and it comes back on its own. */
.pacing.pacing-paused .pacing-bar span { background: var(--warn); }
.pacing.pacing-paused .pacing-text { color: var(--warn); font-weight: 600; }


/* ---------- N2: one visual system ------------------------------------- */

/* An empty state is a card, not a sentence floating in the middle of the
   page. Materials' Declined tab had one adrift on a cream background, which
   reads as a page that failed to load rather than a folder with nothing in
   it. */
main > .empty, .mat-list > .empty {
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow);
  padding: 22px 18px; text-align: center; margin: 0;
}

/* A button inside a card stands clear of the words above and below it. It was
   sitting hard against a paragraph on Events, which reads as part of the
   sentence rather than as a thing to press. */
.set-card > .btn, .card > .btn { margin: var(--space) 0; }
.set-card > .btn:last-child, .card > .btn:last-child { margin-bottom: 0; }
.set-card > .filters, .set-card > .set-nav { margin-top: var(--space); }

/* A list row on a phone: the words get the width, the controls go underneath.
   `.sec-item` is a flex row and its buttons take a fixed lump out of the
   right, so on the question bank a title wrapped to four lines beside them —
   the same fault the Inbox card row had, in a different list. */
@media (max-width: 620px) {
  .sec-item { flex-wrap: wrap; row-gap: var(--gap, 15px); }
  /* Full width, so every row in a list has the SAME shape. A minimum of 62%
     was tighter and left the list ragged — a short question sat beside its
     icon and a long one below it, which is harder to scan than one wasted
     line per row. The grip and the icon read as a marker above the words. */
  .sec-item > .grow { flex: 1 1 100%; min-width: 0; }
  .q-row .chip, .sec-item > .chip { flex: 0 0 auto; }
}

/* On a phone the figure goes under its label rather than beside it. The grid
   is `1fr auto`, so a long value squeezes the label into two or three lines
   next to it — "Daily / average" against "$9.32 — today is 30% lower" — which
   reads as a table that has come apart. One column, label then figure. */
@media (max-width: 620px) {
  .stat-grid { grid-template-columns: 1fr; gap: 1px 0; }
  .stat-k { margin-top: 8px; }
  .stat-k:first-child { margin-top: 0; }
  .stat-v { text-align: left; font-size: 15px; }
}

/* ---------- the chevron ---------- */

/* One shape for every collapse and expand in the app.
 *
 * There were five: ⌄ ⌃ ▾ ▸ ▴ — four characters and an arrow, drawn by the
 * operating system, in five sizes, some pointing down and some pointing
 * right, and no two toggles in the app agreed on which meant open. Now
 * there is one: a chevron pointing DOWN inside a thin circle, which rotates
 * a half turn to point up when the thing it controls is open.
 *
 * Drawn in CSS rather than as an icon so it works in the entrant page and
 * the admin without either loading the other's script, and in `currentColor`
 * so it takes the colour of whatever bar it sits in.
 */
.chev {
  flex: 0 0 auto;
  /* Three separate things, because they were one and moving the arrow moved
     the ring with it: how thick the arrow is, how thick the ring is, and how
     big the ring is. */
  width: var(--chev-size, 22px); height: var(--chev-size, 22px);
  border-radius: 50%;
  border: var(--chev-ring, 1.3px) solid currentColor;
  display: inline-flex; align-items: center; justify-content: center;
  color: inherit; opacity: .65;
  transition: transform .18s ease, opacity .18s ease;
}
.chev::before {
  content: "";
  /* The arrow grows with the ring it sits in — a 6px chevron inside a 40px
     circle is a dot in a hoop. */
  width: calc(var(--chev-size, 22px) * .27);
  height: calc(var(--chev-size, 22px) * .27);
  /* One number, set once. It is a property rather than a constant because a
     chevron somebody cannot see is a chevron that does not work, and how bold
     it needs to be depends on the ground it is drawn on. */
  border-right: var(--chev-weight, 2.2px) solid currentColor;
  border-bottom: var(--chev-weight, 2.2px) solid currentColor;
  /* Nudged up by the same amount the rotation pushes it down, so the point
     of the chevron sits on the centre of the circle rather than below it. */
  transform: translateY(calc(var(--chev-size, 22px) * -.07)) rotate(45deg);
}
.chev.is-open { transform: rotate(180deg); opacity: .9; }
button:hover > .chev, a:hover > .chev { opacity: 1; }


/* A button's icon is part of its words.
 *
 * The text scaled with the button-size control and the icon beside it did
 * not, so turning the type up left a large label next to a small mark. `em`
 * ties the two together: one number moves both.
 */
.gw-btn svg, .gw-social-btn svg, .gw-way-join svg, .gw-btn-small svg {
  width: 1.2em; height: 1.2em; flex: 0 0 auto;
}

/* ---------- Settings: the brand slots ----------
   A picture, what it is for, and the two things you can do to it. The image
   is capped rather than sized: these are three different shapes — a favicon,
   a signature strip and a full logo — and stretching them to one box would
   misrepresent all three. */
.set-brand {
  padding: var(--gap, 15px) 0;
  border-top: 1px solid var(--line);
}
.set-brand:first-of-type { border-top: 0; }
.set-brand-img {
  display: block; max-width: min(240px, 100%); max-height: 80px;
  width: auto; height: auto; margin: var(--gap-tight-min, 12px) 0;
  background: var(--ivory); border: 1px solid var(--line);
  border-radius: 8px; padding: 6px;
}

/* A switch on a Settings row. The same one the giveaway builder draws — that
   one lives in a stylesheet Settings does not load, and a browser's own
   checkbox beside it reads as a different kind of control rather than the
   same control somewhere else. */
.sec-switch {
  appearance: none; -webkit-appearance: none;
  width: 40px; height: 23px; border-radius: 999px; margin: 0;
  background: var(--line); position: relative; cursor: pointer;
  transition: background .15s ease; flex: 0 0 auto;
}
.sec-switch::after {
  content: ''; position: absolute; top: 2px; left: 2px;
  width: 19px; height: 19px; border-radius: 50%; background: #fff;
  box-shadow: 0 1px 2px rgba(0,0,0,.25); transition: transform .15s ease;
}
.sec-switch:checked { background: var(--go); }
.sec-switch:checked::after { transform: translateX(17px); }

/* A signature shown as the person receiving it will see it, under the box it
   is typed in. HTML that only ever exists as source is HTML nobody checks. */
.set-sig-preview {
  margin-top: var(--gap-tight-min, 12px);
  padding: var(--gap-tight-min, 12px);
  border: 1px dashed var(--line); border-radius: 10px;
  font-size: 13.5px; line-height: 1.5; color: var(--ink-soft);
}

/* A ticked source on a post type reads as ticked, not as "one of these". */
.pt-sources .pt-chip.is-on {
  background: color-mix(in srgb, var(--go) 14%, transparent);
  border-color: var(--go); color: var(--go); font-weight: 650;
}

/* Why a comment is in Skipped.
 *
 * Every skip in the database said "Tom" and nothing else — the button, the
 * swipe and anything automatic alike — so one set aside by accident read
 * exactly like one set aside on purpose. */
.skip-why {
  margin: 6px 0 0; font-size: 12px; color: var(--ink-soft);
}
.skip-why.is-question { color: var(--danger); font-weight: 650; }
/* A question that was set aside is the one worth a second look. */
.skip-question {
  margin: 4px 0 0; font-size: 12.5px; color: var(--danger);
}

/* The post a bulk row is under — a working link now, not a tooltip. */
.bi-post.is-link { color: inherit; text-decoration: underline;
                   text-underline-offset: 2px; }
.bi-post.is-link:hover { text-decoration-thickness: 2px; }

/* A reply that was never posted because the comment had gone. Quiet on
   purpose: it is information, not a fault, and it sits in its own pile away
   from the one marked "needs you". */
.sending-card.state-gone { opacity: .85; }
.gone-note {
  margin: var(--gap-tight-min, 12px) 0 0; font-size: 13px;
  color: var(--ink-soft);
}
.sendstate.gone { color: var(--ink-soft); }

/* ---------- the accordion's pin ----------
 *
 * `section.js` owns the behaviour and hands the same button to every screen
 * that rolls its sections up: the form builder, the post types in Settings,
 * and whatever adopts it next. So the look lives here rather than in any one
 * screen's sheet.
 *
 * ON THE RIGHT, past the chevron. It is about the section rather than part of
 * opening it, and a control that changes what the NEXT click does has no
 * business sitting where the next click is aimed.
 */
.fb-sec-pin {
  flex: 0 0 auto;
  padding: 0 var(--gap, 15px);
  background: none; border: 0; cursor: pointer;
  font-size: 13px; line-height: 1; color: var(--ink-soft);
  opacity: .45;
}
.fb-sec-pin:hover { opacity: 1; background: var(--ivory); }
.fb-sec-pin.is-on { opacity: 1; }

/* ---------- reviews, as a source in this inbox ----------
 *
 * A review card is a comment card with two differences and no third: it says
 * plainly what it is, and it carries the stars. Everything else — the head,
 * the draft box, the action row — is the same markup and the same rules, so
 * answering one feels like answering the other, which is the point.
 *
 * The star colour is the review box's own accent, not a token borrowed from
 * this page, so a card here and the box on the recipe page agree about what
 * a star looks like.
 */
.badge.badge-reviews { background: var(--orange); }
/* THE WEB COMMENTS SOURCE. Its own colour, and the reason is the same one
   `.badge-emails` gives one line down, only sharper here: this Inbox now has
   two sources with "comments" in the name and they are not the same pile —
   one is the owner's own recipe page, one is Meta. A card that is answered as
   the wrong one is a reply sent to a stranger on a network nobody meant to be
   on. `--go-deep` rather than `--orange`, which is the reviews badge, and
   rather than `--go`, which is 4.0:1 and not enough behind 12px white. */
.badge.badge-webcomments { background: var(--go-deep); }
/* The Emails source. Its own colour so a card that is mail is never read as a
   comment — the whole reason every source's card wears a badge. */
.badge.badge-emails { background: var(--ink-soft); }
/* The subject line, which is the one piece an email card has that a comment
   card does not. Same size and colour as the review card's page reference,
   because they do the same job: what the words underneath are about. */
.email-card .postctx { display: flex; }
.email-card .postcap { font-size: 13px; font-weight: 650; color: var(--ink); }
.badge.badge-track {
  background: transparent; color: var(--ink-soft);
  border: 1px solid var(--line); text-transform: none; letter-spacing: 0;
  font-weight: 600;
}
.rv-src-stars {
  color: #F47C20; font-size: 14px; letter-spacing: 1px; line-height: 1;
  white-space: nowrap;
}
/* The page it was written under, in the slot a post's caption uses. */
.review-card .postctx { display: flex; }
.review-card .postcap { font-size: 12.5px; color: var(--ink-soft); }

/* ---------- a direct message, as a card in the same inbox ----------
 *
 * The same card again, with two things a comment card has no need of: the
 * conversation above the draft, and the 24-hour window said out loud when it
 * has closed. See the note over `renderDMCard` in app.js.
 */
.badge.badge-dm { background: var(--ink-soft); }

.dm-card .convo {
  max-height: 220px;
  overflow-y: auto;
}
.dm-card .convo-line { font-size: 13px; line-height: 1.45; }
.dm-card .convo-who { color: var(--ink-soft); font-weight: 650; }
.dm-card .convo-line.is-mine .convo-who { color: var(--go); }
.dm-card .convo-line.is-mine { color: var(--ink); }
.dm-card .convo-said { white-space: pre-wrap; }
/* A message that carried a photo or a file. The paperclip says so without
   fetching it: a DM's attachments are behind Meta's own signed URLs and this
   app does not put them on its disk. */
.dm-card .convo-att { color: var(--ink-soft); }

/* NOT A QUIET TAG. Outside the window an approved answer will be refused by
   Meta, so this is the one thing on the card that has to be read before the
   draft under it is. */
.waiting-tag.dm-closed {
  background: #FDECEC;
  color: var(--danger);
  border-color: var(--danger);
  font-weight: 700;
}

/* A standing condition, said once above the list rather than on every card.
   Not `.pile-head`, which upper-cases: a sentence containing an .env variable
   name shouted in capitals is harder to read than the thing it is explaining. */
.dm-note {
  font-size: 13px;
  line-height: 1.5;
  color: var(--ink-soft);
  background: var(--card);
  border: 1px solid var(--line);
  border-left: 3px solid var(--danger);
  border-radius: 10px;
  padding: 10px 12px;
  margin: 0 0 12px;
}

/* The rule between the networks and the two things that are not networks, in
   the Source pull-down. It is doing real work: it is what says ⭐ Reviews and
   💬 DMs are a different kind of answer to the same question. */
.mf-rule {
  height: 1px;
  background: var(--line);
  margin: 6px 8px;
}

/* A CARD SOMEBODY HAS REPORTED, in the answering queue.
 *
 * A BOX, NOT A BADGE IN THE CORNER. A reported review is not a review with an
 * extra property on it — it is the one thing in that list somebody outside
 * this building is waiting on an answer about, and the rest of the pile can
 * wait until Thursday. It is also already at the top: the order is decided in
 * `cards` in app/review_pipeline.py, so the screen and the batch lane agree.
 *
 * `--danger` is the app's own rust, the one a failed send already uses. No
 * new colour was invented for this.
 */
.card.card-reported {
  border-color: var(--danger);
  box-shadow: 0 0 0 1px var(--danger);
}

.badge.badge-reported {
  background: var(--danger);
  color: #fff;
  border-color: var(--danger);
}

/* WHAT WAS ATTACHED, on a card in the answering queue.
 *
 * Same job and same size as `.ra-shots` on the Reviews tab — a working
 * thumbnail, not a gallery — and deliberately the same numbers, because the
 * two lists are read one after the other and a picture that changed size
 * between them reads as two different screens. */
.card-shots {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 10px 0 0;
}

.card-shot {
  display: block;
  width: 92px;
  height: 92px;
  border-radius: 8px;
  overflow: hidden;
  border: 1px solid var(--line);
  background: var(--paper-2, #f3efe7);
}

.card-shot img { width: 100%; height: 100%; object-fit: cover; display: block; }

.card-shot:hover { border-color: var(--go); }

.card-shot:focus-visible { outline: 2px solid var(--go); outline-offset: 2px; }

/* ---------------------------------- the owner's three dots on a Desk card */

/* TOP RIGHT, AND THE SAME CORNER ON EVERY CARD — WHICH IS NOT THE END OF THE
   HEAD ROW.
   `margin-left:auto` pinned it to the end of `.card-head`, and that row
   already carries `padding-right: 78px` to keep clear of the two controls a
   COMMENT card puts in this corner absolutely (`.card-x` and `.card-more`).
   So "the end of the row" was 96px short of the card's edge, and the menu sat
   in the middle of nowhere. A review card draws neither of those, so the
   corner is free and this takes it the same way they do. */
.card.review-card { position: relative; }

.card-menu { position: absolute; top: 6px; right: 6px; z-index: 3; }

.card-menu-btn {
  width: 32px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 1px solid transparent;
  border-radius: 8px;
  background: none;
  color: var(--ink-soft);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
}

.card-menu-btn:hover { border-color: var(--line); color: var(--ink); }

.card-menu-btn:focus-visible { outline: 2px solid var(--go); outline-offset: 2px; }

/* WIDE ENOUGH FOR THE LONGEST LABEL, which is the delete-all. A menu whose
   items wrap is a menu somebody misreads in a hurry. */
.card-menu-pop {
  position: absolute;
  top: 36px;
  right: 0;
  z-index: 20;
  min-width: 268px;
  padding: 6px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--paper, #fff);
  box-shadow: 0 10px 30px rgba(0, 0, 0, .16);
  display: flex;
  flex-direction: column;
  gap: 2px;
  text-align: left;
}

.card-menu-item {
  display: block;
  width: 100%;
  padding: 9px 10px;
  border: 0;
  border-radius: 7px;
  background: none;
  color: var(--ink);
  font: inherit;
  font-size: 14px;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}

.card-menu-item:hover { background: var(--paper-2, #f3efe7); }

.card-menu-item:focus-visible { outline: 2px solid var(--go); outline-offset: -2px; }

/* THE THREE THAT SHUT SOMEBODY OUT read as what they are. Not red on hover
   only — a person scanning this menu should see which half of it is heavy
   before their pointer gets there. */
.card-menu-item--heavy { color: var(--go-deep, #b3261e); font-weight: 650; }

.card-menu-pop--ask { min-width: 300px; padding: 12px; gap: 10px; }

.card-menu-ask { margin: 0; font-size: 14px; line-height: 1.45; }

.card-menu-ask-row { display: flex; gap: 8px; }

@media (max-width: 560px) {
  /* On a phone the card is nearly the viewport, so a 268px menu hanging off
     the right edge would go off it. Pinned to both edges instead. */
  .card-menu-pop { right: 0; left: auto; min-width: 0; width: 84vw; max-width: 300px; }
}

/* The three tabs across the top of the People index — Active, Blocked,
   Deleted. Excel-style: one index, three views, the current one raised.
   A separate page for the difficult people is a page nobody opens. */
.ppl-tabs { display: flex; gap: 4px; margin: 0 0 14px; flex-wrap: wrap; }
.ppl-tab {
  border: 1px solid var(--line, #E7E2DA);
  border-bottom-color: transparent;
  background: var(--card, #fff);
  color: var(--muted, #8A8279);
  padding: 8px 14px;
  border-radius: 8px 8px 0 0;
  font: inherit;
  font-size: 13.5px;
  cursor: pointer;
}
.ppl-tab.is-on {
  color: var(--ink);
  font-weight: 650;
  border-color: var(--line, #E7E2DA);
  border-bottom-color: var(--card, #fff);
}
/* The per-card actions sit apart from the card's own click target, which
   opens the person — see `stopPropagation` in stateActions. */
.ppl-actions { display: flex; gap: 6px; margin-top: 8px; }

/* The placeholder tile on a recipe card whose picture is not known yet — the
   website's recipe-meta endpoint has not answered for that id, or has no
   image for it. Same box as `.postthumb` so the row does not jump sideways
   when the real picture arrives on a later draw. */
.postthumb--none { background: var(--line, #E7E2DA); display: inline-block; }
/* The headline is a link now rather than plain text: to the recipe when the
   website has told us its address, and to this desk's own page view when it
   has not. */
a.postcap--link { text-decoration: none; color: inherit; }
a.postcap--link:hover { text-decoration: underline; }

/* ---------- the People card's mailing side ---------- */
/* Only drawn when the Mailing section is on. The chips are the card's own;
   these two say which way round the list status is, in the card's existing
   go and quiet colours rather than new ones. */
.p-mailing .p-key { flex-basis: 62px; }
.p-mail-addr { font-weight: 600; overflow-wrap: anywhere; white-space: normal; }
.p-mail-on { border-color: var(--go); color: var(--go); }
.p-mail-off { color: var(--ink-soft); font-style: italic; }
