/* The palette is gilt on dark stone: the brass of a college gate against the
   near-black a phone gets used on at midnight. It replaces a violet that could
   have belonged to any product with a login form. Green sits under the greys so
   the ground reads as stone rather than as a screenshot of a dashboard, and the
   one saturated colour on any screen is the person's own university. */
:root {
  --bg: #0a0d0c;
  --surface: #0f1312;
  --surface-2: #161a19;
  --line: #1e2422;
  --text: #f4f1ea;
  --muted: #8d958f;
  --primary: #c2a05c;
  --gold: #e3b341;
  --teal: #2fd4c4;
  --coral: #e2745a;
  /* Ivory carries the primary action. Brass filled every button, and a screen
     where the most expensive colour is also the most common one reads as a
     theme rather than as a hierarchy. Brass is now an edge: the mark, an active
     letter, a hairline under the thing you are on. */
  --ink: #f4f1ea;
  --radius: 12px;
  /* One spacing scale, eight to the step, so nothing lands on a number
     somebody picked by eye. */
  --s1: 4px;
  --s2: 8px;
  --s3: 16px;
  --s4: 24px;
  --s5: 40px;
  --s6: 64px;
  /* Motion. Two curves and three durations, used everywhere, so the whole app
     moves the same way. The out curve decelerates into place for anything
     arriving; the spring overshoots by a hair for anything a thumb just
     pressed. Nothing here runs longer than 260ms: past that a phone feels
     slow rather than considered. */
  --ease-out: cubic-bezier(0.2, 0, 0, 1);
  --ease-spring: cubic-bezier(0.34, 1.4, 0.64, 1);
  --fast: 120ms;
  --mid: 180ms;
  --slow: 260ms;
  --font-body:
    "DM Sans", ui-sans-serif, -apple-system, "Segoe UI", Roboto, sans-serif;
  /* Fraunces carries the whole voice of the thing. Georgia stands in while it
     loads and if the network never gives it up. */
  --font-head: "Fraunces", Georgia, "Times New Roman", "Iowan Old Style", serif;
}

* {
  box-sizing: border-box;
}

/* Every animation in this file is decoration on top of something that already
   works, so the setting is honoured globally rather than rule by rule. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* Class selectors like `.gate { display: grid }` outrank the UA default
   `[hidden] { display: none }`, so `el.hidden = true` silently does nothing.
   Every visibility toggle in app.js goes through the hidden attribute, so
   this rule has to win. */
[hidden] {
  display: none !important;
}

body {
  margin: 0;
  background:
    radial-gradient(900px 520px at 15% -10%, #16211d 0%, transparent 70%),
    var(--bg);
  color: var(--text);
  font: 15px/1.5 var(--font-body);
  min-height: 100vh;
}

/* Headings carry the serif; everything else stays in DM Sans. Sizes and
   colours are left to the per-component rules further down. */
h1,
h2,
h3,
h4 {
  font-family: var(--font-head);
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1.15;
  margin: 0 0 var(--s2);
}

/* Four steps, each clearly apart from the next. Everything else is body or
   meta; there is no fifth size. */
h1 {
  font-size: clamp(1.8rem, 4vw, 2.4rem);
}
h2 {
  font-size: 1.3rem;
}
h3 {
  font-size: 1.05rem;
}
h4 {
  font-size: 0.95rem;
}

.hint {
  color: var(--muted);
  font-size: 0.88rem;
  line-height: 1.6;
}

button {
  font: inherit;
  cursor: pointer;
  border: 1px solid var(--line);
  background: var(--surface-2);
  color: var(--text);
  border-radius: 10px;
  padding: 8px 14px;
  transition:
    transform var(--fast) var(--ease-spring),
    background var(--mid) var(--ease-out),
    border-color var(--mid) var(--ease-out),
    opacity var(--mid) var(--ease-out);
}
/* A press that produces nothing for 300ms reads as a dropped tap, and on a
   phone there is no cursor to prove the finger landed. */
button:active {
  transform: scale(0.97);
}
button:disabled {
  opacity: 0.55;
  transform: none;
}
button:hover {
  border-color: var(--primary);
}
button.primary,
button[type="submit"] {
  background: var(--ink);
  border-color: var(--ink);
  color: #0a0d0c;
  font-weight: 600;
}

button.primary:hover,
button[type="submit"]:hover {
  background: #fffdf7;
  border-color: #fffdf7;
}

/* Secondary actions are outlines of the ground they sit on, not filled chips.
   Fewer filled shapes is most of what separates elegant from busy. */
button:not(.primary):not([type="submit"]):not(.danger) {
  background: transparent;
}
/* Destructive actions borrow the same coral the app already uses for errors,
   outlined rather than filled so they never out-shout the primary action. */
button.danger {
  border-color: var(--coral);
  color: var(--coral);
}
button.danger:hover {
  background: var(--coral);
  color: #12100a;
}

/* Checkboxes and radios are not text fields: stretching one to the width of
   its row drew a 300px square next to a two-word label. */
input[type="checkbox"],
input[type="radio"] {
  width: auto;
  accent-color: var(--primary);
}

input,
select,
textarea {
  width: 100%;
  font: inherit;
  color: var(--text);
  background: transparent;
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 11px 13px;
  transition: border-color var(--mid) var(--ease-out);
}

input:hover,
select:hover,
textarea:hover {
  border-color: color-mix(in srgb, var(--primary) 45%, var(--line));
}
input:focus,
select:focus,
textarea:focus {
  outline: 2px solid var(--primary);
  outline-offset: 1px;
}
label {
  display: block;
  margin-bottom: 12px;
  color: var(--muted);
  font-size: 13px;
}
label input,
label select,
label textarea {
  margin-top: 5px;
  color: var(--text);
}

/* ----------------------------------------------------- index rail, pull */

/* Hundreds of societies in one scroll is a list nobody finishes. The rail
   pins to the side of the list and jumps to a letter, and the letters that
   have nothing behind them are never drawn. Pattern from Ant Design Mobile's
   IndexBar. */
.indexed {
  position: relative;
}

/* Fixed to the edge of the screen rather than sticky inside the list: a
   sticky float stopped painting once the page was tens of thousands of pixels
   down, which is exactly where a rail is needed. It lives inside the societies
   view, so it disappears with it. */
#society-index {
  position: fixed;
  right: 2px;
  top: 50%;
  transform: translateY(-50%);
  display: grid;
  gap: 1px;
  padding: 6px 3px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--surface-2) 92%, transparent);
  border: 1px solid var(--line);
  z-index: 25;
}

.index-letter {
  border: 0;
  background: none;
  color: var(--muted);
  font-size: 0.66rem;
  line-height: 1;
  padding: 3px 6px;
  min-height: 0;
  border-radius: 999px;
}

.index-letter:active {
  background: var(--accent, var(--primary));
  color: var(--bg);
  transform: none;
}

/* The letter a run of cards files under. It is a heading, not a divider, so it
   carries the display face and sits tight to the first card under it. */
.index-head {
  font-family: var(--font-head);
  color: var(--accent, var(--primary));
  margin: 18px 0 2px;
  font-size: 0.95rem;
  letter-spacing: 0.08em;
  position: sticky;
  top: calc(var(--appbar-h, 64px));
  background: var(--bg);
  padding: 4px 0;
  z-index: 4;
}

/* Pull to refresh. The indicator lives above the page and follows the finger
   at a third of the distance, which is what makes it feel attached. */
