:root {
  --bg: #0a0a0f;
  --surface: #1a1a2e;
  /* The recessed ground and the hairline, spelled once.

     These two names were already used by this sheet and by operator.html's own styles -- 20
     reads between them -- while being declared only in the landing page's and the learn
     pages' `:root`. A `border` shorthand holding an undefined custom property is invalid at
     computed-value time, so every longhand falls back to its initial value and
     `border-style` initial is `none`: measured in Chrome, `border: 1px solid var(--line)`
     computed to `none` at `0px`, which means the moderation console drew no border on any
     card, row, tile or field. It stayed unnoticed because the two pages where the tokens do
     exist look correct, and CSS treats an undefined token as an absence rather than an error.

     The values are copied from web/site/index.html and learn/learn.css rather than invented,
     so one name means one colour across all three documents. test_tokens.js fails the build
     if a sheet reads a token that no loaded stylesheet declares, which is the check that
     would have caught this the day it was written. */
  --surface-2: #12121f;
  --line: #2a2a4a;
  --text: #f0f0f5;
  --text-secondary: #8888aa;
  --accent: #ff2d78;
  --accent-light: rgba(255,45,120,0.15);
  --success: #00d4ff;
  --spotlight: #ffe600;  /* the map's neon yellow, reused for the AI's featured voice */
  /* The map's four kinds of marker, in four colours that are all already in the
     basemap (neon-nights.json draws roads in --pin-dating, water in --pin-friend,
     labels in --pin-society and one violet layer in --pin-crowd). A person on the
     dating lens, a person on the friends lens, a count of people, and a society: four
     questions the map gets asked, and one colour each to answer them. KB, 2026-10-08:
     "We should make that colour different and unique and under friends the societies
     and users should have their distinct colours as well (must be compatible with our
     theme!)" — compatible meant not inventing a hue the tiles do not already use. */
  --pin-dating: #ff2d78;
  --pin-friend: #00d4ff;
  --pin-crowd: #7b2ff7;
  --pin-society: #ffe600;
  --radius: 16px;
  --shadow-card: 0 2px 12px rgba(0,0,0,0.4);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  --safe-top: env(safe-area-inset-top, 0px);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
/* The type scale is rem for exactly one reason: this dial. A phone renders
   1rem as 16px and every size is the pixel value it always was - nothing
   moves. A wide screen turns the whole app up one notch with a single
   number, because the app was drawn at phone sizes and shown whole on a
   1843px desktop: "Is everything very small? Maybe its the font sizes etc"
   (KB, 2026-10-11). One dial, not 243 rewrites. */
html { font-size: 16px; }
@media (min-width: 768px) { html { font-size: 18px; } }

body {
  font-family: -apple-system, BlinkMacSystemFont, 'SF Pro', 'Inter', system-ui, sans-serif;
  background: var(--bg); color: var(--text);
  overflow: hidden; height: 100vh; height: 100dvh; -webkit-font-smoothing: antialiased;
}
.hidden { display: none !important; }

/* THE BOX RULE. A tick box is drawn by this stylesheet, never by the browser.

   KB, 2026-10-04: "Boxes in the dealbreakers should also be aligned and fixed with the
   same col". KB, 2026-10-11, pointing at the box on the account-deletion sheet: "the box
   needs to look like all of our other boxes! How can we make that rule explicit?"

   Explicit here means keyed on the element rather than on a class, so a box that arrives
   without the right class still arrives in the house style. Before this the recipe was
   written out three times (`.edit-check`, `.mine-notif-row`, `.soc-event-flag`) while a
   fourth box, the one on the deletion sheet, had none of it and came up as Chrome mobile's
   white square with a blue tick: a system control sitting in the middle of a page that has
   its own colour.

   Selected means filled, with no tick drawn inside it (KB, 2026-10-04: "Remove the tick
   marks. The red fill for selection is perfect").

   test_box_rule.js fails the build if a second recipe appears in this file. That is the
   mechanism by which this stays true rather than being a memory. */
input[type="checkbox"] {
  appearance: none;
  -webkit-appearance: none;
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  margin: 0;
  border-radius: 5px;
  border: 1px solid rgba(255, 255, 255, 0.18);
  background: rgba(255, 255, 255, 0.05);
  cursor: pointer;
  transition: border-color .15s ease, background .15s ease, box-shadow .15s ease;
}
input[type="checkbox"]:hover { border-color: var(--accent); }
input[type="checkbox"]:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-light);
}
input[type="checkbox"]:checked {
  background: var(--accent);
  border-color: var(--accent);
  box-shadow: 0 0 10px rgba(255, 45, 120, 0.45);
}

