/* Mispache — structural styles. Colors are CSS custom properties defined in
   themes.css (8 seasonal day/night palettes) and optionally overridden
   per-user via an inline style attribute on <html>. */
* { box-sizing: border-box; }
/* The `hidden` attribute must always win — otherwise display rules on the
   same element (e.g. .btn's display:inline-block) would show hidden things. */
[hidden] { display: none !important; }

/* Harabara — the display face behind the logo (titles / large text). */
@font-face {
  font-family: "Harabara";
  src: url("fonts/Harabara.ttf") format("truetype");
  font-display: swap;
}
h1, h2, .hero-title { font-family: "Harabara", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }

/* The footer sits at the foot of the window on every page, however short the
   content is: the body is a column and <main> takes up whatever height is left
   over, so its background panel reaches the footer instead of leaving a gap of
   bare page above it. `dvh` after `vh` so a mobile browser's disappearing
   toolbar cannot leave the footer floating mid-screen. */
body {
  margin: 0;
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  color: var(--text);
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  line-height: 1.55;
}

a { color: var(--accent-2); text-decoration: none; }
a:hover { text-decoration: underline; }

/* width: 100% is load-bearing. <main> is a flex item of <body> (see below), and
   a flex item with `margin: 0 auto` is *not* stretched -- it is sized to fit its
   content and then centred. Without this, a page whose content is narrow (an
   empty admin queue, a profile) collapses to a sliver in the middle of the
   screen instead of filling the width the topbar and footer already use. */
.container { width: 100%; max-width: 960px; margin: 0 auto; padding: 0 16px; }
/* flex: 1 1 auto — grow into whatever height the body has left, so the page's
   background panel runs all the way down to the footer (see `body` above). */
.main { flex: 1 1 auto; padding: 24px 16px 64px; background: var(--bg-soft); box-shadow: 0 0 500px 50px var(--border); }

/* ---- top bar ---- */
.topbar {
  background: var(--bg-soft);
  border-bottom: 1px solid var(--border);
  position: sticky;
  top: 0;
  z-index: 10;
}
.topbar-inner { display: flex; align-items: center; justify-content: space-between; gap: 16px; height: 56px; }
/* Logo — square tile, rounded top-left + bottom-right, palette-aware glyph.
   Filled with the accent, like .btn-primary: it is a link, and it should read
   as one. The glyph is --card rather than the white .btn-primary uses, because
   four of the eight palettes have a bright accent at night (spring-night is
   #63d98b), where white on the accent is nearly invisible -- 1.8:1 against
   --card's 8.5:1. Every theme is above 3:1 this way. */
.logo {
  display: inline-flex; align-items: center; justify-content: center;
  width: 36px; height: 36px; flex-shrink: 0;
  background: var(--accent);
  border: 1px solid var(--accent);
  border-radius: 11px 0 11px 0;
  box-shadow: 0 1px 5px rgba(0, 0, 0, 0.22);
  color: var(--card);
  text-decoration: none;
}
.logo:hover { text-decoration: none; filter: brightness(1.15); }
.logo-mark {
  display: block; width: 24px; height: 24px;
  background: currentColor;
  -webkit-mask: url("logo.svg") center / contain no-repeat;
  mask: url("logo.svg") center / contain no-repeat;
}
.logo-lg { display: flex; width: 76px; height: 76px; border-radius: 22px 0 22px 0; margin: 0 auto 20px; }
.logo-lg .logo-mark { width: 52px; height: 52px; }
.topnav { display: flex; align-items: center; gap: 14px; font-size: 0.92rem; flex-wrap: wrap; }
.nav-user { color: var(--accent); font-weight: 600; }
.nav-badge {
  background: var(--accent); color: #fff; border-radius: 999px;
  font-size: 0.68rem; padding: 1px 6px; margin-left: 3px; vertical-align: top;
}
.topbar-left { display: flex; align-items: center; gap: 10px; }
.topbar-right { display: flex; align-items: center; gap: 10px; }
.icon-btn {
  background: transparent; border: none; color: var(--text); cursor: pointer;
  font-size: 1.25rem; line-height: 1; padding: 6px 8px; border-radius: 6px;
}
.icon-btn:hover { background: var(--card-2); }
.menu-wrap { position: relative; }
.menu {
  position: absolute; top: calc(100% + 6px); right: 0; min-width: 200px;
  background: var(--card); border: 1px solid var(--border); border-radius: 10px;
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.28); padding: 6px; z-index: 50;
  display: flex; flex-direction: column; gap: 2px;
}
.menu[hidden] { display: none; }
.menu-left { left: 0; right: auto; }
/* The top-right context menu opens inward, or it runs off the screen. */
.menu-right { left: auto; right: 0; }
.menu-item {
  display: block; width: 100%; padding: 8px 10px; border-radius: 6px;
  color: var(--text); text-decoration: none; font-size: 0.92rem; text-align: left;
  background: transparent; border: none;
}
.menu-item:hover { background: var(--card-2); text-decoration: none; }
.menu-item-btn { cursor: pointer; font: inherit; }
.menu-item-muted { color: var(--muted); }
.page-context { display: flex; align-items: center; gap: 8px; }
.handle { color: var(--text); font-weight: 600; }

/* ---- cards & layout ---- */
.card {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 20px;
  margin-bottom: 20px;
}
.card-head { border-bottom: 1px solid var(--border); margin-bottom: 14px; padding-bottom: 10px; }
.card-head h2 { margin: 0; font-size: 1.15rem; }
/* Dashboard layout: a centered two-column feed area (main + side rail),
   flanked on wide screens by two fixed, transparent floating rails that hug
   the left/right edges of the viewport. */
.two-col { display: grid; grid-template-columns: minmax(0, 1fr) 300px; gap: 20px; align-items: start; }
.widget-col { min-width: 0; }
.col-side .card { margin-bottom: 20px; }

/* Floating rails: fixed to the screen edges, transparent (no panel behind
   them), wide, and independently scrollable. Their width is exactly the gap
   between the viewport edge and the 960px centered content. */
.float-rail {
  position: fixed;
  top: 80px;
  bottom: 24px;
  width: calc((100vw - 960px) / 2);
  overflow-y: auto;
  z-index: 1;
  background: transparent;
  padding: 1em;
}
.rail-left { left: 0; }
.rail-right { right: 0; }
.float-rail .card { margin-bottom: 16px; }

/* On narrower screens there's no room to float beside the feed, so the rails
   fold away (a desktop-only feature). */
@media (max-width: 1279px) {
  .float-rail { display: none; }
}
@media (max-width: 760px) {
  .two-col { grid-template-columns: 1fr; }
}

/* ---- hero / landing ---- */
.hero { text-align: center; padding: 48px 0 32px; }
.hero-title { font-size: 2.4rem; margin: 0 0 10px; background: linear-gradient(90deg, var(--accent), var(--accent-2)); -webkit-background-clip: text; background-clip: text; color: transparent; }
.hero-sub { color: var(--muted); font-size: 1.1rem; max-width: 560px; margin: 0 auto 22px; }
.hero-actions { display: flex; gap: 12px; justify-content: center; }
.feature-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 20px; }

/* The three promises under the hero, one to a line, divided like a list in a
   printed directory rather than dressed up as a sales pitch. */
.hero-points {
  list-style: none; margin: 0 auto 28px; padding: 0; max-width: 480px;
  font-size: 1rem; line-height: 1.5;
}
.hero-points li { padding: 8px 0; border-top: 1px solid var(--border); }
.hero-points li:first-child { border-top: 0; }

/* The three feature cards: same size, same weight, no iconography. */
.feature h3 { margin: 0 0 8px; font-size: 1.05rem; }
.feature p { margin: 0; font-size: 0.95rem; line-height: 1.6; }

/* The theme demonstration at the foot of the landing page. */
.theme-demo { display: flex; align-items: center; justify-content: space-between; gap: 18px; flex-wrap: wrap; }
.theme-demo-title { margin: 0 0 4px; font-size: 1.05rem; }
.theme-demo-text p { margin: 0; max-width: 520px; }
.theme-switch {
  display: inline-flex; align-items: center; gap: 10px; flex-shrink: 0;
  background: var(--card-2); border: 1px solid var(--border); color: var(--text);
  border-radius: 999px; padding: 8px 16px; font: inherit; font-size: 0.9rem;
  font-weight: 600; cursor: pointer;
}
.theme-switch:hover { border-color: var(--accent); }
.theme-switch-swatches { display: inline-flex; }
.theme-switch-swatch {
  width: 14px; height: 14px; border-radius: 50%;
  background: var(--accent); border: 1px solid var(--border);
}
.theme-switch-swatch-2 { background: var(--accent-2); margin-left: -5px; }

/* ---- Content Exposure Level onboarding + settings ---- */
.cel-scale { list-style: none; margin: 0 0 16px; padding: 0; }
.cel-scale li { padding: 10px 0; border-top: 1px solid var(--border); }
.cel-scale li:first-child { border-top: 0; }
.cel-scale p { margin: 4px 0 0; }
/* The baseline slider (Settings, onboarding). Named `cel-slider`, not
   `cel-picker`: the composer has a `cel-picker` too, and a shared name meant
   this block's margins were applied to that one's pill. */
.cel-slider { margin: 8px 0 20px; }
.cel-slider-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px; }
.cel-slider-value { font-size: 1.4rem; font-weight: 700; color: var(--accent); min-width: 2ch; text-align: center; }
.cel-slider input[type="range"] { width: 100%; }
.cel-slider-desc { margin: 8px 0 0; min-height: 1.2em; }
.cel-examples { margin-top: 12px; }
.cel-examples summary { cursor: pointer; color: var(--muted); font-size: 0.85rem; }
.cel-examples .cel-scale { margin: 8px 0 0; }
/* The full scale on the explainer page (core/cel.html). */
.cel-table { width: 100%; border-collapse: collapse; margin: 0 0 18px; font-size: 0.9rem; }
.cel-table th, .cel-table td { text-align: left; vertical-align: top; padding: 8px 10px 8px 0; border-top: 1px solid var(--border); }
.cel-table thead th { color: var(--muted); font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.03em; border-top: 0; }
.cel-table td:first-child { white-space: nowrap; }

/* ---- buttons ---- */
.btn {
  display: inline-block; border: 1px solid var(--border); background: var(--card-2);
  color: var(--text); padding: 8px 16px; border-radius: 8px; cursor: pointer;
  font-size: 0.9rem; font-weight: 600; text-decoration: none;
}
.btn:hover { text-decoration: none; filter: brightness(1.15); }
.btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; }
/* The "go ahead, delete it" button, in the same red the error toasts use. Only
   ever worn by a control that destroys something (static/dialog.js sets it from
   its `danger` option). */
.btn-danger { background: var(--danger); border-color: var(--danger); color: #fff; }
.btn-ghost { background: transparent; }
.btn-sm { padding: 5px 12px; font-size: 0.82rem; }
.btn-block { width: 100%; }
.inline-form { display: inline; }
.link-btn {
  background: none; border: none; color: var(--muted); cursor: pointer;
  font-size: 0.82rem; padding: 0; text-decoration: underline;
}
.link-btn:hover { color: var(--danger); }

/* ---- toast notifications ---- */
.toasts {
  position: fixed; z-index: 1000;
  display: flex; flex-direction: column; gap: 10px;
  pointer-events: none;
}
.toasts-top-left { top: 64px; left: 16px; align-items: flex-start; }
.toasts-top-right { top: 64px; right: 16px; align-items: flex-end; }
.toasts-bottom-left { bottom: 16px; left: 16px; align-items: flex-start; }
.toasts-bottom-right { bottom: 16px; right: 16px; align-items: flex-end; }
.toast {
  pointer-events: auto;
  display: flex; align-items: center; gap: 10px;
  padding: 10px 14px;
  border-radius: 14px 0 14px 0; /* rounded TL + BR, pointy TR + BL — the logo corner language */
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.24);
  font-size: 0.9rem; font-weight: 600;
  max-width: 360px;
  opacity: 1; transition: opacity 0.3s ease, transform 0.3s ease;
}
.toast--leaving { opacity: 0; transform: translateY(-6px); }
.toast-success {
  background: #e3f6e8; color: #14522b;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.24),
              inset 0 0 0 1px rgba(20, 82, 43, 0.35),
              inset 0 3px 8px rgba(20, 82, 43, 0.20);
}
.toast-error {
  background: #fdecea; color: #7f1d14;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.24),
              inset 0 0 0 1px rgba(127, 29, 20, 0.35),
              inset 0 3px 8px rgba(127, 29, 20, 0.20);
}
.toast-info, .toast-message {
  background: #e6f2fb; color: #14466e;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.24),
              inset 0 0 0 1px rgba(20, 70, 110, 0.35),
              inset 0 3px 8px rgba(20, 70, 110, 0.20);
}
.toast-close {
  background: none; border: none; color: inherit; cursor: pointer;
  font-size: 1rem; line-height: 1; padding: 2px 4px; opacity: 0.85; flex-shrink: 0;
}
.toast-close:hover { opacity: 1; }

/* ---- forms ---- */
/* A field that is wrong, right where the field is. Currently the file inputs:
   see static/uploads.js. */
.field-error { display: block; color: var(--danger); font-size: 0.82rem; margin-top: 2px; }
input[type="file"].is-too-big { color: var(--danger); }