#pull {
  position: fixed;
  top: calc(var(--appbar-h, 64px) + 4px);
  left: 50%;
  margin-left: -16px;
  width: 32px;
  height: 32px;
  display: grid;
  place-items: center;
  border-radius: 999px;
  background: var(--surface-2);
  border: 1px solid var(--line);
  z-index: 60;
  transition: transform var(--fast) var(--ease-out);
}

.pull-spinner {
  width: 14px;
  height: 14px;
  border-radius: 999px;
  border: 2px solid var(--line);
  border-top-color: var(--accent, var(--primary));
}

#pull.ready .pull-spinner {
  border-top-color: var(--accent, var(--primary));
  transform: scale(1.15);
}

#pull.spinning .pull-spinner {
  animation: spin 700ms linear infinite;
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

/* --------------------------------------------------------- empty states */

/* A network product is empty by definition on the day it opens, and the people
   who see it emptiest are the first members, who are the ones who have to fill
   it. So an empty panel is written as a screen: the mark, what belongs here,
   and the button that puts the first one in. Pattern borrowed from Ant Design
   Mobile's ErrorBlock, which pairs an illustration with a description and
   leaves room for an action. */
.empty {
  display: grid;
  justify-items: center;
  text-align: center;
  gap: 8px;
  padding: 34px 18px 30px;
  max-width: 38ch;
  margin: 0 auto;
}

.empty-mark {
  width: 56px;
  height: 56px;
  fill: none;
  stroke: var(--accent, var(--primary));
  stroke-width: 6;
  stroke-linecap: round;
  opacity: 0.55;
}

/* The arch draws itself, the same gesture as the landing screen, so an empty
   screen looks made rather than missing. */
.empty-mark path {
  stroke-dasharray: 120;
  stroke-dashoffset: 120;
  animation: draw 700ms var(--ease-out) 120ms forwards;
}

.empty-mark path:nth-child(2) {
  animation-delay: 200ms;
}
.empty-mark path:nth-child(3) {
  animation-delay: 280ms;
}

.empty-title {
  font-family: var(--font-head);
  font-size: 1.12rem;
  margin: 6px 0 0;
}

.empty-body {
  color: var(--muted);
  margin: 0;
  line-height: 1.55;
  font-size: 0.92rem;
}

.empty-action {
  margin-top: 10px;
  min-height: 44px;
  padding: 0 20px;
}

/* -------------------------------------------------------------- motion */

/* A screen arrives rather than appearing. The distance is small on purpose:
   16px is enough for the eye to catch the direction and not enough to make
   anyone wait. Views are toggled with the hidden attribute, so this replays on
   every switch without a line of JavaScript. */
.view:not([hidden]) {
  animation: rise var(--slow) var(--ease-out) both;
}

@keyframes rise {
  from {
    opacity: 0;
    transform: translateY(16px);
  }
}

/* The first few cards in a list follow the screen in, one after another. Only
   the first eight: past that nobody is watching, and the wait would be real. */
.view:not([hidden]) .card:nth-child(-n + 8),
#room-list li:nth-child(-n + 8),
#messages li:nth-child(-n + 8) {
  animation: rise-small 220ms var(--ease-out) both;
}

.view:not([hidden]) .card:nth-child(2),
#room-list li:nth-child(2) {
  animation-delay: 40ms;
}
.view:not([hidden]) .card:nth-child(3),
#room-list li:nth-child(3) {
  animation-delay: 80ms;
}
.view:not([hidden]) .card:nth-child(4),
#room-list li:nth-child(4) {
  animation-delay: 120ms;
}
.view:not([hidden]) .card:nth-child(n + 5),
#room-list li:nth-child(n + 5) {
  animation-delay: 150ms;
}

@keyframes rise-small {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
}

/* A message you just sent should look like it landed, not like the page
   reloaded underneath you. */
#messages li:last-child {
  animation: land 240ms var(--ease-spring) both;
}

@keyframes land {
  from {
    opacity: 0;
    transform: translateY(10px) scale(0.99);
  }
}

/* Waiting states. A panel that is blank while a request is in flight reads as
   broken; three grey bars read as "nearly there". */
.skeleton {
  display: grid;
  gap: 10px;
  padding: 4px 0;
}

.skeleton span {
  display: block;
  height: 14px;
  border-radius: 7px;
  background: linear-gradient(
    90deg,
    var(--surface-2) 0%,
    var(--line) 50%,
    var(--surface-2) 100%
  );
  background-size: 200% 100%;
  animation: sweep 1.1s linear infinite;
}

.skeleton span:nth-child(2) {
  width: 82%;
}
.skeleton span:nth-child(3) {
  width: 64%;
}

@keyframes sweep {
  to {
    background-position: -200% 0;
  }
}

/* The moment worth celebrating: an unclaimed society becomes yours. One pulse
   of the accent around the card, then it stops. */
.just-claimed {
  animation: claimed 900ms var(--ease-out);
}

@keyframes claimed {
  0% {
    box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 55%, transparent);
  }
  70% {
    box-shadow: 0 0 0 14px transparent;
  }
  100% {
    box-shadow: 0 0 0 0 transparent;
  }
}

/* The unread badge should catch the eye the moment it changes, and never
   again until it changes next. */
.bell-pop {
  animation: pop 320ms var(--ease-spring);
}

@keyframes pop {
  0% {
    transform: scale(0.6);
  }
  60% {
    transform: scale(1.18);
  }
}

/* ------------------------------------------------------------- landing */

/* The first screen anyone sees. It is one column, centred, and it says what
   the thing is before it asks for anything. The arch is the product in one
   shape: two columns, one span, two universities. */
#landing {
  min-height: 100vh;
  min-height: 100svh;
  display: grid;
  place-items: center;
  padding: 40px 22px;
}

.landing-inner {
  width: min(620px, 100%);
  text-align: center;
}

.landing-mark {
  width: 64px;
  height: 64px;
  stroke: var(--primary);
  margin-bottom: 22px;
}

/* The arch draws itself once, on the first screen anyone sees, and nothing
   else on the page moves while it does. One orchestrated moment, not a page
   of things sliding about. */
.landing-mark path {
  stroke-dasharray: 200;
  stroke-dashoffset: 200;
  animation: draw 700ms var(--ease-out) 80ms forwards;
}

@keyframes draw {
  to {
    stroke-dashoffset: 0;
  }
}

#landing h1,
.landing-lede,
.landing-actions,
.landing-proof,
.landing-foot {
  animation: rise var(--slow) var(--ease-out) both;
}

#landing h1 {
  animation-delay: 300ms;
}
.landing-lede {
  animation-delay: 380ms;
}
.landing-actions {
  animation-delay: 460ms;
}
.landing-proof {
  animation-delay: 540ms;
}
.landing-foot {
  animation-delay: 600ms;
}

#landing h1 {
  font-family: var(--font-head);
  font-size: clamp(2.1rem, 7vw, 3.4rem);
  font-weight: 600;
  line-height: 1.05;
  letter-spacing: -0.015em;
  margin: 0 0 14px;
}

.landing-lede {
  color: var(--muted);
  font-size: 1.05rem;
  line-height: 1.6;
  margin: 0 auto 30px;
  max-width: 46ch;
}

.landing-actions {
  display: flex;
  gap: 10px;
  justify-content: center;
  flex-wrap: wrap;
  margin-bottom: 42px;
}

.landing-actions button {
  min-height: 48px;
  padding: 0 22px;
  font-size: 1rem;
}

/* Three claims, each one checkable inside the app. The number carries the
   weight, so it takes the display face and the body text stays quiet. */
.landing-proof {
  display: grid;
  gap: 22px;
  margin: 0 0 34px;
  text-align: left;
}