/* Ceremony */
.ceremony { position: fixed; inset: 0; z-index: 1000; background: rgba(10,10,15,0.95); display: flex; align-items: center; justify-content: center; transition: opacity 0.6s ease, transform 0.6s ease; cursor: pointer; }
.ceremony.fade-out { opacity: 0; transform: scale(1.05); pointer-events: none; }
.ceremony-inner { text-align: center; padding: 2rem; }
.ceremony-logo { width: 64px; height: 64px; margin: 0 auto 2rem; display: flex; align-items: center; justify-content: center; box-shadow: 0 0 30px rgba(255,45,120,0.5); border-radius: 18px; overflow: hidden; animation: logo-pop 0.5s cubic-bezier(0.175,0.885,0.32,1.275) both; }
@keyframes logo-pop { from { transform: scale(0); opacity: 0; } to { transform: scale(1); opacity: 1; } }
.ceremony-lines { min-height: 140px; }
.ceremony-line { color: var(--text-secondary); font-size: 0.9375rem; line-height: 2.2; opacity: 0; transform: translateY(10px); transition: opacity 0.4s ease, transform 0.4s ease; }
.ceremony-line.visible { opacity: 1; transform: translateY(0); }
.ceremony-line.done { color: var(--text); }
.ceremony-progress { width: 200px; height: 3px; margin: 1.5rem auto 0; background: #2a2a4a; border-radius: 2px; overflow: hidden; }
.ceremony-progress-fill { height: 100%; width: 0%; background: var(--accent); transition: width 5s linear; }
.ceremony.active .ceremony-progress-fill { width: 100%; }

/* Map */
.map { position: fixed; inset: 0; z-index: 1; }

/* The collapsed attribution is MapLibre's own button; it shipped as a white pill, which
   reads as a browser element over a map that is otherwise all our own palette. */
.map .maplibregl-ctrl-attrib {
  background: rgba(10, 12, 24, 0.62);
  color: #cfd3e6;
  border-radius: 12px;
}
.map .maplibregl-ctrl-attrib a { color: #cfd3e6; }

/* At phone width the bottom edge of the map is behind the sheet's peek, which would bury
   the button that opens the attribution. A licence line nobody can reach is not a licence
   line, so it sits just above the peek instead. The height is the measured peek (121px at
   390x701, sheet top 580 of innerHeight 701); when a profile is open the sheet covers the
   map entirely, which is unavoidable and also irrelevant, because the map is not what is
   being read then. */
@media (max-width: 767px) {
  .map .maplibregl-ctrl-bottom-right { bottom: 121px; }
}

/* The map's honest state: one card over a field with nothing to plot. Below the
   sheet and the top bar, and it never intercepts a map gesture. The explicit
   .map-note.hidden is load-bearing - `.hidden` alone loses to `.map-note`'s own
   display, which is how a "hidden" panel ends up on screen. */
.map-note {
  position: fixed; bottom: calc(var(--safe-bottom) + 132px); left: 50%;
  transform: translateX(-50%);
  z-index: 12; max-width: min(420px, calc(100vw - 48px));
  padding: 7px 12px; border-radius: 10px;
  background: rgba(12, 12, 26, 0.78);
  color: var(--text-secondary); font-size: 0.75rem; line-height: 1.4;
  text-align: center; pointer-events: none;
}
.map-note.hidden { display: none; }
.map-note p { margin: 0; }
@media (min-width: 768px) {
  .map-note { left: calc(340px + (100vw - 340px) / 2); }
}

/* GPS locate. The crosshair sits on the map's right edge, above the compact
   attribution, where Google Maps puts its own blue-dot button and where a
   thumb lands without hunting. It rides above the sheet's peek and clear of
   the map-note line, so the two captions never trade pixels. */
.locate-btn {
  position: fixed; right: 14px; z-index: 13;
  bottom: calc(var(--safe-bottom) + 132px);
  width: 42px; height: 42px; border-radius: 50%;
  background: var(--surface); border: 1.5px solid #2a2a4a;
  color: var(--text-secondary);
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.45); cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease;
}
/* Lit while the watch runs, in the friends-lens cyan: the dot on the map is
   the same colour, so button and dot read as one fact. */
.locate-btn.active { color: var(--pin-friend); border-color: var(--pin-friend); }
/* Waiting on the permission prompt: the glyph pulses, because the tap did
   land and an answer is owed. */
.locate-btn.waiting svg { animation: bell-pulse 1.6s ease-in-out infinite; }
/* Its own display rule hides it: .locate-btn sets display: flex, and a rule
   with its own display beats a bare .hidden. */
.locate-btn.hidden { display: none; }
.locate-note {
  position: fixed; right: 14px; z-index: 13;
  bottom: calc(var(--safe-bottom) + 182px);
  max-width: min(300px, calc(100vw - 76px));
  padding: 7px 12px; border-radius: 10px;
  background: rgba(12, 12, 26, 0.85);
  color: var(--text-secondary); font-size: 0.75rem; line-height: 1.4;
  text-align: right; pointer-events: none;
}
.locate-note.hidden { display: none; }
.locate-note p { margin: 0; }
/* The blue dot. Out of the normal flow like every other marker element
   (check_pin_placement.py reads this class too: MapLibre positions a marker
   by transform alone, and a marker left in flow lands a dot-height per marker
   below its coordinate). pointer-events: none because the dot is a fact about
   you, not something to click; the pins under it stay tappable. */
.tm-locate {
  position: absolute;
  width: 16px; height: 16px; border-radius: 50%;
  background: var(--pin-friend); border: 3px solid #eaf9ff;
  box-shadow: 0 0 10px rgba(0, 212, 255, 0.7), 0 1px 4px rgba(0, 0, 0, 0.5);
  pointer-events: none;
}

/* List panel */
.list-panel {
  position: fixed; top: 0; left: 0; bottom: 0; z-index: 15;
  width: 340px; background: var(--bg);
  border-right: 1px solid #2a2a4a;
  display: flex; flex-direction: column;
  padding-top: calc(var(--safe-top) + 70px);
}
.list-header {
  padding: 0 16px 12px;
}
.list-header-top {
  display: flex; align-items: baseline; gap: 8px; margin-bottom: 10px;
}
.list-header h2 { font-size: 1.125rem; font-weight: 700; }
.list-count {
  font-size: 0.75rem; color: var(--accent); font-weight: 700;
  background: var(--accent-light); padding: 2px 8px; border-radius: 6px;
}
.list-search {
  display: flex; align-items: center; gap: 8px;
  background: #12121f; border: 1px solid #2a2a4a; border-radius: 10px;
  padding: 10px 12px; transition: border-color 0.2s ease;
}
.list-search:focus-within { border-color: var(--accent); }
.list-search svg { flex-shrink: 0; color: var(--text-secondary); }
.list-search input {
  flex: 1; border: none; background: none; outline: none;
  font-size: 0.875rem; color: var(--text); font-family: inherit;
}
.list-search input::placeholder { color: #555; }
/* The demo cast's one-time acknowledgement. It reuses the confirm dialog's frame
   because it is the same kind of thing: the app stopping and waiting for an
   answer. The backdrop carries no handler, so clicking past it is not a way out. */
.demo-box h3 { font-size: 1.1875rem; margin: 0 0 12px; }
.demo-box p { color: #c9c9dd; }
.demo-box .btn-done {
  /* An acknowledgement has to look like something you press. .btn-done is a
     quiet underlined link elsewhere ("Back to map"), which here read as a third
     sentence of copy. */
  width: 100%;
  margin-top: 18px;
  padding: 12px 24px;
  background: var(--accent);
  color: #fff;
  border: 0;
  border-radius: 12px;
  font-size: 0.9375rem;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
}

/* What your AI says about you, on your own page. Framed like the quote on a
   match card, because it is the same object seen from the other side. */
.mine-agent {
  margin: 0 14px 14px;
  padding: 12px 14px;
  border: 1px solid var(--line);
  border-left: 3px solid var(--spotlight);
  border-radius: 12px;
  background: rgba(255, 255, 255, .03);
}
.mine-agent-head {
  font-size: 0.6875rem; text-transform: uppercase; letter-spacing: .5px;
  color: var(--text-secondary); margin-bottom: 8px;
}
.mine-agent-text {
  margin: 0; color: var(--text); font-size: 0.9062rem; line-height: 1.55;
  font-style: italic;
}
.mine-agent-text::before { content: "\201C"; color: var(--spotlight); font-weight: 700; }
.mine-agent-text::after { content: "\201D"; color: var(--spotlight); font-weight: 700; }
.mine-agent-who { margin: 10px 0 0; font-size: 0.75rem; color: var(--text-secondary); line-height: 1.4; }
.list-scroll {
  flex: 1; overflow-y: auto; padding: 0 12px 100px;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: thin; scrollbar-color: #3a3a5a transparent;
}
.list-scroll::-webkit-scrollbar { width: 6px; }
.list-scroll::-webkit-scrollbar-track { background: transparent; }
.list-scroll::-webkit-scrollbar-thumb { background: #3a3a5a; border-radius: 3px; }
.list-scroll::-webkit-scrollbar-thumb:hover { background: var(--accent); }
.list-card {
  background: var(--surface); border-radius: 14px; padding: 14px;
  margin-bottom: 10px; display: flex; align-items: center; gap: 12px;
  cursor: pointer; border: 1px solid #2a2a4a;
  transition: transform 0.15s ease, border-color 0.2s ease;
  animation: card-in 0.3s cubic-bezier(0.32,0.72,0,1) both;
}
.list-card:nth-child(2) { animation-delay: 0.04s; }
.list-card:nth-child(3) { animation-delay: 0.08s; }
.list-card:nth-child(4) { animation-delay: 0.12s; }
.list-card:active { transform: scale(0.97); }
.list-card.active { border-color: var(--accent); box-shadow: 0 0 12px rgba(255,45,120,0.15); }
@keyframes card-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
.list-card .lc-avatar {
  width: 44px; height: 44px; border-radius: 12px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center; font-size: 1.25rem;
}
.list-card .lc-info { flex: 1; min-width: 0; }
/* The row is a flex line so the name can be the only thing that truncates. It used to carry
   nowrap + overflow:hidden + text-overflow:ellipsis itself, which is what turned the check
   mark into "✓ ..." -- a rule written for people with long names was eating the badge, and
   the badge is the one item here a reader cannot guess at. */
.list-card .lc-info h3 {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  font-size: 0.9375rem;
  font-weight: 600;
  min-width: 0;
}
/* flex-wrap is the whole argument here. The first version let the name shrink to make room
   for the badge, which fixed the badge and truncated "Peerone" to "Pee..." -- trading the
   one thing a reader can recover (open the profile, the name is on it) for the one they
   cannot. With wrap, a long name still ellipsizes on its own, and a badge that will not fit
   drops to a second line instead of squeezing its neighbour. */
.lc-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
           min-width: 0; max-width: 100%; flex: 0 1 auto; }
.lc-chat-badge { font-size: 0.75rem; margin-left: 4px; }
/* Societies two people share, on the board card. Nothing renders when there are
   none; the server only sends what is common. */
.lc-soc { color: var(--pin-society); font-weight: 600; }
.list-card .lc-info .lc-avatar img { width: 100%; height: 100%; border-radius: 12px; object-fit: cover; }
.list-card .lc-info p { font-size: 0.75rem; color: var(--text-secondary); margin-top: 2px; }
.list-card .lc-score {
  background: var(--accent-light); color: var(--accent);
  font-size: 0.8125rem; font-weight: 700; padding: 4px 8px; border-radius: 8px; flex-shrink: 0;
}
.list-card .lc-actions {
  display: flex; flex-direction: column; gap: 4px; flex-shrink: 0;
}
.list-card .lc-actions button {
  width: 28px; height: 28px; border-radius: 8px; border: none;
  background: #2a2a4a; color: var(--text-secondary); cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  font-size: 0.75rem; transition: background 0.15s ease, color 0.15s ease;
}
.list-card .lc-actions button:hover { background: var(--accent); color: white; }
.list-card .lc-actions .btn-archive:hover { background: #6c5ce7; }
.list-card .lc-actions .btn-delete:hover { background: #e74c3c; }
.list-empty {
  text-align: center; padding: 40px 20px; color: var(--text-secondary); font-size: 0.875rem;
}

/* Tabs */
.list-tabs { display: flex; gap: 4px; margin-bottom: 10px; }
.list-tab {
  flex: 1; padding: 8px; border: none; border-radius: 8px;
  background: #12121f; color: var(--text-secondary); font-size: 0.8125rem;
  font-weight: 600; cursor: pointer; transition: all 0.2s ease;
}
.list-tab.active { background: var(--accent); color: white; }
.list-tab:hover:not(.active) { background: #2a2a4a; }

/* Minimize button */
.list-header-actions { margin-left: auto; }
.btn-minimize {
  width: 30px; height: 30px; border-radius: 8px; border: 1px solid #2a2a4a;
  background: var(--surface); color: var(--text-secondary); cursor: pointer;
  display: flex; align-items: center; justify-content: center; font-size: 0.75rem;
  transition: all 0.15s ease;
}
.btn-minimize:hover { border-color: var(--accent); color: var(--accent); }

/* The archive, which is a state of the list rather than a destination. It appears when
   there is something in it and disappears when there is not, so a person who has never
   archived anyone never sees the word. */
.btn-archived {
  height: 30px; padding: 0 10px; border-radius: 8px; border: 1px solid #2a2a4a;
  background: var(--surface); color: var(--text-secondary); cursor: pointer;
  font-size: 0.75rem; font-weight: 600; margin-right: 6px; transition: all 0.15s ease;
}
.btn-archived:hover { border-color: var(--accent); color: var(--accent); }
.btn-archived.on { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn-archived.hidden { display: none; }

/* The Socials feed, the other half of the match column. Cards stack vertically and
   read top-down rather than centring a face, because what they carry is a paragraph
   with a date on it. */
.feed-filter { display: flex; gap: 6px; margin-bottom: 12px; }
.feed-chip {
  padding: 6px 12px; border-radius: 999px; border: 1px solid #2a2a4a;
  background: #12121f; color: var(--text-secondary); font-size: 0.7812rem;
  font-weight: 600; cursor: pointer; transition: all 0.2s ease;
}
.feed-chip.active { background: var(--accent); border-color: var(--accent); color: #fff; }
.feed-card {
  background: var(--surface); border: 1px solid #2a2a4a; border-radius: 14px;
  padding: 13px 14px; margin-bottom: 10px; cursor: pointer;
  transition: transform 0.15s ease, border-color 0.2s ease;
  animation: card-in 0.3s cubic-bezier(0.32,0.72,0,1) both;
}
.feed-card:nth-child(2) { animation-delay: 0.04s; }
.feed-card:nth-child(3) { animation-delay: 0.08s; }
.feed-card:active { transform: scale(0.97); }
.feed-card:hover { border-color: #3a3a5a; }
.feed-card-top { display: flex; align-items: center; gap: 8px; }
.feed-soc {
  font-size: 0.8438rem; font-weight: 700; color: var(--text); min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.feed-tag {
  margin-left: auto; flex-shrink: 0; font-size: 0.6562rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.04em; padding: 2px 7px;
  border-radius: 999px; border: 1px solid #2a2a4a; color: var(--text-secondary);
}
.feed-tag.mine { color: var(--accent); border-color: rgba(255,45,120,0.4); }
.feed-where { font-size: 0.75rem; color: var(--text-secondary); margin-top: 3px; }
.feed-text { font-size: 0.8438rem; line-height: 1.45; color: var(--text); margin: 8px 0 0; }
.feed-event { margin-top: 9px; font-size: 0.7812rem; font-weight: 600; color: var(--accent); }
.feed-event.past { color: var(--text-secondary); font-weight: 400; }
.feed-posted { margin-top: 8px; font-size: 0.7188rem; color: var(--text-secondary); }
.feed-empty { font-size: 0.8438rem; line-height: 1.5; color: var(--text-secondary); padding: 8px 2px 20px; }

/* Confirm modal */
.confirm-modal { position: fixed; inset: 0; z-index: 200; display: flex; align-items: center; justify-content: center; }
/* The profile sheet opened from inside the society modal rides above it, and its Close
   lands back on the roster (KB, 2026-10-11). The actions row rides with the sheet. */
body.sheet-over-modal #sheet { z-index: 210; }
body.sheet-over-modal .match-actions { z-index: 215; }
.confirm-backdrop { position: absolute; inset: 0; background: rgba(0,0,0,0.6); }
.confirm-box {
  position: relative; background: var(--surface); border-radius: 16px;
  padding: 24px; max-width: 320px; width: 90%; border: 1px solid #2a2a4a;
  box-shadow: 0 20px 60px rgba(0,0,0,0.5);
  animation: win-pop 0.45s cubic-bezier(.34,1.56,.64,1) both;
}
.confirm-box p { font-size: 0.9375rem; line-height: 1.5; margin-bottom: 20px; }
/* The announcement box, dressed (KB, 2026-10-14: "this announcement board is
   so lame. We should make it transparent maybe 15% and also have a sleek
   themed edges"). Fifteen percent off the panel so the board shows through,
   blur behind it so the see-through reads as glass rather than a bug, and the
   edge is the site's own pink-to-cyan rather than a generic border - the two
   colours are the map's own marker language, so the popup wears the product.
   The fill clips to the padding box and the gradient to the border box, which
   is what leaves the ring visible as the border. */
/* The frame turns (KB, 2026-10-16: "Is there a way to make the box here
   have the colourful outer frame move clockwise?"). Yes: the straight
   pink-to-cyan becomes a conic sweep whose start angle animates, and an
   increasing conic angle IS clockwise. The padding-box/border-box two-
   layer trick carries over unchanged - only the outer paint turns. A
   browser without @property simply never advances the angle: the frame
   stays a pretty static sweep rather than breaking, which is the right
   shape of fallback for a thing that is pure delight. And a person who
   asked their system for less motion gets exactly what they asked for. */
@property --tm-angle { syntax: "<angle>"; initial-value: 0deg; inherits: false; }
@keyframes tm-frame-turn { to { --tm-angle: 360deg; } }
#announce-modal .announce-box {
  background:
    linear-gradient(rgba(26, 26, 46, 0.85), rgba(26, 26, 46, 0.85)) padding-box,
    conic-gradient(from var(--tm-angle), var(--accent), var(--success), var(--accent)) border-box;
  border: 2px solid transparent;
  animation: tm-frame-turn 6s linear infinite;
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  box-shadow: 0 0 24px rgba(255, 45, 120, 0.22), 0 0 56px rgba(0, 212, 255, 0.12),
              0 20px 60px rgba(0, 0, 0, 0.45);
}
@media (prefers-reduced-motion: reduce) {
  #announce-modal .announce-box { animation: none; }
}
/* The team compose surface and its door (KB, 2026-10-14: "a more on brand
   button... a back button in yellow etc! Modern and intuitive not text
   descriptions"). The door pill is the sign-out family filled with the
   brand's own pink-to-cyan, so the one action that opens something looks
   like it. On the surface the arrow is the map's neon yellow, the clip is a
   round bordered button, and the send pill waits disabled until there is
   something to send. Nothing on it is a sentence. */
.btn-team {
  background: linear-gradient(135deg, rgba(255, 45, 120, 0.92), rgba(255, 92, 149, 0.92));
  border: none; color: #fff;
}
.btn-team:hover { background: linear-gradient(135deg, #ff2d78, #ff5c95);
  box-shadow: 0 0 18px rgba(255, 45, 120, 0.4); }
.team-head { display: flex; align-items: center; gap: 12px; margin: 4px 0 14px; }
.team-head h2 { font-size: 1.125rem; font-weight: 700; margin: 0; }
.team-back {
  background: none; border: none; color: var(--spotlight);
  font-size: 1.5rem; line-height: 1; cursor: pointer;
  padding: 6px 12px 6px 4px; border-radius: 10px;
}
.team-back:active { transform: translateX(-2px); }
#sheet-team textarea {
  width: 100%; box-sizing: border-box; min-height: 132px; resize: vertical;
  background: var(--surface-2); border: 1px solid var(--line); color: var(--text);
  border-radius: 14px; padding: 14px; font: inherit; font-size: 0.9375rem;
}
#sheet-team textarea:focus { outline: none; border-color: rgba(255, 45, 120, 0.55); }
.team-attach { display: flex; gap: 10px; margin: 12px 0; flex-wrap: wrap; }
.team-thumb {
  width: 72px; height: 72px; border-radius: 12px; position: relative;
  background-size: cover; background-position: center;
  border: 1px solid var(--line);
}
.team-thumb-x {
  position: absolute; top: -7px; right: -7px; width: 22px; height: 22px;
  border-radius: 50%; border: none; background: #e74c3c; color: #fff;
  /* rem, not px: the type scale is one dial on html, so a px here stops scaling on
     desktop. This rule was the reason test_ux_audit_fixes.js had been red since 2026-10-14
     while nothing ran it. */
  font-size: 0.8125rem; line-height: 1; cursor: pointer; padding: 0;
}
.team-actions { display: flex; gap: 12px; align-items: center; }
.team-clip {
  width: 46px; height: 46px; border-radius: 50%; flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--line); color: var(--text-secondary);
  font-size: 1.15rem; cursor: pointer; background: var(--surface-2);
}
.team-clip:active { transform: scale(0.94); }
.btn-send-team {
  flex: 1; padding: 13px; border: none; border-radius: 14px; cursor: pointer;
  background: linear-gradient(135deg, var(--accent), #ff5c95);
  color: #fff; font-size: 0.9375rem; font-weight: 700;
  transition: opacity 0.15s ease, transform 0.15s ease;
}
.btn-send-team:disabled { opacity: 0.35; cursor: default; }
.btn-send-team:not(:disabled):active { transform: scale(0.98); }
.confirm-actions { display: flex; gap: 10px; }
.btn-danger {
  flex: 1; padding: 12px; border: none; border-radius: 12px;
  background: #e74c3c; color: white; font-size: 0.875rem; font-weight: 600;
  cursor: pointer; transition: transform 0.15s ease;
}
.btn-danger:active { transform: scale(0.96); }

/* Your own surface. The gallery renders here and nowhere else - see the note
   on #sheet-mine in index.html for why it left the peek card. */
/* The bar's furniture is neutral, and pink is a state rather than a coat of paint. Every
   road on the map is --accent, so a bar of pink-outlined pink-worded buttons was a second
   copy of the road network lying on top of the first: nothing in it could mean anything,
   because everything in it said the same thing. What is left pink is what needs you: the
   bell's count, and the board switch when the board is shut. Yellow stays on Societies,
   because that colour is already the map's answer to "where are the societies".
   KB, 2026-10-10: "Dont you think theres too much red up there? Since every road on the
   map is red..." -- yes. */
/* Every control in the bar is the same chip: a faint lift off the bar rather than a box
   drawn with a hard line. They were not -- You had a filled ground and Connect did not,
   which is a leftover from when pink-on-pink made one look primary and the other
   secondary; once both went neutral the difference read as a mistake. KB, 2026-10-10:
   "The Connect looks different than you".
   Colour now lives in the glyph and in the states, not in the boxes. */
.topbar-home, .topbar-you, .topbar-connect, .topbar-bell, .topbar-mode,
.topbar-societies, .topbar-toggle {
  height: 38px; border-radius: 12px; flex-shrink: 0; cursor: pointer;
  border: 1px solid rgba(255, 255, 255, 0.09);
  background: rgba(255, 255, 255, 0.05);
  color: var(--text);
  font-family: inherit; font-size: 0.8125rem; font-weight: 700;
}
.topbar-you { padding: 0 14px; }
.topbar-you:active { transform: scale(0.96); }

/* Connect button: sits beside You in the top-right, secondary to it. */
.topbar-connect { padding: 0 14px; }
.topbar-connect:active { transform: scale(0.96); }

/* Societies: the friends lens' companion button (societies.js). Yellow to match its
   pins rather than pink to match the other buttons, so the bar and the map agree about
   which one opens the societies. Hidden until the lens is friends. */
.topbar-societies {
  position: relative; padding: 0 12px;
  /* The colour stays because the map uses it for every society; it stays on the glyph
     rather than the box so the bar does not grow a second yellow thing to look at. */
  color: var(--pin-society);
  cursor: pointer; white-space: nowrap;
  display: inline-flex; align-items: center; gap: 6px;
}
.topbar-societies.hidden { display: none; }
.topbar-societies:active { transform: scale(0.96); }
.soc-badge {
  position: absolute; top: -6px; right: -6px; min-width: 18px; height: 18px;
  padding: 0 4px; border-radius: 9px; background: var(--pin-society); color: #1a1600;
  font-size: 0.6875rem; font-weight: 800; line-height: 18px; text-align: center;
  /* A ring of the bar's own ground, because a solid yellow disc sitting on a map whose
     place labels are yellow reads as part of the map. */
  box-shadow: 0 0 0 2px rgba(10, 10, 15, 0.92);
}
@media (max-width: 720px) {
  /* The word goes before the badge does: the count is the reason you are tapping. */
  .topbar-societies .soc-word { display: none; }
  .topbar-societies { padding: 0 10px; }
}

/* The mode lens, immediately right of Connect (KB, 2026-10-05). One button
   labelled with where it takes you, not two tabs: "you can have one and it
   basically changes between the two! So if you are on dating it'll view the
   friends tab and vice versa." Hidden by default; app.js shows it only for a
   both-mode account, so a single-mode person never sees a control with one
   setting. Same box as Connect, because it is the same kind of thing: a
   secondary action beside the primary You. */
/* The lens switch keeps its emoji, which is already colour, and loses the pink box, so the
   bar has one yellow thing and no accidental pink. */
.topbar-mode { padding: 0 14px; }
.topbar-mode.hidden { display: none; }
.topbar-mode:active { transform: scale(0.96); }
/* Narrow screens: the word goes, the emoji stays. A person who chose the
   lens knows which flower is which. */
@media (max-width: 560px) {
  .topbar-mode { padding: 0 10px; }
  .topbar-mode .mode-word { display: none; }
}

/* News bell. Same box as the secondary actions, with a count pinned to its corner:
   the count is the thing you are checking, so it belongs on the thing you press.
   It pings with a pulse rather than a colour change, because a small dot that
   quietly turns pink is a dot nobody notices out of the corner of their eye. */
.topbar-bell {
  position: relative;
  display: flex; align-items: center; justify-content: center;
}
/* News fills the bell. On a map whose roads are all pink outlines, a pink outline is
   camouflage and a solid chip is unmistakably a control, so the one thing in the bar that
   means "look here" takes the fill and the dark ink that goes with it. */
.topbar-bell.has-news {
  background: var(--accent); border-color: var(--accent); color: #1d0410;
}
.topbar-bell:active { transform: scale(0.96); }
.bell-badge {
  position: absolute; top: -6px; right: -6px; min-width: 18px; height: 18px;
  padding: 0 4px; border-radius: 9px; background: var(--accent); color: #fff;
  font-size: 0.6875rem; font-weight: 700; line-height: 18px; text-align: center;
}
/* One pop when the count RISES, not a pulse forever: the endless version
   nagged every 1.6 seconds whether anything happened or not. The motion
   demo's arrival pop is the one KB picked (2026-10-11: "The society number
   increasing like that is also very cool! we should add that to the
   notifications"). app.js retriggers .pop on a rising count. */
.bell-badge.pop { animation: bell-pop .45s cubic-bezier(.34,1.56,.64,1); }
@keyframes bell-pop {
  0% { transform: scale(1); }
  40% { transform: scale(1.5); }
  100% { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .bell-badge.pop { animation: none; }
}
.news-row {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  font-size: 0.9375rem; line-height: 1.4; margin-bottom: 12px;
}
.news-row a { color: var(--accent); font-weight: 700; text-decoration: none; flex-shrink: 0; }
.news-col { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.news-note { color: var(--text-secondary); font-style: italic; font-size: 0.8438rem;
  line-height: 1.4; }
.news-empty { font-size: 0.9375rem; line-height: 1.5; color: var(--text-secondary); margin-bottom: 20px; }

/* View toggle button */
/* The board/map switch, in the same family as the Dating switch beside it: a word and a
   drawn glyph, not a generic grid that could belong to any app on the phone. */
.topbar-toggle {
  padding: 0 14px;
  font: inherit; font-size: 0.8438rem; font-weight: 700;
  transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease;
}
.topbar-toggle:active { transform: scale(0.96); }
.topbar-toggle svg { flex-shrink: 0; }
.topbar-toggle.is-needed {
  color: var(--accent); border-color: var(--accent); background: var(--accent-light);
}

/* Desktop: map fills remaining space */
@media (min-width: 768px) {
  .map { left: 340px; }
  /* The sheet's desktop offset does NOT live here. This block sits above the base
     `.sheet { left: 0 }` rule, and a media query adds no specificity, so the later base
     rule won and the sheet covered the whole list panel on a desktop -- the same trap the
     .topbar note below documents. The rule lives after the base rule instead. */
}

/* Mobile: list is a toggle overlay */
@media (max-width: 767px) {
  .list-panel {
    width: 100%; border-right: none;
    transform: translateX(-100%);
    transition: transform 0.3s cubic-bezier(0.32,0.72,0,1);
  }
  .list-panel.open {
    transform: translateX(0);
    box-shadow: 4px 0 30px rgba(0,0,0,0.5);
  }
}

/* Top bar */
/* The bar is the landing page's header: near-black, blurred, with a hairline under it.
   It used to fade to transparent, which let the map come up behind the buttons and made
   the row look pasted on. KB, 2026-10-10: "Can the top layer have a black background like
   we have on the landing page? I think it would make it look more pro." The blur is the
   same one the sheet already uses, so the map reads as a place behind the bar rather than
   as a texture under the text. */
.topbar {
  position: fixed; top: 0; left: 0; right: 0; z-index: 10;
  padding: calc(var(--safe-top) + 12px) 16px 12px;
  background: rgba(10, 10, 15, .82);
  backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
  border-bottom: 1px solid #2a2a4a;
  display: flex; align-items: center; gap: 10px;
}
/* The top bar's flex spacer: the count label it replaced is gone for good
   (fake search field -> duplicate toggle -> plain text -> nothing). */
.topbar-spacer { flex: 1; }

/* At phone width every control in the bar keeps its glyph and loses its word, the board
   switch included. Seven controls with words cannot fit 390px -- measured at 465px, with
   the last one 75px off the edge of the screen -- and the two buttons beside it already
   trade the word before the badge. The state still reads: the glyph changes between the
   map and the list, and the accent still says the board is the way back. */
@media (max-width: 560px) {
  .topbar { padding-left: 12px; padding-right: 12px; gap: 8px; }
  .topbar-toggle { padding: 0 10px; }
  .topbar-toggle .tg-word { display: none; }
  .topbar-you, .topbar-connect { padding: 0 12px; }
}



/* Home. The mark is wide (viewBox 54x25), so it is sized by height and keeps its
   own ratio; the box matches the other topbar controls at 38px so the tap target
   is the same as the ones beside it. */
.topbar-home {
  width: 38px;
  display: flex; align-items: center; justify-content: center;
  text-decoration: none; transition: border-color 0.15s ease, transform 0.15s ease;
}
.topbar-home img { height: 13px; width: auto; display: block; }
.topbar-home:hover { border-color: var(--accent); }
.topbar-home:active { transform: scale(0.94); }

/* The topbar belongs to the map half on a desktop. It spans the viewport and
   sits under the list panel (z-index 10 against the panel's 15), so its left end
   was painted behind the sidebar: the home mark existed in the DOM and was
   invisible, and the search pill started 276px too far left. This rule lives
   below the .topbar rule on purpose - same specificity, and the later one wins.
   Measured before the move: the mark still reported x=16 and stayed covered. */
/* KB, 2026-10-09: "The TwiceMet logo is awkwardly in the middle when it should be
   all the way to the left. It should go to the middle when the left panel is open."
   The rule used to be unconditional: on any desktop width the topbar started 340px in,
   whether or not the list panel was actually on screen. With the panel closed that left
   the home mark floating a third of the way across an empty map, which is the "middle"
   he is pointing at. The offset is now a state, not a width: 0 with the panel closed,
   340px with it open, set by positionTopbar() in app.js beside the two other elements
   that move with the panel. The base .topbar rule already says left: 0, so a browser
   without the JS gets the logo at the left edge, which is the correct default rather
   than the old bug. */
@media (min-width: 768px) {
  .topbar { left: 0; }
}

/* Bottom Sheet */
.sheet { position: fixed; bottom: 0; left: 0; right: 0; z-index: 20; background: var(--surface); border-radius: 20px 20px 0 0; box-shadow: 0 -4px 30px rgba(255,45,120,0.1), 0 -2px 20px rgba(0,0,0,0.5); transition: transform 0.35s cubic-bezier(0.32,0.72,0,1), height 0.35s cubic-bezier(0.32,0.72,0,1); max-height: 85vh; max-height: 85dvh; padding-bottom: var(--safe-bottom); border-top: 1px solid #2a2a4a; display: flex; flex-direction: column; }
/* Expanding means becoming taller. The sheet is anchored to the bottom of the
   viewport, so translating it up by (85dvh - 120px) - what this used to do -
   pushed its top above the screen: the name, the photo and the strip were all
   off-viewport and only the action bar stayed visible. */
.sheet.expanded { height: 85vh; height: 85dvh; }
.sheet.expanded { animation: win-pop .5s cubic-bezier(.34,1.56,.64,1); }
/* The three tabs KB named -- You, Connect, Societies -- let the map through. KB,
   2026-10-08: "you, connect and societies tabs should be slight transparent. So you see
   the map. Maybe 10%?" Ten percent is what the map needs to be recognisable as a place
   rather than a smear of colour under text, and it is the most the type can take: the
   sheets carry 13-16px body copy over whatever the basemap happens to be drawing there.

   Keyed with :has() on which surface is showing rather than a class added at three call
   sites in two files, because the peek lives in this same container and stays solid --
   a two-line strip would turn to mush over a road. The profile sheet was carved out of
   this rule when the tabs first went glass, on the theory that a photo needed a solid
   stage; KB, 2026-10-11, compared the two and called it: "the society view has an
   element of transparency, but the profile view doesn't... they all should be like
   that". And the theory was wrong anyway -- the photo is opaque content ON the glass,
   the map shows around it, not through it. Same colour as --surface, 10% of the map
   through, one blur; not a second palette. If a browser has no :has() the rule is
   dropped and the sheets stay solid, which is the old behaviour rather than a break. */
.sheet:has(#sheet-mine:not(.hidden)),
.sheet:has(#sheet-connect:not(.hidden)),
.sheet:has(#sheet-societies:not(.hidden)),
.sheet:has(#sheet-full:not(.hidden)),
.sheet.glass {
  background: rgba(42, 42, 70, 0.95);
  backdrop-filter: blur(10px) saturate(1.1);
  -webkit-backdrop-filter: blur(10px) saturate(1.1);
}
/* And the transparency must not depend on the blur working: on a phone the
   glass is simply more open, so the map reads through even where the blur
   fails (the Android transform bug above) or is absent altogether. */
@media (max-width: 767px) {
  .sheet:has(#sheet-mine:not(.hidden)),
  .sheet:has(#sheet-connect:not(.hidden)),
  .sheet:has(#sheet-societies:not(.hidden)),
  .sheet:has(#sheet-full:not(.hidden)),
  .sheet.glass {
    background: rgba(26, 26, 46, 0.84);
  }
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .sheet:has(#sheet-mine:not(.hidden)),
  .sheet:has(#sheet-connect:not(.hidden)),
  .sheet:has(#sheet-societies:not(.hidden)),
  .sheet:has(#sheet-full:not(.hidden)),
  .sheet.glass {
    background: rgba(26, 26, 46, 0.84);
  }
}
/* The window chrome exists only on a wide screen: hidden by default, shown by the media block below. The order matters - the base rule must come FIRST or it outranks the media rule at equal specificity and the fullscreen and close buttons vanish from the desktop (the exact bug KB reported 2026-10-11: no fullscreen button, impossible to close things). */
.sheet-win { display: none; }

/* The motion lives OUTSIDE the media block on purpose: a @keyframes inside
   a conditional rule only exists when the condition matches, and the phone
   deserves the same pop as the desktop (KB, 2026-10-11: "none of these
   features exist on mobile view"). The variables it composes with are
   always zero below 768px: the phone sheet pops in place, the desktop
   window pops where it was dragged. */
  @keyframes win-pop {
    0% { transform: translate(var(--wx, 0px), var(--wy, 0px)) scale(.6); opacity: 0; }
    60% { transform: translate(var(--wx, 0px), var(--wy, 0px)) scale(1.04); opacity: 1;
      border-color: var(--spotlight); box-shadow: 0 0 26px rgba(255,230,0,.35); }
    100% { transform: translate(var(--wx, 0px), var(--wy, 0px)) scale(1); border-color: #2a2a4a; }
  }
  @keyframes win-out {
    /* the FROM is explicit on purpose: with only a TO, the implicit start is
       the element's own computed transform - and .mini's own transform is
       translateY(100%), OFF-SCREEN. The window jumped out of sight in frame
       one and faded invisibly; the live matrix proved it (KB, 2026-10-11:
       "it does NOT pop like its supposed to"). Start where it stands. */
    from { transform: translate(var(--wx, 0px), var(--wy, 0px)) scale(1); opacity: 1; }
    to { transform: translate(var(--wx, 0px), var(--wy, 0px)) scale(.7); opacity: 0; }
  }

/* Desktop: the sheet is a window on the map, not a page. KB, 2026-10-11,
   after playing with the demo: "Instead of profiles and you page etc taking
   up the whole page make them only take up some of the page but it can be
   moved around with the option to go full screen if you want" - and then
   "We are ready for deployment". The phone keeps the bottom sheet and the
   thumb gestures untouched; past 768px the same machinery wears a window
   frame: it floats over the map, the handle row drags it in both axes and
   it stays where it is dropped, the square fills the viewport, the cross
   parks it. Centering is a calc, not a transform - the transform belongs
   to the drag. */
@media (min-width: 768px) {
  .sheet {
    left: calc(50% - min(380px, 46vw)); right: auto; bottom: auto; top: 12vh; top: 12dvh;
    transform: translate(var(--wx, 0px), var(--wy, 0px));
    width: min(760px, 92vw); height: 78vh; height: 78dvh; max-height: none;
    border: 1px solid #2a2a4a;
    border-radius: 16px;
    box-shadow: 0 14px 44px rgba(0,0,0,.55);
  }
  .sheet.expanded { height: 78vh; height: 78dvh; }
  /* Gone the way it came: the demo lift - shrink and fade - replaces the
     slide-off on desktop; forwards holds the end state and the base
     rule already makes the parked sheet click-through. The phone
     keeps the slide-down below 768px. */
  .sheet.expanded { animation: win-pop .5s cubic-bezier(.34,1.56,.64,1); }
  .sheet.mini { animation: win-out .3s cubic-bezier(.34,1.56,.64,1) forwards; }
  /* Panes arrive rather than appear: the animation restarts every time the
     .hidden class flips off, so each open lands with the demo pop. */
  .sheet.fs { left: 12px; right: 12px; top: 12px; bottom: 12px;
    width: auto; height: auto; --wx: 0px; --wy: 0px; transform: none;
    animation: fs-pop .4s cubic-bezier(.34,1.56,.64,1); }
  @keyframes fs-pop {
    0% { transform: scale(.94); opacity: .4; }
    60% { transform: scale(1.015); opacity: 1;
      border-color: var(--spotlight); box-shadow: 0 0 26px rgba(255,230,0,.35); }
    100% { transform: scale(1); border-color: #2a2a4a; }
  }
  .sheet-win { display: block; position: absolute; top: 2px; padding: 7px 12px;
    background: none; border: none; color: var(--spotlight); font-size: 1.35rem;
    line-height: 1; cursor: pointer; border-radius: 8px; }
  .sheet-win:hover { background: rgba(255,255,255,.07); }
  /* One close per surface: the header cross is the phone way out; on a
     desktop the chrome cross owns closing, so the big header one
     retires (KB, 2026-10-11: "why do we have two close buttons?"). */
  .match-close, .mine-close { display: none; }
  #sheet-fs { right: 56px; }
  #sheet-min { right: 12px; color: var(--text-secondary); }
}

.sheet-handle { padding: 10px 0 6px; display: flex; justify-content: center; cursor: grab; flex-shrink: 0; touch-action: none; position: relative; }
/* The window chrome lives on the handle row and exists only on a wide
   screen: a phone sheet is already the whole bottom of the screen, so
   fullscreen and put-away have nothing to do there. */
.sheet-handle-bar { width: 36px; height: 4px; border-radius: 2px; background: #3a3a5a; }
/* the sheet follows the finger, so the transition must be off mid-drag */
.sheet.dragging { transition: none; }
/* parked: dragged past the peek, out of the map's way. The floating pill that
   used to mark it is gone (KB, 2026-10-03); the You tab, the Connect tab, or
   any pin or card brings the sheet back. */
.sheet.mini { transform: translateY(100%); pointer-events: none;
  animation: win-out .3s cubic-bezier(.34,1.56,.64,1) forwards; }
.sheet-content { padding: 0 20px 20px; overflow-y: auto; flex: 1 1 auto; min-height: 0; -webkit-overflow-scrolling: touch; }

/* Peek */
.sheet-peek { min-height: 80px; display: flex; align-items: center; }

/* Your profile - the only home the upload strip has */
.sheet-mine { padding: 4px 0 8px; }
.mine-header { display: flex; gap: 14px; align-items: center; margin-bottom: 4px; }
/* The cross that closes a tab is a step back, so it wears the step-back yellow like
   every other way out. KB, 2026-10-08: "Back yellow is the theme. Not grey. If you see
   grey change it to our yellow." */
.mine-close { background: none; border: none; color: var(--spotlight); font-size: 1.6rem; cursor: pointer; padding: 4px 10px; line-height: 1; border-radius: 8px; }
/* The profile way out was a big ghost button at the END of the page -
   KB, 2026-10-11: not a big thing at the end, be consistent and use the
   yellow close button. It is now the same yellow cross the tabs wear, in
   the header, where a way out belongs. */
.build-stamp { color: var(--text-secondary); font-size: 0.75rem; text-align: center;
  margin: 10px 0 0; letter-spacing: .04em; }
.match-close { margin-left: auto; background: none; border: none; color: var(--spotlight);
  font-size: 1.6rem; cursor: pointer; padding: 4px 10px; line-height: 1; align-self: flex-start; }
.mine-close:hover { opacity: .72; background: var(--surface); }
.mine-avatar {
  width: 52px; height: 52px; border-radius: 50%; flex-shrink: 0;
  background: var(--accent-light); border: 2px solid var(--accent);
  display: flex; align-items: center; justify-content: center; font-size: 1.5rem;
}
.mine-title h2 { font-size: 1.125rem; font-weight: 700; }
.mine-title p { font-size: 0.75rem; color: var(--text-secondary); margin-top: 2px; }

/* === Your pairing ID === */
/* The string on this surface that exists to be spoken aloud, so it gets the two
   things a spoken string needs: room between the glyphs, and a typeface where 2
   and Z, 5 and S are not cousins. The monospace is not decoration, it is the
   difference between "K seven F" and a guess. user-select:all makes one press
   select the whole handle, which is the manual route when the clipboard is
   blocked (see the copy handler in app.js). The card around it is quieter than
   the photo grid below: this is a fact you read once, not a thing you use. */
.mine-pairing {
  margin: 14px 0 4px; padding: 12px 14px 13px; border-radius: 12px;
  background: rgba(255,255,255,0.035); border: 1px solid rgba(255,255,255,0.07);
}
.mine-pairing-head {
  display: flex; justify-content: space-between; align-items: center;
  font-size: 0.8125rem; font-weight: 600; color: #f0f0f5;
}
.mine-pairing-copy {
  background: none; border: 1px solid rgba(255,45,120,0.45); color: var(--accent);
  font-size: 0.6875rem; font-weight: 600; padding: 3px 10px; border-radius: 999px;
  cursor: pointer; transition: background 0.2s ease;
}
.mine-pairing-copy:hover { background: rgba(255,45,120,0.12); }
.mine-pairing-value {
  margin-top: 7px; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 1.3125rem; font-weight: 600; letter-spacing: 0.14em; color: var(--text);
  -webkit-user-select: all; user-select: all; word-break: break-all;
}
.mine-pairing-fine {
  margin: 8px 0 0; font-size: 0.75rem; line-height: 1.5; color: var(--text-secondary);
}
/* The six boxes. Wide enough for a thumb, monospace so 1 and I cannot be
   argued about, and one accent ring at a time so the eye always knows where
   the next digit lands. */
.pin-connect {
  margin: 10px 0 4px; padding: 12px 14px 13px; border-radius: 12px;
  background: rgba(255,255,255,0.035); border: 1px solid rgba(255,255,255,0.07);
}
.pin-boxes { display: flex; gap: 8px; margin-top: 10px; }
.pin-box {
  flex: 1; min-width: 0; height: 46px; border-radius: 10px; text-align: center;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 1.375rem; font-weight: 600; color: var(--text);
  background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.12);
  outline: none; caret-color: var(--accent);
  -webkit-appearance: none; appearance: none;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.pin-box:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-light); }
.pin-box:disabled { opacity: 0.45; }
.pin-box.pending { animation: pin-pulse 1s ease-in-out infinite; }
@keyframes pin-pulse {
  0%, 100% { border-color: var(--accent); box-shadow: 0 0 0 0 var(--accent-light); }
  50% { border-color: rgba(255,45,120,0.35); box-shadow: 0 0 0 5px rgba(255,45,120,0.06); }
}
.pin-shake { animation: pin-shake 0.4s cubic-bezier(0.36, 0.07, 0.19, 0.97) both; }
@keyframes pin-shake {
  10%, 90% { transform: translateX(-1px); }
  20%, 80% { transform: translateX(2px); }
  30%, 50%, 70% { transform: translateX(-4px); }
  40%, 60% { transform: translateX(4px); }
}
.pin-state { font-size: 0.6875rem; font-weight: 600; color: var(--accent); }
.mine-pairing-fine.pin-ok { color: #34d399; }
.mine-pairing-fine.pin-bad { color: #ff6b6b; }
.peek-empty { text-align: center; width: 100%; padding: 10px 0; color: var(--text-secondary); font-size: 0.875rem; }
.peek-empty-icon { font-size: 1.75rem; margin-bottom: 6px; }
.peek-card { display: flex; align-items: center; gap: 12px; width: 100%; animation: slide-up 0.3s cubic-bezier(0.32,0.72,0,1) both; }
@keyframes slide-up { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
.peek-avatar { width: 48px; height: 48px; border-radius: 14px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 1.375rem; }
.peek-info { flex: 1; min-width: 0; }
.peek-info h3 { font-size: 1rem; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.peek-info p { font-size: 0.8125rem; color: var(--text-secondary); margin-top: 2px; }
.peek-score { background: var(--accent-light); color: var(--accent); font-size: 0.8125rem; font-weight: 700; padding: 4px 10px; border-radius: 8px; flex-shrink: 0; }

/* Full detail */
.sheet-full { padding-bottom: 80px; }

/* Desktop. The sheet spans everything right of the 340px list panel, which on a 1440px
   screen is far more width than a profile wants: the photo, the prose and the buttons all
   hugged the left edge and the right half of the panel sat empty. The content is capped and
   centred so it reads as one object rather than a phone layout stuck to one side, and the
   action row comes out of the viewport into that column -- it is fixed to left:20px, which
   on desktop laid Close and Match across the list panel. Sticky keeps it at the bottom of
   the scroll area, where the fixed version was, without the full-viewport width. */
@media (min-width: 768px) {
  /* The sheet used to be pinned clear of the 340px list panel here. It is a
     centered window now and answers only to the window block above; the pin
     guards that this anchor stays retired. */
  .sheet-content > * { max-width: 760px; margin-left: auto; margin-right: auto; }
  .sheet-full { padding-bottom: 8px; }
}
.match-header { display: flex; gap: 14px; align-items: flex-start; margin-bottom: 16px; cursor: grab; touch-action: none; }
.match-avatar { width: 64px; height: 64px; border-radius: 18px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 1.875rem; }
.match-title h2 { font-size: 1.25rem; font-weight: 700; }
.match-title p { font-size: 0.875rem; color: var(--text-secondary); margin-top: 2px; }
.match-badge { display: inline-block; margin-top: 6px; background: var(--accent-light); color: var(--accent); font-size: 0.8125rem; font-weight: 700; padding: 4px 10px; border-radius: 8px; }
/* Sits under the badge and says what the number means. Deliberately quiet: it is
   an explanation, not a second headline, and it hides itself when there is no
   percentile to explain. */
.match-badge-note { display: block; margin-top: 6px; font-size: 0.75rem; line-height: 1.4; color: var(--text-dim, #9a9ab0); }
.match-badge-note.hidden { display: none; }
/* The caveat is not a footnote on the number, it is a limit on it: "99%" earned
   from two answered questions needs to be read before the number, not under it.
   So it takes the card's own ink instead of the dim grey a reader skips, and no
   box or icon is added to make a scene about it. */
.match-badge-note.caveat { color: var(--text); }
/* Photos: one large, a thumb for each (up to 5). renderPhotos() paints them as
   background-image, so nothing here loads a src and no path reaches markup. */
.match-hero { width: 100%; max-width: 640px; border-radius: 18px; margin-bottom: 10px; background-color: #1a1a2e; border: 1px solid rgba(255, 45, 120, 0.25); overflow: hidden; }
/* the photo keeps its own ratio: a portrait is tall, a golf shot shows the
   swing, and neither is cropped to a square just to fit a box */
.match-hero img { display: block; width: 100%; height: auto; }
.match-photos { display: flex; gap: 8px; margin-bottom: 16px; overflow-x: auto; }
.photo-thumb { width: 56px; height: 56px; border-radius: 12px; flex-shrink: 0; cursor: pointer; background-color: #1a1a2e; background-size: cover; background-position: center; border: 2px solid transparent; padding: 0; }
.photo-thumb.active { border-color: var(--accent); }
.match-summary { font-size: 0.9375rem; line-height: 1.5; margin-bottom: 14px; font-style: italic; }
/* The "nothing published yet" line is an explanation, not a quoted summary,
   so it drops the quote styling the summary normally carries. */
.match-summary.bare-note { font-style: normal; opacity: 0.8; }
/* The agent's own line about them: a pull-quote under its own kicker, set apart
   from the scored rows so it reads as a voice, not a data field. Hidden when empty. */
.match-agent {
  margin: 0 0 14px; padding: 12px 14px; border-radius: 12px;
  border-left: 3px solid var(--spotlight); background: rgba(255,230,0,0.10);
}
.match-agent-label {
  font-size: 0.6875rem; font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase; color: var(--spotlight); margin-bottom: 6px;
}
.match-agent-quote {
  color: var(--text); font-size: 0.9062rem; line-height: 1.55; font-style: italic;
}
.match-agent-quote::before { content: "\201C"; color: var(--spotlight); font-weight: 700; }
.match-agent-quote::after { content: "\201D"; color: var(--spotlight); font-weight: 700; }
/* "In their own words": deliberately the sibling of the agent block, not a
   copy of it. The agent's line is a yellow pull-quote in quotation marks --
   someone talking about the person. This one is the person talking, so it is
   unitalicised and unquoted, in the app's own rose rather than the agent's
   spotlight yellow. The contrast is the point: mediated versus unmediated,
   legible at a glance. */
.match-own {
  margin: 0 0 14px; padding: 12px 14px; border-radius: 12px;
  border-left: 3px solid var(--accent); background: rgba(255,45,120,0.08);
}
.match-own-label {
  font-size: 0.6875rem; font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase; color: var(--accent); margin-bottom: 6px;
}
.match-own-quote {
  color: var(--text); font-size: 0.9062rem; line-height: 1.55;
}
/* "They matched you": the press note, at the top of the card. It is the most
   decision-relevant line on the whole profile -- someone has already reached
   out and said why -- so it is the loudest block: filled rose, white text, no
   pull-quote styling, because it is a person speaking to you, not a blurb
   about them. */
.match-reached {
  margin: 0 20px 14px; padding: 13px 15px; border-radius: 14px;
  background: var(--accent); color: #fff;
}
.match-reached-label {
  font-size: 0.6875rem; font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase; opacity: 0.85; margin-bottom: 5px;
}
.match-reached-note {
  font-size: 0.9688rem; line-height: 1.5; color: #fff;
}
.match-tags { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 20px; }
.match-tags .tag { background: #f3f0eb; padding: 6px 12px; border-radius: 20px; font-size: 0.8125rem; color: var(--text-secondary); }
.match-section { margin-bottom: 20px; }
.match-section h4 { font-size: 0.8125rem; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-secondary); margin-bottom: 10px; }
.match-dimensions { display: flex; flex-direction: column; gap: 8px; }
.dimension { display: flex; align-items: center; gap: 10px; font-size: 0.875rem; }
.dimension-bar { flex: 1; height: 6px; background: #2a2a4a; border-radius: 3px; overflow: hidden; }
.dimension-fill { height: 100%; border-radius: 3px; background: var(--accent); transition: width 0.6s cubic-bezier(0.32,0.72,0,1); }
.dimension-label { min-width: 100px; color: var(--text-secondary); }
.dimension-value { font-weight: 600; min-width: 32px; text-align: right; }
/* The tier's sentence, shown where bars would be when the answers exist but
   their visibility tier keeps them off the page. Quiet, not an error state. */
.dims-note { font-size: 0.8125rem; color: var(--text-secondary); font-style: italic; padding: 2px 0; }
.match-fields { display: flex; flex-direction: column; gap: 8px; }
.field-row { display: flex; justify-content: space-between; align-items: center; font-size: 0.875rem; padding: 8px 0; border-bottom: 1px solid #2a2a4a; }
.field-row:last-child { border-bottom: none; }
.field-key { color: var(--text-secondary); }
.field-val { font-weight: 500; }
/* Channel stamp chip (audit M1): this row's value is what an AI stated for
   the person, never confirmed through a human channel. Sits inside the value
   so the two-child row layout is untouched; quiet by design - it qualifies
   the row, it does not shout at the sheet. */
.field-unseen { font-size: 0.6875rem; font-weight: 400; color: var(--text-secondary);
  border: 1px solid #2a2a4a; border-radius: 999px; padding: 1px 8px;
  white-space: nowrap; }

/* A field that has a page of its own. Understated on purpose: it is a way out of the
   sheet to a page the site already publishes, not a call to action. */
.field-link { color: inherit; text-decoration: underline;
              text-decoration-color: rgba(255,45,120,.5);
              text-underline-offset: 3px; }
.field-link:hover { color: #ff2d78; }

/* Location */
.match-location { margin-bottom: 20px; }
.loc-locked { display: flex; align-items: center; gap: 8px; font-size: 0.8125rem; color: var(--text-secondary); background: #12121f; padding: 12px 14px; border-radius: 10px; }
.loc-shared { display: flex; align-items: center; gap: 8px; font-size: 0.875rem; color: var(--text); background: rgba(0,212,255,0.08); padding: 12px 14px; border-radius: 10px; }

/* Actions */
.match-actions { position: fixed; bottom: calc(20px + var(--safe-bottom)); left: 20px; right: 20px; display: flex; flex-wrap: wrap; gap: 12px; z-index: 25;
  /* The row floats over scrolling content, and the Block/Unmatch buttons are
     transparent, so content used to read THROUGH the row (measured on a phone
     viewport 2026-10-05: the MBTI chip sat under the ghost button and both
     words showed at once). The desktop sticky override already carries its own
     surface for the same reason; the fixed rule was the one that lacked it. */
  background: var(--surface); padding: 10px; border-radius: 14px; }
/* The press note takes a full row of the wrapped action bar, so the buttons
   drop below it; when it is hidden the buttons share one row as before. The
   input borrows the edit-field look so it reads as part of the app, not a
   browser prompt. */
.match-note { flex-basis: 100%; }
#press-note {
  width: 100%; padding: 10px 12px; border-radius: 12px; font: inherit; font-size: 0.875rem;
  background: #12121e; border: 1px solid #2a2a4a; color: var(--text, #f0f0f5);
}
#press-note:focus { outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(255, 45, 120, 0.15); }
/* The bar grew 20px taller with its padding, so the reservation under the
   scroll must grow with it, or the last line of a profile parks behind the
   bar it cannot scroll clear of. Mobile-only: on desktop the row is sticky
   inside the flow and the media block above keeps the reservation at 8px,
   and a bare later rule here would beat that override the same way the old
   `.sheet { left: 0 }` did -- exclusive media queries cannot collide. */
@media (max-width: 767.98px) {
  .sheet-full { padding-bottom: 100px; }
}
/* Desktop: the row was fixed to the viewport, so Close and Match stretched from x=20 across
   the list panel and to the far edge. Sticky puts it at the bottom of the scroll area the
   same way the fixed version did, but inside the capped column, so it lands under the
   profile it belongs to. This block must stay BELOW the rule above: same specificity, later
   one wins, and the attempt to override it from the earlier desktop block simply lost. */
@media (min-width: 768px) {
  .match-actions {
    position: sticky;
    bottom: 0;
    left: auto;
    right: auto;
    margin-top: 14px;
    padding: 12px 0;
    background: var(--surface);
  }
}/* Where a press leaves you, in the body under the location. Not a button and not
   in the action row: the row is fixed to the bottom and .sheet-full reserves one
   row for it, so a sentence in there would sit on top of the profile. */
.match-state {
  margin: 14px 20px 0; padding: 11px 14px; border-radius: 12px;
  background: #22223c; border: 1px dashed #3a3a5c;
  color: var(--text-secondary); font-size: 0.8438rem; line-height: 1.45;
}
.btn-primary, .btn-ghost { flex: 1; padding: 14px; border: none; border-radius: 14px; font-size: 1rem; font-weight: 600; cursor: pointer; transition: transform 0.15s ease; }
.btn-primary { background: var(--accent); color: white; box-shadow: 0 4px 20px rgba(255,45,120,0.4); }
.btn-primary:active { transform: scale(0.96); }
/* Grey, and deliberately so. On 2026-10-08 KB asked for yellow wherever he saw grey, and
   this was the rule I applied it to -- every Cancel, Keep, Skip and Close. He looked at the
   profile row the next morning: Close in yellow beside Chat, Unmatch and Block in grey, and
   said the obvious thing, that it reads unnatural. Of course it does. Those four are peer
   choices on one surface, and yellow is the loudest colour the theme has, so painting one
   of them yellow promotes it -- Close started competing with the purple Chat button, which
   is the opposite of what a dismiss is for.

   So the rule he was actually stating is narrower than the words, and the narrower rule is
   the one to keep: yellow marks a control whose only job is to take you back or get out of
   the way (an arrow, an X, the word Back, a bare Close under a panel). A button that is one
   option among several stays grey, whatever it says. See the back controls and .mine-close,
   which keep the yellow and are exactly the controls he named. */
.btn-ghost { background: #2a2a4a; color: var(--text-secondary); }
.btn-ghost:active { transform: scale(0.96); }
.btn-chat {
  flex: 1; padding: 14px; border: none; border-radius: 14px;
  font-size: 1rem; font-weight: 600; cursor: pointer;
  background: #7b2ff7; color: white; text-align: center; text-decoration: none;
  box-shadow: 0 4px 16px rgba(123,47,247,0.3); transition: transform 0.15s ease;
}
.btn-chat:active { transform: scale(0.96); }

/* Matched */
.matched { position: fixed; inset: 0; z-index: 60; background: var(--bg); display: flex; align-items: center; justify-content: center; animation: fade-in 0.4s ease; }
/* The match celebration. Above .matched (60) because that overlay paints an
   opaque background, and transparent to touch: a person pressing "Chat now"
   must never hit a canvas first. fireworks.js also refuses to draw anything at
   all when the browser asked for reduced motion, so this element is inert for
   them rather than merely empty. */
.fireworks { position: fixed; inset: 0; z-index: 70; width: 100vw; height: 100vh; height: 100dvh; pointer-events: none; }
/* The burst canvas sits above the sheet on purpose: photos and list
cards vanish from under it, and a pointer must never be swallowed by a
crowd of dying pixels. Same reason as .fireworks above. */
.tm-burst { position: fixed; inset: 0; z-index: 230; width: 100vw; height: 100vh; height: 100dvh; pointer-events: none; }
/* The mark that says a card is fiction. Muted and small on purpose: it informs
   the card, it does not become the card. A real profile never gets one (people.js
   demoBadge), which is the direction the doctrine actually protects. */
.card-demo { display: inline-block; vertical-align: middle; font-size: 0.625rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: #9a86c9; background: rgba(154,134,201,0.14); border: 1px solid rgba(154,134,201,0.35); border-radius: 999px; padding: 2px 7px; margin-left: 6px; }
.match-name-row { display: flex; align-items: center; gap: 2px; }
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
.matched-inner { text-align: center; padding: 2rem;
  animation: win-pop 0.45s cubic-bezier(.34,1.56,.64,1) both; }
.matched-icon { font-size: 3rem; margin-bottom: 16px; animation: bounce 0.6s cubic-bezier(0.175,0.885,0.32,1.275); }
@keyframes bounce { 0% { transform: scale(0); } 60% { transform: scale(1.2); } 100% { transform: scale(1); } }
.matched-inner h2 { font-size: 1.375rem; margin-bottom: 8px; }
.matched-sub { color: var(--text-secondary); font-size: 0.9375rem; margin-bottom: 24px; }
.btn-share-city { background: var(--surface); border: 2px solid var(--accent); color: var(--accent); padding: 14px 28px; border-radius: 14px; font-size: 1rem; font-weight: 600; cursor: pointer; transition: all 0.2s ease; display: block; margin: 0 auto; }
.btn-share-city:hover { background: var(--accent-light); }
.btn-share-city.shared { background: var(--success); border-color: var(--success); color: white; }
.matched-note { margin-top: 12px; font-size: 0.8125rem; color: var(--text-secondary); }
/* The underline goes with the grey: a yellow word already reads as the control it is,
   and the shared rule owns its colour now. */
.btn-done { margin-top: 20px; }
.btn-chat-now {
  display: inline-block; margin-top: 16px; padding: 12px 24px;
  background: #7b2ff7; color: white; border-radius: 12px;
  font-size: 0.9375rem; font-weight: 600; text-decoration: none;
  box-shadow: 0 4px 16px rgba(123,47,247,0.3); transition: transform 0.15s ease;
}
.btn-chat-now:active { transform: scale(0.96); }

/* Pins */
.tm-pin { width: 44px; height: 44px; border-radius: 50%; background: var(--surface); border: 3px solid var(--accent); display: flex; align-items: center; justify-content: center; font-size: 1.25rem; box-shadow: 0 0 12px rgba(255,45,120,0.4), 0 2px 8px rgba(0,0,0,0.3); cursor: pointer; transition: transform 0.2s cubic-bezier(0.32,0.72,0,1); /* absolute, not relative: MapLibre places a marker by transform alone, and that only works if the element is out of normal flow. With `relative` each pin keeps its flow position and the transform is added on top of it, so pin N sits 44px x N too low - Bristol landed in the Bay of Biscay, Barcelona in North Africa. `absolute` still anchors the .pin-score / .pin-city children, because an absolutely positioned element is a containing block too. */ position: absolute; }
.tm-pin:hover, .tm-pin.active { transform: scale(1.2); }
.tm-pin .pin-score { position: absolute; top: -6px; right: -6px; background: var(--accent); color: white; font-size: 0.625rem; font-weight: 700; padding: 2px 5px; border-radius: 6px; }
.tm-pin .pin-city { position: absolute; bottom: -18px; left: 50%; transform: translateX(-50%); font-size: 0.625rem; color: var(--pin-society); white-space: nowrap; font-weight: 500; }
/* Group pin: one marker for everyone who shares a rounded coordinate
   (notes/map_pin_collisions_2026-10-02). The count is the honest answer to
   "how many people are around here": a pin that reads "2" is true, one pin
   hiding two people is a lie by omission, and spreading them would claim a
   precision the data deliberately does not have. */
.tm-pin-group { border-style: dashed; /* position again, though .tm-pin already says it: check_pin_placement.py reads each marker class's own rule, and a class the check cannot prove out-of-flow fails it. */ position: absolute; border-color: var(--pin-crowd); box-shadow: 0 0 12px rgba(123,47,247,0.45), 0 2px 8px rgba(0,0,0,0.3); }
.tm-pin .pin-count { font-size: 1.125rem; font-weight: 800; color: var(--pin-crowd); }
/* A person on the friends lens is cyan. Same pin, same size, same label: only the
   colour says which board you are looking at, because the lens is a change of what
   you are open to, not a change of what a person looks like. */
.tm-pin-friend { border-color: var(--pin-friend); box-shadow: 0 0 12px rgba(0,212,255,0.45), 0 2px 8px rgba(0,0,0,0.3); }
.tm-pin-friend .pin-score { background: var(--pin-friend); color: #04121a; }
/* A society pin. Square where a person is round, yellow where a person is pink or
   cyan, and never merged into a people group: two facts at one rounded coordinate
   stay two markers (notes/map_pin_collisions_2026-10-02). The shape carries the
   distinction as well as the hue, because a phone in sunlight is not a colour test. */
.tm-pin-society { width: 46px; height: 46px; border-radius: 13px; border-color: var(--pin-society); box-shadow: 0 0 14px rgba(255,230,0,0.4), 0 2px 8px rgba(0,0,0,0.35); /* its own position, for the same reason .tm-pin-group restates it */ position: absolute; }
.tm-pin-society .pin-count { color: var(--pin-society); font-size: 0.9375rem; }
/* Several societies in one city. Same square and same yellow, with a dashed edge:
   the shape says society, the dashes say more than one, which is the distinction the
   pin has to carry. The people crowd pin is a dashed circle for the same reason. */.tm-pin-society-group { border-style: dashed; position: absolute; /* restated for the same reason .tm-pin-society restates it: the guard reads each marker class on its own, and MapLibre positions a marker by transform alone, so a class that falls back to normal flow renders one pin-height per pin too low */ }
/* A society that shares a city with people hangs off their pin instead of standing
   beside it, because a city is one point and a second marker on the same point has to
   be moved somewhere, and "somewhere" is a lie about location. Yellow square, yellow
   edge, the society's own shape at badge size: same language as the standalone pin. */
.pin-society-badge { position: absolute; top: -7px; left: -7px; min-width: 19px; height: 19px;
  padding: 0 4px; border-radius: 6px; background: var(--surface);
  border: 2px solid var(--pin-society); color: var(--pin-society);
  font-size: 0.6875rem; font-weight: 700; display: flex; align-items: center;
  justify-content: center; box-shadow: 0 0 8px rgba(255, 230, 0, 0.35); }
/* Something on this week. A halo outside the square rather than a fifth colour: the palette
   rule is that a pin hue must already be in the basemap, and yellow is already what a
   society is, so the new fact is carried by the ring and its slow pulse. It stays a ring
   with the animation switched off, which is why the signal is the shape and the pulse is
   only the emphasis.
   A child of the marker, never a border on .tm-pin-society: that border *is* the society
   colour, and overwriting it would make "has an event" and "is a society" the same mark.
   The radius is the pin's 13px plus the 6px it sits out, so the halo is concentric. */
.pin-event-ring {
  position: absolute; top: -6px; left: -6px; right: -6px; bottom: -6px;
  border: 2px solid var(--pin-society); border-radius: 19px; opacity: 0.8;
  pointer-events: none;
  animation: bell-pulse 1.6s ease-in-out infinite;
}
/* The same fact on a badge hung off a people pin, where there is no room for a ring: filled
   where an ordinary badge is hollow. Same hue, so nothing new had to be invented. */
.pin-society-badge.event {
  background: var(--pin-society); color: #04121a;
  animation: bell-pulse 1.6s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
  .pin-event-ring, .pin-society-badge.event { animation: none; }
}

/* Smaller windows pop in like the sheet; reduced motion takes the spring,
   never the window. */
@media (prefers-reduced-motion: reduce) {
  .confirm-box, .crop-box, .reason-card, .matched-inner,
  .society-box { animation: none; }
}

/* The city line a society-group pin leaves behind, and the way back out of it. */
.soc-city-filter { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin: 0 0 10px; padding: 9px 12px; background: var(--surface); border: 1px solid #2a2a4a; border-radius: 12px; font-size: 0.8125rem; color: var(--text-secondary); }
.soc-city-filter button { background: none; border: none; color: var(--accent); font-size: 0.8125rem; font-family: inherit; cursor: pointer; padding: 2px 4px; }
/* Spiderfy is gone (2026-10-03): a group pin's tap opens the city's list,
   not an arc of member pins. Its rules and the .pin-alias label went with
   it; check_pin_placement.py still guards every marker class that remains. */

/* The city filter's header bar in the list: what is shown, and the way back. */
.city-filter-bar { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 10px 14px; margin-bottom: 8px; background: var(--surface); border: 1px solid #2a2a4a; border-radius: 12px; font-size: 0.8125rem; color: var(--text-secondary); }
.city-filter-bar button { background: none; border: none; color: var(--accent); font-size: 0.8125rem; font-family: inherit; cursor: pointer; padding: 2px 4px; }

/* Toast */
.toast { position: fixed; bottom: calc(100px + var(--safe-bottom)); left: 50%; transform: translateX(-50%) translateY(20px); background: var(--text); color: var(--bg); padding: 12px 20px; border-radius: 12px; font-size: 0.875rem; display: flex; align-items: center; gap: 8px; box-shadow: 0 8px 30px rgba(0,0,0,0.15); opacity: 0; transition: opacity 0.3s ease, transform 0.3s ease; z-index: 100; white-space: nowrap; }
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }

/* === Your photos (peek view) === */
/* Grid of slots, not a strip: five is countable at a glance, and the add tile
   is always the next empty slot, so the cap is visible without a counter
   (the counter is there anyway because "5/5" beats counting dots). */
.my-photos { margin: 14px 0 4px; }
.my-photos-head {
  display: flex; justify-content: space-between; align-items: baseline;
  font-size: 0.8125rem; font-weight: 600; color: #f0f0f5; margin-bottom: 8px;
}
.my-photos-count { font-size: 0.6875rem; color: #8888aa; font-weight: 400; }
.my-photos-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 6px; }
/* Reassurance, below the grid: the empty-looking surface is the design, and an
   empty gallery next to "your profile" reads as a bug without it. */
.mine-note { margin: 14px 0 4px; font-size: 0.8125rem; line-height: 1.55; color: var(--text-secondary); }
.my-photo { position: relative; aspect-ratio: 1; border-radius: 10px; overflow: hidden; border: 1px solid #2a2a4a; }
.my-photo img { width: 100%; height: 100%; object-fit: cover; display: block; }
.my-photo-del {
  position: absolute; top: 3px; right: 3px; width: 20px; height: 20px;
  border-radius: 50%; border: none; background: rgba(10,10,15,0.72);
  color: #f0f0f5; font-size: 0.8125rem; line-height: 20px; cursor: pointer; padding: 0;
}
.my-photo-del:hover { background: #e74c3c; }
.my-photo-add {
  aspect-ratio: 1; border-radius: 10px; border: 1px dashed #2a2a4a;
  background: none; color: #8888aa; font-size: 1.375rem; cursor: pointer;
}
.my-photo-add:hover { border-color: #ff2d78; color: #ff2d78; }

/* Choosing and framing the picture. The whole tile is the tap target, because a
   five-column grid on a phone has no room for a second control the size of a
   fingertip, and a 20 px frame icon beside the 20 px delete button is two targets
   nobody hits reliably. The accent ring plus the word is the state, not a hint: a
   person who chose a picture should be able to see that it took. */
.my-photo { cursor: pointer; }
.my-photo.is-avatar { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.my-photo-badge {
  position: absolute; left: 3px; bottom: 3px; padding: 1px 5px; border-radius: 6px;
  background: var(--accent); color: #fff; font-size: 0.5625rem; font-weight: 700;
}
.my-photos-hint { margin: 8px 0 0; font-size: 0.75rem; line-height: 1.5; color: var(--text-secondary); }

/* The framing editor. It borrows the confirm modal's frame (backdrop, box, button
   pair) and adds one thing the rest of the site has no use for: a stage clipped to a
   circle, because that is the shape every reader sees. Measured in the browser on
   2026-10-05: with a square stage and a ring drawn on top, a face framed at the left
   edge of the square sat inside the crop and outside the circle, and the You sheet
   then showed a cheek. A square you can fill is a promise the circle breaks, so the
   stage is the circle. */
.crop-modal { position: fixed; inset: 0; z-index: 210; display: flex; align-items: center; justify-content: center; }
.crop-backdrop { position: absolute; inset: 0; background: rgba(0,0,0,0.72); }
.crop-box {
  position: relative; background: var(--surface); border-radius: 16px; padding: 20px;
  width: min(360px, 92vw); border: 1px solid #2a2a4a; box-shadow: 0 20px 60px rgba(0,0,0,0.5);
  animation: win-pop 0.45s cubic-bezier(.34,1.56,.64,1) both;
}
.crop-box h3 { font-size: 1.0625rem; margin: 0 0 6px; }
.crop-hint { font-size: 0.8125rem; line-height: 1.5; color: var(--text-secondary); margin: 0 0 14px; }
/* touch-action: none is what makes a drag a drag on a phone instead of a scroll of
   the sheet underneath, which is the difference between a tool and an accident. */
.crop-stage {
  position: relative; width: 100%; aspect-ratio: 1; overflow: hidden;
  border-radius: 50%; background: #10101c; touch-action: none; cursor: grab;
  box-shadow: inset 0 0 0 1px rgba(255,255,255,0.14);
}
.crop-stage.dragging { cursor: grabbing; }
.crop-stage img { position: absolute; top: 0; left: 0; max-width: none; }
.crop-ring {
  position: absolute; inset: 0; border-radius: 50%; pointer-events: none;
  box-shadow: inset 0 0 24px rgba(10,10,20,0.45);
}
.crop-zoom { display: flex; align-items: center; gap: 10px; margin: 14px 2px 16px; font-size: 0.8125rem; color: var(--text-secondary); }
.crop-zoom input[type=range] { flex: 1; accent-color: var(--accent); }
.crop-actions { display: flex; gap: 10px; }

/* Account erasure zone: quiet until touched, loud once asked. */
/* Themed 2026-10-02 at KB's note that sign out and delete were "very tame and
   not themed": both are outline pills now, accent ring for leaving, red ring
   for erasing. Still lighter than btn-primary - nothing in this sheet should
   compete with matching - and the two colours keep them unmistakable apart. */
.mine-signout { margin: 18px 0 0; }
.btn-quiet {
  background: none; border: none; color: rgba(255,255,255,0.55);
  font-size: 0.875rem; cursor: pointer; padding: 10px 0; text-decoration: underline;
}
.btn-quiet:hover { color: var(--accent); }
.btn-quiet:active { transform: scale(0.97); }
.btn-signout {
  width: 100%; padding: 12px; border-radius: 12px;
  background: none; border: 1px solid rgba(255,45,120,0.5);
  color: var(--accent); font-size: 0.875rem; font-weight: 600; cursor: pointer;
  transition: background 0.15s ease, transform 0.15s ease;
}
.btn-signout:hover { background: rgba(255,45,120,0.12); box-shadow: 0 0 14px rgba(255,45,120,0.25); }
.btn-signout:active { transform: scale(0.97); }

/* Edit your details: the neutral sibling of the two themed pills. The form
   inside is built by app.js from the server's spec, so every rule here is
   shape and colour only - no rule assumes a field name. */
.mine-edit { margin: 18px 0 0; }
/* The language picker. Same box language as the row it sits under, because it is the
   same kind of thing: one account fact, changed here, stored on the server. The label is
   deliberately not the accent colour -- a control that changes the words on the screen is
   not a call to action. */
.mine-lang { margin: 14px 0 0; display: flex; align-items: center; gap: 10px; }
.mine-lang-label { flex: 1; font-size: 0.875rem; color: var(--text, #f0f0f5); }
.lang-select {
  background: #1a1a2e; border: 1px solid #2a2a4a; border-radius: 10px;
  color: #f0f0f5; padding: 8px 30px 8px 10px; font-size: 0.875rem; font-family: inherit;
  appearance: none; -webkit-appearance: none; outline: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23ff2d78' stroke-width='2' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 10px center;
}
.lang-select:focus { border-color: var(--accent); }
.btn-soft {
  width: 100%; padding: 12px; border-radius: 12px;
  background: none; border: 1px solid rgba(255,255,255,0.25);
  color: var(--text, #f0f0f5); font-size: 0.875rem; font-weight: 600; cursor: pointer;
  transition: border-color 0.15s ease, transform 0.15s ease;
}
.btn-soft:hover { border-color: var(--accent); }
.btn-soft:active { transform: scale(0.97); }
.btn-soft:disabled { opacity: 0.5; cursor: default; }
.edit-form { margin-top: 12px; padding: 14px; border-radius: 12px;
  background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.08); }
.edit-row { display: block; margin: 10px 0; }
.edit-row > span { display: block; font-size: 0.75rem; color: #8888aa; margin-bottom: 4px; }
/* The form's fields, and only its fields.

   `:not([type="checkbox"])` is load-bearing, not tidiness. This rule used to say
   `.edit-row input`, and a checkbox is an input, so it reached into the "Interested in"
   row and stretched every 18px box to `width: 100%` with 10px of padding: four black
   slabs with their labels pushed outside them and clipped off the right-hand edge of a
   phone, and the two selected ones filled accent pink because `:checked` outranks this
   rule on specificity. KB's screenshot on 2026-10-11 called it "what's going on here?
   Why are boxes so big?" and he was right to ask.

   The house box recipe at the top of this file is the only thing that draws a checkbox,
   and it wins by default now that nothing here claims one. test_box_rule.js holds the
   check that keeps it that way, including for this sheet, whose markup is built in JS
   and so cannot be read off the pages. */
.edit-row input:not([type="checkbox"]), .edit-row select, .edit-row textarea {
  width: 100%; padding: 9px 11px; border-radius: 12px; font: inherit; font-size: 0.875rem;
  background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.09);
  color: var(--text, #f0f0f5);
}
.edit-row textarea { resize: vertical; min-height: 68px; line-height: 1.5; }
.edit-row input:not([type="checkbox"]):focus, .edit-row select:focus, .edit-row textarea:focus { outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(255, 45, 120, 0.15); }
/* On brand, KB's word for this page on 2026-10-05. The native select arrow is
   the one part of this form that belongs to the operating system rather than
   the app: a white chevron on a dark box, in a product whose entire palette
   is ink and rose. The control now draws its own, in the accent, and the
   focus ring is the same rose wash the rest of the app uses. */
/* The popup list an open <select> shows is drawn by the operating system,
   not the app: without explicit colours the options arrive on system white
   with system blue, which is how a dark app grew a white rectangle
   (KB, 2026-10-11, screenshot of the religion dropdown). The option boxes
   take the surface and the text colour; the OS keeps its highlight shape,
   but it can only pick it from these colours now. */
select option { background: #1a1a2e; color: var(--text); }
select option:disabled { color: var(--text-secondary); }

.edit-row select {
  appearance: none; -webkit-appearance: none;
  padding-right: 34px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23ff2d78' stroke-width='2' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 12px center;
}
/* Dealbreakers, and anything else that posts a list of values.
   KB, 2026-10-04: "Boxes in the dealbreakers should also be aligned and fixed with the
   same col". Two separate faults were in that row.

   The layout: the label was an inline element with an input and a text node in it, so the
   browser wrapped the word onto its own line and the boxes floated centred above a row of
   captions. Fixed by making each item a grid cell with the box and its words on one line,
   and by putting the items in a two-column grid so the columns actually line up instead of
   drifting with the length of the word.

   The colour: a bare `input[type=checkbox]` is drawn by the browser, and Chrome mobile
   draws it white with a blue tick. On this surface that is the second bright system
   control in a dark sheet, after the dials. The box is drawn here instead, in the same
   accent the rest of the sheet uses, with the ring's glow when checked. */
.edit-checks {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px 12px;
  padding: 4px 0;
}
.edit-check {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 0.8125rem;
  color: var(--text);
  cursor: pointer;
  min-width: 0;
}
.edit-check span { min-width: 0; }   /* long words wrap inside the row, not over it */
.edit-note { margin: 16px 0 4px; font-size: 0.75rem; color: #8888aa; }

/* A locked answer: shown, and visibly not an input.
   The value is set at input size and colour rather than inheriting the label's dim 12px,
   because a person's name and religion are their answers and must not read as disabled
   placeholder text. The box is the shape of an input without a caret, a focus ring or a
   cursor, so the difference from an editable field is legible without being a greyed-out
   suggestion that invites the tap. */
.edit-row.locked { margin: 10px 0 14px; }
.locked-value {
  display: block; padding: 10px 12px; border-radius: 10px; font-size: 0.875rem;
  background: #12121e; border: 1px dashed #2a2a4a; color: var(--text, #f0f0f5);
  cursor: default;
}
.locked-note { display: block; margin-top: 4px; font-size: 0.75rem; color: #7d7d99; }
.edit-actions { display: flex; gap: 10px; margin-top: 14px; align-items: center; }
.edit-actions .btn-soft { width: auto; padding: 10px 22px; }
.edit-status { min-height: 18px; margin: 10px 0 0; font-size: 0.8125rem; color: var(--accent); }

.mine-danger { margin: 24px 0 8px; padding-top: 16px; border-top: 1px solid rgba(255,255,255,0.08); }
.btn-danger-quiet {
  width: 100%; padding: 12px; border-radius: 12px;
  background: none; border: 1px solid rgba(231,76,60,0.55);
  color: #e74c3c; font-size: 0.875rem; font-weight: 600; cursor: pointer;
  transition: background 0.15s ease, transform 0.15s ease;
}
.btn-danger-quiet:hover { background: rgba(231,76,60,0.14); }
.btn-danger-quiet:active { transform: scale(0.97); }
.mine-danger-confirm {
  margin-top: 10px; padding: 14px; border-radius: 12px;
  background: rgba(231,76,60,0.10); border: 1px solid rgba(231,76,60,0.35);
  font-size: 0.8125rem; line-height: 1.5;
}
.mine-danger-actions { display: flex; gap: 10px; margin-top: 12px; }

/* The exit survey. Same panel language as the confirmation it sits in front of, so it
   reads as one conversation and not a new screen. */
.exit-reasons { display: flex; flex-wrap: wrap; gap: 8px; margin: 10px 0 4px; }
.exit-reasons button {
  background: #1a1a2e; border: 1px solid #2a2a4a; border-radius: 999px;
  color: #f0f0f5; padding: 8px 14px; font-size: 0.8125rem; font-family: inherit;
  cursor: pointer;
}
.exit-reasons button.on { border-color: #ff2d78; background: rgba(255,45,120,.12); }
.exit-story { margin-top: 10px; }
.exit-story textarea {
  width: 100%; background: #1a1a2e; border: 1px solid #2a2a4a; border-radius: 12px;
  color: #f0f0f5; padding: 10px 12px; font-size: 0.875rem; font-family: inherit;
  resize: vertical; outline: none; -webkit-appearance: none; appearance: none;
}
.exit-story textarea:focus { border-color: #ff2d78; }
.exit-contact { display: flex; gap: 8px; align-items: center; margin-top: 8px;
                font-size: 0.8125rem; color: #a0a0c0; }
.mine-danger-fine { font-size: 0.75rem; color: #8888aa; margin: 10px 0 0; }

/* Safety controls in the chat topbar and the sheet they open. */
.chat-safety {
  background: transparent; border: 1px solid #2a2a4a; border-radius: 10px;
  color: #a0a0c0; font-size: 1.125rem; line-height: 1; padding: 6px 10px; cursor: pointer;
}
.chat-safety:hover { border-color: #ff2d78; color: #ff2d78; }
.safety-sheet {
  position: fixed; inset: 0; background: rgba(5, 5, 12, .72);
  display: flex; align-items: flex-end; justify-content: center; z-index: 60;
  padding: 0;
}
.safety-sheet.hidden { display: none; }
.safety-panel {
  background: #12121f; border-top: 1px solid #2a2a4a; border-radius: 18px 18px 0 0;
  width: 100%; max-width: 520px; padding: 20px 18px calc(env(safe-area-inset-bottom, 12px) + 20px);
  display: flex; flex-direction: column; gap: 10px;
}
.safety-panel h4 { margin: 0 0 4px; font-size: 1rem; color: #f0f0f5; font-weight: 600; }
.safety-option {
  background: #1a1a2e; border: 1px solid #2a2a4a; border-radius: 12px;
  color: #f0f0f5; text-align: left; padding: 12px 14px; cursor: pointer;
  display: flex; flex-direction: column; gap: 4px; font-family: inherit;
}
.safety-option:hover { border-color: #ff2d78; }
.safety-option strong { font-size: 0.875rem; font-weight: 600; }
.safety-option span { font-size: 0.75rem; color: #a0a0c0; line-height: 1.45; }
/* The way out of a safety sheet. It used to be bare grey text (KB, 2026-10-11:
   "the nothing go back is ugly") - the one control in the panel that did not
   look pressable, which makes leaving the hardest gesture on the screen. Now a
   full-width outline button in the same family as the option cards above it,
   and still the quietest thing in the panel: same radius and padding, but no
   fill and no colour, because declining to report is not an action that wants
   emphasis. */
.safety-cancel {
  background: transparent; border: 1px solid #2a2a4a; border-radius: 12px;
  color: #a0a0c0; font-size: 0.875rem; font-weight: 600;
  padding: 11px 14px; cursor: pointer; font-family: inherit;
  transition: border-color 0.15s ease, color 0.15s ease;
}
.safety-cancel:hover { border-color: #4a4a6a; color: #f0f0f5; }
.safety-hint { font-size: 0.75rem; color: #8888aa; margin: 0 0 6px; }
.report-list { display: flex; flex-direction: column; gap: 6px; max-height: 46vh; overflow-y: auto; }
.report-item {
  background: #1a1a2e; border: 1px solid #2a2a4a; border-radius: 10px;
  color: #f0f0f5; font-size: 0.8125rem; padding: 9px 11px; text-align: left;
  cursor: pointer; font-family: inherit;
}
.report-item:hover { border-color: #ff2d78; }
/* The category a report is filed under. A select rather than the row of chips this
   replaced: eight buttons on a phone is a thing you scroll past and then pick the first
   of, and the value that sorts the moderator's queue is the thing worth making deliberate.
   Sized for a thumb and given the same field language as the note under it. */
.report-cat-label {
  display: block; font-size: 0.7188rem; font-weight: 700; letter-spacing: 0.06em;
  text-transform: uppercase; color: #8888aa; margin: 0 0 10px;
}
.report-cat {
  display: block; width: 100%; margin-top: 6px; background: #1a1a2e;
  border: 1px solid #2a2a4a; border-radius: 12px; color: #f0f0f5;
  padding: 11px 12px; font-size: 0.9375rem; font-family: inherit;
  -webkit-appearance: none; appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, #8888aa 50%),
                    linear-gradient(135deg, #8888aa 50%, transparent 50%);
  background-position: calc(100% - 18px) calc(50% + 2px), calc(100% - 13px) calc(50% + 2px);
  background-size: 5px 5px, 5px 5px; background-repeat: no-repeat;
}
.report-cat:focus { border-color: #ff2d78; outline: none; }
#report-detail,
#soc-report-detail {
  width: 100%; background: #1a1a2e; border: 1px solid #2a2a4a; border-radius: 12px;
  color: #f0f0f5; padding: 10px 12px; font-size: 0.875rem; font-family: inherit;
  resize: vertical; outline: none; -webkit-appearance: none; appearance: none;
  margin-bottom: 14px;
}
#report-detail:focus, #soc-report-detail:focus { border-color: #ff2d78; }
/* The report box carries a dropdown and a paragraph, so it gets the width the confirm
   box's 320px default would not give it. Unstyled, the textarea inside arrived as a white
   browser-default well, which is the one thing on this screen that must not look like it
   belongs to the operating system. */
#soc-report-modal .confirm-box { max-width: 400px; }

/* A withdrawn message. Dimmed and italic rather than struck through, because it is a gap
   the transcript honestly reports, not text still readable underneath. */
.msg.unsent { opacity: .5; font-style: italic; font-size: 0.8125rem; }
.msg-actions { display: block; margin-top: 4px; }

/* The editable scored fields and the empty-basis explanation. */
.mine-basis { margin: 18px 0 0; padding: 12px 14px; border: 1px solid #3a2a3a;
              border-radius: 14px; background: rgba(255,45,120,.06); }
.mine-basis p { margin: 0 0 10px; font-size: 0.8125rem; color: #d0d0e0; line-height: 1.5; }
.mine-fields { margin: 18px 0 0; padding-top: 14px; border-top: 1px solid #1a1a2e; }
.mine-fields-note { font-size: 0.75rem; color: #8888aa; margin: 6px 0 10px; }
.field-row { display: flex; gap: 10px; align-items: center; justify-content: space-between;
             padding: 7px 0; }
.field-row label { font-size: 0.8125rem; color: #f0f0f5; flex: 1; }
.field-row select {
  background: #1a1a2e; border: 1px solid #2a2a4a; border-radius: 10px; color: #f0f0f5;
  padding: 8px 10px; font-size: 0.875rem; font-family: inherit; min-width: 120px;
  -webkit-appearance: none; appearance: none; outline: none;
}
.field-row select:focus { border-color: #ff2d78; }

/* Notification switches and the block list on the You sheet. */
.mine-notif { margin: 18px 0 0; padding-top: 14px; border-top: 1px solid #1a1a2e; }
.mine-notif-row {
  display: flex; gap: 10px; align-items: center; font-size: 0.875rem; color: #f0f0f5;
  padding: 6px 0; cursor: pointer;
}
.mine-notif-note { font-size: 0.75rem; color: #8888aa; margin: 8px 0 0; }
.mine-blocked { margin: 18px 0 0; padding-top: 14px; border-top: 1px solid #1a1a2e; }
.blocked-row {
  display: flex; gap: 10px; align-items: center; justify-content: space-between;
  font-size: 0.8125rem; color: #a0a0c0; padding: 7px 0;
}
.blocked-row .btn-ghost { font-size: 0.75rem; padding: 5px 10px; }
.blocked-row .btn-ghost:disabled { opacity: .45; cursor: default; }

/* Taking a press back. Quiet on purpose: it is a rare action, and a loud one next to
   Chat invites the click. */
.btn-unmatch {
  background: transparent; border: 1px solid #3a3a5a; border-radius: 12px;
  color: #a0a0c0; padding: 12px 16px; font-size: 0.875rem; font-family: inherit;
  cursor: pointer;
}
.btn-unmatch:hover { border-color: #ff2d78; color: #ff2d78; }
.btn-unmatch:disabled { opacity: .5; cursor: default; }

/* The connect card: one frame around the whole procedure, on the profile it
   builds. The inner ID and code boxes keep their own skins; this styles the
   head, the numbered steps, the URL chip and the status. */
.connect-card {
  margin: 14px 0 4px; padding: 14px 16px 15px; border-radius: 12px;
  background: rgba(255,255,255,0.035); border: 1px solid rgba(255,255,255,0.07);
}
.connect-card .mine-pairing { margin: 8px 0 6px; background: none; border: none; padding: 0; }
.connect-card .pin-connect { margin: 8px 0 2px; }
.connect-head {
  display: flex; align-items: center; gap: 8px;
  font-size: 0.875rem; font-weight: 700; color: var(--text);
}
.connect-icon { font-size: 1.0625rem; }
.connect-state {
  margin-left: auto; font-size: 0.8125rem; font-weight: 700; color: var(--success);
  text-align: right;
}
/* Waiting is not success, and a failure to reach the server is not good news. The
   chip was success green in every state, so "Not connected yet" - the state almost
   everyone is in while they set this up - was the quietest and the greenest thing on
   the card. */
.connect-state.waiting { color: var(--text-secondary); }
.connect-state.bad { color: var(--accent); }
.connect-steps { margin: 10px 0 0; padding-left: 20px; }
.connect-steps li { margin: 10px 0; font-size: 0.8125rem; color: var(--text-secondary); line-height: 1.5; }
.connect-steps li strong { color: var(--text); }
.connect-url {
  display: block; margin: 7px 0 4px; padding: 8px 11px; border-radius: 8px;
  background: rgba(0,0,0,0.35); border: 1px solid rgba(255,255,255,0.08);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.7812rem; color: var(--success); user-select: all;
}
.connect-copy {
  background: none; border: 1px solid rgba(255,45,120,0.45); color: var(--accent);
  font-size: 0.6875rem; font-weight: 600; padding: 3px 10px; border-radius: 999px;
  cursor: pointer;
}
/* The prompt is a sentence a person sends to their AI, not a URL to paste into a
   settings field, so it gets the same box without the monospace. A block rather
   than a fixed-height field on purpose: a textarea sized in rows clipped the
   second sentence at phone width and left dead space at desktop, because one
   fixed height cannot fit both. This one is as tall as the words are. */
.connect-prompt {
  margin: 7px 0 4px; padding: 8px 11px; border-radius: 8px;
  background: rgba(0,0,0,0.35); border: 1px solid rgba(255,255,255,0.08);
  color: var(--text); font-size: 0.7812rem; line-height: 1.5;
}
.connect-prompt span { display: block; }
.connect-prompt:focus { outline: none; border-color: rgba(255,45,120,0.45); }
.connect-copy:hover { background: rgba(255,45,120,0.12); }
.connect-handoff {
  display: inline-block; margin-top: 10px; font-size: 0.7812rem; font-weight: 600;
  color: var(--accent); text-decoration: none;
}
.connect-handoff:hover { text-decoration: underline; }

/* The pairing fallback: quiet until opened, then it holds the ID and the
   six boxes with the same skins they have always worn. */
.connect-alt { margin: 12px 0 2px; }
/* The route for an agent that cannot open a browser is the one route some people
   have, not a footnote. It was fine print in secondary grey; KB could not find it
   twice, and a person who cannot see the way out assumes there isn't one. */
.connect-alt summary {
  font-size: 0.8438rem; font-weight: 700; color: var(--spotlight);
  cursor: pointer; list-style: none;
}
.connect-alt summary::before { content: "+ "; color: var(--accent); }
.connect-alt[open] summary::before { content: "\2212 "; }
.connect-alt summary:hover { color: var(--text); }
.connect-alt summary::marker { display: none; }

/* The dials. A range input with a sentence beside it rather than a number, because the
   marks say what each position means and "3" does not. */
.mine-dials { margin: 14px 0; }
.dial-row { display: flex; flex-direction: column; gap: 6px; padding: 10px 0;
            border-bottom: 1px solid #2a2a4a; }
.dial-row:last-child { border-bottom: none; }
.dial-row label { font-size: 0.8125rem; color: #ccccdd; }

/* The track and the thumb, drawn rather than left to the browser.
   `accent-color: #7c5cff` was the whole of it, and it failed twice over: that purple is in
   no part of the palette (the brand is --accent pink on navy), and accent-color only tints
   the native control, so Chrome mobile drew a bright white track that sat in the middle of
   a dark sheet like a system dialog. KB, on the live site: "the dials and text underneath
   the dials need to be more on brand."

   The filled portion is painted from a custom property the client sets as you drag, which is
   the only way to get a two-tone track cross-browser without shipping a slider library. */
.dial-row input[type=range] {
  -webkit-appearance: none; appearance: none;
  width: 100%; height: 26px; background: transparent; cursor: pointer;
  /* --fill is the fraction travelled, written by the client on input. The default keeps the
     track correct before the first event, so a slider never opens as a bare grey line. */
  --fill: 50%;
  background: linear-gradient(to right,
              var(--accent) 0%, var(--accent) var(--fill),
              #2a2a4a var(--fill), #2a2a4a 100%);
  background-size: 100% 4px; background-position: center; background-repeat: no-repeat;
  border-radius: 999px;
}
.dial-row input[type=range]::-webkit-slider-runnable-track { height: 4px; background: transparent; }
.dial-row input[type=range]::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 18px; height: 18px; margin-top: -7px; border-radius: 50%;
  background: var(--accent); border: 2px solid #12121e;
  /* The same glow the ring logo and the primary buttons carry, so the control reads as
     part of the app rather than a widget that arrived with the browser. */
  box-shadow: 0 0 0 4px rgba(255, 45, 120, 0.18), 0 0 12px rgba(255, 45, 120, 0.45);
}
.dial-row input[type=range]::-moz-range-track { height: 4px; background: #2a2a4a; }
.dial-row input[type=range]::-moz-range-progress { height: 4px; background: var(--accent); }
.dial-row input[type=range]::-moz-range-thumb {
  width: 16px; height: 16px; border-radius: 50%; background: var(--accent);
  border: 2px solid #12121e; box-shadow: 0 0 0 4px rgba(255, 45, 120, 0.18);
}
.dial-row input[type=range]:focus-visible { outline: none; }
.dial-row input[type=range]:focus-visible::-webkit-slider-thumb {
  box-shadow: 0 0 0 4px rgba(255, 45, 120, 0.35), 0 0 14px rgba(255, 45, 120, 0.6);
}

/* The word under the slider is the only place a member learns what they are choosing, so it
   carries the accent rather than the page's secondary grey, and sits at a size a thumb
   cannot cover with a fingertip. */
.dial-readout { font-size: 0.8125rem; color: var(--accent); font-weight: 500; letter-spacing: .2px; }

/* The verification mark, and the upload section. The check is deliberately not green:
   green next to a name reads "trusted account", and what was verified is one specific
   thing about a photo, not the person. */
/* The verification mark: the landing page's shield at card size, no word beside it (KB,
   2026-10-04: "You dont need to say photo... Just the icon next to their name is enough").
   It is an inline <svg>, so the box rules that used to draw a chip are gone; what stays is
   the size, the alignment against the name's baseline, and the flex rules that stopped the
   card row from clipping it. cursor:help and tabindex go together -- the tooltip is
   reachable by hover and by keyboard, and the claim itself lives in the SVG's title and
   aria-label rather than in visible text. */
.card-verified {
  width: 15px;
  height: 15px;
  flex: 0 0 auto;
  vertical-align: -2px;
  cursor: help;
  filter: drop-shadow(0 1px 4px rgba(0, 214, 255, 0.35));
}
.card-verified:focus-visible {
  outline: none;
  filter: drop-shadow(0 0 6px rgba(255, 45, 120, 0.8));
}
/* Verification is an upgrade, not a form field: the brand gradient frames it, and it
   breathes below the dials' Save (KB 2026-10-25: cramped against Save, off-brand).
   Gradient border = two-layer clip trick, with a transparent 1px border. */
.mine-verify { margin: 26px 0; padding: 16px 16px 18px; border: 1px solid transparent;
               border-radius: 16px;
               background: linear-gradient(160deg, rgba(255, 45, 120, 0.07), #1b1930 45%) padding-box,
                           linear-gradient(120deg, var(--accent), var(--success)) border-box; }
.verify-hook { margin: 0 0 4px; font-size: 1.05rem; font-weight: 700; color: var(--text); }
/* The goal icon beside the hook: the earned mark's own SVG, scaled up from card size.
   Same glow, same gradient -- the icon on the heading and the icon on a card must read
   as one object, or the promise and the reward look like two different systems. */
.verify-hook-mark { width: 20px; height: 20px; vertical-align: -4px; margin-right: 8px;
  filter: drop-shadow(0 0 6px rgba(0, 214, 255, 0.45)); }
.verify-why { margin: 0 0 10px; font-size: 0.85rem; font-weight: 600; color: var(--accent); }
/* The mark sits inline before the state line. Empty means nothing to take up room, so
   the unverified sentence starts where it always did. */
#verify-mark:empty { display: none; }
#verify-mark { margin-right: 6px; }
.verify-code { font-family: ui-monospace, monospace; font-size: 1.375rem;
               letter-spacing: 4px; text-align: center; margin: 10px 0;
               color: #e8e6ff; background: #1b1830; padding: 8px;
               border-radius: 8px; }
.verify-steps input[type=file] { font-size: 0.75rem; color: #8888aa; margin: 8px 0; }

/* The reason box for unmatch and block. It sits above the sheet rather than replacing it,
   because the thing you are about to do is named on the card behind it and re-naming it
   inside a dialog would only add a second place for the name to be wrong. */
.reason-sheet {
  position: fixed;
  inset: 0;
  z-index: 120;
  background: rgba(6, 5, 14, 0.72);
  display: flex;
  align-items: flex-end;
  justify-content: center;
  padding: 16px;
}
.reason-card {
  width: 100%;
  max-width: 460px;
  background: var(--card, #1b1930);
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 18px;
  padding: 18px;
  animation: win-pop 0.45s cubic-bezier(.34,1.56,.64,1) both;
}
.reason-card h3 { margin: 0 0 4px; font-size: 1.125rem; }
.reason-sub { margin: 0 0 12px; font-size: 0.8125rem; color: var(--text-secondary, #a9a6c4); }
.reason-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
.reason-chip {
  background: #2a2a4a;
  color: var(--text-secondary, #a9a6c4);
  border: 1px solid transparent;
  border-radius: 999px;
  padding: 8px 12px;
  font-size: 0.8125rem;
  cursor: pointer;
}
.reason-chip.on {
  background: rgba(122, 92, 230, 0.22);
  color: #fff;
  border-color: rgba(122, 92, 230, 0.6);
}
.reason-sheet textarea {
  width: 100%;
  box-sizing: border-box;
  background: #14122a;
  color: var(--text, #e8e6f5);
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 12px;
  padding: 10px 12px;
  font: inherit;
  font-size: 0.875rem;
  resize: vertical;
  margin-bottom: 12px;
}
.reason-actions { display: flex; gap: 10px; }
.reason-actions .btn-ghost, .reason-actions .btn-danger { flex: 1; }

/* === The plan block on the You page ===
   Stacked cards rather than a spreadsheet column, because this is read on a phone at
   390px and a four-column table there is a horizontal scroll on the one screen where
   the user is deciding whether to trust us with a card number. The current plan is
   accented rather than checked, so "where am I" is answered before any reading. */
.plan { margin: 18px 0 4px; }
.plan-head {
  display: flex; justify-content: space-between; align-items: baseline;
  font-size: 0.8125rem; font-weight: 600; color: #f0f0f5; margin-bottom: 10px;
}
.plan-now { font-size: 0.75rem; font-weight: 700; color: var(--accent); letter-spacing: .02em; }
.plan-grid { display: flex; flex-direction: column; gap: 10px; }
.plan-card {
  border: 1px solid #2a2a4a; border-radius: 14px; padding: 14px 16px;
  background: var(--surface);
}
/* The accent edge on the plan you are on: a border colour is the quietest mark that
   still survives a screenshot, and it does not shout at the two you are not on. */
.plan-card-now { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent) inset; }
.plan-card-head { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; }
.plan-name { font-size: 1rem; font-weight: 700; color: #f0f0f5; }
.plan-price { font-size: 0.8125rem; color: var(--text-secondary); white-space: nowrap; }
.plan-tag { margin: 4px 0 10px; font-size: 0.7812rem; color: var(--text-secondary); line-height: 1.4; }
.plan-lines { list-style: none; margin: 0; padding: 0; }
.plan-lines li {
  position: relative; padding-left: 18px; margin: 5px 0;
  font-size: 0.8125rem; color: #d8d8e6; line-height: 1.4;
}
/* A tick drawn in CSS, not a character: this box has no emoji font and neither does
   every device, and a glyph that fails to draw leaves the plan table with holes in it. */
.plan-lines li::before {
  content: ""; position: absolute; left: 0; top: 5px; width: 8px; height: 4px;
  border-left: 2px solid var(--accent); border-bottom: 2px solid var(--accent);
  transform: rotate(-45deg);
}
.plan-you { margin: 12px 0 0; font-size: 0.75rem; font-weight: 700; color: var(--accent); }
.plan-toggle {
  margin-top: 10px; width: 100%; background: none; cursor: pointer;
  border: 1px solid #2a2a4a; border-radius: 12px; padding: 10px 12px;
  color: var(--text-secondary); font-family: inherit; font-size: 0.8125rem;
}
.plan-toggle:active { border-color: var(--accent); color: #f0f0f5; }
.plan-fine { margin: 12px 0 0; font-size: 0.75rem; color: var(--text-secondary); line-height: 1.45; }

/* === Societies ===
   The friends board's second half (KB, 2026-10-08). Yellow is the society colour on
   this site because the map already draws societies in yellow; a section that wore the
   brand pink while its pins wore yellow would ask you to hold two mappings at once. */

/* The Moderation door on the society tab. Yellow because it is the society yellow's
   feature, and a badge rather than a sentence because the number is the whole message:
   zero means tap and see nothing, three means sit down first. */
.soc-moderate {
  display: inline-flex; align-items: center; gap: 7px;
  color: var(--spotlight); font-size: 0.875rem; font-weight: 700;
  text-decoration: none; margin: 0 0 12px; padding: 9px 14px;
  background: rgba(255,230,0,0.10); border: 1px solid rgba(255,230,0,0.32);
  border-radius: 12px;
}
.soc-moderate:hover { background: rgba(255,230,0,0.16); }
.soc-moderate span:not(.is-zero) {
  background: var(--spotlight); color: #1a1a2e; border-radius: 9px;
  font-size: 0.7188rem; font-weight: 800; padding: 1px 7px; min-width: 18px;
  text-align: center;
}
.soc-moderate span.is-zero { display: none; }
.soc-moderate.hidden { display: none; }
.soc-tier {
  font-size: 0.8125rem; line-height: 1.5; color: var(--text-secondary);
  margin: 12px 0 14px;
}
.soc-error {
  font-size: 0.8125rem; line-height: 1.5; color: var(--accent);
  background: var(--accent-light); border-radius: 10px; padding: 10px 12px;
  margin: 0 0 12px;
}
.soc-create {
  background: #14122a; border: 1px solid #2a2a4a; border-radius: 14px;
  padding: 14px; margin-bottom: 16px;
}
.soc-field { display: block; font-size: 0.75rem; font-weight: 600; color: var(--text-secondary); margin-bottom: 10px; }
.soc-field input, .soc-field textarea {
  display: block; width: 100%; margin-top: 5px;
  background: #0e0d1f; color: var(--text); border: 1px solid #2a2a4a;
  border-radius: 10px; padding: 10px 12px; font: inherit; font-size: 0.9375rem;
  resize: vertical;
}
.soc-create-fine { font-size: 0.75rem; line-height: 1.5; color: var(--text-secondary); margin: 4px 0 12px; }

/* The typeahead under a directory field. Positioned against the label rather than the
   input because the label is the block that holds both, and a panel anchored to the input
   slides under the keyboard on a phone at 390px. */
.soc-suggest {
  position: absolute; z-index: 30; margin: 2px 0 0; padding: 4px; list-style: none;
  background: #14132b; border: 1px solid #2f2f55; border-radius: 12px;
  box-shadow: 0 10px 26px rgba(0, 0, 0, .5); max-height: 232px; overflow-y: auto;
  width: calc(100% - 8px);
}
.soc-field { position: relative; }
.soc-suggest-item {
  display: block; width: 100%; text-align: left; background: none; border: 0;
  color: var(--text); font: inherit; font-size: 0.9375rem; padding: 9px 11px;
  border-radius: 9px; cursor: pointer;
}
.soc-suggest-item:active, .soc-suggest-item.top { background: #1e1c3d; }
.soc-suggest-tag { font-size: 0.6875rem; color: var(--text-secondary); margin-left: 6px; }

/* A room you may read but not write in. Marked in the list and explained in place, so
   nobody types a message into a noticeboard and only then finds out. */
.soc-chan-locked { opacity: .82; }
.society-room-locked {
  margin: 10px 0 0; padding: 10px 12px; font-size: 0.8125rem; line-height: 1.45;
  color: var(--text-secondary); background: #14132b; border: 1px dashed #2f2f55;
  border-radius: 12px;
}
.soc-create-actions { display: flex; gap: 10px; }
.soc-create-actions button { flex: 1; }

.soc-section-head {
  font-size: 0.6875rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--text-secondary); margin: 18px 0 8px;
}

/* One card per society. The card is the tap target, and its Ask-to-join button stops
   propagation so a tap on the button does not also open the card it sits in. */
.soc-card {
  background: var(--surface); border: 1px solid #2a2a4a; border-radius: 14px;
  padding: 13px 14px; margin-bottom: 10px; cursor: pointer;
  transition: transform 0.15s ease, border-color 0.15s ease;
}
.soc-card:hover, .soc-card:focus-visible { border-color: var(--pin-society); outline: none; }
.soc-card:active { transform: scale(0.99); }
.soc-card-top { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.soc-card-name { font-size: 1rem; font-weight: 700; }
.soc-card-cat {
  font-size: 0.75rem; font-weight: 700; color: var(--pin-society);
  border: 1px solid rgba(255,230,0,0.35); border-radius: 8px; padding: 2px 7px;
  white-space: nowrap;
}
.soc-card-focus { font-size: 0.875rem; color: var(--text); margin: 3px 0 0; }
.soc-card-meta { font-size: 0.875rem; color: var(--text-secondary); margin: 5px 0 0; }
.soc-card-foot { margin-top: 10px; display: flex; gap: 8px; align-items: center; }
.soc-join-btn {
  border: 1px solid rgba(255,230,0,0.45); background: rgba(255,230,0,0.1);
  color: var(--pin-society); font: inherit; font-size: 0.8125rem; font-weight: 700;
  border-radius: 10px; padding: 7px 12px; cursor: pointer;
}
.soc-join-btn:disabled { opacity: 0.6; cursor: default; }
.soc-chip {
  font-size: 0.7812rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .04em; border-radius: 9px; padding: 4px 9px;
  border: 1px solid #2a2a4a; color: var(--text-secondary);
}
.soc-chip.member { color: var(--success); border-color: rgba(0,212,255,0.4); }
.soc-chip.owner { color: var(--pin-society); border-color: rgba(255,230,0,0.4); }
.soc-chip.pending { color: var(--pin-crowd); border-color: rgba(123,47,247,0.45); }
.soc-chip.unread { color: #fff; background: var(--accent); border-color: transparent; }
.soc-chip.refused { color: var(--text-secondary); }

/* The society tab. Wider than a confirm box and scrollable: it carries a description,
   a roster, a channel list and a room of messages, and a 320px box would turn the
   first three into a fight. */
.society-box {
  max-width: 560px; width: 94%; max-height: 86vh; max-height: 86dvh; overflow-y: auto; padding: 22px;
  animation: win-pop 0.45s cubic-bezier(.34,1.56,.64,1) both;
}
/* The society sheet is a bottom sheet, not a card pasted mid-screen. The live
   composition of 2026-10-11 (KB: "I told you to fix this box and you made it
   worse") was a content-sized box centred by .confirm-modal: short content
   floated it in the middle of the screen with a dead half-screen under it and
   the societies list poking out above its top edge, text cut mid-line. It now
   rises from the bottom edge, owns the lower three quarters of the viewport,
   and the page behind is the backdrop's business. */
.society-modal { align-items: flex-end; }
.society-modal .society-box {
  width: 100%; max-width: 640px;
  border-radius: 18px 18px 0 0;
  min-height: 76vh; min-height: 76dvh; max-height: 92vh; max-height: 92dvh;
  /* The same glass the sheets wear (KB, 2026-10-11: "they all should be like
     that"). This card floats over the map on every screen size, so the map
     showing 10% through it is the most honest frame it can have. */
  background: rgba(42, 42, 70, 0.95);
  backdrop-filter: blur(10px) saturate(1.1);
  -webkit-backdrop-filter: blur(10px) saturate(1.1);
  /* The old 26px was a guess at a home indicator. The safe-area inset is that measurement
     taken by the browser instead of by me, and on a phone without one it is zero, which is
     the bottom band KB photographed. */
  padding-bottom: calc(10px + var(--safe-bottom));
  /* A column, so the pane inside can be told to fill the sheet rather than being handed a
     height guessed from the sheet's own height. The first attempt at this fix wrote
     `min-height: calc(76dvh - 64px)` on the room pane, which left 27px of nothing under the
     composer whenever the two numbers disagreed -- two ways of saying the same size, which
     is the shape this repo keeps having to stop writing. */
  display: flex; flex-direction: column;
  /* Where the sheet's first row sits, and the only number that decides it. */
  padding-top: 8px;
}
/* A room is a column: header, the list that scrolls, composer. Without this the pane is a
   block and the message list's `flex: 1` has no flex parent to grow in, which is how the
   composer ended up floating with empty sheet under it. */
.soc-pane-room { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
/* While the society is open, the list it came from steps out. Two surfaces
   stacked with one's words cut behind the other's edge read as a broken app;
   the arrow lands the reader back on the list, which returns with it. */
body.soc-modal-open #sheet-societies { display: none; }

/* On a wide screen the sheet floats. A bottom-anchored panel on a desktop
   arrives flush with the screen edge - the rounded end is cut, the end of a
   conversation cannot be brought into view, and the whole thing reads as
   furniture bolted to the window (KB, 2026-10-11: "the whole box should be in
   view... floating over the map"). A card with air on all four sides reads as
   the object it is. The phone keeps the bottom sheet: there, the bottom edge
   IS the design, and the thumb is already down there. */
@media (min-width: 768px) {
  .society-modal { align-items: center; }
  .society-modal .society-box {
    width: 640px; max-width: 92%;
    min-height: 0; max-height: 86vh; max-height: 86dvh;
    border-radius: 18px;
  }
}
/* The board sheet goes entirely, not just its societies list.

   KB, 2026-10-11, on a phone screenshot of a room: "The header is taking soo much space!!
   Same with the bottom. Too much dead space". Measured off that frame, roughly 100 CSS px
   above the room's own header row and 56 below its Send button. The bottom is covered below;
   this is the top. Hiding `#sheet-societies` left the sheet itself on screen, so what the
   reader saw was two bottom sheets stacked: the board sheet's drag handle and its empty body
   as a band, and the society sheet rising in front of it. One surface is open at a time, so
   one is drawn. The map behind stays, which is the point of the sheet being a sheet. */
body.soc-modal-open .sheet { display: none; }
/* Room for two controls at the top right (the overflow and the close), which is what the
   title used to run underneath once the overflow arrived. */
.society-box h3 { font-size: 1.25rem; margin: 0 76px 4px 0; }
.society-meta { font-size: 0.9062rem; color: var(--text-secondary); margin: 0 0 12px !important; }
.society-about { font-size: 0.9688rem; line-height: 1.5; color: var(--text); margin: 0 0 14px !important; }
.society-action { margin-bottom: 12px; }
/* A control that cannot act has to look like it cannot act. The handlers refused an empty
   send long before these buttons were disabled, and a disabled .btn-primary painted itself
   exactly like an enabled one, which is the same dead button in a different place. */
button.needs-text:disabled { opacity: 0.45; cursor: default; }
.society-hint { font-size: 0.8125rem; color: var(--text-secondary); margin: 0 0 12px !important; }

/* The correction form. Same field language as the create form, because it is the same
   act on a society that already exists. */
.soc-edit {
  background: #14122a; border: 1px solid #2a2a4a; border-radius: 12px;
  padding: 12px; margin-bottom: 12px;
}
.soc-edit textarea {
  width: 100%; background: #0e0d1f; color: var(--text); border: 1px solid #2a2a4a;
  border-radius: 10px; padding: 8px 11px; font: inherit; font-size: 0.875rem;
  resize: vertical; box-sizing: border-box;
}
.soc-edit-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 4px; }

.society-requests {
  background: #14122a; border: 1px solid #2a2a4a; border-radius: 12px;
  padding: 12px; margin-bottom: 14px;
}
.soc-ask { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; font-size: 0.875rem; margin-bottom: 8px; }
.soc-ask:last-child { margin-bottom: 0; }
/* A request with nothing said in it is a name and two buttons, which is the report. The note
   is the row's content, so it gets its own line under the name and wraps there instead of
   shoving the decision buttons off the edge. */
.soc-ask-who { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.soc-ask-note {
  font-size: 0.8438rem; font-weight: 400; color: var(--text-secondary); line-height: 1.45;
  overflow-wrap: anywhere;
}
/* The span is always painted, because the only way a typed value should reach this markup
   is through esc(), and a conditional around it puts the raw value in the string. So the
   empty case is hidden here rather than in the template. */
.soc-ask-note:empty { display: none; }
.soc-ask-actions { display: flex; gap: 6px; flex-shrink: 0; }
.soc-ask-actions button { font-size: 0.75rem; padding: 6px 10px; }

/* The room list. Discord's shape, which is what KB pointed at: one room per row, the
   mark that says what kind of room it is, the name, and the news on the right. Chips that
   wrap read as tags; rows read as a list you choose from. */
.soc-board { margin: 14px 0 4px; }
.soc-board-note { font-size: 0.75rem; color: var(--text-secondary); margin: 6px 0 0; }
.soc-board-answering { font-size: 0.7812rem; color: var(--spotlight); margin: 8px 0 2px; }
.soc-board-cancel { background: none; border: none; color: var(--text-secondary);
  font: inherit; font-size: 0.7812rem; cursor: pointer; padding: 0; text-decoration: underline; }
.soc-thread .soc-answer { background: none; border: none; color: var(--text-secondary);
  font: inherit; font-size: 0.7188rem; font-weight: 700; cursor: pointer; padding: 0;
  margin-left: 8px; text-transform: uppercase; letter-spacing: .05em; }
.soc-thread .soc-answer:hover { color: var(--spotlight); }
.soc-board form { display: flex; align-items: flex-end; gap: 8px; margin-top: 8px; }
.soc-board input, .soc-board textarea { flex: 1; min-width: 0; background: var(--surface); color: var(--text);
  border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px; font: inherit;
  font-size: 0.9375rem; line-height: 1.4; box-sizing: border-box; }
.soc-board form button { flex: 0 0 auto; }
.soc-thread { background: var(--surface); border: 1px solid var(--line);
  border-radius: 12px; padding: 9px 12px; margin-bottom: 8px; }
.soc-thread .who { font-size: 0.7188rem; font-weight: 700; color: var(--text-secondary);
  text-transform: uppercase; letter-spacing: .05em; }
.soc-thread .txt { font-size: 0.9375rem; margin-top: 3px; white-space: pre-wrap; }
.soc-thread.soc-is-reply { margin-left: 18px; border-left: 2px solid var(--spotlight); }

.soc-rooms-label {
  font-size: 0.6875rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
  color: var(--text-secondary); margin: 0 0 6px;
}
.society-channels { display: flex; flex-direction: column; gap: 2px; margin-bottom: 12px; }
.soc-chan {
  display: flex; align-items: center; gap: 9px; width: 100%; text-align: left;
  border: none; background: none; color: var(--text-secondary);
  font: inherit; font-size: 0.9375rem; font-weight: 600; border-radius: 9px;
  padding: 9px 11px; cursor: pointer;
}
.soc-chan:hover { background: #14122a; color: var(--text); }
.soc-chan.active { background: #1c1938; color: var(--text); }
.soc-chan-mark { width: 15px; flex-shrink: 0; text-align: center; opacity: .75; font-size: 0.8125rem; }
.soc-chan-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The news marker is yellow because yellow is this feature's colour for a society's own
   news; the badge on the top bar is the same yellow for the same reason. */
.soc-chan-new {
  flex-shrink: 0; font-size: 0.625rem; font-weight: 700; color: var(--pin-society);
  text-transform: uppercase; letter-spacing: .05em;
  background: rgba(255, 230, 0, .12); border-radius: 8px; padding: 2px 7px;
}

.society-add { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
/* The label stays on one line: at phone width a two-line button beside a
   single-line input reads as two different controls. */
.society-add button { white-space: nowrap; flex-shrink: 0; }
.society-add input {
  flex: 1 1 150px; background: #0e0d1f; color: var(--text); border: 1px solid #2a2a4a;
  border-radius: 10px; padding: 8px 11px; font: inherit; font-size: 0.875rem;
}

/* The two panes. The sheet shows the front page or the room, never both, because the
   thing that made the old layout feel cramped was the transcript pushing the room list
   off the top of a phone screen. */
.soc-pane-home { display: flex; flex-direction: column; }
.soc-pane-room { display: flex; flex-direction: column; min-height: 60vh; min-height: 60dvh; }
/* The site's back controls, all of them, in one rule. KB asked twice for "our iconic
   back like the landing page", then named the colour: yellow. Worth recording where that
   yellow actually comes from, because it is not the landing page -- that page's back is
   `--text-secondary` grey and its only yellow is the assistant's thinking dot. The yellow
   is `--pin-society`, the colour this feature has worn all evening on its pins, its chips
   and its room marks. So: bare text, arrow and word together, in that yellow rather than a
   grey that made the control read as disabled. */
.soc-room-back, .btn-done, #exit-back {
  background: none; border: none; color: var(--pin-society);
  font: inherit; font-size: 0.875rem; font-weight: 600;
  cursor: pointer; padding: 4px 0; flex-shrink: 0;
  transition: color .2s ease, opacity .2s ease;
}
.soc-room-back:hover, .btn-done:hover, #exit-back:hover { opacity: .72; }
/* The token is --spotlight, the site's own word for this yellow (the map's neon, also the
   assistant's featured voice). --pin-society is the same value and is the wrong name for
   a control: the back button is not a society.

   This rule is the whole of the yellow-for-grey request, and its edge is the point: it
   covers controls that step back, not buttons that happen to be secondary. #exit-back
   keeps the yellow over its .btn-ghost class because it is an arrow and the word Back;
   #btn-pass, which shares .btn-ghost and sits in a row of four peer actions, does not. */
.soc-room-back, .btn-done, #exit-back { color: var(--spotlight); }
.soc-room-hash { color: var(--text-secondary); font-weight: 700; }
.soc-room-name { font-size: 1rem; font-weight: 700; color: var(--text); }
.society-messages {
  /* Grows into whatever the sheet has left, rather than stopping at a number.

     The other half of KB's 2026-10-11 complaint. The sheet is at least 76dvh tall and the
     message list stopped at 52dvh, so about a quarter of the sheet was empty and the
     composer floated in the middle of it with nothing under it. A list that scrolls should
     own the space it is given; the cap was a guess at a viewport. */
  flex: 1 1 auto; min-height: 200px; overflow-y: auto;
  display: flex; flex-direction: column; gap: 2px;
  background: none; border: none; padding: 0;
}
.soc-room-empty { font-size: 0.8125rem; color: var(--text-secondary); margin: 0; }

/* One message. Avatar in its own column, name and time on the first line of the body,
   text under it. Grouped runs drop the avatar column's content and the name, which is
   what makes a long conversation read as a conversation instead of a list. */
/* One message. Discord's density, which KB asked for by name: the avatar column, the
   name and time on one line, the text under it at a size that reads without leaning in,
   and nothing between rows that is not a message. The row is a tap target for a reply,
   so there is no per-message control eating a line of its own. */
.soc-msg {
  display: grid; grid-template-columns: 40px 1fr; gap: 12px;
  font-size: 1rem; padding: 1px 0 3px; cursor: pointer;
  border-radius: 8px;
}
.soc-msg:active { background: #14122a; }
.soc-msg-avatar {
  width: 40px; height: 40px; border-radius: 50%; background: #1c1938;
  border: 1px solid #2f2b57; color: var(--pin-society);
  display: flex; align-items: center; justify-content: center;
  font-size: 1rem; font-weight: 700;
}
.soc-msg.mine .soc-msg-avatar { color: var(--success); border-color: rgba(0, 212, 138, .4); }
.soc-msg.grouped .soc-msg-avatar { visibility: hidden; }
/* A run from one speaker hangs together; a change of speaker gets a breath. That is how
   Discord reads as a conversation rather than a list of items. */
.soc-msg.grouped { padding-top: 0; }
.soc-msg:not(.grouped) + .soc-msg:not(.grouped) { padding-top: 7px; }
.soc-msg-body { min-width: 0; padding: 0; }
.soc-msg-head { display: flex; align-items: baseline; gap: 8px; }
.soc-msg-who { font-weight: 700; color: var(--pin-society); font-size: 0.9688rem; }
.soc-msg.mine .soc-msg-who { color: var(--success); }
.soc-msg-when { font-size: 0.75rem; color: var(--text-secondary); }
.soc-msg-text { color: var(--text); word-break: break-word; line-height: 1.42; }

/* The reply, drawn the way Discord draws it: above the answer's own header, indented
   into the text column, with an elbow reaching back into the avatar column so the quoted
   line and the answer are visibly one event. A small avatar and an @-prefixed name, and
   the quoted text runs on the same line -- a box of its own is what made the old version
   cost four lines for what Discord spends two on. */
.soc-msg-quote {
  display: flex; align-items: baseline; gap: 6px;
  margin: 0 0 1px; padding: 2px 0 0 24px; position: relative;
  font-size: 0.875rem; color: var(--text-secondary);
}
.soc-msg-quote::before {
  content: ""; position: absolute; left: 2px; top: -7px;
  width: 16px; height: 11px;
  border-left: 2px solid #4a4770; border-bottom: 2px solid #4a4770;
  border-bottom-left-radius: 7px;
}
/* Inline, immediately before the @-name, which is where Discord puts it. An absolute
   position in the avatar column reads as a second speaker marker for the whole row. */
.soc-quote-avatar {
  flex-shrink: 0; align-self: center;
  width: 16px; height: 16px; border-radius: 50%; background: #1c1938;
  border: 1px solid #2f2b57; color: var(--text-secondary);
  display: flex; align-items: center; justify-content: center;
  font-size: 0.5625rem; font-weight: 700;
}
.soc-quote-who { font-weight: 700; color: var(--text-secondary); flex-shrink: 0; }
.soc-quote-text {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  opacity: .78;
}
/* The divider between what you have read and what arrived after. Placed by the server's
   watermark, not by a timestamp the browser guesses at, and set in caps because it is a
   structural marker rather than prose. */
.soc-new-line {
  display: flex; align-items: center; gap: 10px; margin: 9px 0 7px;
  color: var(--accent); font-size: 0.6875rem; font-weight: 700;
  letter-spacing: .07em; text-transform: uppercase;
}
.soc-new-line::before, .soc-new-line::after {
  content: ""; flex: 1; height: 1px; background: rgba(255, 45, 120, .45);
}
.soc-reply-strip {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  background: #14122a; border: 1px solid #2a2a4a; border-left: 3px solid var(--accent);
  border-radius: 9px; padding: 7px 10px; margin-top: 10px;
  font-size: 0.75rem; color: var(--text-secondary);
}
.soc-reply-strip b { color: var(--text); font-weight: 700; }
/* The theme has one way to dismiss something inline: a bare word in the secondary colour
   that lifts to text on hover, the same language as the landing page's back and the
   sheet's text buttons. A bare multiplication sign is a glyph rather than a themed
   control, and it gave the reader nothing to read. */
.soc-reply-cancel {
  border: none; background: none; color: var(--spotlight);
  font: inherit; font-size: 0.75rem; font-weight: 700;
  cursor: pointer; padding: 2px 4px; flex-shrink: 0;
  transition: color .2s ease;
}
.soc-reply-cancel:hover { color: var(--text); }
.society-composer { display: flex; align-items: flex-end; gap: 8px; margin-top: 10px; }
.society-composer input, .society-composer textarea {
  flex: 1; min-width: 0; background: #0e0d1f; color: var(--text); border: 1px solid #2a2a4a;
  border-radius: 10px; padding: 10px 12px; font: inherit; font-size: 0.9375rem; line-height: 1.4;
  box-sizing: border-box;
}
/* flex-end is the Telegram arrangement: as the field takes lines, the send button stays at
   its bottom-right corner rather than floating at the top of a tall box. And the button
   gives the width back to the field -- `.btn-primary` carries flex: 1 for rows where two
   buttons split the width, which in a composer leaves two lines to write in. */
.society-composer button { flex: 0 0 auto; }
/* grow.js owns the height, the resize handle and the overflow, because the three pages that
   load it have three stylesheets and a rule that exists on one and not the others is the
   bug. This is the no-script fallback: a one-row field that scrolls inside itself still
   lets you reach what you wrote. */
textarea.grow { overflow-y: auto; }
/* The confirm-box textareas were inheriting the browser's white on a dark card,
   visible in the profile-report screenshot of 2026-10-11 and in the society box
   beside it. A white well on a dark sheet reads as broken even when it works. */
.confirm-box textarea {
  background: #12121f; color: #f0f0f5; border: 1px solid #2a2a4a; border-radius: 10px;
  padding: 10px 12px; font-family: inherit; font-size: 0.875rem;
}
.confirm-box textarea::placeholder { color: #8888aa; }
/* An announcement can carry a date, and this is where the organiser says so. It is the
   same strip language as the reply bar because it is the same kind of thing: a choice that
   applies to the next message you send, not a permanent part of the room. */
.soc-event-row {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
  background: #14122a; border: 1px solid #2a2a4a; border-left: 3px solid var(--pin-society);
  border-radius: 9px; padding: 7px 10px; margin-top: 10px;
  font-size: 0.7812rem; color: var(--text-secondary);
}
.soc-event-flag { display: flex; align-items: center; gap: 7px; cursor: pointer; }
/* The audience row in the announcements composer (KB, 2026-10-11): who can read this
   one announcement, chosen by the person writing it. */
.soc-aud-row { display: flex; align-items: center; gap: 8px; margin-top: 8px; }
.soc-aud-q { font-size: 0.7812rem; color: var(--text-secondary); }
.soc-aud-opt {
  border: 1px solid #35325e; border-radius: 999px; background: #20203a;
  color: #c8c8e0; font: inherit; font-size: 0.7812rem; font-weight: 600;
  min-height: 30px; padding: 5px 11px; cursor: pointer;
}
.soc-aud-opt.is-on { border-color: var(--accent); color: var(--text); background: #26243f; }
.soc-event-fields { display: flex; flex-wrap: wrap; gap: 8px; width: 100%; }
.soc-event-fields input {
  flex: 1 1 140px; background: #0e0d1f; color: var(--text); border: 1px solid #2a2a4a;
  border-radius: 9px; padding: 7px 9px; font: inherit; font-size: 0.8125rem;
  color-scheme: dark;   /* the native date picker otherwise paints itself for a light page */
}
/* The date on the message it belongs to. Yellow because it is the society's own colour and
   the map's halo is the same fact seen from the other end. */
.soc-msg-event {
  display: inline-flex; align-items: center; gap: 6px; margin-top: 6px;
  border: 1px solid var(--pin-society); border-radius: 8px; padding: 3px 8px;
  color: var(--pin-society); font-size: 0.7812rem; font-weight: 700;
}
/* Whether the room is published. Said to the organiser where the room is open, since that
   is the moment the question is live. */
.soc-room-public {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: 8px; margin: 8px 0 10px; padding: 8px 10px;
  background: #14122a; border: 1px solid #2a2a4a; border-radius: 9px;
  font-size: 0.7812rem; color: var(--text-secondary); line-height: 1.4;
}
.soc-room-public button { flex-shrink: 0; }

/* The society sheet's own furniture. It had none: every block was the same muted lavender
   on the same panel, so the description, the room list, the roster and the button that ends
   the society all read at the same weight, and the only colour on the screen was the
   destructive one. These pieces give the sheet a place for its controls, a place to say
   where you are, a list that reads as a list, and a home for what you press rarely. */
.soc-bar {
  position: sticky; top: 0; z-index: 4;
  display: flex; justify-content: space-between; align-items: center; gap: 2px;
  /* Tight against the title rather than floating a centimetre above it, which is what made
     the controls read as furniture from another sheet. The fade stops short of the name:
     sticky means it sits over the content while scrolling, and a fade that reaches the
     title dims the first line of every society. */
  /* Bleed sideways only, and keep the top tight.

     This used to be `margin: -22px -22px 6px` with 14px of top padding, written when the box
     was a block: the negative top margin cancelled the sheet's 22px of padding so the row sat
     near the edge. The sheet became a flex column and the negative margin stopped being
     honoured, which is how the row ended up 37px below the top edge -- KB, 2026-10-11: "The
     back and general panel and the dots are too low! Theres so much space above them!". The
     top padding on the sheet is now the number that says where the row sits, so there is one
     place to change rather than two that have to agree. */
  margin: 0 -22px 6px; padding: 6px 18px 6px;
  /* Opaque, with a rule under it - not a fade. The fade (a gradient to
     transparent) let message text ghost through the bar while scrolling, so
     the first row of every room arrived half-clipped and ghosted (KB,
     2026-10-11). The fade existed so the sticky bar would not dim the title;
     the title now lives IN the bar, so nothing below the bar needs mercy. */
  background: var(--surface);
  border-bottom: 1px solid #2a2a4a;
}
/* One header row for the sheet: arrow and name on the left, the overflow on the
   right (KB, 2026-10-11, the dead-space complaint). The name truncates rather than
   wrapping; a header that wraps is a header that eats the transcript. */
.soc-bar-left {
  display: flex; align-items: center; gap: 8px;
  min-width: 0; flex: 1;
}
.soc-bar .soc-bar-name {
  font-size: 1rem; font-weight: 700; color: var(--text); margin: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0;
}
.soc-bar .soc-room-name {
  font-size: 1rem; font-weight: 700; color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0;
}
.soc-icon-btn {
  width: 34px; height: 34px; border: 1px solid #35325e; border-radius: 10px;
  background: #20203a; color: var(--text-secondary); cursor: pointer;
  font-size: 1.25rem; line-height: 1; display: grid; place-items: center;
  transition: background .15s ease, color .15s ease;
}
.soc-icon-btn:hover, .soc-icon-btn[aria-expanded="true"] {
  background: #26243f; color: var(--text);
}
/* KB, 2026-10-11, on the society sheet: "theres no X to close it". There was, and it
   worked; it was invisible, a bare glyph on the dark sheet with no edge to press.
   Controls at rest now carry an edge: the eye finds a button, not a smudge. */

/* The overflow. Reporting, leaving and closing stay reachable without being the three
   largest things on the sheet, which is what made it read like a form of consequences. */
.soc-menu {
  position: absolute; top: 48px; right: 16px; min-width: 218px;
  background: #211f3c; border: 1px solid #35325e; border-radius: 14px;
  box-shadow: 0 16px 44px rgba(0, 0, 0, .58); padding: 6px;
  display: flex; flex-direction: column; gap: 2px; z-index: 6;
  animation: card-in .16s cubic-bezier(.32, .72, 0, 1) both;
}
.soc-menu-item {
  background: none; border: none; border-radius: 9px; cursor: pointer;
  color: var(--text); font: inherit; font-size: 0.875rem; text-align: left;
  padding: 10px 12px;
}
.soc-menu-item:hover { background: #2d2a54; }
.soc-menu-item.is-danger { color: #ff6f61; }
.soc-menu-item.is-danger:hover { background: rgba(231, 76, 60, .15); }
.soc-menu-sep { border: none; border-top: 1px solid #35325e; margin: 5px 4px; }

/* The status sits by the name, in the society's own yellow when it is open to anyone and
   in the crowd violet when the founder has shut the door. It is a label, not a control:
   the change lives in the overflow, so reading cannot alter it. */
.soc-status {
  padding: 3px 9px; border-radius: 8px;
  font-size: 0.6875rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
}
.soc-status.is-public { color: var(--pin-society); background: rgba(255, 230, 0, .13); }
.soc-status.is-private { color: #b98cff; background: rgba(123, 47, 247, .22); }
.soc-status.is-closed { color: #ff6f61; background: rgba(231, 76, 60, .16); }

/* The description is a box rather than a loose sentence, because it is the paragraph a
   stranger reads before deciding, and for the people who run the society the box is the
   editor. */
.soc-about-box {
  background: #14132a; border: 1px solid #2a2a4a; border-radius: 12px;
  padding: 12px 14px; margin: 0 0 14px;
}
.soc-about-box .society-about { margin: 0 !important; }
.soc-about-box textarea {
  width: 100%; background: transparent; border: none; color: var(--text);
  font: inherit; font-size: 0.9375rem; line-height: 1.5; resize: none; outline: none;
  min-height: 66px;
}
.soc-about-box textarea::placeholder { color: var(--text-secondary); }
.soc-about-save {
  display: block; margin: 8px 0 0 auto; padding: 6px 14px; border-radius: 999px;
  background: none; border: 1px solid var(--accent); color: var(--accent);
  font: inherit; font-size: 0.8125rem; font-weight: 600; cursor: pointer;
}
.soc-about-save:disabled { border-color: #2a2a4a; color: var(--text-secondary); cursor: default; }

.soc-tabs {
  display: flex; gap: 2px; margin: 0 0 14px;
  border-bottom: 1px solid #2a2a4a;
  /* Four tabs are wider than a 360px sheet, and a tab that runs past the edge is a tab
     nobody can press. The row slides before it clips. */
  overflow-x: auto;
}
.soc-tab {
  background: none; border: none; border-bottom: 2px solid transparent;
  border-radius: 8px 8px 0 0; margin-bottom: -1px; cursor: pointer;
  color: var(--text-secondary); font: inherit; font-size: 0.9062rem; font-weight: 600;
  padding: 10px 12px 11px;
}
.soc-tab:hover { color: var(--text); }
.soc-tab.is-on { color: var(--text); border-bottom-color: var(--accent); }
.soc-tab-n {
  margin-left: 7px; padding: 2px 7px; border-radius: 999px;
  background: var(--accent-light); color: var(--accent);
  font-size: 0.7188rem; font-weight: 700;
}
/* A count with nothing in it still carries its padding and its margin, so an empty chip
   costs about a fifth of a tab. With four tabs on a phone that is the difference between a
   row that fits and one that has to slide. */
.soc-tab-n:empty { display: none; }
/* Narrow screens: the tabs give up padding before the sheet gives up a tab. */
@media (max-width: 560px) {
  .soc-tab { padding: 10px 8px 11px; font-size: 0.875rem; }
}

/* The roster as a list. A name with a "make admin" link butted against it, in a run of
   chips, was the old shape; this is one row per person, with the role and the one action
   the founder has each in its own column. The avatar discs take their colour from the
   sheet's own tint table, so the list has colour in it without anybody being assigned a
   hue by hand. */
.soc-members { list-style: none; margin: 0; padding: 0; }
.soc-member {
  display: flex; align-items: center; gap: 11px; padding: 9px 2px;
  border-bottom: 1px solid rgba(42, 42, 74, .55);
}
.soc-member:last-child { border-bottom: none; }
/* An expanded row gives its actions a line of their own (KB, 2026-10-11, on the
   screenshot where Profile/Make admin/Kick/Ban ate the member's name down to
   "Trol…"): the name keeps the whole first line, the buttons sit under it. */
.soc-member.is-open { flex-wrap: wrap; }
.soc-member.is-open .soc-member-name { flex: 1 1 auto; }
.soc-member.is-open .soc-member-act { margin-top: 6px; }
.soc-avatar {
  width: 30px; height: 30px; flex-shrink: 0; border-radius: 50%;
  display: grid; place-items: center; font-size: 0.8125rem; font-weight: 700;
}
.soc-member-name {
  flex: 1; min-width: 0; font-size: 0.9375rem; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.soc-role {
  flex-shrink: 0; padding: 3px 8px; border-radius: 999px;
  font-size: 0.6562rem; font-weight: 700; letter-spacing: .05em; text-transform: uppercase;
}
.soc-role.is-founder { color: var(--pin-society); background: rgba(255, 230, 0, .13); }
.soc-role.is-admin { color: #b98cff; background: rgba(123, 47, 247, .22); }
.soc-member-when { flex-shrink: 0; font-size: 0.7812rem; color: var(--text-secondary); }
.soc-member-act {
  flex-shrink: 0; background: #20203a; border: 1px solid #35325e; border-radius: 10px;
  color: #c8c8e0; font: inherit; font-size: 0.7812rem; font-weight: 600;
  min-height: 30px; padding: 5px 11px; cursor: pointer;
}
.soc-member-act:hover { border-color: var(--accent); color: var(--text); background: #26243f; }
/* The roster filter, painted by societies.js when the list outgrows a screen. */
.soc-member-search {
  width: 100%; box-sizing: border-box; margin: 0 0 8px; padding: 9px 12px;
  background: #12121f; border: 1px solid #2a2a4a; border-radius: 10px;
  color: var(--text); font: inherit; font-size: 0.875rem;
}
.soc-member-search:focus { outline: none; border-color: var(--accent); }
.soc-members-empty { font-size: 0.875rem; color: var(--text-secondary); margin: 4px 0 0; }

/* Closing is the one destructive thing the sheet offers, and the menu is the only place
   red is allowed, so that red means "this ends something" instead of decorating a foot. */
.soc-close-confirm {
  background: #1a1020; border: 1px solid rgba(231, 76, 60, .35); border-radius: 12px;
  padding: 12px;
}
.soc-close-confirm p { font-size: 0.8438rem; line-height: 1.5; color: var(--text); margin: 0 0 10px; }

/* Age and gender on the card header (KB, 2026-10-11: "Age and gender could be
   displayed in a nicer way"). The pill is the shape the site already speaks -
   the DEMO tag, the match score, the PRIVATE chip - and the city stays plain
   words, because the chips are the facts and the city is the sentence. */
.pc-chip {
  display: inline-block; padding: 2px 10px; margin: 0 6px 2px 0;
  border: 1px solid #3f3f66; border-radius: 999px;
  font-size: 0.875rem; font-weight: 600; color: var(--text);
  background: rgba(255, 255, 255, .08); vertical-align: 1px;
  /* Sentence case, not the shouty caps of PRIVATE and FOUNDER: these chips
     state facts about a person, not their rank. The stored word stays what
     it is; this is display. The age band has no letters to lift. */
  text-transform: capitalize;
}
/* The city wears the map's label yellow (KB, 2026-10-11: "The city should be
   yellow no just like the map names?") - the same #ffe600 the basemap draws
   its place names in, so the word and the pin speak one grammar. */
.pc-city { font-size: 0.875rem; color: var(--pin-society); }
.card-city { color: var(--pin-society); }

/* Scrollbars wear the theme (KB, 2026-10-11: "fix all scrollbars please").
   Thin and quiet: the thumb is the panels' border grey, the track is nothing,
   hover lifts it one shade. The transparent border keeps the visible thumb
   slim while the hit area stays a comfortable 10px. Firefox takes the two
   standard properties; WebKit takes the pseudo-elements. */
* { scrollbar-width: thin; scrollbar-color: #3a3a5a transparent; }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: #2a2a4a; border-radius: 6px;
  border: 2px solid transparent; background-clip: padding-box;
}
::-webkit-scrollbar-thumb:hover { background: #3a3a5a; background-clip: padding-box; }