.stack-form { display: flex; flex-direction: column; gap: 14px; }
.stack-form label { display: flex; flex-direction: column; gap: 6px; font-weight: 600; font-size: 0.9rem; }
.stack-form input, .stack-form textarea {
  background: var(--bg); color: var(--text); border: 1px solid var(--border);
  border-radius: 8px; padding: 10px; font: inherit; font-weight: 400;
}
.stack-form input:focus, .stack-form textarea:focus { outline: 2px solid var(--accent); border-color: transparent; }
.stack-form select { background: var(--bg); color: var(--text); border: 1px solid var(--border); border-radius: 8px; padding: 8px 10px; font: inherit; }
/* A format hint that belongs to the field above it: pulled up out of the form's
   14px gap so it reads as part of that field, not as a break before the next. */
.stack-form .field-hint { margin: -10px 0 0; font-weight: 400; }
.notif-settings-row { display: flex; flex-wrap: wrap; gap: 16px; align-items: flex-end; }
.notif-setting select { min-width: 170px; }
.notif-setting input[type="number"] { width: 120px; }
.reset-row { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 10px; }
.form-actions { display: flex; gap: 10px; }
.draft-banner { display: flex; justify-content: space-between; align-items: center; gap: 10px; flex-wrap: wrap; background: rgba(94, 234, 213, 0.08); border: 1px solid rgba(94, 234, 213, 0.3); border-radius: 8px; padding: 10px 14px; font-size: 0.88rem; margin-bottom: 16px; }
.draft-banner[hidden] { display: none; }
.auth-card { max-width: 420px; margin: 40px auto; }
/* A form page with substance to it -- a group's name, description, picture and
   interests. Wider than a login box, still a comfortable measure rather than a
   full-width field. */
.form-card { max-width: 640px; margin: 40px auto; }

/* ---- profile ---- */
.profile-header { display: flex; gap: 18px; align-items: flex-start; }
.profile-meta h1 { margin: 0 0 6px; font-size: 1.5rem; }
.handle { color: var(--muted); font-size: 1rem; font-weight: 500; }
.bio { margin: 6px 0; }
.friend-actions { display: flex; gap: 8px; align-items: center; margin-top: 12px; flex-wrap: wrap; }
.avatar {
  display: inline-flex; align-items: center; justify-content: center;
  width: 40px; height: 40px; border-radius: 50%;
  background: linear-gradient(135deg, var(--accent), var(--accent-2));
  color: #10101a; font-weight: 800;
}
.avatar-lg { width: 72px; height: 72px; font-size: 1.8rem; flex-shrink: 0; }

/* ---- the sandboxed custom page (fills the viewport) ---- */
/* flex: 0 0 auto — the full-page profile is already viewport-sized; it must
   not be stretched or squashed by the sticky-footer rule in `body`. */
.profile-page .main { flex: 0 0 auto; max-width: 100%; padding: 0; display: flex; flex-direction: column; height: calc(100vh - 56px); background: transparent; box-shadow: none; }
.profile-page .footer { display: none; }
.profile { display: flex; flex-direction: column; height: 100%; min-height: 0; }
.profile-action-bar {
  flex: 0 0 auto; display: flex; align-items: center; justify-content: space-between;
  gap: 12px; padding: 8px 16px; background: var(--bg-soft);
  border-bottom: 1px solid var(--border);
}
.profile-action-identity { display: flex; align-items: center; gap: 10px; min-width: 0; }
.profile-action-identity .handle { color: var(--text); font-weight: 600; }
.profile-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.profile-frame {
  flex: 1 1 auto; min-height: 0; width: 100%; border: 0; background: #fff; display: block;
}
.avatar-sm { width: 32px; height: 32px; font-size: 0.9rem; }
.avatar-img { display: block; border-radius: 50%; object-fit: cover; width: 40px; height: 40px; }
.avatar-img.avatar-sm { width: 32px; height: 32px; }
.avatar-img.avatar-lg { width: 72px; height: 72px; }
.avatar-settings { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; padding-bottom: 18px; border-bottom: 1px solid var(--border); margin-bottom: 18px; }
.avatar-settings .avatar-img, .avatar-settings .avatar { border-radius: 20px 0 20px 0; }
.page-context .avatar-img, .page-context .avatar { border-radius: 12px 0 12px 0; }
/* The group a wall post came from, on the post's own page: the picture links
   back to the room, and the sentence beside it names it. The link must not
   underline on hover the way a text link does, because it is a picture. */
.post-context-group { display: inline-flex; align-items: center; }
.post-context-group:hover { text-decoration: none; filter: brightness(1.1); }
.avatar-actions { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.avatar-actions .inline-form { display: flex; align-items: center; gap: 8px; }
.avatar-actions input[type="file"] { color: var(--muted); font-size: 0.85rem; max-width: 240px; }

/* ---- settings: theme picker & palette ---- */
.theme-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 8px; }
.theme-option { display: flex; align-items: center; gap: 8px; border: 1px solid var(--border); border-radius: 8px; padding: 8px 10px; cursor: pointer; background: var(--card); }
.theme-option input { width: auto; }
.swatches { display: inline-flex; gap: 3px; flex-shrink: 0; }
.swatch { width: 12px; height: 12px; border-radius: 50%; display: inline-block; flex-shrink: 0; border: 1px solid rgba(0, 0, 0, 0.15); }
.palette-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); gap: 8px; }
.palette-field { position: relative; display: flex; flex-direction: column; gap: 4px; font-size: 0.8rem; font-weight: 600; }
/* Only the field itself: the control the picker builds nests its own inputs
   inside this label, and they should not inherit the field's box. */
.palette-field > input { width: 100%; padding: 6px 8px; background: var(--bg); color: var(--text); border: 1px solid var(--border); border-radius: 6px; font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; }

/* ---- color picker (MispacheColorPicker, see static/colorpicker.js) ------- */
/* The native input stays in the DOM — it is the field that posts — but once
   the custom control exists it is the hidden one. Not `display: none`, which
   would take it out of the form. */