.landing-proof div {
  border-top: 1px solid var(--line);
  padding-top: 14px;
}

.landing-proof dt {
  font-family: var(--font-head);
  font-size: 1.5rem;
  color: var(--primary);
  margin-bottom: 4px;
}

.landing-proof dd {
  margin: 0;
  color: var(--muted);
  line-height: 1.5;
}

.landing-foot {
  display: flex;
  gap: 18px;
  justify-content: center;
  flex-wrap: wrap;
  font-size: 0.85rem;
}

.landing-foot a {
  color: var(--muted);
}

@media (min-width: 720px) {
  .landing-proof {
    grid-template-columns: repeat(3, 1fr);
  }
}

/* A quiet way back out of the gate, level with the heading it sits above. */
.link-button {
  border: 0;
  background: none;
  color: var(--muted);
  padding: 0 0 10px;
  font-size: 0.9rem;
}

.link-button:hover {
  color: var(--text);
  border-color: transparent;
}

/* The university question, asked before anything is typed. Two choices, both
   the size of a thumb, and the one chosen takes its own accent so the answer
   is visible in the colour of the screen from that second on. */
#uni-choice {
  border: 0;
  padding: 0;
  margin: 0 0 var(--s4);
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--s2);
}

#uni-choice legend {
  color: var(--muted);
  font-size: 13px;
  margin-bottom: var(--s2);
  padding: 0;
}

#uni-choice button {
  min-height: 48px;
  font-size: 1rem;
}

#uni-choice button[aria-pressed="true"] {
  border-color: var(--accent, var(--primary));
  color: var(--text);
  box-shadow: inset 0 0 0 1px var(--accent, var(--primary));
}

/* ---------------------------------------------------------------- gate */

.gate {
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 24px;
}
.gate-card {
  width: min(560px, 100%);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 20px;
  padding: 32px;
}
/* Wide tracking suited a six-letter word. On a three-word name it pulled the
   title across the whole card and stopped reading as one thing, so the
   tracking drops as the name got longer. */
.gate-card h1 {
  letter-spacing: 0.14em;
  font-size: 26px;
  margin: 0 0 8px;
}
.tagline {
  color: var(--muted);
  margin: 0 0 24px;
}
.hint {
  color: var(--muted);
  font-size: 13px;
}
.switch {
  display: flex;
  gap: 8px;
  margin-bottom: 18px;
}
.switch button {
  flex: 1;
  background: transparent;
}
/* The same accent the university choice uses, so one screen never shows two
   different ideas of what "selected" looks like. */
.switch button.active {
  background: transparent;
  border-color: var(--accent, var(--primary));
  color: var(--text);
}
.form-row {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
}
.error {
  color: var(--coral);
}
.status {
  color: var(--teal);
}

/* ---------------------------------------------------------------- app shell */

.topbar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 20px;
  padding: 14px 22px;
  border-bottom: 1px solid var(--line);
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  position: sticky;
  top: 0;
  backdrop-filter: blur(8px);
}
.wordmark {
  font-family: var(--font-head);
  letter-spacing: 0.12em;
  font-weight: 700;
  flex: 0 0 auto;
}
/* The tab strip grows with every module we add. Once it no longer fits beside
   the wordmark it wraps onto its own full-width row, so every tab stays
   visible; only below that does it fall back to scrolling within itself.
   Without this the topbar overflowed the viewport and focusing a tab scrolled
   the whole page sideways, clipping the wordmark. */
.topbar nav {
  display: flex;
  gap: 8px;
  flex: 1 1 auto;
  min-width: 0;
  /* Sixteen destinations do not fit beside the wordmark on a laptop, and
     scrolling hid the last one behind an invisible edge: Alerts was cut in
     half at 1440px. They wrap onto a second row instead, so every tab is
     reachable without a horizontal gesture nobody knows is there. */
  flex-wrap: wrap;
  row-gap: 8px;
}
.topbar nav .tab {
  flex: 0 0 auto;
  border: 0;
  background: transparent;
  color: var(--muted);
  padding: 6px 2px;
  margin-right: var(--s3);
  border-bottom: 1px solid transparent;
  border-radius: 0;
}

.topbar nav .tab:hover {
  color: var(--text);
  border-color: transparent;
}
/* The active tab is named by a hairline under the word, which is the quietest
   mark that still reads at a glance. */
.tab.active {
  color: var(--text);
  background: transparent;
  border-bottom-color: var(--primary);
}
/* The tab strip now takes a full row of its own, which left this block
   wrapping under it and clipping against the left edge. It gets its own row
   and aligns to the right of it. */
.who {
  flex: 1 1 100%;
  justify-content: flex-end;
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 12px;
  color: var(--muted);
  font-size: 13px;
}

main {
  padding: var(--s5) var(--s4) var(--s6);
}

.view > h2 {
  margin-top: var(--s5);
}

.view > h2:first-child {
  margin-top: 0;
}
.view {
  max-width: 1100px;
  margin: 0 auto;
}

#view-rooms {
  display: grid;
  grid-template-columns: 280px 1fr;
  gap: 20px;
  align-items: start;
}
/* The room list and the conversation are two columns of one page, not two
   panels floating on it. The rule between them does the separating. */
.rooms {
  background: transparent;
  border: 0;
  border-right: 1px solid var(--line);
  border-radius: 0;
  padding: 0 var(--s4) 0 0;
}

.chat {
  background: transparent;
  border: 0;
  border-radius: 0;
  padding: 0;
}

/* Tracked-out uppercase labels are the commonest tell in a generated
   interface. These are section names in the display face, sized down. */
.rooms h2,
.chat h2 {
  font-family: var(--font-head);
  font-size: 0.95rem;
  text-transform: none;
  letter-spacing: 0;
  color: var(--muted);
  margin: 0 0 var(--s3);
}
#room-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 6px;
}
#room-list button {
  width: 100%;
  text-align: left;
  background: transparent;
  border-color: transparent;
}
/* The room you are in is marked by a brass edge on the left, the same hairline
   language as the tab strip, rather than by a filled box. */
#room-list button.active {
  background: transparent;
  border-color: transparent;
  border-left: 2px solid var(--primary);
  padding-left: 10px;
  color: var(--text);
}
#room-list .meta {
  display: block;
  color: var(--muted);
  font-size: 12px;
}

#messages {
  list-style: none;
  margin: 0 0 14px;
  padding: 0;
  display: grid;
  gap: 12px;
  /* Grid rows stretch to fill the track by default, so two messages in a
     320px panel became two 150px blocks of empty space with a violet rule
     down the side. Rows sit at their own height and the panel keeps its
     minimum, which is what makes a quiet room look quiet. */
  align-content: start;
  min-height: 320px;
  max-height: 60vh;
  overflow-y: auto;
}
#messages li {
  border-left: 3px solid var(--primary);
  padding: 2px 0 2px 12px;
}
.msg-head {
  display: flex;
  gap: 8px;
  align-items: baseline;
  flex-wrap: wrap;
  font-size: 12px;
  color: var(--muted);
}
.msg-action,
.msg-delete {
  border: 1px solid var(--border, currentColor);
  background: transparent;
  color: var(--muted);
  border-radius: 999px;
  padding: 1px 10px;
  font-size: 11px;
  min-height: 0;
}
.msg-name {
  color: var(--text);
  font-weight: 600;
  font-size: 14px;
}
.badge {
  border: 1px solid currentColor;
  border-radius: 999px;
  padding: 1px 8px;
  font-size: 11px;
}
.verified {
  color: var(--teal);
}
#composer {
  display: flex;
  gap: 8px;
}

