/* Leaving Board — hand-written CSS, no framework, no build step.
   Light/dark aware; mobile-first; system font stack. */

:root {
  --bg: #f6f4ef;
  --bg-elev: #ffffff;
  --text: #1d1b17;
  --muted: #5f5a51;
  --border: #e2ddd2;
  --accent: #2f5d50;
  --accent-strong: #234439;
  --accent-contrast: #ffffff;
  --accent-soft: #e6efea;
  --radius: 14px;
  --radius-sm: 9px;
  --shadow: 0 1px 2px rgba(29, 27, 23, .06), 0 8px 24px -12px rgba(29, 27, 23, .18);
  --shell: 62rem;
  --font: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial,
          "Noto Sans", sans-serif;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #16181a;
    --bg-elev: #1e2124;
    --text: #eceef0;
    --muted: #a7adb4;
    --border: #2f3438;
    --accent: #7fc9ad;
    --accent-strong: #9ad9c1;
    --accent-contrast: #10221c;
    --accent-soft: #22302b;
    --shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 8px 24px -12px rgba(0, 0, 0, .6);
  }
}

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 1rem;
  line-height: 1.6;
}

a { color: var(--accent); }
a:hover { color: var(--accent-strong); }

:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}

.shell {
  width: 100%;
  max-width: var(--shell);
  margin-inline: auto;
  padding-inline: 1.25rem;
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  background: var(--accent);
  color: var(--accent-contrast);
  padding: .6rem 1rem;
  border-radius: 0 0 var(--radius-sm) 0;
  z-index: 100;
}
.skip-link:focus { left: 0; }

/* ---------- header ---------- */

.site-header {
  border-bottom: 1px solid var(--border);
  background: var(--bg-elev);
}

.header-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  min-height: 4rem;
  flex-wrap: wrap;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: .55rem;
  font-weight: 700;
  font-size: 1.075rem;
  letter-spacing: -.01em;
  color: var(--text);
  text-decoration: none;
}
.brand:hover { color: var(--text); }
.brand-mark {
  display: inline-grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  border-radius: 8px;
  background: var(--accent);
  color: var(--accent-contrast);
  font-size: 1.05rem;
}

.site-nav { display: flex; gap: .75rem; align-items: center; }

/* ---------- main ---------- */

main { flex: 1 0 auto; padding-block: 3rem 4rem; }

.hero { max-width: 44rem; margin-inline: auto; text-align: center; padding-block: 1rem; }

.eyebrow {
  margin: 0 0 .75rem;
  font-size: .8rem;
  font-weight: 700;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--accent);
}

h1 {
  margin: 0 0 1rem;
  font-size: clamp(2rem, 1.2rem + 3.4vw, 3.1rem);
  line-height: 1.12;
  letter-spacing: -.025em;
}

.lede {
  margin: 0 auto;
  max-width: 38rem;
  font-size: clamp(1.02rem, .96rem + .3vw, 1.15rem);
  color: var(--muted);
}

.hero-cta {
  margin: 2rem 0 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .75rem;
}

.cta-note { font-size: .875rem; color: var(--muted); }

/* ---------- buttons ---------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .5rem;
  padding: .62rem 1.1rem;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  font: inherit;
  font-weight: 600;
  line-height: 1.2;
  text-decoration: none;
  cursor: pointer;
  transition: background-color .15s ease, border-color .15s ease, transform .05s ease;
}
.btn:active { transform: translateY(1px); }

.btn-primary {
  background: var(--accent);
  color: var(--accent-contrast);
  border-color: var(--accent);
  box-shadow: var(--shadow);
}
.btn-primary:hover { background: var(--accent-strong); border-color: var(--accent-strong); color: var(--accent-contrast); }

.btn-ghost {
  background: transparent;
  color: var(--text);
  border-color: var(--border);
}
.btn-ghost:hover { border-color: var(--accent); color: var(--accent); }

.btn-lg { padding: .85rem 1.6rem; font-size: 1.075rem; border-radius: 12px; }
.btn-sm { padding: .4rem .8rem; font-size: .875rem; }

/* ---------- home sections ---------- */

.steps, .reassure { margin-top: 4.5rem; }

.steps-heading {
  margin: 0 0 1.5rem;
  font-size: 1.3rem;
  letter-spacing: -.01em;
  text-align: center;
}

.step-grid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 1rem;
  grid-template-columns: 1fr;
}
@media (min-width: 44rem) {
  .step-grid { grid-template-columns: repeat(3, 1fr); }
}

.step {
  background: var(--bg-elev);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1.35rem 1.35rem 1.5rem;
  box-shadow: var(--shadow);
}
.step h3 { margin: 0 0 .5rem; font-size: 1.05rem; }
.step p { margin: 0; color: var(--muted); font-size: .95rem; }

.feature-list {
  max-width: 40rem;
  margin-inline: auto;
  padding-left: 1.15rem;
  color: var(--muted);
}
.feature-list li { margin-bottom: .5rem; }
.feature-list strong { color: var(--text); }

/* ---------- contribution landing page (T9, GET /b/{slug}) ----------

   The page a colleague opens from the shared link. It reuses the home page's
   hero (.hero/.eyebrow/.lede/.hero-cta) so the two ways into the product look
   like the same product; only the signature count, the posts section and the
   empty state are new. */

.board-hero .lede strong { color: var(--text); }

/* The signature count, stated just above the CTA: the one number a visitor
   wants before deciding to join in. */
.board-count {
  margin: 1.5rem 0 0;
  font-size: 1.05rem;
  font-weight: 600;
  color: var(--text);
}

.board-posts { margin-top: 4rem; }