.palette-field > input.cp-native {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.cp { position: relative; display: block; }
.cp-swatch {
  display: flex; align-items: center; gap: 8px; width: 100%;
  padding: 4px 6px; font: inherit; font-weight: 600; cursor: pointer;
  background: var(--bg); color: var(--text);
  border: 1px solid var(--border); border-radius: 6px;
}
.cp-swatch:hover { border-color: var(--accent); }
.cp-swatch:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.cp-chip { width: 22px; height: 22px; flex: 0 0 auto; border-radius: 5px; border: 1px solid var(--border); }
.cp-hexlabel { font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; font-size: 0.78rem; }
.cp-pop {
  position: absolute; top: calc(100% + 6px); left: 0; z-index: 40;
  display: flex; flex-direction: column; gap: 10px; width: 232px; padding: 10px;
  background: var(--card); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: 0 14px 34px rgba(0, 0, 0, 0.28);
}
.cp-pop[hidden] { display: none; }
.cp-area { position: relative; height: 130px; border-radius: 8px; cursor: crosshair; touch-action: none; }
.cp-area:focus-visible, .cp-hue:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.cp-area-layer {
  position: absolute; inset: 0; border-radius: 8px;
  background:
    linear-gradient(to top, #000, rgba(0, 0, 0, 0)),
    linear-gradient(to right, #fff, hsl(var(--cp-hue, 0), 100%, 50%));
}
.cp-cursor {
  position: absolute; width: 14px; height: 14px; margin: -7px 0 0 -7px;
  border-radius: 50%; border: 2px solid #fff; pointer-events: none;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.55);
}
.cp-hue {
  position: relative; height: 14px; border-radius: 999px; cursor: ew-resize; touch-action: none;
  background: linear-gradient(to right, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00);
}
.cp-hue-cursor {
  position: absolute; top: -3px; width: 6px; height: 18px; margin-left: -3px;
  border-radius: 3px; border: 2px solid #fff; pointer-events: none;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.55);
}
.cp-row { display: flex; align-items: center; gap: 8px; }
.cp-hex {
  flex: 1 1 auto; width: 100%; padding: 5px 7px;
  background: var(--bg); color: var(--text); border: 1px solid var(--border); border-radius: 6px;
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; font-size: 0.78rem;
}
.cp-done { flex: 0 0 auto; }
fieldset { border: 1px solid var(--border); border-radius: var(--radius); padding: 14px; margin: 0; }
legend { font-weight: 700; padding: 0 6px; }

/* ---- posts ---- */
.post-form textarea { width: 100%; background: var(--bg); color: var(--text); border: 1px solid var(--border); border-radius: 8px; padding: 10px; font: inherit; resize: vertical; }
/* The composer's footer stays visible. The article modal is taller than most
   windows, so without this the Post button would be the one thing you had to
   scroll to find. (Sticky in a box that does not scroll is just where it always
   was, so the bulletin and image modals are unaffected.) */
.post-form-foot {
  --foot-control-height: 36px;
  display: flex; justify-content: space-between; align-items: center; gap: 12px;
  position: sticky; bottom: 0; z-index: 2; margin-top: 10px;
  padding: 10px 0 16px; background: var(--card); border-top: 1px solid var(--border);
}
/* The privacy chip, the level pill and the button are one row of one height:
   three controls at three heights along the bottom read as a mistake. */
.vis-picker-trigger, .cel-picker-trigger {
  box-sizing: border-box; height: var(--foot-control-height);
}
.post-form-actions .btn {
  box-sizing: border-box; height: var(--foot-control-height);
  display: inline-flex; align-items: center;
}
/* The post's own settings, at the end, after the writing: who can see it on the
   left, the level and the button on the right. */
.post-form-actions {
  display: flex; align-items: center; gap: 8px; margin-left: auto;
  height: var(--foot-control-height);
}
/* Both pickers are a <details> around a <summary>: the details is made a flex
   box too, or its inline layout leaves baseline space under the pill and the
   three controls in the footer end up at three heights. */
.vis-picker, .cel-picker { display: inline-flex; align-items: center; }
.vis-picker-trigger {
  display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
  list-style: none; padding: 6px 10px; border: 1px solid var(--border);
  border-radius: 999px; background: var(--card-2); color: var(--muted);
  font-size: 0.82rem;
}
.vis-picker-trigger::-webkit-details-marker { display: none; }
.vis-picker-trigger:hover { border-color: var(--accent); }
.vis-picker-label { font-weight: 600; color: var(--text); }
.vis-picker-panel {
  position: fixed; z-index: 200; left: 50%; top: 12vh; transform: translateX(-50%);
  width: min(380px, calc(100vw - 24px)); max-height: min(74vh, 520px); overflow: auto;
  padding: 12px; background: var(--card); border: 1px solid var(--border);
  border-radius: 12px; box-shadow: 0 18px 50px rgba(0, 0, 0, 0.35);
}
.vis-picker-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; margin-bottom: 8px; }
.vis-picker-link { font-size: 0.82rem; }
.vis-picker-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.vis-choice { display: flex; align-items: flex-start; gap: 9px; padding: 7px 8px; border-radius: 8px; cursor: pointer; }
.vis-choice:hover { background: var(--bg); }
.vis-choice:has(input:checked) { background: var(--card-2); box-shadow: inset 0 0 0 1px var(--accent); }
.vis-choice input { flex: 0 0 auto; margin: 3px 0 0; accent-color: var(--accent); }
.vis-choice-text { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.vis-choice-name { font-weight: 600; color: var(--text); }
.vis-choice:has(input:checked) .vis-choice-name { color: var(--accent); }
.vis-picker-note { margin: 10px 0 0; padding-top: 9px; border-top: 1px solid var(--border); }
/* The scrolling body ends where the footer does: a scroll container's content
   is visible in its own bottom padding, so a strip of padding below a sticky
   footer is a strip where scrolled content peeks out under the Post button. */
.post-form.modal-body { padding-bottom: 0; }
.post-list { list-style: none; margin: 0; padding: 0; }
.post { padding: 12px 0; border-bottom: 1px solid var(--border); }
.post:last-child { border-bottom: none; }
/* pre-line, not normal: a bulletin and a picture's description both arrive with
   their newlines turned into <br> (helpers.nl2br), and `normal` collapsed the
   breaks around each one into a single space, so a paragraph somebody typed
   came out as one long line. pre-line keeps the breaks and still collapses runs
   of spaces. A markdown article is its own thing: it carries real <p> tags. */
.post-body { margin: 0 0 6px; white-space: pre-line; overflow-wrap: anywhere; }
.empty { color: var(--muted); }
.muted { color: var(--muted); }
.small { font-size: 0.82rem; }
.head-row { display: flex; justify-content: space-between; align-items: center; gap: 10px; }
.content-list { list-style: none; margin: 0; padding: 0; }
.content-row { display: flex; justify-content: space-between; align-items: center; gap: 14px; padding: 12px 0; border-bottom: 1px solid var(--border); flex-wrap: wrap; }
.content-row:last-child { border-bottom: none; }
.content-main { flex: 1; min-width: 0; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.content-excerpt { color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
.content-thumb { width: 48px; height: 48px; object-fit: cover; border-radius: 6px; flex-shrink: 0; }
.content-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.vis-select { background: var(--bg); color: var(--text); border: 1px solid var(--border); border-radius: 6px; padding: 4px 6px; font-size: 0.8rem; }
/* The composers' shared field block (see static/composer.js): a labelled
   control, one per line, in a row that wraps. */
/* A labelled field: the image post's caption and "tag friends". No flex-basis:
   inside a column, a flex-basis is a *height*.
   (The visibility and CEL controls used to live in a row of their own here;
   they are in the footer now, and the privacy picker in _visibility_picker.html
   keeps its own styles.) */
.compose-field { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.compose-field-label { font-size: 0.78rem; color: var(--muted); }
.compose-field select, .compose-field input[type="text"] {
  width: 100%; background: var(--bg); color: var(--text);
  border: 1px solid var(--border); border-radius: 8px; padding: 8px 10px; font: inherit;
}
.compose-text { margin-bottom: 10px; }
.compose-text textarea {
  width: 100%; background: var(--bg); color: var(--text); border: 1px solid var(--border);
  border-radius: 8px; padding: 10px; font: inherit; resize: vertical;
}
.compose-image { display: flex; flex-direction: column; gap: 10px; margin-bottom: 10px; }
/* "1,240 of 2,000 characters", under the field it is about. */
.compose-budget {
  display: block; margin-top: 4px; min-height: 1.15em;
  font-variant-numeric: tabular-nums;
}
.compose-budget.is-near { color: var(--text); }
.compose-budget.is-over { color: var(--danger); font-weight: 600; }
/* "Resumed last session": a pill in the composer's head, to the left of the
   close button, where it takes no room from the writing and moves nothing when
   it appears. Its title says how old the draft is. */
.draft-note {
  display: inline-flex; align-items: center; flex: 0 0 auto; height: 22px;
  margin-left: auto;   /* against the close button, not in the middle */
  padding: 0 10px; border-radius: 999px; white-space: nowrap;
  font-size: 0.76rem; font-weight: 600; color: var(--text);
  background: color-mix(in srgb, var(--accent-2) 22%, transparent);
}
.draft-note[hidden] { display: none; }

/* Changing type resizes the dialog, which used to happen in one jump. The
   composer measures before and after and animates between the two boxes (see
   static/composer.js); this is the transition it borrows. The body stops
   scrolling for the half second it lasts, so no scrollbar flickers past. */
.modal.is-resizing { transition: width 500ms ease, height 500ms ease; }
.modal.is-resizing .post-form > .modal-body { overflow: hidden; }
@media (prefers-reduced-motion: reduce) {
  .modal.is-resizing { transition: none; }
}

/* ---- pill tag picker (reusable MispacheTagPicker) ---- */
.post-form > .tag-picker { margin-bottom: 10px; }
.tag-picker { display: flex; flex-direction: column; gap: 6px; }
.tag-picker-pills {
  display: flex; flex-wrap: wrap; gap: 4px;
  /* A wrapped flex container stretches its *lines* to fill the container by
     default, and `align-items: stretch` then makes each pill fill its line: a
     lone pill in the composer's reserved two rows came out as tall as the
     reservation. Both are pinned to the start so a pill is always a pill. */
  align-items: flex-start; align-content: flex-start;
}
.tag-picker-pill {
  display: inline-flex; align-items: center; gap: 2px;
  background: var(--card-2); border: 1px solid var(--border);
  border-radius: 999px; padding: 1px 4px 1px 10px; font-size: 0.85rem;
  /* A whole number of pixels, whatever the font rounds its line box to. */
  min-height: 22px;
}
.tag-picker-x {
  background: transparent; border: none; cursor: pointer; color: var(--muted);
  font-size: 1.05rem; line-height: 1; padding: 0 4px; border-radius: 50%;
}
.tag-picker-x:hover { color: var(--text); }
/* The inferred (ghost) pill: the same pill, wearing ＋ instead of × and a
   dotted outline, so it reads as "not yours yet". It sits in the same list as
   the chosen tags; the container reserves two rows of height so one appearing
   under what somebody is writing cannot shove the rest of the form around. */
.tag-picker--reserved .tag-picker-pills { min-height: 2.9rem; }
.tag-picker-pill.is-suggested {
  background: transparent; border-style: dashed; border-color: var(--muted); color: var(--muted);
}
.tag-picker-pill.is-suggested:hover { border-color: var(--accent); color: var(--accent); }
.tag-picker-plus { color: var(--accent); font-size: 0.95rem; }
.tag-picker-plus:hover { color: var(--accent-2); }
.tag-picker-row { position: relative; display: flex; align-items: center; gap: 6px; }
.tag-picker-row .tag-picker-input {
  flex: 1; min-width: 0; background: var(--bg); color: var(--text);
  border: 1px solid var(--border); border-radius: 8px; padding: 7px 10px; font: inherit;
}
.tag-picker-row .tag-picker-input:focus { outline: 2px solid var(--accent); border-color: transparent; }
.tag-picker-add {
  flex-shrink: 0; width: 34px; height: 34px; display: inline-flex; align-items: center;
  justify-content: center; border: none; border-radius: 8px;
  background: var(--accent); color: #fff; cursor: pointer; font-size: 1rem; line-height: 1;
}
.tag-picker-add:hover { filter: brightness(1.15); }
.tag-picker-suggest {
  position: absolute; top: calc(100% + 4px); left: 0; right: 0; z-index: 40;
  list-style: none; margin: 0; padding: 4px; max-height: 200px; overflow-y: auto;
  background: var(--card); border: 1px solid var(--border); border-radius: 10px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
}
.tag-picker-option { padding: 7px 10px; border-radius: 6px; cursor: pointer; font-size: 0.9rem; }
.tag-picker-option:hover, .tag-picker-option.is-highlighted { background: var(--card-2); }
.post-head { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; }
.post-head-text { flex: 1; min-width: 0; }
.post-author { font-weight: 600; color: var(--text); }
.post-head-meta { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.post-date { color: var(--muted); font-size: 0.82rem; text-decoration: none; }
.post-date:hover { text-decoration: underline; color: var(--accent); }
/* "in The cool group" on a wall post: the room it is in, with the room's
   picture as a leaf the size of a word. Muted, because it is context and the
   post is the point. */
.post-in-group { color: var(--muted); font-size: 0.82rem; }
.post-group { color: var(--muted); font-weight: 600; text-decoration: none; display: inline-flex; align-items: center; gap: 4px; vertical-align: middle; }
.post-group:hover { color: var(--accent); text-decoration: underline; }
.tiny-avatar-leaf { border-radius: 6px 0 6px 0; }
.feed-image { max-width: 100%; height: auto; border-radius: 10px; display: block; margin: 6px 0; }
/* A picture is a door (to the gallery, or to the slideshow module taking the
   click over), so it reads as one. */
.feed-image-link { display: block; }
.feed-image-link .feed-image { margin: 6px 0 0; cursor: zoom-in; }
.feed-image-link:hover .feed-image { box-shadow: 0 0 0 2px var(--accent); }

/* A gallery's grid: up to four cells, and the count of the ones not shown on
   the fourth. One picture never comes through here -- it renders like an image
   post always did (see the branch in core/_post.html). */
.gallery-grid {
  display: grid; gap: 4px; margin: 6px 0 0;
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
.gallery-grid--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.gallery-grid--3 { grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-rows: repeat(2, minmax(0, 1fr)); }
.gallery-grid--3 .gallery-cell:first-child { grid-row: span 2; }
/* Two wide, one tall: a 3-cell mosaic reads better than a 3x1 strip. */
.gallery-grid--3 .gallery-cell { min-height: 96px; }
.gallery-grid--4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.gallery-cell {
  position: relative; display: block; overflow: hidden; border-radius: 10px;
  background: var(--card-2); aspect-ratio: 4 / 3;
}
.gallery-grid--1 .gallery-cell { aspect-ratio: 16 / 9; }
.gallery-cell:first-child:last-child { aspect-ratio: auto; }
.gallery-thumb { width: 100%; height: 100%; object-fit: cover; display: block; }
.gallery-cell:hover .gallery-thumb { opacity: 0.88; }
.gallery-more {
  position: absolute; inset: 0; display: flex; align-items: center;
  justify-content: center; background: rgba(0, 0, 0, 0.55); color: #fff;
  font-size: 1.5rem; font-weight: 700; pointer-events: none;
}

/* ---- the slideshow (static/gallery.js, _gallery_viewer.html) -------------

   The picture is the stage: no card around it, on a dimmed backdrop, so it is
   the biggest thing on the screen. The caption, the tags and the actions live
   in a panel beside it -- a real card, because that half is chrome. The module
   names every slot (`data-gallery-*`), so this layout can be rearranged here
   without touching the JavaScript. */
.gallery-viewer { position: fixed; inset: 0; z-index: 300; display: flex; align-items: center; justify-content: center; }
.gallery-viewer[hidden] { display: none; }
.gallery-viewer-backdrop { position: absolute; inset: 0; background: rgba(6, 8, 12, 0.93); }
.gallery-viewer-frame {
  position: relative; z-index: 1; display: grid; align-items: center;
  grid-template-columns: minmax(0, 1fr) minmax(0, 340px);
  gap: 22px; width: 100%; height: 100%; padding: 28px 68px;
}
.gallery-viewer-stage { margin: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; min-width: 0; }
.gallery-viewer-image {
  max-width: 100%; max-height: calc(100dvh - 96px);
  object-fit: contain; border-radius: 6px; box-shadow: 0 24px 70px rgba(0, 0, 0, 0.55);
}
/* A very wide picture and a short window: let the stage give height back to the
   caption panel rather than pushing it off the screen. */
.gallery-viewer-count { color: rgba(255, 255, 255, 0.72); }
.gallery-viewer-side {
  align-self: center; max-height: calc(100dvh - 96px); overflow-y: auto;
  background: var(--card); border: 1px solid var(--border); border-radius: 14px;
  padding: 16px; color: var(--text); box-shadow: 0 20px 60px rgba(0, 0, 0, 0.45);
}
.gallery-viewer-who { margin: 0 0 6px; font-weight: 600; }
.gallery-viewer-caption { margin: 0 0 8px; line-height: 1.5; white-space: pre-wrap; }
.gallery-viewer-tags { margin: 0 0 8px; }
.gallery-viewer-actions { flex-wrap: wrap; gap: 10px; margin-top: 10px; }
.gallery-viewer-comments { margin-top: 12px; border-top: 1px dashed var(--border); }
.gallery-viewer-comments .comment-list { border-top: none; margin-top: 6px; padding-top: 0; }
.gallery-viewer-nav {
  position: absolute; top: 50%; transform: translateY(-50%); z-index: 2;
  width: 44px; height: 64px; border: 1px solid var(--border); border-radius: 12px;
  background: var(--card); color: var(--text); font-size: 1.8rem; line-height: 1;
  cursor: pointer; opacity: 0.9;
}
.gallery-viewer-nav:hover { border-color: var(--accent); opacity: 1; }
.gallery-viewer-prev { left: 10px; }
.gallery-viewer-next { right: 10px; }
.gallery-viewer.is-single .gallery-viewer-nav { display: none; }
.gallery-viewer-close { position: absolute; top: 12px; right: 14px; z-index: 3; }
@media (max-width: 900px) {
  .gallery-viewer-frame {
    grid-template-columns: minmax(0, 1fr); align-content: start;
    padding: 52px 12px 16px; overflow-y: auto; gap: 12px;
  }
  .gallery-viewer-image { max-height: 52dvh; }
  .gallery-viewer-side { max-height: none; align-self: stretch; }
  .gallery-viewer-nav { top: auto; bottom: 12px; transform: none; height: 44px; }
}

/* ---- the gallery editor (static/galleryeditor.js) ------------------------

   "Caption each photo": one row per picture, in the order they will appear.
   The row keeps the thumbnail small and gives the fields the room, because the
   writing is the point and the picture is a reminder of which one it is. */
.gallery-editor { position: fixed; inset: 0; z-index: 320; display: flex; align-items: center; justify-content: center; padding: 20px; }
.gallery-editor[hidden] { display: none; }
.gallery-editor-backdrop { position: absolute; inset: 0; background: rgba(6, 8, 12, 0.78); }
.gallery-editor-frame { position: relative; z-index: 1; height: auto; max-height: min(88vh, 760px); }
.gallery-editor-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 14px; }
.gallery-editor-row {
  display: grid; grid-template-columns: 96px minmax(0, 1fr) auto; gap: 12px;
  align-items: start; padding: 10px; border: 1px solid var(--border);
  border-radius: 12px; background: var(--card-2);
}
.gallery-editor-thumb { display: block; overflow: hidden; border-radius: 8px; background: var(--card); aspect-ratio: 4 / 3; position: relative; }
.gallery-editor-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.gallery-editor-fields { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
.gallery-editor-fields .tag-picker { margin: 0; }
.gallery-editor-cel select { max-width: 100%; }
.gallery-editor-tools { display: flex; flex-direction: column; gap: 4px; }
.gallery-editor-tools .icon-btn:disabled { opacity: 0.35; cursor: default; }
.gallery-edit-btn { align-self: flex-start; }
@media (max-width: 700px) {
  .gallery-editor-row { grid-template-columns: 64px minmax(0, 1fr); }
  .gallery-editor-tools { grid-column: 1 / -1; flex-direction: row; justify-content: flex-end; }
}
.markdown-body { line-height: 1.6; }
.markdown-body h1, .markdown-body h2, .markdown-body h3 { margin: 12px 0 6px; }
.markdown-body p { margin: 0 0 8px; }
.markdown-body ul, .markdown-body ol { margin: 0 0 8px; padding-left: 22px; }
.markdown-body code { background: var(--card-2); border-radius: 4px; padding: 1px 5px; }
.markdown-body pre { background: var(--bg); border: 1px solid var(--border); border-radius: 8px; padding: 10px; overflow-x: auto; }
.markdown-body blockquote { border-left: 3px solid var(--border); margin: 0 0 8px; padding-left: 12px; color: var(--muted); }
.post-actions { display: flex; align-items: center; gap: 14px; margin-top: 8px; }
.comment-list { list-style: none; margin: 8px 0 0; padding: 8px 0 0; border-top: 1px dashed var(--border); }
.comment-list li { padding: 2px 0; }
.comment-form { display: flex; gap: 8px; margin-top: 8px; }
/* The comment box and its reply box are textareas now, so that Shift+Enter can
   put a line break in a comment (static/enterkey.js). One row is what the old
   `<input>` looked like; `resize: vertical` and the same padding keep them
   looking like the field they replaced, and `.comment-form input` above is left
   alone for any form that still uses an input. */
.comment-form input, .comment-form textarea {
  flex: 1; background: var(--bg); color: var(--text); border: 1px solid var(--border);
  border-radius: 8px; padding: 7px 10px; font: inherit; resize: vertical;
  min-height: 0; line-height: 1.4;
}
.comment-row { display: flex; gap: 8px; align-items: flex-start; }
.comment-main { flex: 1; }
.comment-author { font-weight: 600; color: var(--text); margin-right: 6px; }
/* pre-line, not normal: a comment may be several lines and renders them with
   `<br>` (helpers.nl2br), and `normal` collapses the breaks around each one. */
.comment-text { color: var(--text); white-space: pre-line; }
.comment-actions { display: flex; align-items: center; gap: 12px; margin-top: 4px; flex-wrap: wrap; }
.comment-actions .link-btn { font-size: 0.8rem; }
.reply-box summary { cursor: pointer; color: var(--muted); font-size: 0.8rem; }
.comment .comment-list { margin-left: 20px; border-left: 2px solid var(--border); padding-left: 10px; }
.tag { display: inline-flex; align-items: center; gap: 4px; background: var(--card-2); border: 1px solid var(--border); border-radius: 999px; padding: 2px 8px; margin-left: 4px; font-size: 0.8rem; color: var(--text); text-decoration: none; vertical-align: middle; }
.tag:hover { border-color: var(--accent); text-decoration: none; }
.tiny-avatar { width: 16px; height: 16px; border-radius: 50%; object-fit: cover; }
.post-tags { margin: 4px 0 0; }
.content-tags { display: flex; flex-wrap: wrap; gap: 4px; }
.content-tags .tag { margin-left: 0; }

/* ---- link previews (cards on posts + the compose draft) ---- */
.link-preview {
  display: flex; gap: 12px; margin: 8px 0; color: inherit; text-decoration: none;
  border: 1px solid var(--border); border-radius: 12px; overflow: hidden;
  background: var(--card-2);
}
a.link-preview:hover { border-color: var(--accent); text-decoration: none; }
.link-preview-thumb {
  width: 130px; height: 96px; object-fit: cover; flex-shrink: 0;
  background: var(--card); align-self: stretch;
}
.link-preview-body {
  display: flex; flex-direction: column; gap: 2px; padding: 10px 12px;
  min-width: 0; align-self: center; flex: 1;
}
.link-preview-site {
  font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--muted);
}
.link-preview-title {
  font-weight: 600; color: var(--text);
  overflow: hidden; text-overflow: ellipsis;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
}
.link-preview-desc {
  font-size: 0.85rem; color: var(--muted); overflow: hidden;
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
}
.link-preview-area { margin-top: 8px; }
.link-preview-draft { position: relative; }
.link-preview-draft .link-preview-main {
  display: flex; gap: 12px; flex: 1; min-width: 0;
  color: inherit; text-decoration: none;
}
.link-preview-draft .link-preview-main:hover { text-decoration: none; }
.link-preview-x {
  position: absolute; top: 6px; right: 6px; z-index: 2;
  width: 26px; height: 26px; display: inline-flex; align-items: center; justify-content: center;
  background: var(--card); border: 1px solid var(--border); border-radius: 50%;
  color: var(--muted); font-size: 0.95rem; cursor: pointer; line-height: 1;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.18);
}
.link-preview-x:hover { color: var(--text); border-color: var(--accent); }
.link-preview-draft .link-preview-thumb { border-radius: 0; }
.link-preview-loading {
  align-items: center; gap: 10px; padding: 12px 14px; color: var(--muted);
  font-size: 0.88rem;
}
.link-preview-spinner {
  width: 16px; height: 16px; border-radius: 50%; flex-shrink: 0;
  border: 2px solid var(--border); border-top-color: var(--accent);
  animation: link-preview-spin 0.8s linear infinite;
}
@keyframes link-preview-spin { to { transform: rotate(360deg); } }
.notif-cat-list { list-style: none; margin: 0; padding: 0; }
.notif-cat-list li { display: flex; justify-content: space-between; align-items: center; padding: 6px 0; border-bottom: 1px solid var(--border); }
.notif-cat-list li:last-child { border-bottom: none; }
.notif { display: flex; justify-content: space-between; gap: 10px; }
.notif-identity { display: flex; align-items: center; gap: 10px; min-width: 0; flex: 1; }
.notif-copy { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.notif-line { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; }
.notif-excerpt { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 480px; }
.notif-actions { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
.prefs summary { cursor: pointer; color: var(--muted); margin: 10px 0 4px; font-size: 0.85rem; }
.prefs-form { display: flex; flex-wrap: wrap; align-items: center; gap: 14px; margin-top: 6px; }
.prefs-form label { display: inline-flex; align-items: center; gap: 6px; font-size: 0.9rem; }
.prefs-form .muted { flex-basis: 100%; }

/* ---- friends ---- */
.friend-grid { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 10px; }
.friend-chip {
  display: flex; align-items: center; gap: 8px; background: var(--card-2);
  border: 1px solid var(--border); border-radius: 999px; padding: 6px 12px;
  font-size: 0.88rem; color: var(--text); width: fit-content;
}
.friend-chip:hover { border-color: var(--accent); text-decoration: none; }
.friend-tile { display: flex; flex-direction: column; gap: 6px; align-items: flex-start; }
/* user cards: big circle avatar + name pill overlapping its bottom */
.user-grid { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px 10px; }
.user-grid > li { display: flex; justify-content: center; }
.user-card { display: flex; flex-direction: column; align-items: center; text-decoration: none; color: var(--text); max-width: 130px; }
.user-card-avatar { width: 76px; height: 76px; border-radius: 18px 0 18px 0; object-fit: cover; display: block; }
.user-card .avatar.user-card-avatar { display: flex; align-items: center; justify-content: center; font-size: 2.4rem; color: #fff; border-radius: 18px 0 18px 0; }
.user-card-name { position: relative; z-index: 1; margin-top: -15px; background: var(--card-2); border: 1px solid var(--border); border-radius: 12px 0 12px 0; padding: 2px 12px; font-size: 0.8rem; color: var(--text); white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
.user-card:hover .user-card-avatar { box-shadow: 0 0 0 2px var(--accent); }
.user-tile { display: flex; flex-direction: column; align-items: center; gap: 6px; }
.row-list { list-style: none; margin: 0; padding: 0; }
.row-item { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 0; border-bottom: 1px solid var(--border); }
.row-actions { display: flex; gap: 8px; }
/* friend-request grid (friends page + notifications) */
.request-grid { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
.request-tile { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; padding: 16px 12px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--card-2); text-align: center; }
.request-tile .row-actions { justify-content: center; }
/* An invitation in the same tile as a friend request, because it is the same
   question. The identity is a room rather than a person, so its pill holds the
   page's leaf instead of an avatar, and the inviter sits under it -- two lines
   where a friend request has one, which is what makes the two cards tell
   themselves apart at a glance. */
.invitation-tile .friend-chip { max-width: 100%; }
/* The size comes from here and not from `.leaf`, which is 76px for a page's own
   header. The tile is a small card in a grid of small cards: a leaf at 76px
   filled it, and one with its size taken away shrank to its content and came out
   smaller than the friend request beside it. 32px is what `.avatar-sm` is, so the
   two cards in the grid are the same weight, and the room for a long group name
   and the "invited by" line under it is the whole reason the pill wraps. */
.invitation-tile .leaf { width: 32px; height: 32px; flex: 0 0 auto; }
.invitation-tile .friend-chip > span { overflow-wrap: anywhere; line-height: 1.3; }
.invitation-inviter { display: block; margin-top: -6px; }
/* The three decline-all buttons on the decisions card: a label and a small row,
   so they read as one control with a choice rather than as three unrelated
   buttons competing with the card's heading. The row wraps rather than
   overflowing, because the card is narrow on a phone and a clipped "Both" is
   worse than a second line. */
.decline-all-group { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
.decline-all-group .muted { margin-right: 2px; }
/* dense friends grid */
.user-grid-8 { grid-template-columns: repeat(8, 1fr); }
/* two-column top friends in the sidebar */
.top-friends-grid { grid-template-columns: repeat(2, 1fr); }
/* ---- the one ad slot ------------------------------------------------
   We know you are smart enough to find a way to hide this, please don't.

   There is exactly one ad on this site, and it pays for the server. The box
   reserves its height whether or not anything is loaded into it, so the side
   column never shifts when an ad arrives.

   These class names are the ones generic filter lists match, so anybody
   running an ad blocker will not see this box -- and we are not going to
   obfuscate the names to win that argument, because it is not winnable and it
   would punish the people who are happy to look at one ad. The space is
   reserved for everyone else.

   If the ad becomes a 300x250 rectangle, this number becomes 250px. */
.ad-slot { min-height: 120px; }
.ad-placeholder {
  display: flex; align-items: center; justify-content: center;
  min-height: 120px; padding: 12px; text-align: center;
  border: 1px dashed var(--border); border-radius: var(--radius);
  background: var(--card-2); color: var(--muted); font-size: 0.85rem;
}
@media (max-width: 760px) {
  .request-grid { grid-template-columns: repeat(2, 1fr); }
  .user-grid-8 { grid-template-columns: repeat(4, 1fr); }
}
.badge { background: var(--card-2); border: 1px solid var(--border); border-radius: 999px; padding: 4px 12px; font-size: 0.82rem; }
.badge-friends { color: var(--ok); border-color: rgba(94, 234, 122, 0.4); }

/* ---- misc ---- */
.notice { background: rgba(94, 234, 213, 0.08); border: 1px solid rgba(94, 234, 213, 0.3); border-radius: 8px; padding: 12px 14px; font-size: 0.88rem; margin-top: 18px; }
.footer { flex: 0 0 auto; border-top: 1px solid var(--border); padding: 24px 0; color: var(--muted); font-size: 0.85rem; text-align: center; background: var(--bg); }

/* ---- dashboard feed: header controls + widget editor ---- */
.feed-toolbar { display: flex; align-items: center; gap: 12px; margin-left: auto; flex-wrap: wrap; }
.feed-controls { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.feed-control { display: inline-flex; align-items: center; gap: 5px; font-size: 0.85rem; color: var(--muted); cursor: pointer; white-space: nowrap; }
.feed-control select { background: var(--bg); color: var(--text); border: 1px solid var(--border); border-radius: 8px; padding: 5px 8px; font: inherit; }
.feed-control input[type="checkbox"] { accent-color: var(--accent); }
.feed-more-li { text-align: center; padding-top: 12px; }
.feed-more { cursor: pointer; }
/* client-rendered widgets (feed + user-defined): controls row + mount */
.widget-client-controls { display: flex; align-items: center; justify-content: flex-end; gap: 10px; flex-wrap: wrap; margin-left: auto; }
.widget-client-mount { min-height: 16px; }
.feed-control-input { display: inline-flex; align-items: center; gap: 6px; }
.feed-control input[type="text"], .feed-control input[type="number"] { background: var(--bg); color: var(--text); border: 1px solid var(--border); border-radius: 8px; padding: 4px 8px; font: inherit; width: 170px; max-width: 100%; min-width: 0; box-sizing: border-box; }
body.dashboard-edit .widget-client-controls { visibility: hidden; }
/* custom widget editor */
.custom-widget-fields { display: flex; flex-wrap: wrap; gap: 14px; padding: 12px 18px 0; align-items: flex-end; }
.custom-widget-fields .notif-setting { display: flex; flex-direction: column; gap: 6px; font-weight: 600; font-size: 0.9rem; flex: 1; min-width: 180px; }
.custom-widget-fields input[type="text"], .custom-widget-fields select { background: var(--bg); color: var(--text); border: 1px solid var(--border); border-radius: 8px; padding: 8px 10px; font: inherit; font-weight: 400; }
#custom-widget-source { flex: 1 1 0; min-height: 0; margin: 14px 18px; background: var(--bg); color: var(--text); border: 1px solid var(--border); border-radius: 8px; padding: 12px; font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; font-size: 0.82rem; line-height: 1.5; resize: vertical; }

/* widget editor modal */
.modal-backdrop { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; padding: 20px; z-index: 100; }
.modal-backdrop[hidden] { display: none; }
.modal { background: var(--card); border: 1px solid var(--border); border-radius: 14px; width: 100%; max-width: 936px; height: min(88vh, 760px); max-height: 92vh; display: flex; flex-direction: column; overflow: hidden; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35); }
.modal-head { display: flex; align-items: center; justify-content: space-between; padding: 14px 18px; border-bottom: 1px solid var(--border); }
.modal-head h2 { margin: 0; font-size: 1.15rem; }
.modal-hint { margin: 0; padding: 12px 18px; font-size: 0.85rem; color: var(--muted); border-bottom: 1px solid var(--border); }
.modal-hint code { background: var(--card-2); border-radius: 4px; padding: 1px 5px; }
#widget-form { display: flex; flex-direction: column; flex: 1; min-height: 0; }
#widget-source { flex: 1; margin: 14px 18px; min-height: 320px; background: var(--bg); color: var(--text); border: 1px solid var(--border); border-radius: 8px; padding: 12px; font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; font-size: 0.82rem; line-height: 1.5; resize: vertical; white-space: pre; tab-size: 2; }
.modal-actions { display: flex; align-items: center; gap: 12px; padding: 14px 18px; border-top: 1px solid var(--border); }
.modal-actions .muted.small { margin-left: auto; }

/* The question box (static/dialog.js): the same modal, one size smaller and
   only as tall as its words. It is not `.modal-sm`'s height:auto alone because
   this one must also stay narrow on a wide screen, where a 936px composer fits
   but a yes/no question does not. */
.modal-confirm { max-width: 440px; height: auto; }
.dialog-message { margin: 0; line-height: 1.5; }
.dialog-actions { justify-content: flex-end; border-top: none; padding-top: 4px; }

/* The held button (static/dialog.js).

   A red answer is pressed and held -- three seconds for now -- and this is the
   progress: a circle that fills around the button as the press continues, so the
   wait is visible rather than a button that seems not to have worked.

   **It is a separate element, not a pseudo-element on the button.** Three
   attempts at doing it on the button all failed the same way: a `::after` is
   clipped by its own box, so a ring larger than the button is cut off; and a
   circle drawn through a rounded rectangle keeps crossing the button's edges and
   its label, which produced stray flecks and a sliced "D". A sibling element has
   none of those problems. It is just a circle, behind the button.

   **Bigger than the button on purpose.** `static/dialog.js` measures the button
   and gives the circle a diameter of 1.1x the button's width, centred on it, so
   the button sits inside with a margin all round. On a phone the finger holding
   the button covers the button; a ring at the button's own edge is under the
   fingertip, and one at 1.1x is not.

   **Two colours, and the whole circle is drawn first.** This is what makes it
   read as progress rather than as a stray arc appearing out of nowhere: a
   complete circle in a quiet theme colour (the card's second surface, the same
   one buttons and pills sit on) is the track, with a soft shadow so it stands
   off the card behind it, and the `--danger` arc grows around it from nothing to
   a full turn. Watching a full circle become a filled one is the oldest progress
   idiom there is, and it needs no legend.

   The angles are the point: `--hold` (0 to 1, written every animation frame)
   times 360deg is where the arc currently ends, and a circle's angle *is* linear
   distance from its centre, so the arc grows at a constant visual speed -- which
   a ring following a rectangle's outline could not do. */
.dialog-actions { justify-content: flex-end; border-top: none; padding-top: 4px; position: relative; }
.dialog-ring {
  position: absolute;
  pointer-events: none;
  z-index: 0;
  border-radius: 50%;
  opacity: 0;
  transition: opacity 0.15s ease-out;
  /* A whisper of depth, so the track is a thing sitting above the card rather
     than a line drawn on it. */
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.22);
}
/* The full circle: the track the progress fills in. `card-2` is the site's
   second surface colour, so on a light palette it is a soft grey and on a dark
   one a raised slate, which keeps a quiet circle visible either way. The border
   is the same `--border` hairline every card uses, because that is the edge the
   rest of the site draws. */
.dialog-ring::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: var(--card-2);
  border: 1px solid var(--border);
  box-sizing: border-box;
  /* The track is a ring, not a disc: it must not cover the button sitting in
     the middle of it. The inner half is masked away, leaving the band. */
  -webkit-mask: linear-gradient(#000 0 0) content-box exclude,
                linear-gradient(#000 0 0);
  mask: linear-gradient(#000 0 0) content-box exclude,
        linear-gradient(#000 0 0);
  padding: 5px;
}
/* The progress arc: the danger colour, growing clockwise from the top over the
   track, masked to the same 5px band so the two are the same circle. The top of
   a conic gradient is 12 o'clock, which is where a progress arc is expected to
   start. */
.dialog-ring::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: conic-gradient(
    var(--danger) calc(var(--hold, 0) * 360deg),
    transparent 0
  );
  -webkit-mask: linear-gradient(#000 0 0) content-box exclude,
                linear-gradient(#000 0 0);
  mask: linear-gradient(#000 0 0) content-box exclude,
        linear-gradient(#000 0 0);
  padding: 5px;
  box-sizing: border-box;
}
.dialog-ring.is-holding { opacity: 1; }
/* The circle hangs outside the actions row and off the bottom of the dialog, so
   the modal must not clip it. */
.modal-confirm { overflow: visible; }
.dialog-hold-note { margin-right: auto; }
.dialog-actions .btn.needs-hold { cursor: pointer; }

/* CodeMirror (vendored) — themed to the site palette */
.CodeMirror { flex: 1 1 0; min-height: 0; height: auto; margin: 14px 18px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg); color: var(--text); font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; font-size: 0.82rem; line-height: 1.5; }
.CodeMirror-scroll { min-height: 0; }
.CodeMirror-gutters { background: var(--bg-soft); border-right: 1px solid var(--border); }
.CodeMirror-linenumber { color: var(--muted); }
.CodeMirror-cursor { border-left: 2px solid var(--accent); }
.CodeMirror-selected { background: var(--card-2); }
.CodeMirror-focused .CodeMirror-selected { background: var(--card-2); }
.cm-s-default .cm-keyword { color: var(--accent-2); }
.cm-s-default .cm-string, .cm-s-default .cm-string-2 { color: var(--ok); }
.cm-s-default .cm-comment { color: var(--muted); font-style: italic; }
.cm-s-default .cm-number, .cm-s-default .cm-atom { color: var(--accent); }
/* Object keys (e.g. `name:` in a widget literal) tokenize as property/def —
   use the body text color so they stay readable on light palettes. */
.cm-s-default .cm-def, .cm-s-default .cm-property { color: var(--text); }
.cm-s-default .cm-operator { color: var(--text); }
.cm-s-default .cm-variable, .cm-s-default .cm-variable-2 { color: var(--text); }

/* Editor theme picker row (above CodeMirror in the widget editor modals).
   Choosing a real CodeMirror theme swaps the whole editor look; picking
   "Default (site colors)" keeps the palette styling above. */
.editor-theme-row { display: flex; align-items: center; justify-content: flex-end; padding: 12px 18px 0; }
.editor-theme-label { display: inline-flex; align-items: center; gap: 8px; font-size: 0.85rem; color: var(--muted); }
.editor-theme-select { font: inherit; font-size: 0.85rem; color: var(--text); background: var(--card-2); border: 1px solid var(--border); border-radius: 6px; padding: 3px 6px; min-width: 180px; }

/* ---- dashboard widgets: edit mode, palette, compose modal ---- */
.widget-card { position: relative; }

/* "What's on your mind?" — three doors into the composer.

   A fully rounded pill with a soft three-stop gradient, a soft drop shadow,
   and two shallow insets (a light top, a dark bottom) for a little dimension.
   No solid 3D edge under it and no movement when pressed: it is a flat pill
   that reads as a button, not a lump of plastic.

   The gloss is deliberately almost gone. There is still a faint radial lift in
   the upper left and a blurred sheen, but both are a fraction of a typical
   candy button's, and the light and dark tones are mixed only a little away
   from the accent, so the gradient stays a gradient rather than a shine.

   The colours come from the theme's own `--accent`, so each palette gets its
   own depth of one hue; the pink stays in the example it came from. */
.compose-quick { display: flex; gap: 12px; }
.compose-quick-btn {
  --candy: var(--accent);
  --candy-light: color-mix(in srgb, var(--accent), #fff 30%);
  --candy-dark: color-mix(in srgb, var(--accent), #000 24%);

  position: relative; overflow: hidden;
  flex: 1 1 0; min-width: 0;
  padding: 13px 18px;
  border: none; border-radius: 999px;
  font: inherit; font-size: 0.95rem; font-weight: 700; letter-spacing: 0.02em;
  color: #fff; cursor: pointer;
  /* A browser without color-mix still gets a solid accent pill, because the
     gradient below then computes to nothing. */
  background-color: var(--accent);
  background-image:
    radial-gradient(ellipse 120% 140% at 30% 15%,
      rgba(255, 255, 255, 0.26) 0%,
      rgba(255, 255, 255, 0.11) 8%,
      rgba(255, 255, 255, 0.02) 22%,
      rgba(255, 255, 255, 0) 40%),
    linear-gradient(180deg,
      var(--candy-light) 0%,
      var(--candy) 48%,
      var(--candy-dark) 100%);
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.28);
  box-shadow:
    inset 0 2px 3px rgba(255, 255, 255, 0.18),
    inset 0 -4px 6px rgba(0, 0, 0, 0.26),
    0 4px 10px rgba(0, 0, 0, 0.26);
  transition: box-shadow 0.08s ease, filter 0.08s ease, background-image 0.08s ease;
}
/* A faint sheen rather than a highlight. */
.compose-quick-btn::after {
  content: ""; position: absolute;
  top: 14%; left: 15%; width: 24%; height: 26%;
  background: rgba(255, 255, 255, 0.13);
  border-radius: 50%; filter: blur(7px); pointer-events: none;
}
.compose-quick-btn:hover { filter: brightness(1.05); }
/* Pressed: the gradient turns over and the insets swap. It does not move. */
.compose-quick-btn:active {
  background-image:
    radial-gradient(ellipse 120% 140% at 30% 85%,
      rgba(255, 255, 255, 0.12) 0%,
      rgba(255, 255, 255, 0.05) 10%,
      rgba(255, 255, 255, 0) 30%),
    linear-gradient(180deg,
      var(--candy-dark) 0%,
      var(--candy) 55%,
      var(--candy-light) 100%);
  box-shadow:
    inset 0 4px 7px rgba(0, 0, 0, 0.3),
    inset 0 -2px 3px rgba(255, 255, 255, 0.12),
    0 2px 5px rgba(0, 0, 0, 0.2);
}
.compose-quick-btn:active::after {
  top: auto; bottom: 12%; left: 36%; width: 22%; height: 18%;
  background: rgba(255, 255, 255, 0.09);
}
.compose-quick-btn:focus-visible { outline: 2px solid var(--accent-2); outline-offset: 3px; }
@media (max-width: 420px) { .compose-quick { flex-direction: column; } }
.widget-controls {
  position: absolute; top: 14px; right: 10px;
  display: none; align-items: center; justify-content: flex-end; gap: 2px;
  padding: 4px 8px; z-index: 5;
  background: var(--card-2);
  border-radius: 12px 0 12px 0;
}
.widget-controls .icon-btn { font-size: 1rem; padding: 3px 7px; }
body.dashboard-edit .widget-controls { display: flex; }
/* While editing, hide a widget's own interactive header toolbar (e.g. the
   feed's sort/type filters) without reflowing, so the edit controls take its
   place in the same space and the layout doesn't shift. */
body.dashboard-edit .widget-suppress { visibility: hidden; }
body.dashboard-edit .widget-card { outline: 1px dashed var(--accent); outline-offset: 4px; }

/* ---- drag-and-drop reorder ---- */
.drag-handle { touch-action: none; cursor: grab; }
body.drag-active { user-select: none; cursor: grabbing; }
/* The grabbed card turns transparent in place while a transparent placeholder
   follows the cursor (see dnd.js). */
body.drag-active .widget-card.is-dragging { opacity: 0.3; outline: 2px dashed var(--accent); }
.drag-placeholder {
  box-sizing: border-box;
  border: 2px dashed var(--accent);
  border-radius: var(--radius);
  background: transparent;
  margin-bottom: 20px;
}

.modal-sm { max-width: 520px; height: auto; max-height: 92vh; }
.modal-body { padding: 16px 18px; overflow-y: auto; }

/* --- the composer (the modal's, and a group wall's) ------------------------
   One form, three types. The panel around the form carries data-post-type
   (static/composer.js writes it), and that is the whole of "the modal changes
   when you change the type": a bulletin stays a small dialog, an article grows
   into a writing surface, an image post makes room for the drop zone. */
.modal[data-compose-panel][data-post-type="article"] {
  max-width: min(1100px, 96vw);
  height: min(92vh, 940px);
}
.modal[data-compose-panel][data-post-type="image"] { max-width: 620px; }

/* The composer's form wraps its own head (the type selector lives in the
   heading), so the form is the modal's flex column and the body inside it is
   what scrolls. The other modals keep the plain head + body shape. */
.modal { min-width: 0; }
.modal > .post-form { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
.post-form > .modal-body { flex: 1 1 auto; min-height: 0; }

/* The image post, where there is room for it: the picture on the left and the
   words about it on the right, so the whole form fits without a scroll. A
   narrow window keeps the single column, and a form that scrolls is no sin. */
@media (min-width: 820px) {
  .modal[data-compose-panel][data-post-type="image"] { max-width: 780px; }
  .modal[data-compose-panel][data-post-type="image"] .modal-body {
    display: grid; grid-template-columns: 1fr 1fr; gap: 0 18px; align-items: start;
  }
  .modal[data-compose-panel][data-post-type="image"] .post-form-foot { grid-column: 1 / -1; }
  .modal[data-compose-panel][data-post-type="image"] .compose-image { grid-column: 1; }
  .modal[data-compose-panel][data-post-type="image"] .tag-picker { grid-column: 2; }
}

/* The type selector (templates/_post_type_picker.html has both shapes). Real
   radios underneath either one, so arrow keys work and the value submits on its
   own; only the styling differs. */
.post-type-tab { position: relative; min-width: 0; cursor: pointer; }
.post-type-radio {
  position: absolute; width: 1px; height: 1px; margin: 0; padding: 0;
  opacity: 0; pointer-events: none;
}
.post-type-tab-face { display: flex; align-items: center; }
.post-type-tab-icon { font-size: 1.05rem; line-height: 1; }
.post-type-tab-label { font-weight: 600; font-size: 0.92rem; color: var(--text); }
.post-type-radio:focus-visible + .post-type-tab-face { outline: 2px solid var(--accent-2); outline-offset: 2px; }

/* (1) The row: a group's wall, which has no dialog heading. Every type is
   labelled, with its one-line hint. The hint wraps rather than clipping: the
   segments are about 160px wide and "A longer piece, with formatting" cut off
   mid-word reads as a bug. */
.post-type-tabs--row { display: flex; gap: 6px; margin-bottom: 14px; }
.post-type-tabs--row .post-type-tab { flex: 1 1 0; }
.post-type-tabs--row .post-type-tab-face {
  gap: 9px; height: 100%; padding: 8px 11px;
  border: 1px solid var(--border); border-radius: 10px;
  background: var(--bg); color: var(--muted);
}
.post-type-tabs--row .post-type-tab-text { display: flex; flex-direction: column; min-width: 0; }
.post-type-tabs--row .post-type-tab-hint { font-size: 0.72rem; line-height: 1.25; color: var(--muted); }
.post-type-tabs--row .post-type-tab:hover .post-type-tab-face { border-color: var(--accent); }
.post-type-tabs--row .post-type-radio:checked + .post-type-tab-face {
  border-color: var(--accent); background: var(--card-2);
  box-shadow: inset 0 0 0 1px var(--accent);
}
.post-type-tabs--row .post-type-radio:checked + .post-type-tab-face .post-type-tab-label { color: var(--accent); }
@media (max-width: 620px) {
  .post-type-tabs--row { flex-direction: column; }
}

/* (2) The head: the chosen type reads as part of the title ("New Bulletin 💬")
   and the others are icon-only buttons beside it, which is compact and leaves
   room for more types later. `order` pulls the chosen one to the front wherever
   it sits in the markup, so the name is always next to "New"; every other
   label is clipped rather than removed, so its accessible name and its hover
   title still carry it. Without :has() the names simply all show. */
.compose-head { align-items: center; gap: 10px; }
.compose-title {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin: 0; font-size: 1.15rem;
}
.compose-title-lead { font-weight: 700; }
.post-type-tabs--head { display: inline-flex; align-items: center; gap: 6px; }
.post-type-tabs--head .post-type-tab:has(input:checked) { order: -1; }
.post-type-tabs--head .post-type-tab-face {
  gap: 7px; padding: 3px 9px; border: 1px solid transparent; border-radius: 999px;
  color: var(--muted);
}
/* The types you are not writing are light pills of the palette's second accent
   at a fifth strength, so they read as buttons without competing with the
   title; the margin is what separates them from the named type and from each
   other. */
.post-type-tabs--head .post-type-tab:not(:has(input:checked)) .post-type-tab-face {
  margin-left: 6px;
  background: color-mix(in srgb, var(--accent-2) 20%, transparent);
}
.post-type-tabs--head .post-type-tab:hover .post-type-tab-face {
  background: color-mix(in srgb, var(--accent-2) 34%, transparent);
}
.post-type-tabs--head .post-type-tab:has(input:checked) .post-type-tab-face {
  padding-left: 0; background: transparent; border-color: transparent;
}
.post-type-tabs--head .post-type-tab:has(input:checked) .post-type-tab-label {
  font-size: 1.15rem; font-weight: 700;
}
.post-type-tabs--head .post-type-tab:not(:has(input:checked)) .post-type-tab-label {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}

/* --- the CEL picker (static/celpicker.js) ---------------------------------
   A disclosure whose panel is the whole 0-21 continuum, plus the two examples
   per level that the explainer at /moderation/cel prints. It is position:fixed
   because the composer it sits in is a scrollable modal body, which would clip
   an absolutely-positioned child; the rules below are also the no-JavaScript
   fallback (a centred popup), and celpicker.js overwrites left/top when it can
   anchor it to the field. */
/* A compact pill, and a fixed width: the level changes while somebody is
   looking at the footer and the Post button must not slide about. It wears the
   inverted chip (near-black on a day palette, near-white on a night one)
   because it is the one control down here that is not a button of the site's
   own colour. */
.cel-picker-trigger {
  display: inline-flex; align-items: center; justify-content: center;
  box-sizing: border-box; width: 72px; padding: 0 8px;
  border: 1px solid currentColor; border-radius: 999px; cursor: pointer;
  list-style: none; font-size: 0.82rem; font-weight: 600;
  font-variant-numeric: tabular-nums; letter-spacing: 0.01em;
  /* Dark with light text in *every* palette: mixing the palette's own text
     colour into black keeps the theme's cast, where using --text as the
     background would turn the pill pale on the four night themes. */
  background: color-mix(in srgb, var(--text) 14%, #000);
  color: color-mix(in srgb, var(--card) 30%, #fff);
}
.cel-picker-trigger::-webkit-details-marker { display: none; }
.cel-picker-trigger:hover, .cel-picker[open] .cel-picker-trigger {
  background: color-mix(in srgb, var(--text) 24%, #000);
}
.cel-picker-trigger-number { font-weight: 700; }
.picker-caret { display: inline-block; color: var(--muted); font-size: 0.75rem; transition: transform 0.15s; }
details[open] > summary .picker-caret { transform: rotate(180deg); }
.cel-picker-panel {
  position: fixed; z-index: 200; left: 50%; top: 12vh; transform: translateX(-50%);
  width: min(560px, calc(100vw - 24px)); max-height: min(74vh, 560px); overflow: auto;
  padding: 12px; background: var(--card); border: 1px solid var(--border);
  border-radius: 12px; box-shadow: 0 18px 50px rgba(0, 0, 0, 0.35);
}
.cel-picker-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; margin-bottom: 10px; }
.cel-picker-link { font-size: 0.82rem; }
.cel-picker-body { display: grid; grid-template-columns: 220px 1fr; gap: 12px; }
.cel-picker-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 4px; align-content: start; }
.cel-choice { position: relative; cursor: pointer; }
.cel-choice input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
.cel-choice-number {
  display: flex; align-items: center; justify-content: center; padding: 6px 0;
  border: 1px solid var(--border); border-radius: 8px; background: var(--bg);
  font-size: 0.85rem; font-variant-numeric: tabular-nums;
}
.cel-choice:hover .cel-choice-number { border-color: var(--accent); color: var(--accent); }
.cel-choice input:checked + .cel-choice-number {
  background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 700;
}
.cel-choice input:focus-visible + .cel-choice-number { outline: 2px solid var(--accent-2); outline-offset: 1px; }
.cel-picker-detail { min-height: 132px; padding-left: 12px; border-left: 1px solid var(--border); }
.cel-picker-detail-where {
  margin: 0; font-size: 0.75rem; color: var(--muted);
  text-transform: uppercase; letter-spacing: 0.04em;
}
.cel-picker-detail-name { margin: 2px 0 8px; font-weight: 600; }
.cel-picker-detail-examples { margin: 0; padding-left: 18px; }
.cel-picker-detail-examples li { margin-bottom: 5px; }
.cel-picker-detail-note { margin: 10px 0 0; }
@media (max-width: 700px) {
  .cel-picker-panel, .vis-picker-panel {
    left: 8px; right: 8px; top: auto; bottom: 8px; width: auto;
    transform: none; max-height: 76vh;
  }
  .cel-picker-body { grid-template-columns: 1fr; }
  .cel-picker-detail { min-height: 0; padding: 10px 0 0; border-left: 0; border-top: 1px solid var(--border); }
}

/* --- the mini CEL picker (_cel_select.html's `cel_mini`) -------------------
   The same CEL:13 pill a post wears, over a grid of bare numbers: no names and
   no examples, because a row per photo has no room to read "CEL 13 · PG-13
   Benchmark" twenty-two times. The numbers wear the same tooltips as the big
   picker, so the meaning is a hover away rather than gone.

   It opens *in place*: the gallery editor rebuilds its rows, and a floating
   panel keeps listeners on `document` that every rebuild would leave behind. */
.cel-mini { position: relative; display: inline-block; }
.cel-mini-trigger {
  display: inline-flex; align-items: center; gap: 4px; cursor: pointer;
  border: 1px solid var(--border); border-radius: 999px; padding: 1px 10px;
  background: var(--card-2); color: var(--muted); font-size: 0.8rem; line-height: 1.7;
}
.cel-mini-trigger::-webkit-details-marker { display: none; }
.cel-mini-trigger:hover, .cel-mini[open] .cel-mini-trigger { border-color: var(--accent); color: var(--text); }
.cel-mini-trigger-text { white-space: nowrap; }
.cel-mini-number { font-weight: 700; font-variant-numeric: tabular-nums; }
.cel-mini-panel {
  display: grid; grid-template-columns: repeat(8, minmax(0, 1fr)); gap: 4px;
  margin-top: 6px; padding: 8px; max-width: 322px;
  background: var(--bg); border: 1px solid var(--border); border-radius: 12px;
}
.cel-mini-choice { position: relative; cursor: pointer; }
.cel-mini-choice input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
.cel-mini-choice-number {
  display: flex; align-items: center; justify-content: center; padding: 5px 0;
  border: 1px solid var(--border); border-radius: 8px; background: var(--card);
  font-size: 0.85rem; font-variant-numeric: tabular-nums;
}
.cel-mini-choice:hover .cel-mini-choice-number { border-color: var(--accent); color: var(--accent); }
.cel-mini-choice input:checked + .cel-mini-choice-number {
  background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 700;
}
.cel-mini-choice input:focus-visible + .cel-mini-choice-number {
  outline: 2px solid var(--accent-2); outline-offset: 1px;
}
@media (max-width: 700px) {
  .cel-mini-panel { grid-template-columns: repeat(6, minmax(0, 1fr)); max-width: 258px; }
}

/* --- the image drop zone (static/dropzone.js) -----------------------------
   A <label> around the real file input, so the click and the keyboard are the
   browser's own; drag-and-drop and the preview are the module's. */
.dropzone {
  position: relative; display: block; padding: 22px 16px; text-align: center;
  border: 2px dashed var(--border); border-radius: 12px; background: var(--bg);
  cursor: pointer; transition: border-color 0.15s, background 0.15s;
}
.dropzone:hover, .dropzone.is-dragging { border-color: var(--accent); background: var(--card-2); }
.dropzone.is-dragging { border-style: solid; }
.dropzone:focus-within { outline: 2px solid var(--accent-2); outline-offset: 2px; }
/* One picture outside a composer: a group's picture, a page's, an avatar. Same
   zone, less room, because it sits inside a form card rather than owning one. */
.dropzone--single { padding: 14px 12px; }
.dropzone--single .dropzone-title { font-size: 0.92rem; }
.dropzone--single .dropzone-hint { line-height: 1.45; }
.dropzone-input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
.dropzone-face { display: flex; flex-direction: column; align-items: center; gap: 3px; }
.dropzone-icon { font-size: 1.3rem; line-height: 1; color: var(--accent); }
.dropzone-title { font-weight: 600; }
.dropzone-list {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column; gap: 6px;
}
.dropzone-item {
  display: flex; align-items: center; gap: 10px; padding: 6px 8px;
  border: 1px solid var(--border); border-radius: 10px; background: var(--card-2);
}
.dropzone-thumb { width: 44px; height: 44px; object-fit: cover; border-radius: 8px; flex: 0 0 auto; }
.dropzone-item-text { display: flex; flex-direction: column; flex: 1; min-width: 0; }
.dropzone-item-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dropzone-remove {
  background: transparent; border: none; cursor: pointer; color: var(--muted);
  font-size: 1rem; line-height: 1; padding: 2px 6px; border-radius: 50%;
}
.dropzone-remove:hover { color: var(--danger); }

/* --- a picture being prepared for upload -----------------------------------
   Every chosen photo is shrunk and re-encoded in the browser before anything is
   sent (static/imageprep.js). A row waiting on that says so: a spinner where the
   size will go, and a word beside it. The two blocks below are the whole of it,
   shared by the drop zone's rows, the gallery editor's rows, and the composer's
   "stay on this page" line, so one photo cannot look like two different things
   depending on which surface is showing it. */
.upload-spinner {
  display: inline-block; width: 11px; height: 11px; margin-right: 5px;
  vertical-align: -1px; border-radius: 50%;
  border: 2px solid var(--border);
  border-top-color: var(--accent);
  animation: upload-spin 0.7s linear infinite;
}
@keyframes upload-spin { to { transform: rotate(360deg); } }
/* A visitor who asked for less motion gets a pulse, not a rotation: the point is
   "something is happening", and a still dot would say nothing at all. */
@media (prefers-reduced-motion: reduce) {
  .upload-spinner { animation: upload-pulse 1.2s ease-in-out infinite; }
  @keyframes upload-pulse { 50% { opacity: 0.35; } }
}
.dropzone-item-status { color: var(--accent); }
.dropzone-item.is-unprepared .dropzone-item-status,
.gallery-editor-row.is-unprepared .gallery-editor-status { color: var(--danger); }
/* The picture is being worked on but is perfectly usable already, so the row is
   dimmed rather than disabled: taking it back or moving it must stay possible. */
.dropzone-item.is-preparing .dropzone-thumb,
.gallery-editor-row.is-preparing .gallery-editor-thumb img { opacity: 0.55; }
.upload-status {
  display: flex; align-items: center; gap: 2px; margin: 8px 0 0;
  color: var(--muted); font-size: 0.85rem;
}
.upload-status.is-problem { color: var(--danger); }
/* Over the photo rather than under it: the editor's rows are a fixed grid, and a
   line that appears and disappears would shuffle every row below it. */
.gallery-editor-status {
  position: absolute; left: 0; right: 0; bottom: 0;
  display: flex; align-items: center;
  padding: 3px 6px; background: var(--card); color: var(--accent);
  border-top: 1px solid var(--border);
}

/* --- the article's markdown editor (static/mdeditor.js) -------------------
   Toast UI Editor, vendored, fetched the first time somebody picks Article.
   The vendor stylesheet is inserted at the *start* of <head> so these rules
   (same specificity, later in the cascade) win, and everything is scoped to
   .md-editor so the theme's own palette is what the editor wears. */
.md-editor { --md-height: 320px; margin-bottom: 10px; }
.md-editor-host { height: var(--md-height); }
.modal[data-compose-panel][data-post-type="article"] .md-editor { --md-height: min(54vh, 540px); }
.wall-form .md-editor { --md-height: min(50vh, 420px); }
.md-editor-note { margin: 6px 0 0; }
/* Until the editor is actually up the block stays hidden, so a browser running
   no JavaScript falls back to the plain markdown textarea (the composer only
   hides it with `data-hide-for-type`, which is JavaScript) instead of showing
   an empty frame. mdeditor.js clears this class when it mounts or gives up. */
.md-editor--pending { display: none !important; }
/* The bundle could not load (offline, blocked): fall back to the plain field,
   which the composer un-hides, and say so. */
.md-editor--unavailable .md-editor-host { display: none; }
.md-editor .toastui-editor-defaultUI {
  border: 1px solid var(--border); border-radius: 10px; background: var(--card);
  font-family: inherit;
}
.md-editor .toastui-editor-defaultUI-toolbar { background: var(--card-2); border-bottom: 1px solid var(--border); }
.md-editor .toastui-editor-toolbar-divider { background: var(--border); }
.md-editor .toastui-editor-defaultUI-toolbar button {
  color: var(--text);
  /* The vendor stylesheet draws the button as a near-white box; on a night
     palette that reads as a row of little light rectangles. The border is the
     hover state here, so it is transparent at rest. */
  border-color: transparent;
}
.md-editor .toastui-editor-defaultUI-toolbar button:not(:disabled):hover {
  background: var(--bg);
  border-color: var(--border);
}
.md-editor .toastui-editor-ww-container,
.md-editor .toastui-editor-md-container,
.md-editor .toastui-editor-main { background: var(--card); }
.md-editor .toastui-editor-contents { color: var(--text); font-family: inherit; font-size: 1rem; }
.md-editor .toastui-editor-contents h1,
.md-editor .toastui-editor-contents h2,
.md-editor .toastui-editor-contents h3,
.md-editor .toastui-editor-contents h4,
.md-editor .toastui-editor-contents h5,
.md-editor .toastui-editor-contents h6 { color: var(--text); border-bottom-color: var(--border); }
.md-editor .toastui-editor-contents p,
.md-editor .toastui-editor-contents li { color: var(--text); }
.md-editor .toastui-editor-contents blockquote { color: var(--muted); border-left-color: var(--border); }
.md-editor .toastui-editor-contents code,
.md-editor .toastui-editor-contents pre { background: var(--bg); color: var(--text); }
.md-editor .toastui-editor-contents a { color: var(--accent); }
.md-editor .toastui-editor-contents hr { background: var(--border); border-color: var(--border); }
.md-editor .toastui-editor-contents table th,
.md-editor .toastui-editor-contents table td { border-color: var(--border); color: var(--text); }
.md-editor .toastui-editor-contents table th { background: var(--card-2); }
.md-editor .toastui-editor-md-splitter { background: var(--border); }
.md-editor .toastui-editor-mode-switch { background: var(--card-2); border-top: 1px solid var(--border); }
.md-editor .toastui-editor-mode-switch .tab-item { background: var(--card-2); border-color: var(--border); color: var(--muted); }
.md-editor .toastui-editor-mode-switch .tab-item.active { background: var(--card); color: var(--accent); border-bottom-color: var(--card); }
.md-editor .toastui-editor-md-tab-container { background: var(--card-2); }
.md-editor .toastui-editor-md-tab-container .tab-item { background: var(--card-2); border-color: var(--border); color: var(--muted); }
.md-editor .toastui-editor-md-tab-container .tab-item.active { background: var(--card); color: var(--accent); }
.md-editor .toastui-editor-popup { background: var(--card); border-color: var(--border); box-shadow: 0 12px 34px rgba(0, 0, 0, 0.32); }
.md-editor .toastui-editor-popup-body label { color: var(--muted); }
.md-editor .toastui-editor-popup-body input[type="text"] {
  background: var(--bg); color: var(--text); border-color: var(--border);
}
.md-editor .toastui-editor-dropdown-toolbar { background: var(--card); border-color: var(--border); }
.md-editor .toastui-editor-context-menu { background: var(--card); border-color: var(--border); color: var(--text); }
.md-editor .toastui-editor-context-menu li:not(.disabled):hover { background: var(--card-2); }
.md-editor .toastui-editor-tooltip { background: var(--card-2); color: var(--text); }
.md-editor .toastui-editor-ok-button { background: var(--accent); color: #fff; }
.md-editor .toastui-editor-close-button { background: var(--card-2); color: var(--text); }

.widget-palette { list-style: none; margin: 0; padding: 12px; overflow-y: auto; display: flex; flex-direction: column; gap: 8px; }
.widget-option {
  display: flex; align-items: center; gap: 12px; width: 100%; text-align: left;
  background: var(--card-2); border: 1px solid var(--border); border-radius: 8px;
  padding: 10px 12px; cursor: pointer; color: var(--text); font: inherit;
}
.widget-option:hover { border-color: var(--accent); }
.widget-option-icon { font-size: 1.4rem; flex-shrink: 0; }
.widget-option-text { display: flex; flex-direction: column; gap: 2px; }
.widget-palette-foot { display: flex; align-items: center; gap: 10px; padding: 12px; border-top: 1px solid var(--border); }
.btn:disabled { opacity: 0.55; cursor: not-allowed; }

/* ---------------------------------------------------------------------------
   Moderation: sanction banner, the report form, and the admin console
   --------------------------------------------------------------------------- */

/* The banner a warned/restricted user always sees, so a sanction is never a
   silent mystery. Sits at the top of <main>, above the page content. */
.sanction-banner {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px;
  margin-bottom: 18px; padding: 12px 16px;
  border: 1px solid var(--border); border-left: 4px solid var(--accent-2);
  border-radius: var(--radius); background: var(--card-2); color: var(--text);
}
.sanction-banner strong { color: var(--accent-2); }
.sanction-banner.sanction-restrict { border-left-color: var(--danger); }
.sanction-banner.sanction-restrict strong { color: var(--danger); }
.sanction-reason { color: var(--muted); font-size: 0.9rem; }

/* The little "⚑ report" affordance on posts and comments. */
.report-link { color: var(--muted); text-decoration: none; }
.report-link:hover { color: var(--danger); text-decoration: underline; }

/* --- the report form ------------------------------------------------------ */
.report-card { max-width: 640px; margin: 0 auto; }
.report-target {
  background: var(--card-2); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 12px 14px; margin: 14px 0;
}
.report-target-head { display: flex; align-items: baseline; gap: 8px; }
.report-excerpt { margin: 6px 0; color: var(--text); }
.report-reasons { border: 1px solid var(--border); border-radius: var(--radius); padding: 12px 14px; margin: 0 0 16px; }
.report-reasons legend { color: var(--muted); font-size: 0.85rem; padding: 0 6px; }
.report-reason { display: flex; align-items: center; gap: 8px; padding: 5px 0; cursor: pointer; }
.report-detail { display: flex; flex-direction: column; gap: 6px; margin-bottom: 16px; }
.report-detail textarea { width: 100%; }
/* The structured CEL reason: a level picker and a one-word box, in place of the
   free-text one (static/report.js swaps them). */
.report-cel { border: 1px solid var(--border); border-radius: var(--radius); padding: 12px 14px; margin: 0 0 16px; }
.report-cel legend { color: var(--muted); font-size: 0.85rem; padding: 0 6px; }
.report-cel-level { display: flex; flex-direction: column; gap: 6px; margin-bottom: 10px; }
.report-cel-word { display: flex; flex-direction: column; gap: 6px; }
.report-cel-word input { max-width: 220px; }
.report-cel-line { margin: 8px 0 0; }
.report-actions { display: flex; justify-content: flex-end; gap: 10px; }

/* --- the admin console ---------------------------------------------------- */
.report-queue, .sanction-history, .admin-user-list { list-style: none; margin: 0; padding: 0; }
.report-row { padding: 14px 0; border-bottom: 1px solid var(--border); }
.report-row:last-child { border-bottom: none; }
.report-row-head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.report-detail-text { margin: 8px 0; padding-left: 12px; border-left: 3px solid var(--border); color: var(--muted); font-style: italic; }
.report-row-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 10px; }
.resolve-form input[type="text"] { min-width: 180px; }

.admin-search { display: flex; gap: 8px; margin-bottom: 12px; }
.admin-search input[type="text"] { flex: 1; }
.admin-user { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; padding: 10px 0; border-bottom: 1px solid var(--border); }
.admin-user:last-child { border-bottom: none; }
.sanction-form { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-left: auto; }
.sanction-form input[type="text"] { min-width: 200px; }

.sanction-row { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; padding: 10px 0; border-bottom: 1px solid var(--border); }
.sanction-row:last-child { border-bottom: none; }
.sanction-history-reason { color: var(--muted); font-style: italic; }
.badge-warn { border-color: var(--accent-2); color: var(--accent-2); }
.badge-restrict { border-color: var(--danger); color: var(--danger); }

@media (max-width: 640px) {
  .sanction-form { margin-left: 0; width: 100%; }
  .sanction-form input[type="text"] { min-width: 0; flex: 1; }
}

/* ---------------------------------------------------------------------------
   Account security: inline form errors and the settings subsections
   --------------------------------------------------------------------------- */

/* Errors shown next to the form that produced them (validation on a page that
   has no toast to attach to, e.g. an expired reset link). */
.form-error {
  margin: 0 0 14px; padding: 10px 12px;
  border: 1px solid var(--danger); border-left: 4px solid var(--danger);
  border-radius: var(--radius); background: var(--card-2); color: var(--danger);
}

/* A heading separating the account block from the profile form above. The
   settings page groups its controls with <fieldset><legend>, so the account
   rows reuse that rather than inventing another container. */
.settings-subhead {
  margin: 26px 0 12px; padding-top: 18px;
  border-top: 1px solid var(--border); font-size: 1.05rem; color: var(--text);
}
.settings-inline { display: flex; align-items: flex-end; gap: 10px; flex-wrap: wrap; }
.settings-inline label { flex: 1; min-width: 220px; }
.badge-verified { border-color: var(--ok); color: var(--ok); }
.badge-unverified { border-color: var(--accent-2); color: var(--accent-2); }

/* ---------------------------------------------------------------------------
   Legal pages (privacy policy) and the "this is a placeholder" notice
   --------------------------------------------------------------------------- */
.legal-page { max-width: 760px; margin: 0 auto; }
.legal-page h1 { margin-top: 0; }
.legal-page h2 { font-size: 1.05rem; margin: 24px 0 8px; }
.legal-page ul { padding-left: 20px; }
.legal-page li { margin-bottom: 6px; }

.placeholder-banner {
  display: flex; flex-direction: column; gap: 6px;
  margin-bottom: 20px; padding: 12px 16px;
  border: 1px dashed var(--accent-2); border-left: 4px solid var(--accent-2);
  border-radius: var(--radius); background: var(--card-2); color: var(--text);
}
.placeholder-banner strong { color: var(--accent-2); }
.placeholder-banner code { color: var(--muted); }

/* The "under construction" notice for groups and pages
   (pages/_under_construction.html). It is worn by the whole page every shut
   door answers with, and, compactly, by the two dashboard widgets, so it has to
   read as a box inside a card and as a card's whole body.

   The striped band is the whole of the flair: this is a temporary state, and it
   should read as a friendly "not yet" rather than as an error or a warning. It
   is an opaque accent against the notice's own background, so every palette
   gets stripes in its own colour with no palette-specific rule here. */
.under-construction {
  overflow: hidden;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--card-2);
}
.under-construction-tape {
  height: 10px;
  background: repeating-linear-gradient(
    -45deg,
    var(--accent) 0 12px,
    var(--card-2) 12px 24px
  );
  opacity: 0.75;
}
.under-construction-note { padding: 14px 16px; }
.under-construction-note p { margin: 0 0 10px; }
.under-construction-note p:last-child { margin-bottom: 0; }
.under-construction-note strong { color: var(--accent); }

.footer-links { margin-left: 8px; }
.footer-links a { color: var(--accent-2); }

/* The "confirm your email" banner: a nudge, not a punishment, so it uses the
   accent rather than the danger colour. */
.verify-banner {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px;
  margin-bottom: 18px; padding: 12px 16px;
  border: 1px solid var(--border); border-left: 4px solid var(--accent);
  border-radius: var(--radius); background: var(--card-2); color: var(--text);
}
.verify-banner strong { color: var(--accent); }
.verify-banner-actions { display: flex; gap: 12px; margin-left: auto; }

/* Keep like + comment together on the left and push the CEL pill and "report"
   to the far right, so interacting and escalating read as two separate things.
   Scoped to posts: the comment action row is narrow and already crowded. */
.post-actions .cel-pill { margin-left: auto; }
.post-actions .report-link { margin-left: 0; }

/* --- sharing (reblog) and closed comments -------------------------------- */
/* A control that cannot be used is a <span>, not a disabled <button>: browsers
   do not fire tooltips on disabled form controls, and the tooltip is the whole
   explanation. `cursor: help` says "there is a reason here, hover me". */
.post-actions .share-btn { color: var(--muted); }
.post-actions .share-btn:hover { color: var(--accent); }
.post-actions .share-btn.is-shared { color: var(--accent); font-weight: 600; }
.post-actions .share-link.is-off,
.post-actions .comment-count-link.is-off {
  opacity: .55;
  cursor: help;
  text-decoration: none;
}

/* A reblog: who shared it, then the post. The card inside keeps its own
   margins small so the credit line reads as belonging to it. */
.post-share { border-left: 3px solid var(--accent); }
.share-credit {
  display: flex;
  align-items: center;
  gap: .4rem;
  flex-wrap: wrap;
  margin: 0 0 .5rem;
  font-size: .9rem;
  color: var(--muted);
}
.share-credit .share-symbol { font-size: 1rem; }
.share-credit .share-actor,
.share-credit .share-author { color: var(--accent); font-weight: 600; text-decoration: none; }
.share-credit .share-actor:hover,
.share-credit .share-author:hover { text-decoration: underline; }
.share-credit .share-when { margin-left: auto; opacity: .8; }
.share-inner .post-card { padding: 0; background: none; border: 0; }

/* The note left where the comment box would be. */
.comments-off-note {
  display: flex;
  align-items: center;
  gap: .35rem;
  flex-wrap: wrap;
  margin: .5rem 0 0;
}
.comments-off-note form { display: inline; }

/* A checkbox with a sentence after it (compose + settings). */
.checkbox-row {
  display: flex;
  align-items: baseline;
  gap: .45rem;
  flex-wrap: wrap;
  margin: .5rem 0;
  font-size: .9rem;
}
.checkbox-row input[type="checkbox"] { margin: 0; }

/* ---- a group's or page's own page, and its management page --------------- */
/* The sandboxed frame is the whole viewport under the topbar, exactly like a
   person's page (see pages/custom.html). */
.custom-page .main {
  flex: 0 0 auto; max-width: 100%; padding: 0; display: flex; flex-direction: column;
  height: calc(100vh - 56px); background: transparent; box-shadow: none;
}
.custom-page .footer { display: none; }
.custom-frame-wrap { display: flex; flex: 1 1 auto; min-height: 0; }
/* The management page: a column of cards, one per job. */
.admin-block { margin-top: 14px; }
.member-list { margin-bottom: 12px; }
.member-list .row-item { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.danger-zone { border-color: var(--danger); }

/* ---- invitations --------------------------------------------------------
   .invite-fresh is the one-shot panel holding a brand-new link: it exists on
   exactly one page load, because the token is stored hashed. */
.invite-fresh {
  background: var(--card-2); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 14px 16px; margin: 14px 0;
}
.invite-fresh > p:first-child { margin-top: 0; }
.invite-fresh > p:last-child { margin-bottom: 0; }
.invite-copy { display: flex; gap: 8px; align-items: center; margin: 10px 0; }
.invite-copy input {
  flex: 1 1 auto; min-width: 0; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.82rem;
}
.invite-new {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 12px 0 4px; margin-top: 6px; border-top: 1px solid var(--border);
}
.invite-field { display: flex; align-items: center; gap: 6px; font-size: 0.9rem; }
.invite-field input { width: 6.5rem; }
.invite-reusable { margin: 0; }
.invite-list { margin-top: 8px; }
.invite-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.invite-when { color: var(--muted); font-size: 0.85rem; min-width: 7rem; }
.invite-expiry { white-space: nowrap; }
.invite-state { flex: 1 1 auto; font-size: 0.9rem; }
.invite-open { color: var(--ok); }
.invite-used { color: var(--muted); }
.invite-expired { color: var(--muted); opacity: .8; }

/* The "somebody invited you" note on the sign-up card, and the closed door. */
.invite-note {
  background: rgba(94, 234, 213, 0.08); border: 1px solid rgba(94, 234, 213, 0.3);
  border-radius: 8px; padding: 10px 12px; font-size: 0.9rem; margin: 0 0 14px;
}
.invite-closed { background: var(--card-2); border-color: var(--border); color: var(--muted); }

/* ---- messages ------------------------------------------------------------
   Threads are bubbles; the inbox is rows. Deliberately plain: this is the
   backbone, and the interface is expected to change once somebody has looked
   at it for a while. */
.message-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.message-row-unread .message-name { font-weight: 600; }
.message-who {
  display: flex; align-items: center; gap: 8px; min-width: 11rem;
  color: var(--text); text-decoration: none;
}
.message-who:hover .message-name { text-decoration: underline; }
.message-preview { flex: 1 1 12rem; min-width: 0; }
.message-when { white-space: nowrap; }

.message-card { display: flex; flex-direction: column; }
.bubbles {
  list-style: none; margin: 0; padding: 4px 0; display: flex;
  flex-direction: column; gap: 8px; max-height: 55vh; overflow-y: auto;
}
.bubble {
  max-width: min(34rem, 78%);
  padding: 8px 12px;
  border-radius: 14px;
  background: var(--card-2);
  border: 1px solid var(--border);
}
.bubble-theirs { align-self: flex-start; border-bottom-left-radius: 4px; }
.bubble-mine {
  align-self: flex-end; border-bottom-right-radius: 4px;
  background: var(--accent); color: #ffffff; border-color: transparent;
}
.bubble-mine .bubble-when { color: rgba(255, 255, 255, 0.75); }
.bubble-body { display: block; }
.bubble-when { display: block; margin-top: 3px; font-size: 0.75rem; }

.message-form { display: flex; gap: 8px; align-items: flex-end; margin-top: 12px; }
.message-form textarea { flex: 1 1 auto; resize: vertical; min-height: 2.6rem; }

/* ---- pages & groups ------------------------------------------------------ */
/* The page shell fills the container, so groups and pages line up with the nav
   and the topbar like every other page (it used to cap itself at 880px). */
.page-shell { max-width: 100%; }
.page-head h1 { margin: 0 0 6px; }
.page-about { margin: 6px 0 0; }
.page-meta { margin-top: 8px; }
.page-actions { margin-top: 12px; }
.wall-form { display: flex; gap: 8px; align-items: flex-end; }
.wall-form textarea { flex: 1 1 auto; resize: vertical; min-height: 2.4rem; }
/* The wall's composer: one form, three types (see static/composer.js). */
.wall-form.stack-form { flex-direction: column; align-items: stretch; }
.wall-form.stack-form textarea { width: 100%; }
.wall-type { font-size: 0.85rem; }
.wall-image { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.wall-image input[type="text"] { flex: 1 1 200px; min-width: 0; }
.wall-form-foot { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.wall-form-foot .btn { margin-left: auto; }
/* A post that speaks for a page rather than a person. */
.badge-page-author { border-color: var(--accent); color: var(--accent); }
/* The CEL indicator: a small, quiet link to the explainer. It sits with the
   actions, not the byline -- it is reference, not part of the post. */
.cel-pill {
  display: inline-block; border: 1px solid var(--border); border-radius: 999px;
  padding: 1px 8px; font-size: 0.72rem; line-height: 1.5; color: var(--muted);
  text-decoration: none; white-space: nowrap;
}
.cel-pill:hover { color: var(--accent); border-color: var(--accent); text-decoration: none; }
.menu-item-form { display: flex; gap: 6px; align-items: center; padding: 4px 12px; }
.menu-item-form select { background: var(--bg); color: var(--text); border: 1px solid var(--border); border-radius: 6px; padding: 4px 6px; }
.page-admin { margin-top: 12px; }
.page-admin details > summary { cursor: pointer; }
.page-admin .stack-form { margin-top: 10px; }
.page-admin .invite-copy { margin-top: 12px; }
.page-columns {
  display: grid; grid-template-columns: 1fr 260px; gap: 14px; margin-top: 14px;
  align-items: start;
}
@media (max-width: 760px) { .page-columns { grid-template-columns: 1fr; } }
.page-members .row-item { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }

.wall-internal { margin: 0; font-size: .85rem; }
.internal-flag {
  list-style: none; margin: 4px 0 0; padding: 3px 8px; border-radius: 6px;
  font-size: .8rem; color: var(--muted); background: var(--card-2);
  border: 1px dashed var(--border);
}
.page-pending { margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--border); }
.page-pending .row-item { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }

/* A page's picture, in the same leaf shape as the people cards -- so a group
   looks like a member of the site rather than a logo stuck on top of it. */
.leaf { width: 76px; height: 76px; border-radius: 18px 0 18px 0; object-fit: cover; display: block; }
.leaf-letter {
  display: flex; align-items: center; justify-content: center;
  background: var(--card-2); border: 1px solid var(--border);
  font-size: 2rem; color: var(--text);
}
.leaf-lg { width: 104px; height: 104px; }
.page-head-top { display: flex; gap: 16px; align-items: center; }
.page-head-top h1 { margin: 0; }
@media (max-width: 520px) { .page-head-top { flex-direction: column; align-items: flex-start; } }

/* Interests on a page. The sign is about *you*: + adds the interest to your
   own list, × means you already have it and takes it off. Plain text for
   somebody who is not signed in -- there is nowhere to put it. */
.interest-row { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; margin-top: 10px; }
.interest-pill { font-family: inherit; font-size: 0.8rem; cursor: pointer; }
.interest-pill:hover { border-color: var(--accent); }
.interest-pill.is-mine { border-color: var(--accent); color: var(--accent); }

/* The master pages: a search box, then cards. */
.search-row { display: flex; gap: 8px; align-items: center; margin-top: 10px; }
.search-row input[type="search"] { flex: 1 1 auto; }
.page-grid { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 12px; }
.page-card { display: flex; gap: 12px; align-items: flex-start; }
.page-card-photo { flex: 0 0 auto; }
.page-card-photo .leaf { width: 64px; height: 64px; }
.page-card-body { min-width: 0; flex: 1 1 auto; }
.page-card-title { margin: 0; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-weight: 600; }
.page-card-title a { color: var(--text); }
.page-card-about { margin: 4px 0 0; }
.page-card-count { margin: 6px 0 0; }
.page-card .content-tags { margin-top: 6px; }
.page-card-action { margin-top: 8px; }
@media (max-width: 520px) { .page-card { flex-direction: column; } }

/* ---- the media center and albums (media/index.html, media/album.html) ----

   The grid is pictures, square-cropped so a wall of them reads as a contact
   sheet; each tile opens the slideshow at itself. The album tools live in a
   <details> per tile rather than a form per photo, so a hundred pictures do
   not cost a hundred selects. */
.media-shell { display: flex; flex-direction: column; gap: 16px; }
.media-grid {
  list-style: none; margin: 0; padding: 0; display: grid;
  grid-template-columns: repeat(auto-fill, minmax(124px, 1fr)); gap: 8px;
}
.media-tile { position: relative; }
.media-tile-open {
  display: block; position: relative; overflow: hidden; border-radius: 10px;
  aspect-ratio: 1 / 1; background: var(--card-2);
}
.media-thumb { width: 100%; height: 100%; object-fit: cover; display: block; }
.media-tile-open:hover .media-thumb { opacity: 0.88; }
.media-tile-badge {
  position: absolute; right: 6px; bottom: 6px; padding: 0 8px;
  border-radius: 999px; background: rgba(0, 0, 0, 0.6); color: #fff;
  font-size: 0.75rem; line-height: 1.6;
}
.media-tile-tools { position: absolute; left: 6px; top: 6px; }
.media-tile-tools > summary {
  list-style: none; cursor: pointer; width: 26px; height: 26px; display: flex;
  align-items: center; justify-content: center; border-radius: 999px;
  background: rgba(0, 0, 0, 0.55); color: #fff; font-weight: 700;
}
.media-tile-tools[open] > summary { background: var(--accent); }
.media-tile-tools > form {
  position: absolute; left: 0; top: 32px; z-index: 5; width: 230px;
  background: var(--card); border: 1px solid var(--border); border-radius: 10px;
  padding: 10px; box-shadow: 0 12px 30px rgba(0, 0, 0, 0.3);
}
.album-grid {
  list-style: none; margin: 0; padding: 0; display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 14px;
}
.album-card { display: flex; flex-direction: column; gap: 8px; }
.album-cover { display: block; aspect-ratio: 4 / 3; border-radius: 12px; overflow: hidden; background: var(--card-2); }
.album-cover img { width: 100%; height: 100%; object-fit: cover; display: block; }
.album-cover-empty { display: flex; align-items: center; justify-content: center; height: 100%; font-size: 2rem; }
.album-card-body { display: flex; flex-direction: column; gap: 4px; }
.album-name { font-weight: 600; }
.album-tools > summary { cursor: pointer; }
.album-tools > form, .album-tools > ul { margin-top: 8px; }
.media-form-row { display: flex; gap: 12px; flex-wrap: wrap; }
.media-form-row .compose-field { flex: 1 1 200px; }
@media (max-width: 600px) {
  .media-grid { grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); }
}

/* ---- member ranks (pages/manage.html) ---- */
/* One control per member: the rank they hold is the selected option, and
   nothing happens until Set is pressed, so brushing past a dropdown cannot
   change somebody's standing on a group. */
.rank-form { display: inline-flex; align-items: center; gap: 6px; margin-right: 4px; }
.rank-select { display: inline-flex; align-items: center; gap: 6px; }
.rank-select select {
  background: var(--bg); color: var(--text); border: 1px solid var(--border);
  border-radius: 8px; padding: 4px 8px; font: inherit; font-size: 0.82rem;
}
.member-list .row-item { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