.people {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 12px;
}
.person,
.queue-item {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 16px;
}
.person h3 {
  margin: 0 0 4px;
  font-size: 16px;
}
.person p {
  color: var(--muted);
  font-size: 13px;
}
#filters {
  display: grid;
  grid-template-columns: 2fr 1fr 1fr auto;
  gap: 10px;
  margin-bottom: 18px;
}
#interests-form {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 10px;
}
#interests-hint {
  margin: 8px 0 18px;
}
.match-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
}
.match-score {
  font-weight: 700;
  font-size: 15px;
  color: var(--primary);
  font-variant-numeric: tabular-nums;
}
.match-reasons {
  margin: 8px 0;
  padding-left: 18px;
  color: var(--muted);
  font-size: 13px;
  line-height: 1.5;
}
#queue {
  display: grid;
  gap: 12px;
}
.queue-item pre {
  background: var(--surface-2);
  padding: 10px;
  border-radius: 10px;
  overflow-x: auto;
  font-size: 12px;
}
.queue-actions {
  display: flex;
  gap: 8px;
}
.flag {
  color: var(--gold);
  font-size: 12px;
}

/* ---- cards: feed posts, events, composers ---- */
/* A card used to be a box with a border, a background and a radius, stacked
   inside panels that also had all three. Three nested rectangles is what makes
   an interface look cheap. A card is now a block of content with air around it
   and a hairline underneath, and only the things you can act on carry an edge. */
.card {
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--line);
  border-radius: 0;
  padding: var(--s4) 0;
}

.card:last-child {
  border-bottom: 0;
}

#feed,
#events,
#notifications {
  display: grid;
  gap: 12px;
  max-width: 720px;
}

.post-body,
.event-detail {
  margin: 8px 0 0;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.post-actions,
.event-actions {
  display: flex;
  gap: 8px;
  align-items: center;
  margin-top: 12px;
}

.like.liked {
  border-color: var(--coral);
  color: var(--coral);
}

.event h3 {
  margin: 0 0 4px;
  font-size: 16px;
}

/* Unread stands out by weight and a rail rather than colour alone: the palette
   is already carrying verified and flag meaning elsewhere. */
.notif {
  background: var(--surface);
  border: 1px solid var(--line);
  border-left: 3px solid transparent;
  border-radius: var(--radius);
  padding: 12px 16px;
}
.notif.unread {
  border-left-color: var(--primary);
  font-weight: 500;
}
.notif p {
  margin: 0;
}

.notif-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  max-width: 720px;
  margin-bottom: 14px;
}
.notif-head h2 {
  margin: 0;
  font-size: 14px;
  letter-spacing: 0.01em;
  color: var(--muted);
}

/* The badge is the element itself, sitting inside the Alerts tab button. */
.bell {
  display: inline-block;
  min-width: 18px;
  margin-left: 6px;
  padding: 0 5px;
  border-radius: 9px;
  background: var(--coral);
  color: #1a0f0c;
  font-size: 11px;
  font-weight: 700;
  line-height: 18px;
  text-align: center;
}
.bell[hidden] {
  display: none;
}

@media (max-width: 820px) {
  #view-rooms {
    grid-template-columns: 1fr;
  }
  #filters,
  .form-row {
    grid-template-columns: 1fr;
  }
  /* On narrow screens pair the wordmark with the sign-out block on one row and
     give the tabs their own, rather than letting all three stack (which costs
     an extra ~43px of sticky header). The strip still scrolls within itself. */
  .topbar nav {
    order: 3;
    width: 100%;
  }
  .people {
    grid-template-columns: 1fr;
  }
  #society-detail {
    position: fixed;
    inset: 0;
    z-index: 40;
    overflow-y: auto;
    max-width: none;
    margin: 0;
    padding: var(--s4) var(--s3) var(--s6);
    background: var(--bg);
  }
}

/* ---- module grids: marketplace, societies, housing, mentors, questions ---- */

/* Every module lists cards of roughly one shape, so they share one grid rather
   than each growing its own. */
.cards {
  display: grid;
  gap: 12px;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  align-items: start;
  margin-bottom: 24px;
}

.card h3 {
  margin: 0 0 4px;
  font-size: 16px;
}

.society-link {
  cursor: pointer;
}
.society-link:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 3px;
}
.society-detail {
  max-width: 720px;
  margin: 0 auto;
  padding: var(--s3) 0 var(--s6);
}
.society-back {
  margin-bottom: var(--s4);
}
.society-detail-head {
  border-bottom: 1px solid var(--line);
  margin-bottom: var(--s4);
  padding-bottom: var(--s3);
}
.society-detail-head p,
.society-detail-head h2 {
  margin-bottom: var(--s1);
}
.society-description {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.society-detail-section {
  border-top: 1px solid var(--line);
  margin-top: var(--s4);
  padding-top: var(--s3);
}
.society-detail-section ul {
  margin: 0;
  padding-left: 20px;
}
.society-application {
  display: grid;
  gap: var(--s2);
}
.society-detail-actions {
  margin-top: var(--s4);
}

/* Filter bars sit on one line and wrap as a unit on narrow screens. */
.inline {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
  margin-bottom: 16px;
}
.inline input,
.inline select {
  width: auto;
  min-width: 140px;
}

/* A checkbox and its label read as one control, not as a stray box. */
.check {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--muted);
  white-space: nowrap;
}
.check input {
  width: auto;
  margin: 0;
}

/* Card actions can outgrow one line once a select and an input join them. */
.card .post-actions {
  flex-wrap: wrap;
}
.card .post-actions input,
.card .post-actions select {
  width: auto;
  min-width: 0;
  flex: 1 1 120px;
}

/* ---- questions ---- */

.thread {
  margin-top: 12px;
  border-top: 1px solid var(--line);
  padding-top: 12px;
  display: grid;
  gap: 8px;
}
.answer {
  background: var(--surface-2);
  border-radius: 10px;
  padding: 10px 12px;
}
.answer p {
  margin: 0 0 4px;
  overflow-wrap: anywhere;
}
.answer-form {
  display: grid;
  gap: 8px;
  padding: 10px 12px;
}

/* ---- assistant ---- */

#assistant-log {
  display: grid;
  gap: 10px;
  max-width: 720px;
  max-height: 60vh;
  overflow-y: auto;
  margin-bottom: 16px;
}

/* Sides carry the speaker, so the two roles stay apart even when the colour
   difference is hard to see. */
.bubble {
  max-width: 80%;
  padding: 10px 14px;
  border-radius: var(--radius);
  border: 1px solid var(--line);
}
.bubble p {
  margin: 0;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.bubble.user {
  justify-self: end;
  background: var(--primary);
  border-color: var(--primary);
  color: #140f2b;
}
.bubble.assistant {
  justify-self: start;
  background: var(--surface);
}

/* ---- search ---- */

.hit {
  cursor: pointer;
}
.hit:hover,
.hit:focus-within {
  border-color: var(--primary);
}
#search-results h2 {
  margin: 0 0 10px;
  font-size: 13px;
  letter-spacing: 0.01em;
  color: var(--muted);
}
.search-chips,
.search-suggestions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: -4px 0 16px;
}
.search-chip,
.search-suggestion,
.search-clear {
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 7px 12px;
  background: var(--surface);
  color: var(--ink);
}
.search-suggestion {
  background: var(--surface);
}
.search-clear {
  color: var(--muted);
}

/* ---- admin platform ---- */