/* An empty board is an invitation, not a failure: no grid, no echo of empty
   space — one friendly card that says the first message can be theirs. */
.board-empty {
  margin-top: 1.5rem;
  padding: 2rem 1.5rem;
  text-align: center;
  border: 1px dashed var(--border);
  border-radius: var(--radius);
  background: var(--bg-elev);
  color: var(--muted);
}
.board-empty p { margin: 0 auto; max-width: 34rem; }
.board-empty p + p { margin-top: .6rem; }
.board-empty strong { color: var(--text); font-size: 1.075rem; }

/* ---------- flash + errors ---------- */

.flash {
  margin: 1.25rem 0 0;
  padding: .75rem 1rem;
  border: 1px solid var(--accent);
  border-left-width: 4px;
  border-radius: var(--radius-sm);
  background: var(--accent-soft);
  color: var(--text);
}

.error-page { max-width: 34rem; margin-inline: auto; text-align: center; }
.error-code {
  margin: 0;
  font-size: clamp(3.5rem, 2rem + 8vw, 6rem);
  font-weight: 800;
  line-height: 1;
  color: var(--border);
  letter-spacing: -.04em;
}

/* ---------- checkpoint pages (check your inbox) ---------- */

.checkpoint { max-width: 38rem; margin-inline: auto; text-align: center; }
.checkpoint .lede strong { color: var(--text); word-break: break-word; }
.checkpoint-list {
  list-style: none;
  margin: 2rem 0 1.5rem;
  padding: 0;
  text-align: left;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg-elev);
}
.checkpoint-list li {
  margin: 0;
  padding: .75rem 1rem;
  color: var(--muted);
  font-size: .95rem;
}
.checkpoint-list li + li { border-top: 1px solid var(--border); }
.checkpoint-list strong { color: var(--text); }
.checkpoint-resend { margin: 1.5rem 0; text-align: left; }
.checkpoint-resend summary {
  cursor: pointer;
  color: var(--muted);
  font-size: .95rem;
}
.checkpoint-resend p { color: var(--muted); font-size: .95rem; }
.resend-form { margin: 0; }

/* ---------- footer ---------- */

.site-footer {
  border-top: 1px solid var(--border);
  background: var(--bg-elev);
  padding-block: 1.25rem;
}
.footer-inner {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem 1.25rem;
  justify-content: space-between;
  align-items: center;
  color: var(--muted);
  font-size: .875rem;
}
.footer-inner p { margin: 0; }
.footer-links { display: flex; gap: 1rem; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    transition-duration: .001ms !important;
  }
}

/* ---------- sign-in form + link problem (T5) ---------- */

.auth-form { max-width: 34rem; margin-inline: auto; }
.auth-form .lede { margin-inline: 0; }

.auth-form-body {
  margin-top: 2rem;
  padding: 1.5rem 1.5rem 1.75rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-elev);
  box-shadow: var(--shadow);
  text-align: left;
}

.field { margin-bottom: 1.25rem; }
.field label {
  display: block;
  margin-bottom: .35rem;
  font-weight: 600;
  font-size: .95rem;
}
.field input {
  width: 100%;
  padding: .6rem .7rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg);
  color: var(--text);
  font: inherit;
}
.field input:focus-visible { border-color: var(--accent); }
.field-hint {
  margin: .4rem 0 0;
  font-size: .85rem;
  color: var(--muted);
}

.auth-actions { margin: 1.5rem 0 .5rem; }
.auth-actions .btn { width: 100%; }

/* ---------- create-board form + organiser dashboard (T7) ---------- */