#admin-stats {
  display: grid;
  gap: 12px;
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  margin-bottom: 24px;
}
.stat {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 12px 14px;
  display: grid;
  gap: 2px;
}
.stat-value {
  font-size: 22px;
  font-weight: 600;
}
.stat-name {
  font-size: 12px;
  color: var(--muted);
}

/* A suspended account is marked by a rail and the word itself in the subtitle,
   not by colour alone. */
.user.suspended {
  border-left: 3px solid var(--coral);
}

#audit {
  display: grid;
  gap: 6px;
  max-width: 720px;
}
.audit-row {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 10px;
  align-items: baseline;
  padding: 8px 0;
  border-bottom: 1px solid var(--line);
}
.audit-action {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12px;
  color: var(--teal);
}

@media (max-width: 820px) {
  .cards,
  #admin-stats {
    grid-template-columns: 1fr;
  }
  .bubble {
    max-width: 92%;
  }
}

/* ---------------------------------------------- reading, checklists, dating */

.reading-items,
.check-items {
  list-style: none;
  margin: 12px 0 0;
  padding: 0;
  display: grid;
  gap: 10px;
  max-width: 760px;
}

.reading-item,
.check-item {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 12px;
}

.reading-item-head {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.reading-item-actions {
  display: flex;
  gap: 8px;
  align-items: center;
  margin-top: 8px;
}

.week-input {
  width: 72px;
}

.check-item label {
  display: flex;
  gap: 8px;
  align-items: center;
  font-weight: 600;
}

.check-item .hint {
  margin: 4px 0 8px;
}

.tips {
  margin-top: 10px;
  display: grid;
  gap: 6px;
}

.tip {
  margin: 0;
  font-size: 0.92rem;
}

.switch-inline {
  display: inline-flex;
  gap: 6px;
  align-items: center;
}

.deck-card {
  max-width: 420px;
}

/* ------------------------------------------------- footer and legal pages */

.app-footer {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: center;
  padding: 16px;
  border-top: 1px solid var(--line);
  font-size: 0.9rem;
}

.support-page .legal-page {
  max-width: 960px;
}

.support-note {
  border-left: 3px solid var(--accent, #c2a05c);
  padding-left: 12px;
}

.support-filters {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin: 24px 0;
}

.support-filters label {
  display: grid;
  gap: 6px;
  min-width: 220px;
}

.support-filters select {
  min-height: 40px;
  padding: 8px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface, white);
  color: inherit;
  font: inherit;
}

.support-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 12px;
  margin-bottom: 28px;
}

.support-card {
  padding: 16px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--surface, white);
}

.support-card h3 {
  margin-top: 0;
}

.support-card dl {
  margin: 0;
}

.support-card dt {
  margin-top: 12px;
  color: var(--muted);
  font-size: 0.85rem;
}

.support-card dd {
  margin: 3px 0 0;
}

.app-footer a {
  color: inherit;
}

@media (max-width: 600px) {
  .topbar {
    gap: 10px;
    padding: 10px 12px;
  }

  .topbar nav {
    gap: 5px;
    margin: 0 -2px;
    padding: 2px;
    scrollbar-width: none;
    scroll-snap-type: x proximity;
  }

  .topbar nav::-webkit-scrollbar {
    display: none;
  }

  .topbar nav .tab {
    scroll-snap-align: start;
    padding: 7px 11px;
  }

  .who {
    gap: 8px;
    min-width: 0;
  }

  #who-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  main {
    padding: 12px;
  }

  .view {
    padding: 8px 0;
  }

  .reading-item,
  .check-item {
    padding: 10px;
  }

  .reading-item-head a,
  .check-item,
  .tip {
    overflow-wrap: anywhere;
  }

  .reading-item-actions,
  .check-item .inline {
    flex-wrap: wrap;
  }

  .reading-item-actions > * {
    min-width: 0;
  }

  .reading-item-actions select {
    flex: 1 1 135px;
  }

  .reading-item-actions .week-input {
    flex: 0 1 72px;
  }

  .check-item label span {
    min-width: 0;
    overflow-wrap: anywhere;
  }

  .tip {
    display: flex;
    gap: 6px;
    align-items: baseline;
    flex-wrap: wrap;
  }

  .tip span {
    min-width: 0;
    flex: 1 1 180px;
  }

  .deck-card {
    width: 100%;
    padding: 12px;
  }

  .deck-card .post-actions {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .deck-card .post-actions button {
    width: 100%;
  }

  /* Stacked, these five lines took a third of a phone screen under every empty
     state. They are a footer: one quiet row that wraps, with the destructive
     control on its own line at the end. */
  .app-footer {
    align-items: center;
    gap: 14px;
    padding: 14px 12px calc(var(--dock-h) + 20px);
    font-size: 0.82rem;
    color: var(--muted);
  }

  .app-footer #delete-account {
    flex-basis: 100%;
    margin-top: 4px;
    font-size: 0.82rem;
    padding: 8px 12px;
  }

  .app-footer a,
  .app-footer button,
  .app-footer p {
    overflow-wrap: anywhere;
  }

  .app-footer button {
    width: 100%;
  }

  .app-footer p {
    margin: 0;
  }
}

.legal-page {
  max-width: 720px;
  margin: 0 auto;
  padding: 32px 20px 64px;
  line-height: 1.55;
}

.legal-page h2 {
  margin-top: 28px;
}

/* ============================================================ phone shell
 *
 * Palette comes from the subject rather than from a theme: Oxford's official
 * dark blue and the muted Cambridge blue, on a cool paper white, with railing
 * ink for text and pillar-box red reserved for things that destroy something.
 * Two universities, two accents, and a person's own university tints their
 * bottom bar, so the colour carries information instead of decorating.
 *
 * Structure is list-first. Phone apps are rows and separators; stacks of
 * rounded cards with soft shadows read as a website in a WebView.
 */

:root {
  /* The phone shell inherits the app's palette rather than inventing a second
     identity. The accent is the person's own university: Cambridge sage, or a
     legible Oxford blue, because on a dark ground the official navy
     disappears. Brass is the fallback before anyone has said which. */
  --dock-h: 58px;
  --oxf: #4f83cc;
  --cam: #6ea8a3;
  --accent: var(--primary);
}

body[data-university="cambridge"] {
  --accent: var(--cam);
}

body[data-university="oxford"] {
  --accent: var(--oxf);
}

#appbar,
#dock,
#sheet,
#offline {
  display: none;
}

@media (max-width: 760px) {
  html {
    -webkit-text-size-adjust: 100%;
  }

  body {
    background: var(--bg);
    color: var(--text);
    overscroll-behavior-y: contain;
  }

  /* The desktop tab row still drives everything; it is just not shown. */
  .topbar nav {
    display: none;
  }

  .topbar {
    display: none;
  }

  /* The shell belongs to the app. On the landing screen and the gate there is
     nothing for a dock to navigate, and a bar reading "Rooms" over a page that
     asks you to sign up is the loudest possible wrong answer. */
  body:not([data-screen="app"]) #appbar,
  body:not([data-screen="app"]) #dock {
    display: none;
  }

  #appbar {
    display: flex;
    position: fixed;
    inset: 0 0 auto 0;
    z-index: 30;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: max(env(safe-area-inset-top), 10px) 16px 10px;
    background: var(--bg);
    border-bottom: 1px solid var(--line);
  }

  #appbar-heading {
    align-items: baseline;
    display: flex;
  }

  #appbar-title {
    font-size: 1.25rem;
    font-weight: 600;
    letter-spacing: -0.015em;
  }

  #appbar-term {
    color: var(--muted);
    font-size: 0.82rem;
    font-weight: 500;
    margin-left: 7px;
  }

  #appbar-action {
    border: 0;
    background: none;
    color: var(--muted);
    font: inherit;
    font-size: 0.95rem;
    padding: 8px;
    min-height: 44px;
  }

  main,
  .view {
    padding-top: 4px;
  }

  /* The offset used to be a guessed 46px while the bar is a safe-area inset
     plus a 44px tap target plus its own padding, so the first control on
     every screen sat underneath it. mobile-shell.js measures the bar and
     publishes --appbar-h; the fallback is the height it computes to on a
     phone with no inset. */
  #app > main {
    padding-top: calc(var(--appbar-h, 64px) + 8px);
    padding-bottom: calc(var(--dock-h) + env(safe-area-inset-bottom) + 12px);
  }

  .view {
    padding-left: 16px;
    padding-right: 16px;
  }

  /* Every tap target reaches the 44px floor, including the ones that were
     styled as inline text links on a laptop. */
  button,
  .tab,
  select,
  input[type="checkbox"] + span {
    min-height: 44px;
  }

  input,
  select,
  textarea {
    font-size: 16px; /* anything smaller makes iOS zoom on focus */
    width: 100%;
    box-sizing: border-box;
  }

  .form-row {
    display: grid;
    gap: 8px;
  }

  /* Cards flatten into rows. One separator, no shadow, full bleed. */
  .card,
  .society,
  .deck-card,
  .queue-item,
  .reading-item,
  .check-item {
    border-radius: 0;
    border: 0;
    border-bottom: 1px solid var(--line);
    box-shadow: none;
    padding: 14px 0;
    background: none;
  }

  .cards {
    display: block;
  }

  #dock {
    display: grid;
    grid-template-columns: repeat(5, 1fr);
    position: fixed;
    inset: auto 0 0 0;
    /* Below the app bar. The two never overlap, and a fixed layer stacked
       above the bar composited a ghost copy of the dock icons over the title
       in captured frames. */
    z-index: 20;
    background: var(--bg);
    border-top: 1px solid var(--line);
    padding-bottom: env(safe-area-inset-bottom);
  }

  .dock-item {
    display: grid;
    justify-items: center;
    gap: 3px;
    height: var(--dock-h);
    border: 0;
    background: none;
    color: var(--muted);
    font: inherit;
    font-size: 0.68rem;
    letter-spacing: 0.01em;
  }

  .dock-item svg {
    width: 22px;
    height: 22px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.7;
    stroke-linecap: round;
    stroke-linejoin: round;
    transition:
      transform var(--mid) var(--ease-spring),
      stroke-width var(--fast) var(--ease-out);
  }

  .dock-item[aria-current="page"] {
    color: var(--accent);
  }

  /* The selected icon lifts. It is 2px and it is the difference between a row
     of links and a bar that knows where you are. */
  .dock-item[aria-current="page"] svg {
    stroke-width: 2.2;
    transform: translateY(-2px) scale(1.06);
  }

  .dock-item:active svg {
    transform: scale(0.92);
  }

  .dock-item,
  #appbar-title {
    transition: color var(--mid) var(--ease-out);
  }

  /* The title crossfades when the screen under it changes, so the bar belongs
     to the screen rather than sitting above it unchanged. */
  #appbar-title.changing {
    animation: title-swap var(--slow) var(--ease-out);
  }

  @keyframes title-swap {
    from {
      opacity: 0;
      transform: translateY(4px);
    }
  }

  #sheet {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 50;
  }

  .sheet-scrim {
    position: absolute;
    inset: 0;
    background: rgba(4, 8, 6, 0.62);
    animation: fade var(--mid) var(--ease-out);
  }

  @keyframes fade {
    from {
      opacity: 0;
    }
  }

  .sheet-panel {
    position: absolute;
    inset: auto 0 0 0;
    background: var(--bg);
    border-radius: 18px 18px 0 0;
    padding: 10px 16px calc(16px + env(safe-area-inset-bottom));
    max-height: 78vh;
    overflow-y: auto;
    /* The sheet answers the tap that opened it: it comes from the edge it is
       attached to, with the overshoot a thumb expects from a physical thing. */
    animation: sheet-rise var(--slow) var(--ease-spring);
  }

  @keyframes sheet-rise {
    from {
      transform: translateY(100%);
    }
  }

  .sheet-grip {
    width: 36px;
    height: 4px;
    border-radius: 2px;
    background: var(--line);
    margin: 2px auto 10px;
  }

  .sheet-row {
    display: grid;
    width: 100%;
    text-align: left;
    gap: 2px;
    padding: 12px 0;
    border: 0;
    border-bottom: 1px solid var(--line);
    background: none;
    font: inherit;
  }

  .sheet-label {
    font-weight: 600;
  }

  .sheet-blurb {
    color: var(--muted);
    font-size: 0.86rem;
  }

  .sheet-close {
    width: 100%;
    margin-top: 12px;
    padding: 12px;
    border: 1px solid var(--line);
    border-radius: 10px;
    background: none;
    font: inherit;
  }

  body.sheet-open {
    overflow: hidden;
  }

  #offline {
    display: block;
    position: fixed;
    left: 0;
    right: 0;
    bottom: calc(var(--dock-h) + env(safe-area-inset-bottom));
    z-index: 45;
    background: var(--surface-2);
    color: var(--text);
    padding: 10px 16px;
    font-size: 0.86rem;
  }

  #offline[hidden] {
    display: none;
  }

  .app-footer {
    padding-bottom: 24px;
  }
}

@media (max-width: 760px) {
  /* Content first. The creation form on each screen lives behind the New
     button in the app bar; opening it is the answer to that tap, which is
     why it is the one place a transition is allowed. */
  .view form.card {
    display: none;
  }

  .view.compose-open form.card {
    display: block;
    border-bottom: 1px solid var(--line);
    padding-bottom: 16px;
    margin-bottom: 4px;
  }

  #appbar-compose {
    margin-left: auto;
    border: 1px solid var(--line);
    border-radius: 999px;
    background: none;
    color: var(--text);
    font: inherit;
    font-size: 0.9rem;
    padding: 6px 14px;
    min-height: 36px;
  }

  #appbar-action {
    margin-left: 0;
  }
}
:where(.week-heading, .week-days) {
  max-width: 760px;
  margin-inline: auto;
}

.week-heading {
  margin-bottom: 1.5rem;
}

.week-heading h1 {
  margin-block: 0.2rem;
}

.week-days {
  display: grid;
  gap: 1.25rem;
}

.week-day {
  display: grid;
  gap: 0.65rem;
}

.week-day h2 {
  margin: 0;
  font-size: 1.05rem;
}

.week-item {
  margin: 0;
}

.week-item .eyebrow {
  margin: 0;
}

/* ============================================================ shell (mobile)
   Owned by the mobile shell branch: press states, rows, chips, push/pop screens. */

/* Root mobile shell token */
:root {
  --surface-2: rgba(250, 250, 245, 0.04);
}

/* Disable tap highlight on all interactive elements */
* {
  -webkit-tap-highlight-color: transparent;
}

/* Allow manipulation on touch without delay */
button,
.row,
.chip,
[role="button"],
a[href] {
  touch-action: manipulation;
}

/* When running in a shell (Capacitor), disable overscroll bounce */
body[data-shell] {
  overscroll-behavior: none;
}

/* Prevent text selection on interactive elements */
.topbar,
.dock,
.row,
.chip,
button {
  user-select: none;
}