/* For screen readers only: a label that is visually implied by layout. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Standalone informational strip (resumed values, validation summary). */
.notice {
  margin: 1.5rem 0 0;
  padding: .75rem 1rem;
  border: 1px solid var(--accent);
  border-left-width: 4px;
  border-radius: var(--radius-sm);
  background: var(--accent-soft);
  color: var(--text);
}
.notice-error { border-color: #b4342c; background: #fbecea; color: #6d1d18; }
@media (prefers-color-scheme: dark) {
  .notice-error { border-color: #e08a82; background: #33191a; color: #ffd9d4; }
}

/* ---------- create-board form ---------- */

.board-new { max-width: 38rem; margin-inline: auto; }
.board-new .lede { margin-inline: 0; }

.board-new-body {
  margin-top: 2rem;
  padding: 1.5rem 1.5rem 1.75rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-elev);
  box-shadow: var(--shadow);
  text-align: left;
}

.field-optional { font-weight: 400; color: var(--muted); }

/* A field that failed validation. The red also reaches the input itself so the
   problem is visible without reading the sentence. */
.field-invalid input { border-color: #b4342c; }
@media (prefers-color-scheme: dark) {
  .field-invalid input { border-color: #e08a82; }
}

.field-error {
  margin: .4rem 0 0;
  font-size: .875rem;
  font-weight: 600;
  color: #8f2820;
}
@media (prefers-color-scheme: dark) {
  .field-error { color: #ffb4ac; }
}

/* ---------- organiser dashboard (T10) ----------

   The organiser's home page, so it is laid out as a stack of cards: state
   summary, share link, settings, the moderation table, reveal and delivery.
   It is wider than the form pages because the messages table needs the room
   (and scrolls rather than squashing on a phone). */

.admin { max-width: 52rem; margin-inline: auto; }
.admin .lede { margin-inline: 0; }
.admin .lede strong { color: var(--text); }

/* At-a-glance state line under the title: a status pill plus one sentence. */
.admin-status {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: .5rem .75rem;
  margin: 1rem 0 0;
}
.admin-status-note { color: var(--muted); font-size: .95rem; }

.badge {
  display: inline-block;
  padding: .15rem .6rem;
  border: 1px solid var(--accent);
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--text);
  font-size: .8rem;
  font-weight: 600;
  letter-spacing: .01em;
  white-space: nowrap;
}
.badge-revealed { border-color: var(--border); background: var(--bg); color: var(--muted); }

.admin-count {
  margin-left: .5rem;
  color: var(--muted);
  font-size: .9rem;
  font-weight: 400;
}
.muted { color: var(--muted); }

/* A settings error inside a card: the notice already has a top margin, but it
   now sits under a heading, so pull it in. */
.admin-card .notice { margin-top: 1rem; }

/* The settings form inside its card. text-align:left because the surrounding
   .hero/.cta-note copy is centred in places and a centred form is unreadable. */
.admin-settings { margin-top: 1rem; text-align: left; }

.admin-empty { margin: 1rem 0 0; color: var(--muted); font-size: .95rem; }
.admin-empty strong { color: var(--text); }
/* The organiser's download links (T21). .share-row-like: they wrap rather than
   squash on a phone, and the hint under them is the same muted size as the
   other hints in a card. */
.export-links { display: flex; flex-wrap: wrap; gap: .5rem; margin: 0 0 .75rem; }
.admin-export .field-hint { margin-top: 0; }
.reveal-actions { margin: 1.25rem 0 .5rem; }

/* The messages table. It scrolls horizontally rather than squashing the
   message column to nothing on a phone — a wrapped table is unreadable, a
   scrolled one is not. */
.admin-table-wrap {
  margin-top: 1rem;
  overflow-x: auto;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg);
}
.admin-table {
  width: 100%;
  border-collapse: collapse;
  font-size: .95rem;
}
.admin-table th,
.admin-table td {
  padding: .7rem .75rem;
  text-align: left;
  vertical-align: top;
  border-bottom: 1px solid var(--border);
}
.admin-table tbody tr:last-child td { border-bottom: 0; }
.admin-table thead th {
  background: var(--bg-elev);
  color: var(--muted);
  font-size: .8rem;
  font-weight: 600;
  letter-spacing: .02em;
  text-transform: uppercase;
  white-space: nowrap;
}

.admin-cell-thumb { width: 6.5rem; }
.admin-thumb {
  display: block;
  width: 6rem;
  height: 3.4rem;
  object-fit: cover;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--accent-soft);
}

.admin-cell-author { width: 11rem; }
.admin-author-name { display: block; color: var(--text); font-weight: 600; }
/* Organiser-only address (see the template header). Smaller and muted so it
   reads as metadata, not as part of the message. */
.admin-author-email {
  display: block;
  color: var(--muted);
  font-size: .85rem;
  /* overflow-wrap rather than word-break: break-all, so an address wraps at a
     sensible point instead of splitting mid-word ("ann@exampl / e.com"). */
  overflow-wrap: anywhere;
  word-break: normal;
}

.admin-cell-message { min-width: 14rem; }
.admin-cell-when { width: 9.5rem; color: var(--muted); font-size: .85rem; white-space: nowrap; }
.admin-cell-actions { width: 11rem; }
/* The row action is a short label; wrapping it to three lines made the column look
   broken, so keep it on one line and let the disclosure shrink to fit. */
.admin-cell-actions .delete-summary { white-space: nowrap; }

/* The shared delete disclosure (T14's partial) inside a table cell: it is
   narrower here than on its own page, so the block stops centring itself. */
.admin-cell-actions .delete-block { max-width: none; margin: 0; }
.admin-cell-actions .delete-summary { padding: .35rem .5rem; font-size: .875rem; }
.admin-cell-actions .delete-body { padding: .75rem .5rem; }
.admin-cell-actions .delete-warning { font-size: .85rem; }

/* Reveal and delivery: the reveal control uses T14's <details> confirmation
   shape (no inline JS — T18's CSP forbids it, and it must work without JS). */
.reveal-disclosure {
  margin-top: 1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg);
}
.reveal-summary {
  padding: .8rem 1rem;
  color: var(--accent);
  font-size: .95rem;
  font-weight: 600;
  cursor: pointer;
}
.reveal-disclosure[open] .reveal-summary {
  border-bottom: 1px solid var(--border);
  color: var(--text);
}
.reveal-form { margin: 0 0 .75rem; }

.job-list {
  margin: 1rem 0 0;
  padding: 0;
  list-style: none;
}
.job {
  padding: .9rem 1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg);
}
.job + .job { margin-top: .6rem; }
.job-ok { border-color: var(--accent); }
.job-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: .5rem; margin: 0; }
.job-subject { font-weight: 600; font-size: .95rem; }
.job-meta { margin: .6rem 0 0; }
.job-error { color: #8f2820; word-break: break-word; }
@media (prefers-color-scheme: dark) {
  .job-error { color: #ffb4ac; }
}

.share-block,
.admin-card {
  margin-top: 2rem;
  padding: 1.35rem 1.35rem 1.5rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-elev);
  box-shadow: var(--shadow);
}

.share-heading { margin: 0 0 .5rem; font-size: 1.1rem; }
.share-note { margin: 0 0 1rem; color: var(--muted); font-size: .95rem; }

.share-row { display: flex; flex-wrap: wrap; gap: .5rem; }
.share-input {
  flex: 1 1 16rem;
  min-width: 0;
  padding: .6rem .7rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg);
  color: var(--text);
  font: inherit;
  font-size: .95rem;
}
.share-status { margin: .6rem 0 0; font-size: .875rem; color: var(--accent); font-weight: 600; }