/* Row primitive: 56px min height, 16px padding, flex layout */
.row {
  min-height: 56px;
  padding: 0 16px;
  display: flex;
  align-items: center;
  gap: 12px;
  border-bottom: 1px solid var(--line);
  border-bottom-width: 0.5px;
  border-bottom-color: var(--muted);
  transition: background-color var(--fast);
}

/* Row leading mark: 36px circle */
.row-mark {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  flex-shrink: 0;
  background-color: var(--surface-2);
}

/* Row title and subtitle */
.row-title {
  flex: 1;
  font-size: 1rem;
  font-weight: 500;
  color: var(--text);
}

.row-sub {
  font-size: 0.875rem;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Row trailing content or chevron */
.row-end {
  flex-shrink: 0;
  color: var(--muted);
}

.row.chevron::after {
  content: "";
  width: 8px;
  height: 12px;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 12'%3E%3Cpath d='M1 1l5 5-5 5' stroke='%23888' fill='none' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E")
    no-repeat center;
  background-size: contain;
  margin-left: auto;
}

/* Active/press state for rows and buttons */
.row:active,
.chip:active,
button:active,
[role="button"]:active {
  transform: scale(0.97);
  background-color: var(--surface-2);
  transition: all var(--fast);
}

/* Chip primitive: 32px pill */
.chip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 32px;
  padding: 0 16px;
  border-radius: 16px;
  border: 1px solid var(--line);
  background-color: var(--bg);
  color: var(--text);
  font-size: 0.875rem;
  font-weight: 500;
  cursor: pointer;
  transition: all var(--fast);
}

.chip.on {
  border-color: var(--accent);
  background-color: color-mix(in srgb, var(--accent) 16%, var(--bg) 84%);
  color: var(--accent);
}

/* Chip row: horizontal scroll, 8px gap, no scrollbar */
.chip-row {
  display: flex;
  gap: 8px;
  overflow-x: auto;
  overflow-y: hidden;
  padding: 0 16px;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}

.chip-row::-webkit-scrollbar {
  display: none;
}

/* Screen navigation: pushed and visible states */
.screen-pushed {
  position: fixed;
  inset: 0;
  transform: translateX(100%);
  background: var(--bg);
  z-index: 1000;
  overflow: auto;
  -webkit-overflow-scrolling: touch;
}

.screen-visible {
  animation: slideInRight var(--ease-out) 280ms forwards;
}

.screen-pushed:not(.screen-visible) {
  animation: slideOutRight var(--ease-out) 280ms forwards;
}

@keyframes slideInRight {
  from {
    transform: translateX(100%);
  }
  to {
    transform: translateX(0);
  }
}

@keyframes slideOutRight {
  from {
    transform: translateX(0);
  }
  to {
    transform: translateX(100%);
  }
}

/* Respect prefers-reduced-motion for animations */
@media (prefers-reduced-motion: reduce) {
  .screen-visible,
  .screen-pushed:not(.screen-visible) {
    animation: none;
    transform: translateX(0) !important;
  }
}

/* No underlined links inside app */
#app a[href] {
  text-decoration: none;
}

/* External links styled as rows */
#app a[href^="http"]:not([href*="ascend.thinkcollegelevel.com"]),
#app a[href^="mailto:"],
#app a[href^="tel:"] {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 0 16px;
  color: var(--muted);
}

#app a[href^="http"]::after {
  content: "";
  width: 16px;
  height: 16px;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M5 3a2 2 0 012-2h6a2 2 0 012 2v2h4V3a2 2 0 012-2h6a2 2 0 012 2v18a2 2 0 01-2 2h-6a2 2 0 01-2-2v-2H7v2a2 2 0 01-2 2H3a2 2 0 01-2-2V3z' fill='%23888'/%3E%3C/svg%3E")
    no-repeat center;
  background-size: contain;
  flex-shrink: 0;
}

/* ============================================================ week (mobile)
   Owned by the week branch: day strip, collapsed days, term card. */

.week-chips {
  display: flex;
  gap: 6px;
  padding: 0 20px;
  overflow-x: auto;
  overflow-y: hidden;
  -webkit-overflow-scrolling: touch;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
}

.week-chip {
  scroll-snap-align: start;
  flex: 0 0 44px;
  height: 44px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  border-radius: 8px;
  background: var(--surface);
  border: 1px solid var(--line);
  color: var(--text);
  font-size: 11px;
  font-weight: 500;
  line-height: 1;
  position: relative;
  cursor: pointer;
  transition:
    background var(--fast) var(--ease-out),
    color var(--fast) var(--ease-out);
}

.week-chip:active {
  background: var(--muted);
}

.week-chip.today {
  background: var(--accent);
  color: white;
  border-color: var(--accent);
}

.week-chip.has-items::after {
  content: "";
  position: absolute;
  bottom: 4px;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: currentColor;
}

.week-chip-day {
  font-size: 10px;
  opacity: 0.7;
}

.week-chip-num {
  font-size: 16px;
  font-weight: 600;
}

.week-slot {
  padding: 0 20px 12px;
  margin-bottom: 8px;
}

.week-slot-card {
  background: var(--surface);
  border-radius: 12px;
  padding: 16px;
  border: 1px solid var(--line);
  cursor: pointer;
  transition: background var(--fast) var(--ease-out);
}

.week-slot-card:active {
  background: var(--muted);
}

.week-slot-card h3 {
  font-size: 16px;
  font-weight: 600;
  margin: 0 0 6px;
}

.week-slot-card p {
  font-size: 13px;
  margin: 0;
  color: var(--muted);
}

.week-day {
  display: none;
}

.week-day.active {
  display: block;
  padding: 0 20px 12px;
}

.week-day h2 {
  font-size: 14px;
  font-weight: 600;
  margin: 0 0 12px;
  color: var(--muted);
}

.week-day-empty {
  font-size: 13px;
  color: var(--muted);
  padding: 8px 0;
}

.row {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 12px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 8px;
  margin-bottom: 8px;
}

.row-mark {
  flex: 0 0 36px;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--muted);
  color: var(--text);
  font-size: 18px;
}

.row-mark svg {
  width: 20px;
  height: 20px;
  stroke: currentColor;
  fill: none;
}

.row-content {
  flex: 1 1 auto;
  min-width: 0;
}

.row-title {
  font-size: 14px;
  font-weight: 600;
  margin: 0;
  word-break: break-word;
}

.row-sub {
  font-size: 12px;
  color: var(--muted);
  margin: 4px 0 0;
}

.row-end {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12px;
  color: var(--muted);
}

.row-pill {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 16px;
  padding: 6px 12px;
  cursor: pointer;
  font-size: 12px;
  font-weight: 500;
  transition: background var(--fast) var(--ease-out);
}

.row-pill:active {
  background: var(--muted);
}

.row-pill.going {
  background: var(--accent);
  color: white;
  border-color: var(--accent);
}

/* ============================================================ lists (mobile)
   Owned by the lists branch: society, study and room rows, filter bar. */

.row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 16px;
  border-bottom: 1px solid var(--line);
  text-decoration: none;
  color: inherit;
  cursor: pointer;
  transition: background-color var(--fast) var(--ease-out);
}

.row:hover {
  background-color: var(--surface-2);
}

.row:active {
  background-color: var(--surface-1);
}

.row-mark {
  flex-shrink: 0;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 600;
  color: white;
  font-size: 0.9rem;
}

.row-content {
  flex: 1;
  min-width: 0;
}

.row-title {
  font-weight: 500;
  display: block;
  margin-bottom: 4px;
}

.row-sub {
  font-size: 0.875rem;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  display: block;
}

.row-end {
  flex-shrink: 0;
  color: var(--muted);
}