.summary-list {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: .35rem 1.25rem;
  margin: 0 0 .75rem;
}
.summary-list dt { color: var(--muted); font-size: .95rem; }
.summary-list dd { margin: 0; font-size: .95rem; word-break: break-word; }

/* ---------- square post block + grid (T12, SPEC §2.2 P6) ----------
   The block is the visual heart of the product: one square card per message,
   rendered by the "post_block" partial on the board, the compose preview, the
   recipient view and the slideshow slides.

   Geometry: the card is square via aspect-ratio, and its height never depends
   on the message — a long message scrolls inside its own box instead of
   stretching the card (the acceptance criterion for long messages). */

/* --post-lines is how many message lines show before the message scrolls. It
   is a custom property so the number lives in one place, and it is redeclared
   per breakpoint because the budget has to shrink with the block: the message
   is capped at --post-lines * line-height, and the banner takes the rest of
   the square. Declared on both the grid (so every block inherits it) and the
   single block (so a lone block in a preview or a slideshow slide still has
   it). */
.post-block,
.post-grid {
  --post-lines: 6;
}

.post-grid {
  display: grid;
  gap: 1.25rem;
  grid-template-columns: 1fr;
  margin: 0;
  padding: 0;
  list-style: none;
}
/* Explicit breakpoints rather than repeat(auto-fill, minmax(...)): the
   acceptance criterion counts columns (1 / 2 / 3 / 4), and auto-fill would
   make that count depend on the container width in ways that are awkward to
   state, let alone verify. */
/* 2 columns */
@media (min-width: 40rem) {
  .post-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .post-grid { --post-lines: 6; }
}
/* 3 columns */
@media (min-width: 56.25rem) {
  .post-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .post-grid { --post-lines: 5; }
}
/* 4 columns: the blocks are at their smallest here, so the message budget
   shrinks with them. */
@media (min-width: 75rem) {
  .post-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .post-grid { --post-lines: 4; }
}

.post-block {
  /* Grid, not flex, for the interior: the banner takes whatever height the
     message and author strip leave (minmax floor so a banner is always
     visible), and the message row is sized by its own content. A flex message
     would be *shrunk* by its siblings and thus cut through the middle of a
     line; a grid row is not. */
  display: grid;
  grid-template-rows: minmax(2.5rem, 1fr) auto auto;
  aspect-ratio: 1 / 1;      /* the whole point: a square block */
  overflow: hidden;         /* keeps the rounded corners clean */
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-elev);
  box-shadow: var(--shadow);
}

/* Banner: the top of the square. It takes a fixed 50% of the block height for
   a message that fits, and yields height back to a longer message (down to
   --post-banner-min) so the message can show whole lines rather than being cut
   through the middle of one. object-fit: cover means the image never distorts,
   whatever height it ends up with. */
.post-block-media {
  position: relative;
  min-height: 0;
  overflow: hidden;
  background: var(--accent-soft);
}

/* Both renditions (animated GIF, still) and the decorative banner share this
   box; object-fit:cover crops rather than distorts. Absolute positioning means
   the <img> cannot influence the block's size, so the square is guaranteed. */
.post-block-img {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Message: below the banner, above the author strip. The background is solid
   (never the GIF) so text contrast is knowable: --text on --bg-elev is
   17.19:1 in light mode and 13.91:1 in dark mode (WCAG AA needs 4.5:1).

   It is the only scroll container in the block: a long message never stretches
   the card (which stays square) and never loses text — the whole message is
   reachable by scrolling. --post-lines caps the height at an exact multiple of
   the line height, so the cut lands on a line boundary and -webkit-line-clamp
   can draw an ellipsis for browsers that support it. (Blink ignores the clamp
   when overflow-y is auto, which is exactly why the max-height carries the
   guarantee and the clamp is only the nicety.) */
.post-block-message {
  min-height: 0;
  margin: 0;
  padding: .6rem .85rem;
  max-height: calc(var(--post-lines) * 1.25rem + 1.2rem);
  overflow-x: hidden;
  overflow-y: auto;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: var(--post-lines);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  font-size: .9rem;
  line-height: 1.25rem;
  color: var(--text);
  background: var(--bg-elev);
  scrollbar-width: thin;
}

/* Author: a solid strip anchored at the bottom of the block, never over the
   image (SPEC §3.5 requires text to sit on a solid strip, not on a GIF).
   Contrast: --accent-contrast on --accent is 7.49:1 light / 8.57:1 dark. */
.post-block-author {
  margin: 0;
  padding: .5rem .85rem;
  background: var(--accent);
  color: var(--accent-contrast);
  font-size: .825rem;
  font-weight: 700;
  line-height: 1.35;
  letter-spacing: .01em;
  overflow-wrap: anywhere;
}

/* ---------- GIF vs still: reduced motion, CSS only ---------- */

/* The still is the reduced-motion rendition: hidden by default, swapped in by the
   media query below. It is declared with a slightly raised specificity because a
   later single-class rule (e.g. `.giphy-preview-img { display: block }`) would
   otherwise win on source order alone and leave both renditions on screen. */
img.gif-still { display: none; }

/* The decorative banner is always shown (it is the only image a GIF-less post
   has); it is named so the intent is explicit next to the two GIF renditions. */
.gif-banner { display: block; }

@media (prefers-reduced-motion: reduce) {
  img.gif-animated { display: none; }
  /* Specificity is deliberately one class + element so these win against any
     `.<class> { display: block }` rule regardless of its position in the file.
     !important is avoided so the toggle stays overridable for e.g. one-off hosts. */
  img.gif-still { display: block; }
}

/* ---------- compose preview frame (T13) ---------- */

/* The preview shows the real block, at roughly the size it will be on a
   two-column board, inside a plain frame. Only the frame is preview-specific:
   .post-grid-preview overrides the column count back to one. */
.post-grid-preview {
  grid-template-columns: 1fr;
  max-width: 20rem;
  margin-inline: auto;
}
.post-preview {
  margin-top: 2rem;
  padding: 1.5rem 1.25rem 1.35rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg);
  box-shadow: var(--shadow);
}
.post-preview-note {
  margin: 1rem 0 0;
  text-align: center;
  font-size: .875rem;
  color: var(--muted);
}

/* ---------- GIPHY attribution ---------- */

/* Rendered once per page by the "giphy_attribution" partial. GIPHY's terms
   require the credit wherever their media appears; a per-block copy would
   print it twenty times in one grid. Contrast: 6.23:1 light / 7.87:1 dark. */
.giphy-attribution {
  margin: 1.5rem 0 0;
  font-size: .8rem;
  color: var(--muted);
}
.giphy-wordmark {
  font-weight: 700;
  letter-spacing: .06em;
}

/* ---------- GIPHY picker (T11, SPEC §2.4) ----------

   The shell (trigger + live preview) sits in the compose form; the search UI is
   a native <dialog>, which gives Esc-to-close, focus trapping, an inert
   background and ::backdrop without a line of modal JS.

   Everything below is display-only: with JavaScript off the shell stays hidden
   (it ships with the `hidden` attribute) and the four hidden GIF inputs submit
   empty, which is the intended text-only degradation. */

.giphy-picker { margin-top: .25rem; }

/* The graceful-degradation notice (SPEC §3.4). Reuses the app's notice look so
   it reads as information, not as a failure the user must fix. */
.giphy-unavailable {
  margin: 1.25rem 0 0;
  padding: .75rem 1rem;
  border: 1px solid var(--border);
  border-left-width: 4px;
  border-radius: var(--radius-sm);
  background: var(--bg);
  color: var(--muted);
  font-size: .9rem;
}

.giphy-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 1rem;
  margin-top: 1.25rem;
}

/* Live preview of the chosen GIF: the same two-image animated/still pair the
   published block uses, at a fixed size so picking a GIF does not reflow the
   form (anti-CLS, SPEC §3.3). */
.giphy-preview {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: .5rem;
}
.giphy-preview-img {
  display: block;
  width: 9rem;
  height: 9rem;
  object-fit: cover;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--accent-soft);
}

/* ---- the dialog ---- */

.giphy-dialog {
  width: min(46rem, calc(100vw - 2rem));
  max-height: min(80vh, 44rem);
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-elev);
  color: var(--text);
  box-shadow: var(--shadow);
}
.giphy-dialog::backdrop { background: rgba(29, 27, 23, .55); }

.giphy-dialog-inner {
  padding: 1.25rem 1.25rem 1.35rem;
  /* The dialog scrolls as a whole on a short screen, so the results grid is
     never the only thing that can scroll (and never gets 0px tall). */
  max-height: inherit;
  overflow-y: auto;
}

.giphy-dialog-title { margin: 0 0 .75rem; font-size: 1.2rem; }

.giphy-search-field { margin-bottom: .75rem; }
.giphy-search-field input { width: 100%; }
.giphy-search-input { font-size: 1rem; }

/* Search/progress feedback: aria-live="polite" in the markup, so a screen
   reader hears the result count without the focus moving. */
.giphy-status {
  margin: 0 0 .75rem;
  min-height: 1.25rem;
  font-size: .9rem;
  color: var(--muted);
}

/* The results grid. auto-fill keeps it responsive without a breakpoint per
   column count; the cells are square to match the blocks on the board. */
.giphy-results {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(6.5rem, 1fr));
  gap: .5rem;
  margin: 0;
  padding: 0;
  list-style: none;
}
.giphy-result { margin: 0; }

/* A real <button>: keyboard-operable for free, and padded past the 24px
   minimum touch target. */
.giphy-result-button {
  display: block;
  position: relative;
  width: 100%;
  aspect-ratio: 1 / 1;
  padding: 0;
  overflow: hidden;
  border: 2px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--accent-soft);
  cursor: pointer;
}
.giphy-result-button:hover { border-color: var(--accent); }

/* The chosen result is marked with aria-pressed (a11y) AND a visible ring, so
   the selection is not conveyed by colour alone. */
.giphy-result-button[aria-pressed="true"] {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

.giphy-result-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.giphy-more-row { margin: 1rem 0 0; text-align: center; }

/* Inside the dialog the attribution sits with the results it credits. */
.giphy-dialog .giphy-attribution { margin: 1rem 0 0; }

.giphy-dialog-actions {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
  justify-content: flex-end;
  margin-top: 1.25rem;
  padding-top: 1rem;
  border-top: 1px solid var(--border);
}

/* Reduced motion: the same swap the board uses (partials_block.html). The
   animation's bytes are avoided in JS as well; this rule is what makes the
   still win if the preference changes while the dialog is open. */
@media (prefers-reduced-motion: reduce) {
  .giphy-dialog,
  .giphy-result-button,
  .btn {
    transition: none;
  }
}

/* ---------- compose → preview → publish (T13) ----------

   The compose form, the preview page and the duplicate-email page. The block
   itself is styled by T12's rules above; nothing here touches .post-block. */

.compose,
.preview {
  max-width: 40rem;
  margin-inline: auto;
}
.compose .lede,
.preview .lede { margin-inline: 0; }
.compose-body {
  margin-top: 2rem;
  padding: 1.5rem 1.25rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-elev);
  box-shadow: var(--shadow);
}