.row.chevron::after {
  content: "›";
  font-size: 1.5rem;
  font-weight: 300;
  color: var(--muted);
  margin-left: 8px;
}

.search-filter {
  position: relative;
  padding: 12px 16px;
  border-bottom: 1px solid var(--line);
}

.search-filter input {
  width: 100%;
  padding: 8px 36px 8px 12px;
  border: 1px solid var(--line);
  border-radius: 6px;
  font-size: 1rem;
}

.search-filter-clear {
  position: absolute;
  right: 26px;
  top: 50%;
  transform: translateY(-50%);
  background: none;
  border: none;
  padding: 4px;
  color: var(--muted);
  cursor: pointer;
  font-size: 1rem;
}

.chip-row {
  display: flex;
  gap: 8px;
  padding: 12px 16px;
  overflow-x: auto;
  overflow-y: hidden;
  border-bottom: 1px solid var(--line);
}

.chip {
  flex-shrink: 0;
  padding: 6px 12px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: 0.875rem;
  cursor: pointer;
  white-space: nowrap;
  transition:
    background-color var(--fast) var(--ease-out),
    border-color var(--fast) var(--ease-out);
}

.chip:hover {
  background-color: var(--surface-2);
}

.chip.active {
  background-color: var(--accent);
  border-color: var(--accent);
  color: white;
}

/* ============================================================ rows, unified
   Three branches each defined .row on 14 September; the last one won and the
   others leaked a card border and a shrink-to-content width into every list.
   One definition, last in the file, so every list row is the same row. */
.row,
button.row {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  min-height: 56px;
  margin: 0;
  padding: 0 16px;
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--line);
  border-radius: 0;
  color: inherit;
  font: inherit;
  text-align: left;
}
.row:active,
button.row:active {
  background: var(--surface-2, rgba(244, 241, 234, 0.04));
}
#society-index li,
#room-list li,
#reading-lists li {
  display: block;
  list-style: none;
  margin: 0;
  padding: 0;
}
.row-title,
.row-sub {
  display: block;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.row-sub {
  color: var(--muted);
  font-size: 0.9em;
}
@media (max-width: 760px) {
  /* The legal footer lives behind More on a phone; the shell's legal screen
     carries the same links. */
  body[data-screen="app"] .app-footer {
    display: none;
  }
}

/* One chevron, drawn, so no font decides its weight. */
.row.chevron::after {
  content: "";
  flex: 0 0 auto;
  width: 8px;
  height: 8px;
  margin-left: auto;
  border: solid var(--muted);
  border-width: 0 1.5px 1.5px 0;
  transform: rotate(-45deg);
  background: none;
  opacity: 0.8;
}

/* Nothing may widen the layout viewport: a row that overflowed by its own
   padding pushed the dock below the screen on 14 September. */
body {
  overflow-x: clip;
}
.row,
button.row,
.week-day,
.week-days,
#week-chips,
.chip-row {
  box-sizing: border-box;
  max-width: 100%;
}
.week-day h2 {
  margin: var(--s4, 24px) 16px var(--s1, 8px);
}
.week-term-card {
  display: block;
  width: 100%;
  box-sizing: border-box;
  margin: 0 0 var(--s3, 16px);
  padding: var(--s3, 16px);
  background: var(--surface);
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent, var(--primary));
  border-radius: 12px;
  color: var(--text);
  font: inherit;
  text-align: left;
  line-height: 1.4;
}
.week-term-card strong {
  font-weight: 600;
}
.week-term-card span {
  color: var(--muted);
}

/* Grid and flex children default to min-width auto, so one unbreakable title
   widened the week track to 453px and pushed the dock off screen. */
#week-days {
  grid-template-columns: minmax(0, 1fr);
}
.week-day,
.week-day.active,
.row-content {
  min-width: 0;
}
.row-content {
  flex: 1 1 auto;
}
.week-day.active {
  padding: 0 0 12px;
}
.week-day h2 {
  padding: 0 16px;
}
/* A checklist item is a thing still to do: an empty ring, never a filled tick. */
.row-mark.kind-checklist {
  background: transparent;
  border: 1.5px solid var(--muted);
  color: transparent;
}

/* Rooms on a phone: the list is the screen, the conversation is pushed. */
@media (max-width: 760px) {
  #view-rooms {
    grid-template-columns: minmax(0, 1fr);
  }
  .rooms,
  .chat {
    min-width: 0;
    border-right: 0;
    padding-right: 0;
  }
  #view-rooms > .chat {
    display: none;
  }
  .room-screen .chat {
    display: block;
    padding: 0 16px;
  }
}
.screen-pushed {
  position: fixed;
  left: 0;
  right: 0;
  top: var(--appbar-h, 56px);
  bottom: calc(var(--dock-h, 59px) + env(safe-area-inset-bottom));
  overflow-y: auto;
  background: var(--bg);
  z-index: 5;
  transform: translateX(100%);
  transition: transform var(--mid, 280ms) var(--ease-out);
}
.screen-pushed.screen-visible {
  transform: translateX(0);
}
@media (prefers-reduced-motion: reduce) {
  .screen-pushed {
    transition: none;
  }
}

/* Back control in the phone app bar, present only while a screen is pushed. */
#appbar-back {
  flex: 0 0 44px;
  width: 44px;
  height: 44px;
  margin-left: -12px;
  border: 0;
  background: transparent;
  color: var(--text);
  position: relative;
}
#appbar-back::before {
  content: "";
  position: absolute;
  left: 18px;
  top: 16px;
  width: 10px;
  height: 10px;
  border: solid currentColor;
  border-width: 0 0 2px 2px;
  transform: rotate(45deg);
}
.room-screen {
  padding: 0 16px 16px;
  box-sizing: border-box;
}

/* Phone: every box counts its padding inside its width. A composer at 100%
   plus 16px of padding widened the layout viewport by 16px and scaled the
   whole screen down; this ends that class of bug for the app subtree. */
@media (max-width: 760px) {
  #app *,
  #app *::before,
  #app *::after {
    box-sizing: border-box;
  }
  .room-screen,
  .room-screen .chat,
  .room-screen form {
    max-width: 100%;
  }
  /* The app bar carries the room name on a phone; the panel heading repeats it. */
  .room-screen .chat > h2 {
    display: none;
  }
  #appbar-heading {
    flex: 1 1 auto;
    min-width: 0;
  }
  #appbar-title {
    display: block;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
}

/* Grid list items default to min-width auto, so the longest nowrap room name
   set the list width, not the screen. */
#room-list,
#society-index ul,
#reading-lists {
  grid-template-columns: minmax(0, 1fr);
}
#room-list li,
#society-index li,
#reading-lists li,
#room-list .row,
#society-index .row,
#reading-lists .row {
  min-width: 0;
  max-width: 100%;
}

.legal-screen {
  padding: 0 16px 24px;
}
.legal-screen h2 {
  font-family: var(--font-head);
  font-size: 1.2rem;
  margin: var(--s4, 24px) 0 var(--s2, 12px);
}
.legal-screen .row a,
.legal-screen .row-link {
  color: inherit;
  text-decoration: none;
  display: block;
  width: 100%;
}
.legal-screen #delete-account {
  margin-top: var(--s5, 32px);
  color: var(--muted);
  background: transparent;
  border: 1px solid var(--line);
  width: 100%;
  min-height: 44px;
}

.society-screen {
  padding: 0 16px 24px;
}

/* The app bar carries back while a screen is pushed; the in-page button is
   for the laptop layout. */
body[data-stack="1"] .society-back {
  display: none;
}