/* The message textarea: same box as .field input, but taller and resizable
   vertically only (horizontal resizing would break the page's measure). */
.field textarea {
  width: 100%;
  padding: .6rem .7rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg);
  color: var(--text);
  font: inherit;
  line-height: 1.5;
  resize: vertical;
}
.field textarea:focus-visible { border-color: var(--accent); }
.field-invalid textarea { border-color: #b4342c; }
@media (prefers-color-scheme: dark) {
  .field-invalid textarea { border-color: #e08a82; }
}

/* The fieldset-free "label" for the GIF picker: it labels a group of controls
   rather than a single input, so it is a <span> styled like a <label>. */
.field-label {
  display: block;
  margin-bottom: .35rem;
  font-weight: 600;
  font-size: .95rem;
}

/* Live character counter. It is a normal <p>, filled in by app.js; without
   JavaScript it simply states the limit, which is never wrong. */
.char-counter {
  margin: .4rem 0 0;
  font-size: .85rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.char-counter.is-low { color: #9a5b00; }
.char-counter.is-over {
  color: #b4342c;
  font-weight: 600;
}
@media (prefers-color-scheme: dark) {
  .char-counter.is-low { color: #e0b060; }
  .char-counter.is-over { color: #ffb4ac; }
}

/* Preview actions: Edit and Publish side by side, Publish first on a phone. */
.preview-actions {
  display: flex;
  flex-wrap: wrap;
  gap: .75rem;
  align-items: center;
  justify-content: center;
  margin-top: 2rem;
}
.preview-actions .btn { min-width: 9rem; }
.preview-publish { margin: 0; }
.preview-error {
  max-width: 34rem;
  margin: 1rem auto 0;
  text-align: center;
}
.preview-note {
  max-width: 34rem;
  margin: 1.5rem auto 0;
  text-align: center;
}

/* ---------- delete a message (T14, shared with T10) ----------

   The confirmation disclosure and the POST form it wraps. No JavaScript is
   involved: <details> is native, and the server refuses a submission without
   the confirm field. See templates/partials_post_delete.html. */

.btn-danger {
  background: #b4342c;
  color: #fff;
  border-color: #b4342c;
}
.btn-danger:hover {
  background: #93261f;
  border-color: #93261f;
  color: #fff;
}

/* The delete control sits below the page's main card and is deliberately quiet
   until it is opened: a destructive action should not compete with Save. */
.delete-block {
  max-width: 40rem;
  margin: 2rem auto 0;
}
.delete-disclosure {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-elev);
}
.delete-summary {
  padding: .8rem 1rem;
  color: var(--muted);
  font-size: .95rem;
  font-weight: 600;
  cursor: pointer;
}
.delete-summary:hover { color: #b4342c; }
.delete-disclosure[open] .delete-summary {
  border-bottom: 1px solid var(--border);
  color: var(--text);
}
.delete-body {
  padding: 1rem;
}
.delete-warning {
  margin: 0 0 1rem;
  font-size: .95rem;
}
.delete-form { margin: 0 0 .75rem; }

@media (prefers-color-scheme: dark) {
  .btn-danger {
    background: #c4534a;
    border-color: #c4534a;
    color: #16181a;
  }
  .btn-danger:hover {
    background: #d76b62;
    border-color: #d76b62;
    color: #16181a;
  }
  .delete-summary:hover { color: #ffb4ac; }
}

/* ---------- the recipient's card (T16, GET /b/{slug}/view) ----------

   The page the departing colleague actually opens. It reuses the shared hero
   (.hero/.eyebrow/.lede) and the T12 grid verbatim — what is new here is only
   the framing: the count, the slideshow link, and a warmer, quieter tone than
   the contribution landing page, which is asking for something. */

.view-hero .lede strong { color: var(--text); }

/* The signature count, stated just under the subtitle — the one fact beyond the
   messages themselves that belongs on the card. */
.view-count {
  margin: 1.5rem 0 0;
  font-size: 1.05rem;
  font-weight: 600;
  color: var(--text);
}

/* "Play as a slideshow" (T17) sits with the count as a secondary action: a
   ghost button next to a quiet note, so it never competes with the card. */
.view-actions {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem .75rem;
  align-items: center;
  justify-content: center;
  margin: 1.25rem 0 0;
}
.view-empty { margin-inline: auto; }

/* ---------- the slideshow deck (T17, GET /b/{slug}/slideshow) ----------

   The deck is the card, one message at a time, for putting on a screen at the
   leaving do. It is rendered SERVER-SIDE from the same blocks the card uses
   (internal/web/templates/slideshow.html + T12's "post_block"), so nothing
   below re-styles a block: these rules only decide which slide is visible and
   how big it is.

   Two modes, one class apart. Without JavaScript the deck is a plain readable
   list — .deck-stage is an <ol>, every slide visible — and the presentation
   controls ship with the `hidden` attribute so no dead control is ever shown
   (the same rule the Copy button and the GIPHY trigger follow). slideshow.js
   adds .is-active to the [data-slideshow] root, which is what turns the list
   into a full-screen presentation: a fixed overlay covering the site chrome,
   one slide shown at a time.

   Full-screen via position:fixed rather than by emptying base.html's <main>:
   the deck lives inside the shared shell (like every page), and covering the
   viewport means the shell's max-width, header and footer do not have to be
   fought with per page — and nothing about the other pages changes. */

.deck { max-width: 52rem; margin-inline: auto; }

.deck-title {
  margin: 0 0 .5rem;
  font-size: clamp(1.6rem, 1.1rem + 2.2vw, 2.4rem);
  text-align: center;
}
.deck-eyebrow {
  margin: 0 0 1rem;
  text-align: center;
  color: var(--muted);
}
.deck-eyebrow strong { color: var(--text); }
.deck-help {
  margin: 0 auto 1.25rem;
  max-width: 38rem;
  text-align: center;
  font-size: .9rem;
  color: var(--muted);
}

/* The stage: an ordered list of slides. list-style is removed because the
   slides are full blocks, not bullet points; <ol> is used for the semantics. */
.deck-stage {
  margin: 0;
  padding: 0;
  list-style: none;
}
.deck-slide { margin: 0 0 1.75rem; }

/* A slide with nothing in it (a block whose message is empty) would still take
   the square, so the last slide's margin is dropped rather than the slide. */
.deck-slide:last-child { margin-bottom: 0; }

/* The exit link, and (once JS runs) the control bar. Both are centred in list
   mode; presentation mode repositions them (below). */
.deck-top,
.deck-bar {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem .75rem;
  align-items: center;
  justify-content: center;
  margin: 0 0 1rem;
}
.deck-counter {
  margin: 0;
  min-width: 3.5rem;
  text-align: center;
  font-size: .95rem;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
}

/* ---- presentation mode (slideshow.js adds .is-active) ---- */

.deck.is-active {
  position: fixed;
  inset: 0;
  z-index: 60;
  max-width: none;
  margin: 0;
  padding: 1rem 1.25rem 1.25rem;
  /* The slide is sized from the viewport height, so the deck itself rarely needs
     to scroll — and overflow:hidden stops a stray scroll from moving the page
     behind the overlay. A message too long for its square scrolls inside
     .post-block-message, which is T12's own behaviour. On a very short screen
     the deck scrolls as a whole rather than clipping the controls away. */
  overflow-x: hidden;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  background: var(--bg);
  color: var(--text);
}

/* Order within the overlay: heading, help and Exit at the top; the slide takes
   the remaining height; the controls and the GIPHY credit sit under it. */
.deck.is-active .deck-title,
.deck.is-active .deck-eyebrow,
.deck.is-active .deck-help,
.deck.is-active .deck-top { order: 0; flex: 0 0 auto; }
.deck.is-active .deck-title { margin-bottom: .25rem; }
.deck.is-active .deck-eyebrow { margin-bottom: .35rem; }
.deck.is-active .deck-help { margin-bottom: .5rem; }

/* Exit is top-right in presentation mode (it is first in the DOM so Tab reaches
   it first — see the template comment). */
.deck.is-active .deck-top {
  width: 100%;
  justify-content: flex-end;
  margin-bottom: .25rem;
}

.deck.is-active .deck-stage {
  order: 1;
  /* Basis 0, not auto: with `auto` the stage's height depends on the slide, and
     the slide's height depends on the stage, so they size each other in a circle
     and the slide overflowed onto the controls. A zero basis makes the stage
     exactly "whatever is left after the heading and the controls", which is a
     definite height the slide can then measure itself against. */
  flex: 1 1 0;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 0;
  width: 100%;
  /* The slide is sized to fit the space left over after the heading and the
     controls (see .post-block below), so it should never spill over them. This
     is the belt to that braces: if a future change makes the block taller than
     its stage, the controls stay clear instead of being overlapped. */
  overflow: hidden;
}

/* One slide at a time. display:none also takes the hidden slides out of the tab
   order, so a keyboard user never tabs into an off-screen block. */
.deck.is-active .deck-slide { display: none; margin: 0; }
/* The current slide is a flex centring box that FILLS the stage, so a percentage
   height on the block inside it resolves against a definite number rather than
   against its own content (which is what made the block size itself and spill
   over the controls). */
.deck.is-active .deck-slide.is-current {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
  min-height: 0;
  width: 100%;
  /* A size container, so the block inside can size itself with cqw/cqh - see
     the .post-block rule. This is what makes "the largest square that fits"
     expressible without guessing the height of the heading and controls. */
  container-type: size;
  animation: deck-fade .22s ease-out;
}

/* The block is the card's block, scaled up: the square is as large as the space
   allows, and the message gets a taller line budget because there is far more
   room for it than in a four-column grid. Nothing inside the block is restyled
   beyond the type size, so it is visibly the same object.

   Sizing is driven by HEIGHT, not by a magic "chrome allowance". The earlier
   version used `min(88vw, 100vh - Nrem)`, and because N was a guess at the
   height of the heading + help + controls + credit, the real layout drifted:
   the controls ended up drawn ON TOP of the block's author strip. Here the
   square takes 88vw when the viewport is wide, or the stage's own height when
   that is the binding constraint - whichever is smaller - and the width follows
   from aspect-ratio. The stage's height is definite once flex has resolved it
   (flex: 1 1 auto with min-height: 0 above), so the percentage resolves against
   the space genuinely left over, and no number has to be kept in sync.
   The 12rem floor stops a very short viewport from collapsing the square. */
.deck.is-active .post-block {
  /* The largest square that fits the space left over, in one expression.
     .deck-slide.is-current is a size container, so 100cqh is exactly the height
     available to the slide (after the heading, help text, controls and credit
     have taken theirs) and 88cqw is almost all of its width. `min()` picks
     whichever is smaller and `aspect-ratio` makes the other axis follow, so the
     block is always square and always inside its stage.

     This replaces two earlier attempts that guessed the chrome height
     (`100vh - Nrem`), which drifted from the real layout: the first let the
     slide overlap the controls, the second made it grow taller than the stage on
     a short viewport. Container units need no such guess to stay in sync.
     `max(...)` floor: on a very short viewport the square shrinks with the space
     rather than collapsing to nothing. */
  width: min(88cqw, 100cqh, 40rem);
  max-height: 100cqh;
  aspect-ratio: 1 / 1;
  margin-inline: auto;
  --post-lines: 9;
}



.deck.is-active .post-block-message {
  font-size: 1.05rem;
  line-height: 1.5rem;
  max-height: calc(var(--post-lines) * 1.5rem + 1.4rem);
  padding: .9rem 1.1rem;
}
.deck.is-active .post-block-author { font-size: .95rem; padding: .6rem 1.1rem; }

.deck.is-active .deck-bar { order: 2; margin: .75rem 0 0; }
.deck.is-active .giphy-attribution { order: 3; margin: .5rem 0 0; }

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

/* Reduced motion: no fade between slides. The auto-advance is switched off by
   slideshow.js (a media query cannot stop a timer); this rule is the CSS half,
   and the GIF/still swap is T12's own rule above. */
@media (prefers-reduced-motion: reduce) {
  .deck.is-active .deck-slide.is-current { animation: none; }
}

/* A narrow screen: the block can no longer fit both axes comfortably, so give it
   a little more of the width. The height/width fit above already handles Short
   screens (which is what the old fixed-formula override here was for, and why it
   is gone: it re-introduced the very guess that let the slide overlap the
   controls). */

/* While the deck is open it covers the viewport, so the page behind it must not
   scroll either (an arrow key or a stray wheel would otherwise move a page the
   user cannot see). slideshow.js adds the class to <html>. */
html.deck-open,
html.deck-open body { overflow: hidden; }

/* The deck's chrome ships with the `hidden` attribute and is revealed by
   slideshow.js. An author `display` rule beats the UA sheet's `[hidden] { display:
   none }` regardless of specificity, so each element that has its own display
   value needs this explicit rule or it would be visible before the script runs
   (and to a browser without JavaScript at all). Scoped to the deck's own
   elements so nothing else in the app is affected. */
.deck-bar[hidden],
.deck-pause[hidden],
[data-deck-autoplay-note][hidden] { display: none; }

.deck-empty { margin-inline: auto; }

/* The Exit link. It is a plain link (it navigates), styled as a ghost button;
   the rule exists so the class is not decoration. In presentation mode it stays
   in the top-right corner and never shrinks; on a narrow screen it wraps like
   everything else in the bar. */
.deck-exit { white-space: nowrap; }

/* The Previous/Next controls. They carry their own classes so a host can style
   them individually (and so the markup self-documents), even though today they
   share the ghost-button look. */
.deck-prev,
.deck-next { white-space: nowrap; }

/* ---------- privacy page (T20, GET /privacy) ----------

   A reading page: one column of prose cards, the same card idiom the dashboard
   uses (.share-block / .admin-card), narrower because it is text rather than a
   table plus forms. Nothing here is interactive, so it carries no form styles. */

.privacy { max-width: 46rem; margin-inline: auto; }
.privacy .lede { margin-inline: 0; }

.privacy-card {
  margin-top: 2rem;
  padding: 1.35rem 1.35rem 1.5rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-elev);
  box-shadow: var(--shadow);
}
.privacy-card h2 {
  margin: 0 0 .75rem;
  font-size: 1.1rem;
  /* The section headings are the landmarks a reader scans for, so they are
     slightly stronger than the muted body copy around them. */
  color: var(--text);
}
.privacy-card p { margin: 0 0 .85rem; }
.privacy-card p:last-child { margin-bottom: 0; }
.privacy-card strong { color: var(--text); }

.privacy-list { margin: 0 0 .9rem; padding-left: 1.15rem; }
.privacy-list li { margin-bottom: .5rem; }
.privacy-list li:last-child { margin-bottom: 0; }

/* The one sentence a reader must not miss: addresses never appear publicly. It
   gets the accent treatment rather than another card so it reads as part of the
   section it belongs to. */
.privacy-emph {
  margin: 0;
  padding: .75rem 1rem;
  border: 1px solid var(--accent);
  border-left-width: 4px;
  border-radius: var(--radius-sm);
  background: var(--accent-soft);
}

.privacy-note { color: var(--muted); font-size: .95rem; }

/* The who-sees-what list reuses .summary-list's two-column grid; it is laid out
   as a single column on a phone (see the narrow-screen rule below). */
.privacy-who { margin-bottom: 0; }

/* ---------- organiser dashboard danger zone (T20, POST /b/{slug}/delete) ----------

   The delete-board control. It is deliberately the loudest thing on a page that
   is otherwise quiet: a red rule down the side and a red-outlined card, so it is
   impossible to reach by accident while scrolling, and it is rendered last. */

.admin-danger { border-color: #d9a9a4; }
@media (prefers-color-scheme: dark) {
  .admin-danger { border-color: #6d3b38; }
}

/* The slug the organiser has to type. Monospace and wrap-capable: it is a
   24-character base64url token, so it must be copyable in full on any width. */
.board-delete-slug {
  display: inline-block;
  padding: .1rem .35rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: .9rem;
  word-break: break-all;
  color: var(--text);
}
.board-delete-form .field { margin-top: 1rem; }
/* The typed code is a token, not prose: monospace so letter shapes are distinct
   and a trailing space is visible, and never autocorrected or autocapitalised
   by a phone keyboard (the input carries the corresponding attributes). */
.board-delete-form input {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

/* On a narrow screen the two-column definition list becomes one column, so a
   long address or a long sentence does not end up in a sliver. */
@media (max-width: 34rem) {
  .privacy-who { grid-template-columns: 1fr; gap: .15rem; }
  .privacy-who dd { margin-bottom: .85rem; }
  .privacy-who dd:last-child { margin-bottom: 0; }
}
