/* Kveikja - mobile-first. No framework, no build step (docs/PLAN.md sec.8).

   Colour is referenced ONLY through these variables: a dark theme that left hard-coded
   hex values behind showed up as white buttons and white chat bubbles on a black page.
   Light values below are the original literals, so nothing about the current look changes. */
:root{
  /* surfaces: neutral cool gray, matching the founder's reference screenshots (white
     cards on #f4f5f7 with hairline borders) instead of the earlier warm cream. Still
     layered page < sunken < card so nesting reads without heavy borders. */
  --bg:#f4f5f7; --card:#ffffff; --card-alt:#fafafb; --sunken:#f1f2f4; --field:#ffffff;
  /* text: near-black body, neutral gray meta (the references use #101411 / #6b7280) */
  --ink:#101411; --ink-soft:#3a4048; --dim:#6b7280;
  /* structure: hairline borders, soft wide shadow like the reference cards */
  --line:#e7e9ec; --radius:14px; --shadow:0 1px 2px rgba(16,20,17,.04),0 10px 30px -22px rgba(16,20,17,.35);
  /* avatar placeholders go neutral instead of green-tinted */
  --init-bg:#eceef1; --init-ink:#4a5158;
  /* Brand colour, dropped from green to the reference's blue at the founder's request.
     Values chosen by measuring contrast, not by eye: white text on #3f6bad is 5.36:1
     (AA) and #3f6bad as link text on a white card is the same 5.36:1. --brand-link
     stays equal here because it already passes; in dark mode it must be lighter. */
  --brand:#3f6bad; --brand-dark:#34598c; --brand-ink:#ffffff; --brand-link:var(--brand);
  --good:#3f6bad; --bad:#a8322b; --warn:#9a6b12;
  --tint-good:#eef6f2; --tint-good-line:#cfe4da; --good-ink:#24584a;
  --tint-good-strong:#e3efe8;
  --tint-bad:#fdf0ef; --tint-bad-line:#f0d3cf; --bad-ink:#7c2620;
  --tint-warn:#f4e8d3; --warn-ink:#7a5312;
  --focus:#8fb4ee; --alert:#e0564a;
  color-scheme: light;
}

/* Shared dark palette (applied to both 'dark' and device-following 'auto').
   Dark GRAY with several distinct shades, per the founder: page < card < raised,
   so boxes are separated by tone rather than only by borders. The green header stays
   but lightens, because #2f6f5e on a dark page reads as nearly black. */

/* Dark palette. Two selectors, deliberately identical values:
     [data-theme="dark"]            -> the user chose dark
     [data-theme="auto"] in a dark-preference device -> follows the system
   An earlier version put both in one selector list, which made 'auto' render dark on a
   light-preference device - i.e. it ignored the setting it claims to follow.
   Four gray levels a clear step apart so nested boxes are visible by tone, not only by
   borders: page #1b1e20 < sunken #202426 < card #262a2c < raised #2d3234 < field #343a3c.
   The green is kept but lightened - #2f6f5e on a dark page reads as nearly black.
   Text on that green is near-black, not white: measured contrast was 3.89 for white
   (fails WCAG AA for small text) against 4.79 for #0e1416. */
:root[data-theme="dark"]{
  --bg:#1b1e20; --card:#262a2c; --card-alt:#2d3234; --sunken:#202426; --field:#343a3c;
  --ink:#e9ebe8; --ink-soft:#ccd1cb; --dim:#9aa29b; --line:#3c4345;
  --brand:#5f8fd6; --brand-dark:#4a7bc0; --brand-ink:#0e1416; --brand-link:#8fb4ee;
  --good:#6cc2a6; --bad:#e0776d; --warn:#d8b26a;
  --tint-good:#24392f; --tint-good-line:#35594b; --good-ink:#a5dcc7;
  --tint-good-strong:#2a4438;
  --tint-bad:#3a2725; --tint-bad-line:#5b3934; --bad-ink:#f0b3ac;
  --tint-warn:#3a3220; --warn-ink:#e6cd97;
  --init-bg:#39413f; --init-ink:#cfd8d1; --focus:#6cc2a6; --alert:#e0564a;
  color-scheme: dark;
}
@media (prefers-color-scheme: dark){
  :root[data-theme="auto"]{
    --bg:#1b1e20; --card:#262a2c; --card-alt:#2d3234; --sunken:#202426; --field:#343a3c;
    --ink:#e9ebe8; --ink-soft:#ccd1cb; --dim:#9aa29b; --line:#3c4345;
    --brand:#5f8fd6; --brand-dark:#4a7bc0; --brand-ink:#0e1416; --brand-link:#8fb4ee;
    --good:#6cc2a6; --bad:#e0776d; --warn:#d8b26a;
    --tint-good:#24392f; --tint-good-line:#35594b; --good-ink:#a5dcc7;
    --tint-good-strong:#2a4438;
    --tint-bad:#3a2725; --tint-bad-line:#5b3934; --bad-ink:#f0b3ac;
    --tint-warn:#3a3220; --warn-ink:#e6cd97;
    --init-bg:#39413f; --init-ink:#cfd8d1; --focus:#6cc2a6; --alert:#e0564a;
    color-scheme: dark;
  }
}

/* Links and accents use the lighter green in dark mode; surfaces keep --brand. */
a{color:var(--brand-link)}
.tabs a.on{color:var(--brand-link)}
.linkish{color:var(--brand-link)}

/* Theme control in the header. Text on the brand bar uses --brand-ink, which is white
   on the dark green of light mode and near-black on the lighter green of dark mode. */
.theme-btn{display:inline-flex;align-items:center;justify-content:center;min-width:34px;gap:6px;padding:5px 8px;border-radius:8px;
  border:1px solid rgba(255,255,255,.22);background:transparent;color:var(--brand-ink);
  cursor:pointer;font:inherit;font-size:.9rem}
.theme-btn:hover{background:rgba(255,255,255,.14)}
.theme-seg{display:flex;gap:4px;margin-top:6px}
.theme-seg button{padding:7px 11px;border-radius:9px;border:1px solid var(--line);
  background:var(--field);color:var(--ink);cursor:pointer;font:inherit}
.theme-seg button.on{border-color:var(--brand);background:var(--tint-good);font-weight:700}

*{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{margin:0;background:var(--bg);color:var(--ink);
  font:16px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif}
a{color:var(--brand);text-decoration:none}
a:hover{text-decoration:underline}
h1,h2{margin:0 0 .5rem;line-height:1.25;font-weight:650}
h1{font-size:1.4rem} h2{font-size:1.1rem}
/* Content column widened ~20-25% at the founder's request (their profile card filled
   1000px and left a large empty area on a wide screen). One number controls it, so the
   top bar, the page body and the footer all line up on the same grid. */
:root{--shell-side:248px; --content-max:1240px; --rail-w:320px;
  /* The sticky bar's real height, measured in the browser (59), not guessed. min-height
     is derived from it so changing one cannot silently desync the other. Any sticky
     element below the bar must use this value: a larger offset pushes it DOWN while the
     page is unscrolled, which is exactly the misalignment the founder marked - .rail was
     sticking at 74px and started 15px below the composer (92 vs 77). */
  --topbar-h:59px;
  /* One number for the space between columns. It was inconsistent before: nav->content
     measured 14px while content->rail measured 28px, because .main and .rail each added
     their own horizontal padding into the same gap. The founder marked both gutters
     ('too much gap between the columns'). Change this one value to open them back up. */
  --col-gap:12px;
  /* One number for the WHOLE three-column block. Derived from the same three tokens the grid
     tracks use, so it cannot disagree with them. Without it the shell is uncapped: .main stops
     growing at --content-max (readable line length on purpose) while the rail keeps sliding right,
     and every spare pixel piles up into ONE gutter. Measured on 2026-10-20 at 1920px: nav->content
     0px but content->rail 97px - the founder's "the left column is moving away when I expand the
     browser". Capping the block instead means extra width becomes symmetric outer margin, which is
     what a wide screen should do, and gutters stay exactly --col-gap at every viewport. */
  --shell-max:calc(var(--shell-side) + var(--content-max) + var(--rail-w));
  /* The dark plate that text sits on when it is painted over a cover photo. Fixed rgba, NOT a theme
     token: the plate must stay dark in both themes because the text on it is near-white either way
     (measured: #f3f5f4 gives 5.68:1 over a pure-white photo and 18.03:1 over a pure-black one - the
     two extremes any family picture falls between). It was previously written out in three rules;
     one property in three places is how they drift apart (see --lb-pad, --col-gap). */
  --photo-plate:rgba(12,16,18,.66);
  --photo-plate-strong:rgba(12,16,18,.8)}
.wrap{max-width:var(--content-max);margin:0 auto;padding:0 14px}
code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.95em}

/* TOP BAR - white, not brand-blue.
   Measured, not eyeballed: the count pill is white text on --alert (#e0564a), which is
   3.75:1 and fails WCAG AA. On a light surface the pill uses --bad (#a8322b) at 6.65:1,
   and in dark mode #0e1416 on #e0776d is 6.22:1. Nav text #3a4048 on white is 10.46:1.
   The earlier blue bar hid that failure because the pill sat on blue instead. */
.topbar{position:sticky;top:0;z-index:30;background:var(--card);
  border-bottom:1px solid var(--line);color:var(--ink)}
/* Wider than the article column (1320 vs 1000): the account chip plus search needs more
   room than a text column, and capping it at body width is what made the header wrap
   onto a second row. Below 1100px wrapping is still allowed - which is what we want on
   a phone, where two rows are fine and an overflow menu would be worse. */
.topbar-in{display:flex;align-items:center;gap:12px;min-height:calc(var(--topbar-h) - 1px);flex-wrap:wrap;
  /* Same max-width as .shell, so the bar stays above the columns it belongs to at every width. It
     used to be its own calc(... + 40px): a second copy of the same arithmetic, and the two drifted -
     measured at 1920px the header sat 29px right of the nav column it claims to align with. */
  max-width:var(--shell-max);
  margin:0 auto;padding:6px 14px}
@media(min-width:1100px){.topbar-in{flex-wrap:nowrap}}
.brand{display:flex;align-items:center;gap:9px;color:var(--ink);font-weight:700;font-size:1.02rem}
.brand:hover{text-decoration:none}
.brand .mark{background:transparent;width:30px;height:30px}
/* Search sits in the bar (founder's reference). It is a GET form to /people, so it uses
   the same Icelandic-aware search as that page and works with JavaScript off. */
.topsearch{flex:1 1 200px;max-width:420px;display:flex}
.topsearch input{width:100%;padding:8px 13px;border-radius:999px;background:var(--sunken);
  border:1px solid transparent;font-size:.9rem}
.topsearch input:focus{background:var(--field);border-color:var(--line)}
/* Messages and notifications live here so their counts stay visible while you read. The
   number sits next to the WORD, not on an unlabeled icon - the bare bell in the nav was
   undecodable without hovering. */
.top-actions{display:flex;gap:2px;align-items:center;margin-left:auto}
.top-actions a{display:inline-flex;align-items:center;gap:7px;color:var(--ink-soft);
  padding:8px 10px;border-radius:9px;font-size:.93rem;white-space:nowrap}
.top-actions a:hover{background:var(--sunken);color:var(--ink);text-decoration:none}
.top-actions a[aria-current="page"]{background:var(--tint-brand);color:var(--brand-ink-tint);
  font-weight:700}
.who{display:flex;align-items:center;gap:8px}
.me{display:flex;align-items:center;gap:7px;color:var(--ink-soft);padding:4px 6px;border-radius:999px}
.me:hover{background:var(--sunken);text-decoration:none}
.me-name{font-size:.9rem}
.logout-link{color:var(--dim)}
.theme-btn{border-color:var(--line);color:var(--ink-soft)}
.theme-btn:hover{background:var(--sunken)}
.mark{display:grid;place-items:center;width:28px;height:28px;border-radius:8px;
  background:rgba(255,255,255,.18);font-weight:800}
.nav{display:flex;gap:2px;flex-wrap:wrap;margin-right:auto}
.nav a{color:var(--brand-ink);padding:6px 7px;border-radius:8px;font-size:.93rem;white-space:nowrap}
.nav a:hover{background:rgba(255,255,255,.14);text-decoration:none}
.nav a.soon{opacity:.55}
.nav-right{margin-left:auto}

/* layout */
/* The cap lives HERE now, not on a .wrap class on the element. max-width on a grid item
   is fine (it fills the track up to the cap); it was `margin:0 auto` from .wrap that
   caused shrink-to-fit and left 145px of dead space beside the feed. --content-max keeps
   text lines readable on a 1920 screen while the column still uses every pixel the grid
   gives it below that. */
/* Horizontal padding is deliberately asymmetric: --col-gap from the nav column on the
   left, and only half a gutter on the right because .rail contributes the other half.
   Sum of the two = one --col-gap, so every gutter in the site is identical. */
.main{padding:18px calc(var(--col-gap) / 2) 40px var(--col-gap);
  display:grid;gap:16px;min-width:0;max-width:var(--content-max)}
.cols{display:grid;gap:16px;grid-template-columns:1fr}
@media(min-width:820px){.cols{grid-template-columns:1.15fr .85fr}.cols-1{grid-template-columns:1fr}}
.narrow{max-width:560px;margin-inline:auto;width:100%}
.card{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);
  padding:18px;box-shadow:var(--shadow)}
.card.quiet{background:var(--card-alt)}
.hero h1{font-size:1.5rem} .lede{color:var(--dim);margin:.2rem 0 1rem}

/* forms */
.stack{display:grid;gap:12px}
label{display:grid;gap:5px;font-size:.9rem;color:var(--ink-soft);font-weight:600}
input,textarea,select{font:inherit;padding:10px 11px;border:1px solid var(--line);
  border-radius:10px;background:var(--field);color:var(--ink);width:100%}
input:focus,textarea:focus,select:focus{outline:2px solid var(--focus);border-color:transparent}
.code-input{text-transform:uppercase;letter-spacing:.18em;font-weight:700}
.sub{font-size:.8rem;color:var(--dim);font-weight:400}
form.inline{display:inline}
.btn{display:inline-block;padding:10px 14px;border-radius:10px;border:1px solid var(--line);
  background:var(--field);color:var(--ink);font:inherit;font-weight:600;cursor:pointer}
.btn:hover{text-decoration:none;background:var(--sunken)}
.btn.primary{background:var(--brand);border-color:var(--brand);color:var(--brand-ink)}
.btn.primary:hover{background:var(--brand-dark)}
.btn.quiet{background:transparent}
.linkish{border:0;background:none;color:var(--brand);font:inherit;cursor:pointer;padding:2px 4px}
.linkish.danger{color:var(--bad)}
.row-actions{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:14px}

/* messages */
.msg{padding:10px 12px;border-radius:10px;font-size:.95rem;margin:0 0 12px;border:1px solid}
.msg.bad{background:var(--tint-bad);color:var(--bad-ink);border-color:var(--tint-bad-line)}
.msg.good{background:var(--tint-good);color:var(--good-ink);border-color:var(--tint-good-line)}
.msg.wait{background:var(--sunken);color:var(--dim);border-color:var(--line)}
.hint{color:var(--dim);font-size:.85rem}

/* avatars */
.av{border-radius:50%;object-fit:cover;display:inline-block;vertical-align:middle;background:var(--init-bg)}
.av-s{width:32px;height:32px} .av-m{width:72px;height:72px} .av-l{width:96px;height:96px}
.init{display:grid;place-items:center;font-weight:800;color:var(--init-ink);background:var(--init-bg);
  font-size:1.1rem;text-transform:uppercase}
.av-s.init{font-size:.85rem} .av-l.init{font-size:2rem}

/* lists */
.plist{list-style:none;margin:0;padding:0;display:grid;gap:2px}
.prow{display:flex;align-items:center;gap:10px;padding:9px;border-radius:10px}
.prow:hover{background:var(--sunken);text-decoration:none}
.pname{font-weight:600}
.tbl{width:100%;border-collapse:collapse;font-size:.95rem}
.tbl th{text-align:left;color:var(--dim);font-size:.8rem;text-transform:uppercase;
  letter-spacing:.04em;padding:6px}
.tbl td{padding:9px 6px;border-top:1px solid var(--line);vertical-align:middle}
.code{background:var(--sunken);padding:3px 7px;border-radius:6px;font-weight:700;letter-spacing:.1em}
/* Badges sit inside flex rows (.row-actions, .prow). A flex item is block-ified and
   stretched to the row's height unless told otherwise, so a status pill next to a 46px
   button became a 58x46 blob with border-radius:999px - the founder's 'white circle'
   where 'Friends' should read. align-self + an explicit line-height make its size come
   from its own text at any position in the row. */
.badge{display:inline-block;align-self:center;font-size:.75rem;line-height:1.6;
  padding:3px 10px;border-radius:999px;font-weight:700;white-space:nowrap}
.badge.super{background:var(--tint-warn);color:var(--warn-ink)} .badge.admin{background:var(--tint-good);color:var(--brand)}
.avatar-row{display:flex;gap:16px;align-items:center;flex-wrap:wrap}
.steps{margin:0 0 14px;padding-left:1.2rem;color:var(--dim)}
.profile-head{display:flex;gap:16px;align-items:center;flex-wrap:wrap}
.bio{margin-top:12px}
.roadmap{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}
.step{font-size:.8rem;padding:4px 9px;border-radius:999px;background:var(--sunken);color:var(--dim)}
.step.done{background:var(--tint-good);color:var(--brand)} .step.next{background:var(--tint-warn);color:var(--warn-ink);font-weight:600}
.audit{margin:0;padding-left:1rem;font-size:.9rem;color:var(--ink-soft)}

.foot{border-top:1px solid var(--line);padding:16px 0;margin-top:20px;color:var(--dim);font-size:.85rem}
.foot .wrap{display:flex;justify-content:space-between;gap:10px}
@media(max-width:520px){.me-name{display:none}.topbar-in{min-height:48px}}

.mini{font-size:.8rem;text-transform:uppercase;letter-spacing:.05em;color:var(--dim);margin:14px 0 6px}

/* A link styled as a button, full width like the real submit button above it (founder: "we should have
   the boxes with as the sign in button"). The Sign in button gets its width for free - it is a grid item
   inside .stack, which stretches every child. An <a> outside that grid only spans its own text, so the
   matching action looked like a different kind of control. display:block + width:100% reproduces exactly
   what the grid was doing, and keeps one shared class for both auth cards rather than two rules. */
.join-btn{display:block;width:100%;box-sizing:border-box;margin-top:6px;text-align:center}

/* --------------------------------------------------- sign in / join (auth pages) -- */
/* Redesigned on the founder's request ("make it look good, like a real social media site"), with the
   slogan they supplied. Two columns on a wide screen - brand left, forms right - collapsing to one
   column below 860px. No JavaScript is needed for any of it: sign in, join and language switching are
   plain forms/links, exactly as before. */
.auth-wrap{display:grid;gap:24px;grid-template-columns:minmax(0,1fr) minmax(320px,460px);
  align-items:center;min-height:calc(100vh - var(--topbar-h) - 80px)}
.auth-wrap.auth-solo{grid-template-columns:minmax(0,1fr) minmax(320px,520px)}
.auth-hero{padding:8px 4% 8px 0}
.auth-brand{margin:0;font-size:clamp(2rem,4.5vw,3.1rem);line-height:1.1;letter-spacing:-.01em}
.auth-slogan{margin:.35rem 0 1rem;font-size:clamp(1.05rem,2vw,1.35rem);color:var(--brand);
  font-weight:600}
.auth-cards{display:grid;gap:14px}
.auth-card{padding:20px}
.auth-card h2{margin-top:0}
.auth-lang{text-align:center;margin:2px 0 0}
.field-birth{display:grid;gap:6px}
.flabel{font-weight:600;font-size:.9rem}
/* The age rule is a stated requirement, so it reads as information rather than an error or a hint. */
.age-notice{margin:.4rem 0 0;font-size:.85rem;color:var(--dim)}
@media(max-width:859px){
  .auth-wrap,.auth-wrap.auth-solo{grid-template-columns:minmax(0,1fr);min-height:0;padding-top:8px}
  .auth-hero{padding:0 0 4px;text-align:center}
}
.join-top{text-align:center;margin-bottom:14px}
details{margin-top:10px}
details summary{cursor:pointer;color:var(--dim);font-size:.9rem}

/* Phase 2+3: composer, posts, album, tabs */
.composer textarea{resize:vertical;min-height:64px}
/* One height for every control in the composer row (founder: "make them boxes level, similar to what we
   did in the photo comments"). Measured before: Add photos 42px, emoji 29px, Post 46px tall with top edges
   8px apart - three different paddings (9/13, 6/10, 10/14) producing three different boxes. align-items:
   center could not fix that; centring boxes of different heights is exactly what looks unlevel.

   Same mechanism as .cform-row above: ONE number, and with *{box-sizing:border-box} an explicit height
   makes the boxes equal whatever their padding differs by - which is the point, since the three controls
   legitimately need different padding (a dashed file chip, a glyph cluster, a word).

   The value is 46px because that is what a plain .btn already measures here (verified, not guessed), so no
   new size enters the site and the Post button keeps the size it has - only the other two come up to
   match. The comment row's 34px is NOT reused: those are .btn sm controls in a compact panel, a different
   scale, and shrinking Post would make it smaller than every other primary button on the site. */
.composer-row{display:flex;gap:10px;align-items:center;flex-wrap:wrap;--composer-ctl-h:46px}
.composer-row .file-label span,
.composer-row .emoji-btn,
.composer-row button[type=submit]{height:var(--composer-ctl-h);border-radius:10px;
  display:inline-flex;align-items:center;justify-content:center;line-height:1}
.file-label{display:inline-flex;align-items:center;cursor:pointer}
.file-label input[type=file]{position:absolute;width:1px;height:1px;opacity:0}
.file-label span{padding:9px 13px;border:1px dashed var(--line);border-radius:10px;background:var(--field);color:var(--dim)}
.file-label input:focus-visible + span{outline:2px solid var(--focus)}
.post-head{display:flex;align-items:center;gap:10px;margin-bottom:8px}
.post-who{display:flex;flex-direction:column;line-height:1.3}
.post-del{margin-left:auto}
.post-body{white-space:pre-wrap;word-break:break-word;margin:6px 0 10px}
.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;margin-top:8px}
.grid.one{grid-template-columns:1fr;max-width:640px}
.grid.two{grid-template-columns:repeat(2,1fr)}
/* The 1:1 rule was applied to EVERY image, including single full-width posts. Thumbnails
   are not square - the pipeline keeps the aspect ratio (real files on this site measure
   320x320, 320x213 and 320x242) - so forcing a square cropped 6 of the 12 existing
   thumbnails. Grids keep the square (that is what a grid needs); a lone image keeps its
   own ratio and is capped by height so one tall photo cannot push the next post off
   the screen. */
.shot img{width:100%;height:100%;object-fit:cover;display:block;border-radius:8px;aspect-ratio:1/1;background:var(--init-bg)}
.grid.one .shot img{aspect-ratio:auto;height:auto;max-height:min(72vh,560px);width:100%;
  object-fit:contain;background:transparent}
.album{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:8px;margin-top:10px}
.tabs{display:flex;gap:4px;border-bottom:1px solid var(--line);margin:6px 0 14px}
.tabs a{padding:9px 13px;color:var(--dim);font-weight:600;border-bottom:2px solid transparent}
.tabs a.on{color:var(--brand);border-bottom-color:var(--brand)}
.statline{display:flex;gap:14px;margin:8px 0 0;font-size:.9rem}
.statline a{color:var(--dim)}
.grow{flex:1}
.btn.sm{padding:6px 10px;font-size:.85rem}
.prow .sub{flex-basis:100%;order:3}
@media(min-width:700px){.prow .sub{flex-basis:auto;order:0;margin-left:8px}}
.searchbar{display:flex;gap:8px;margin-bottom:6px}
.radio{grid-template-columns:auto 1fr;align-items:center;gap:9px;font-weight:500}
.radio input{width:auto}


/* Layout: feed + sidebar (the founder asked for their own photos on the home page) */
.feed-layout{display:grid;gap:16px;grid-template-columns:1fr}
@media(min-width:900px){.feed-layout{grid-template-columns:minmax(0,1fr) 320px;align-items:start}}
/* .side / .feed-layout rules removed: the feed sidebar they belonged to was replaced by
   the shared right rail (_rail.html), and leaving the old grid behind made post cards 493px
   wide beside an 829px composer. Dead CSS is how that bug stayed invisible. */
.me-card{display:flex;align-items:center;gap:12px;color:var(--ink)}
.me-card:hover{text-decoration:none}
.me-name2{font-weight:700}
.mini-stats{list-style:none;display:flex;gap:14px;padding:0;margin:12px 0 0;flex-wrap:wrap}
.mini-stats a{color:var(--dim);font-size:.9rem}
.nine{display:grid;grid-template-columns:repeat(3,1fr);gap:5px;margin-bottom:8px}
/* Count pill. --alert (#e0564a) with white text measured 3.75:1 - below AA. --bad is
   6.65:1 in light and its dark-mode value takes near-black ink at 6.22:1, so the pill
   is legible on both surfaces instead of only on the old blue header. */
.pill{display:inline-block;min-width:17px;padding:0 5px;border-radius:999px;background:var(--bad);
  color:#fff;font-size:.72rem;font-weight:700;text-align:center;line-height:1.5}
:root[data-theme="dark"] .pill,:root[data-theme="auto"] .pill{color:#0e1416}

/* Phase 4-6: reactions, comments, messages, notifications, birthdays */
.reactions{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-top:10px;
  padding-top:10px;border-top:1px solid var(--line)}
.rx{display:inline-flex;align-items:center;gap:5px;padding:5px 9px;border-radius:999px;
  border:1px solid var(--line);background:var(--field);cursor:pointer;font:inherit;line-height:1}
.rx:hover{background:var(--sunken)}
.rx.on{border-color:var(--brand);background:var(--tint-good)}
.rx-e{font-size:1.05rem} .rx-n{font-size:.8rem;font-weight:700;color:var(--dim)}
.rx-static{font-size:1.05rem}
/* .cc is a flex sibling of the bubble/message row. Without flex:none it can be squeezed
   into a narrow column and its text wraps mid-phrase - seen on a group message timestamp
   rendering as 'rétt í' / 'þessu' on two lines. A timestamp should never break: give it
   its own width and let the (longer) message body wrap instead. */
.cc{margin-left:auto;flex:none;white-space:nowrap;font-size:.85rem;color:var(--dim)}

/* ---- post tools (session 8): Share at the bottom right, action menu top right.
   Two separate bands because they are two different acts: sharing is what a reader does with a post,
   the menu is what an author/moderator does TO it. .post-tools is a SIBLING of .reactions, never
   nested inside it - nesting inherits that row's gap/flex-wrap and .cc's margin-left:auto, which is
   what put "0 athugasemdir" on its own line above a stray dot. Same border-top treatment as
   .reactions so a card reads as stacked bands, one per kind of act. */
.post-tools{display:flex;align-items:center;justify-content:flex-end;gap:10px;margin-top:10px;
  padding-top:10px;border-top:1px solid var(--line)}
.share{font-size:.9rem;font-weight:600}
.share.on{color:var(--good)}

/* The menu is a <details>, so it opens with no JavaScript at all (same proven pattern as
   _delete_reason.html) and stays reachable by keyboard. It sits in the card header, where the delete
   control always was (.post-del had margin-left:auto; this takes its place), so it opens DOWNWARD as
   an absolutely positioned panel: in flow it would stretch the header row and shove the post text and
   photo down every time someone opened it. right:0 anchors it to the card's right edge, so at phone
   width it grows leftwards inside the column instead of off the screen. */
.post-menu{margin-left:auto;flex:none;position:relative}
.post-menu>summary{list-style:none;display:inline-flex;align-items:center;justify-content:center;
  min-width:34px;height:30px;border-radius:8px;color:var(--dim);cursor:pointer;font-size:1.15rem;
  line-height:1}
.post-menu>summary::-webkit-details-marker{display:none}
.post-menu>summary:hover,.post-menu>summary:focus-visible{background:var(--sunken);color:var(--ink)}
.pm-body{position:absolute;top:calc(100% + 6px);right:0;z-index:6;display:grid;gap:12px;
  width:min(320px,78vw);padding:12px;border-radius:12px;background:var(--card);
  border:1px solid var(--line);box-shadow:var(--shadow)}
.pm-form{display:grid;gap:8px}
.pm-h{margin:0;font-weight:700;font-size:.9rem}
.pm-h.warn{color:var(--warn)}
.pm-l{font-size:.85rem}
/* A <select> inside a grid label is 100% wide by default, which for a three-option field reads like
   a text box. Cap it so the shape says what it is. */
.pm-form select{max-width:240px}
.pm-del{border-top:1px solid var(--line);padding-top:10px}
/* A post an admin hid from the feed still exists - on its author's profile and at its permalink.
   Marked, not hidden, so the author is never told nothing happened when something did. */
.post.hidden-post{border-left:3px solid var(--warn)}
.comments{margin-top:12px;display:grid;gap:9px}
.comment{display:flex;gap:8px}
.av-xs{width:26px;height:26px} .av-xs.init{font-size:.75rem}
.c-body{background:var(--sunken);border-radius:10px;padding:7px 10px;flex:1;min-width:0}
.c-body p{margin:2px 0;white-space:pre-wrap;word-break:break-word}
.c-name{font-weight:700;font-size:.85rem}
.c-form{display:flex;gap:8px;margin-top:4px}
.c-form input{flex:1}
.linkish.xs{font-size:.78rem;padding:1px 3px}
.threadlist .snippet{flex-basis:100%;order:3}
.thread-head{display:flex;align-items:center;gap:12px;margin-bottom:14px}
.msgs{display:grid;gap:8px;margin-bottom:14px}
.msgrow{display:flex} .msgrow.mine{justify-content:flex-end}
.bubble{max-width:min(560px,86%);background:var(--sunken);border-radius:12px;padding:8px 11px}
.msgrow.mine .bubble{background:var(--tint-good-strong)}
.bubble .who{display:block;font-size:.78rem;font-weight:700;color:var(--dim)}
.bubble p{margin:3px 0;white-space:pre-wrap;word-break:break-word}
.notifs{list-style:none;margin:0;padding:0;display:grid;gap:2px}
.notifs li{display:flex;gap:10px;align-items:center;padding:9px;border-radius:10px}
.notifs li.unread{background:var(--tint-good)}
.bell{position:relative} .bell-pill{position:absolute;top:-4px;right:-6px}
.blist{list-style:none;margin:0;padding:0;display:grid;gap:7px}
.blist li{display:flex;align-items:center;gap:8px;font-size:.9rem}
.blist a{display:flex;align-items:center;gap:8px;color:var(--ink)}
.bd{margin-left:auto;color:var(--dim);font-size:.82rem;white-space:nowrap}
.bd.today{color:var(--bad);font-weight:700}
.bday{margin:4px 0 0}

/* Inbox + reactions inside private messages */
.inbox{list-style:none;margin:0;padding:0;display:grid;gap:6px}
.inbox li a{display:flex;gap:12px;align-items:center;padding:10px;border-radius:12px;
  border:1px solid transparent;color:var(--ink)}
.inbox li a:hover{background:var(--sunken);text-decoration:none}
.inbox li.unread{background:var(--tint-good);border-color:var(--tint-good-line)}
.in-body{flex:1;min-width:0}
.in-top{display:flex;gap:8px;align-items:baseline}
.in-prev{margin:3px 0 0;color:var(--dim);font-size:.9rem;overflow:hidden;
  text-overflow:ellipsis;white-space:nowrap}
.in-prev.own{opacity:.75}
.pill.big{min-width:22px;height:22px;display:grid;place-items:center;font-size:.8rem}
.bubble-wrap{display:flex;align-items:center;gap:6px;max-width:min(640px,90%)}
.msgrow.mine .bubble-wrap{flex-direction:row-reverse}
.rx-picker{display:flex;gap:2px;opacity:0;transition:opacity .12s}
.msgrow:hover .rx-picker,.msgrow:focus-within .rx-picker{opacity:1}
@media(hover:none){.rx-picker{opacity:1}}
.rx.xs{padding:3px 5px;border-radius:8px;font-size:.9rem}
.msg-reacts{display:flex;gap:4px;margin-top:5px;flex-wrap:wrap}
.chip{display:inline-flex;gap:3px;align-items:center;background:var(--field);border:1px solid var(--line);
  border-radius:999px;padding:1px 6px;font-size:.8rem}
.thread-back{margin-left:auto}


/* ---------------------------------------------------------------------------
   Left-sidebar shell (founder's reference screenshots). The nav moved out of a
   full-width top bar into a column on the left; the page content sits beside it.
   Below 900px the same markup becomes a normal top bar, because a 240px rail on a
   phone would leave no room to read anything - so this is one structure that
   reflows rather than two separate navs to keep in sync.
   Brand colours come from the tokens above; nothing here hard-codes a colour.
--------------------------------------------------------------------------- */
.shell{display:block;max-width:var(--shell-max);margin-left:auto;margin-right:auto}
.side-head{background:var(--card);border-right:1px solid var(--line);color:var(--ink)}
.page{min-width:0}
/* Signed out there is no nav, so the column must not render as an empty 248px gutter -
   that read as a broken layout on /login. base.html adds .no-nav in that case. */
.shell.no-nav{grid-template-columns:minmax(0,1fr) !important}
@media(min-width:900px){
  .shell{display:grid;grid-template-columns:var(--shell-side) minmax(0,1fr);align-items:start;
    min-height:100vh}
  /* Same rule for the nav column: stick under the bar, not behind it, and shorten by the
     bar height so its own first item starts at the same y as the composer (18px of padding
     in both columns -> both land at 77). */
  .side-head{position:sticky;top:var(--topbar-h);height:calc(100vh - var(--topbar-h));
    display:flex;flex-direction:column;
    gap:6px;padding:18px 14px;overflow-y:auto}
  .side-brand{display:flex;align-items:center;gap:10px;margin-bottom:14px;color:var(--ink)}
  .side-brand:hover{text-decoration:none}
  .side-brand .mark{background:transparent}
  .brand-name{color:var(--ink);font-size:1.02rem}
  .nav{flex-direction:column;gap:2px;margin-right:0}
  .nav a{color:var(--ink-soft);padding:9px 10px;border-radius:9px;font-size:.95rem;
    display:flex;align-items:center;gap:7px}
  .nav a:hover{background:var(--sunken);color:var(--ink)}
  /* aria-current, not class= : the links that carry a count used to own a class
     attribute, so appending an 'on' class was impossible. #2b4f7c on #eaf1fa measures
     7.35:1 (the plain brand blue on that tint is only 4.71:1). */
  .nav a[aria-current="page"]{background:var(--tint-brand);color:var(--brand-ink-tint);
    font-weight:700}
  .side-foot{margin-top:auto;padding-top:16px;display:flex;align-items:center;gap:8px;
    flex-wrap:wrap;border-top:1px solid var(--line)}
  .who{flex-wrap:wrap}
  .me{color:var(--ink-soft)}
  .me:hover{background:var(--sunken)}
  .theme-btn{border-color:var(--line);color:var(--ink-soft)}
  .theme-btn:hover{background:var(--sunken)}
  .logout-link{color:var(--dim)}
}
/* narrow screens keep the previous horizontal bar, on a card surface */
@media(max-width:899px){
  .side-head{border-right:0;border-bottom:1px solid var(--line);padding:10px 14px;
    display:flex;align-items:center;gap:12px;flex-wrap:wrap;position:sticky;top:0;z-index:20}
  .side-brand{display:flex;align-items:center;gap:9px;color:var(--ink)}
  .brand-name{color:var(--ink)}
  .nav{flex-wrap:wrap;margin-right:auto}
  .nav a{color:var(--ink-soft)}
  .nav a:hover{background:var(--sunken)}
  .theme-btn{border-color:var(--line);color:var(--ink-soft)}
}
/* accent tint for active nav / selected states */
:root{--tint-brand:#eaf1fa; --tint-brand-line:#c7d9ef; --brand-ink-tint:#2b4f7c;}
:root[data-theme="dark"], :root[data-theme="auto"]{--tint-brand:#1e2836;--tint-brand-line:#2f4560;--brand-ink-tint:#a9c8ee}
@media (prefers-color-scheme: light){:root[data-theme="auto"]{--tint-brand:#eaf1fa;--tint-brand-line:#c7d9ef;--brand-ink-tint:#2b4f7c}}
.badge.admin{background:var(--tint-brand);color:var(--brand-ink-tint)}
.rx.on,.theme-seg button.on{border-color:var(--brand);background:var(--tint-brand)}
.step.done{background:var(--tint-brand);color:var(--brand-ink-tint)}
.msg.good{background:var(--tint-brand);color:var(--brand-ink-tint);border-color:var(--tint-brand-line)}


/* ---------------------------------------------------------------------------
   THREE-COLUMN PAGE (founder's annotated reference: samples/website look 03.png)
   nav column | content | right rail. The rail is ONE card whose sections are
   divided by hairlines - the reference separates "Shared media / Shared files /
   Shared links" that way, and separate cards made the right side read as a
   column of unrelated boxes. Below 1100px the rail moves under the content;
   below 900px the nav becomes the horizontal bar again. One DOM structure does
   all three widths - no second copy of the layout to keep in sync.
--------------------------------------------------------------------------- */
.cols3{display:grid;gap:0;grid-template-columns:minmax(0,1fr)}
@media(min-width:1100px){
  .cols3{grid-template-columns:minmax(0,1fr) var(--rail-w);align-items:start}
  /* top MUST equal --topbar-h. Sticky offsets larger than the bar's height do not just
     change where things stick on scroll - they shift the element down at scroll position 0,
     because its natural grid position (59) is above the requested offset. */
  .rail{position:sticky;top:var(--topbar-h)}
}
/* sticky offsets assume the top bar is one row. At widths where it wraps to two rows
   the rail would tuck under it, so stickiness is dropped there instead of guessing a
   second offset. */
@media(min-width:1100px) and (max-width:1239px){.side-head,.rail{position:static}}

/* the other half of the left gutter (see .main); right side keeps a normal inset so the
   card does not touch the viewport edge */
.rail{padding:18px var(--col-gap) 40px calc(var(--col-gap) / 2);min-width:0}
/* Below 1100px .cols3 is one column, so nothing sits beside the content and there is no
   half-gutter to share. Both boxes go back to a full --col-gap on each side; leaving the
   asymmetric padding here would have given 12px on the left and 6px on the right. */
@media(max-width:1099px){.main,.rail{padding-left:var(--col-gap);padding-right:var(--col-gap)}}
.rail-card{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);
  box-shadow:var(--shadow);overflow:hidden}
/* Sections are separated by rules, not by margins: the hairline is what makes the panel
   read as one place with parts, rather than four stacked cards. */
.rail-sec{padding:16px;border-top:1px solid var(--line)}
.rail-sec:first-child{border-top:0}
.rail-sec h2{font-size:.95rem;margin:0 0 10px;color:var(--ink)}
.rail-sec .more{display:inline-block;margin-top:10px;font-size:.88rem}
.rail-empty{color:var(--dim);font-size:.88rem;margin:0}
/* one row per person/thing, with the count or date pushed to the right edge */
.rlist{list-style:none;margin:0;padding:0;display:grid;gap:9px}
.rlist li{display:flex;align-items:center;gap:9px;font-size:.9rem;min-width:0}
.rlist a{display:flex;align-items:center;gap:9px;color:var(--ink);min-width:0}
.rlist .nm{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.rlist .when{margin-left:auto;color:var(--dim);font-size:.82rem;white-space:nowrap}
.rlist .when.today{color:var(--bad);font-weight:700}
/* events: a real calendar is not built yet, so this section says so in one line rather
   than showing a "coming soon" roadmap - dead copy that claims something false was
   recorded as a bug last session. */
.rail-sec.events .ph{display:flex;gap:10px;align-items:center;color:var(--dim);font-size:.88rem}

/* PROFILE CARD + COVER - the founder's third sketch is the one to follow: the image
   FILLS THE FRAME, the name and counts sit on rounded plates on top of it, and one round
   button in the corner changes it. Earlier attempts, both rejected by the founder:
     (a) a banner block above the head -> added ~450px, the page jumped when a photo was
         uploaded ("the size of box changed");
     (b) a background over only the right 62%, fading left -> looked half-painted.
   So: inset:0 and position:absolute. It paints the whole card and takes part in NO
   layout, which is what keeps the size stable - the frame's height comes from the real
   content, never from the image. Uploading a cover does not resize anything; it only
   changes what you see inside the existing frame. */
.profile-card{position:relative;padding:18px;overflow:hidden}
/* The frame exists whether or not a photo is in it, so the card is the same size for
   everyone and NEVER changes when someone uploads - that was the reported bug ("the size
   of box changed after I uploaded the image"). Keying this to .has-cover was my first
   attempt here and it still resized the card on the next visit; a constant frame also
   matches the founder's first screenshot, which pointed at this space asking for a place
   to put a picture. Without a photo you just see the card surface. */
.profile-card .profile-head{min-height:280px}
@media(max-width:699px){.profile-card .profile-head{min-height:210px}}
.cover{position:absolute;inset:0;z-index:0;width:100%;height:100%;object-fit:cover;
  display:block;border-radius:inherit}
/* everything in the card sits above the painted background */
.profile-head,.profile-card .bio,.profile-card .row-actions{position:relative;z-index:1}
/* head is a row: identity block on the left, cover button at the right edge. The base
   .profile-head rule (align-items:center) stays for post_hidden.html, which has no cover
   controls; only the profile card top-aligns. Two identical .profile-head rules with
   different alignment was a real hazard - the last one won for every page. */
.profile-card .profile-head{align-items:flex-start}
.pf-main{display:flex;gap:16px;align-items:center;flex-wrap:wrap;flex:1 1 320px}

/* Text over a photo needs a plate, not a scrim. Measured against the two worst cases a
   photo can be (pure white, pure black): rgba(12,16,18,.66) gives the name #f3f5f4
   5.68:1 over white and 18.03:1 over black; the stat links #e7ebe9 give 5.17:1 and
   16.42:1. All pass AA (4.5) at BOTH extremes, so it holds for any family picture. A uniform overlay cannot promise that - white
   text under a 35% scrim over a white photo is only 2.01:1. The plates are dark in both
   themes because the text on them is near-white either way; --ink would be invisible on
   a dark plate, so these rules set their own colour deliberately. */
.profile-card.has-cover .on-photo,.profile-card.has-cover .statline a,
.profile-card.has-cover .bday>span{display:inline-block;padding:5px 10px;border-radius:9px;
  background:var(--photo-plate);color:#f3f5f4;box-decoration-break:clone;-webkit-box-decoration-break:clone}
.profile-card.has-cover .statline a{color:#e7ebe9;text-decoration:none}
.profile-card.has-cover .statline a:hover{background:var(--photo-plate-strong);text-decoration:underline}

/* The bio (Settings -> "Stutt lýsing") was the one piece of profile text never given a plate, so it
   painted straight onto the cover photo - unreadable over light clothing, exactly what the founder's
   screenshot showed ("Ég er að vestan" on a beige jumper).
   inline-block, NOT a block: my first attempt here was a block with max-width:60ch and it passed every
   colour/radius check while looking wrong - measured 549px of plate around 115px of text, where the
   name plate is 304px around 284px. The founder said "still the same" because a full-width bar does not
   read as "like the rest": those plates hug their words. An inline-block sizes to the content and still
   wraps at max-width for a long bio, so short text gets a small plate and long text stays bounded. */
.profile-card.has-cover .bio{display:inline-block;max-width:min(60ch,100%);
  background:var(--photo-plate);color:#f3f5f4;
  border-radius:9px;padding:8px 12px;box-decoration-break:clone;-webkit-box-decoration-break:clone}
/* the avatar keeps its own ring so a face does not merge into a dark photo */
.profile-card.has-cover .pf-main>.av{box-shadow:0 0 0 3px rgba(255,255,255,.75)}

/* Corner controls. Round, per the sketch. They are a positioned form (z-index above the
   image) - position:relative+z-index is not decoration: an earlier version let the photo
   paint OVER these, which is literally what "it got added on top" described. */
.cover-pick{position:absolute;top:14px;right:14px;z-index:3;display:flex;gap:8px;
  align-items:center}
.cover-btn{display:inline-grid;place-items:center;width:40px;height:40px;border-radius:50%;
  border:1px solid rgba(255,255,255,.55);background:rgba(12,16,18,.55);color:#fff;
  cursor:pointer;padding:0;backdrop-filter:blur(2px)}
.cover-btn:hover{background:rgba(12,16,18,.78);border-color:#fff}
/* The remove button carried class="danger" with no rule attached to it, so the class
   meant nothing. Fixed rgba values (not --bad): --bad is #a8322b in light mode but
   #e0776d in dark, and white text on that is 2.8:1. White on rgba(168,50,43,.92) is
   6.65:1 in every theme, which matches how the plates above deliberately set their own
   colour instead of following the palette. */
.cover-btn.danger:hover{background:rgba(168,50,43,.92);border-color:rgba(255,255,255,.85)}
.cover-btn:focus-visible{outline:2px solid var(--focus);outline-offset:2px}
.cover-btn input[type=file]{position:absolute;width:1px;height:1px;opacity:0}
/* no cover yet: the frame is content-sized, so the button sits in the top-right corner of
   a small card - give it the same treatment but on the page surface, not over a photo */
.profile-card:not(.has-cover) .cover-btn{border-color:var(--line);background:var(--field);
  color:var(--ink-soft)}
.profile-card:not(.has-cover) .cover-btn:hover{background:var(--sunken)}
/* The submit control behind the label stays in the tab order and is announced by screen
   readers; visually hidden, never display:none (which would remove it from both). It
   becomes visible when focused, so keyboard users see what they are activating - and that
   is also the fallback for scripting being off: choose a file, Tab to 'Vista', press Enter,
   and the upload works without a single line of JavaScript. */
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
  clip-path:inset(50%);white-space:nowrap;border:0}
.sr-only:focus-visible{width:auto;height:auto;margin:0;clip-path:none;overflow:visible;
  padding:7px 11px;background:var(--field);border:1px solid var(--line);border-radius:9px;
  color:var(--ink);font:inherit;font-weight:600;cursor:pointer}
/* narrow screens: the corner buttons must not cover the name, so they move to a row
   under the identity block instead of floating over it */
@media(max-width:560px){.cover-pick{position:static;justify-content:flex-end;margin-top:12px}}
/* avatar overlaps nothing and stays a fixed circle; the name block takes the rest */
.pf-main .av{flex:none}
@media(max-width:560px){.cover-pick{margin-left:0;width:100%}}


/* ---------------------------------------------------------------------------
   GROUP CHAT: the member roster lives in the shared RIGHT rail (_rail.html's
   .rail-card / .rail-sec), not in a card under the chat and not in its own grid.
   The old .group-layout/.group-side/.group-main rules are gone - members now use
   the same width, sticky offset (--topbar-h) and section hairlines as feed and
   profile, so there is one right column in this site instead of two that can
   drift. Deleting dead CSS matters: a stale .feed-layout rule is what made post
   cards 493px wide beside an 829px composer last session.
--------------------------------------------------------------------------- */
/* right-aligns the 'add' button inside a flex .prow (was an inline style) */
.add-form{margin-left:auto}
/* separating the roster from the add-a-friend block inside one rail section */
.rail-sec .group-add-h{margin-top:20px;padding-top:16px;border-top:1px solid var(--line)}


/* ---------------------------------------------------------------------------
   BIRTHDAY CONTROLS (founder's request). Two things here.

   1. .date-row replaces <input type="date">. A native date field submits an EMPTY string
      when any single part is unfilled, so picking only the year posted birthday= and
      DELETED a stored birthday while reporting "saved" - silent data loss. Day/month/year
      are now separate values: an incomplete choice is visible and rejected with a message.

   2. .switch is the hide-year toggle. The founder's reference shows state by track colour
      alone; measured, that does not work for the OFF position - no grey fill reaches the
      WCAG 3:1 required for a control's boundary (best light-grey candidate was ~2.2:1 on
      white). So off = outlined, on = filled brand: shape and colour both carry the state.
      Boundary values measured against the surfaces actually behind them:
        light border #7d858f -> 3.73 vs card, 3.42 vs page bg (a plain --line grey is 1.2)
        dark  border #879196 -> 4.50 vs card, 3.59 vs its own fill
      Knob: #6b7280 on the off track = 4.32; white on --brand = 5.36 (light) / 3.29 (dark).
   Hard-coded hex only for these two boundaries - they are contrast-tested against specific
   surfaces, which is the same reason the cover plates set their own colour above.
--------------------------------------------------------------------------- */
/* Flex, not grid. My first version used grid-template-columns with `1fr` for the month, so
   the month box stretched to fill every remaining pixel (the founder's screenshot), and the
   longest label set the row height, leaving a large gap above the shorter fields' controls.
   align-items:flex-end sizes each field to its own content and bottom-aligns the controls,
   which is what makes the labels line up. */
.date-row{display:flex;flex-wrap:wrap;gap:10px 12px;align-items:flex-end}
.date-row select{width:auto;min-width:5.5rem}
.date-row .date-month select{min-width:9.5rem}   /* month names are the widest values */
/* the "year is optional" explanation belongs here, not inside the field label: a 5-line
   label was what made that row unreadably tall */
.date-note{margin:2px 0 0;font-size:.85rem;color:var(--dim)}

.switch-row{margin-top:14px}
/* No !important needed: a class selector (.switch) already outranks the bare `label` rule
   at the top of this file, and that global display:grid was exactly what stacked the box
   above its text - the founder's 'checkbox is misaligned'. */
.switch{display:flex;flex-direction:row;align-items:center;gap:12px;
  cursor:pointer;font-weight:600;color:var(--ink)}
/* the real checkbox is kept in the flow for keyboard/AT use, just not painted */
.switch input[type=checkbox]{position:absolute;width:1px;height:1px;margin:0;opacity:0;
  pointer-events:none}
.switch-track{flex:none;position:relative;width:46px;height:27px;border-radius:999px;
  background:var(--sunken);border:2px solid #7d858f;transition:background .12s,border-color .12s}
.switch-track::after{content:"";position:absolute;top:50%;left:3px;transform:translateY(-50%);
  width:19px;height:19px;border-radius:50%;background:#6b7280;
  transition:left .12s,background .12s}
/* checked == the year IS hidden (field is named 'hide' so the POST matches the label) */
.switch input:checked + .switch-track{background:var(--brand);border-color:var(--brand)}
.switch input:checked + .switch-track::after{left:22px;background:#fff}
.switch input:focus-visible + .switch-track{outline:2px solid var(--focus);outline-offset:2px}
:root[data-theme="dark"] .switch-track,:root[data-theme="auto"] .switch-track{border-color:#879196}
:root[data-theme="dark"] .switch-track::after,:root[data-theme="auto"] .switch-track::after{background:#cfd8d1}
@media (prefers-color-scheme: light){
  :root[data-theme="auto"] .switch-track{border-color:#7d858f}
  :root[data-theme="auto"] .switch-track::after{background:#6b7280}
}
.switch-label{font-size:.95rem}


/* ---------------------------------------------------------------------------
   IMAGE VIEWER (founder's request: open a photo and flip to the next one).
   Overlay is opaque enough that controls stay legible over ANY image - measured,
   rgba(8,9,10,.72) over a pure-white photo still gives white icons 8.34:1, and
   the caption on the full overlay is 16.62:1. A translucent scrim that depends on
   what is behind it cannot promise that, which is the same lesson as the cover
   plates. Colours are fixed here for that reason, not palette-driven.
--------------------------------------------------------------------------- */
html.lb-open{overflow:hidden}          /* the page behind must not scroll under the viewer */
.lb{position:fixed;inset:0;z-index:60;display:flex;align-items:center;justify-content:center;
  /* --lb-pad is ONE value used by both the container padding and the image max-height. They
     had drifted (padding resolved to 40px here while a later rule allowed the image
     100vh - 2.5rem = 40px total), so the photo was 40px taller than the box holding it:
     vertical overflow, and flex align-items:center pushed its top ABOVE the padding edge -
     the 'little misaligned' in the founder's screenshot (image at y=95, panel at y=113). */
  --lb-pad:clamp(8px,2.5vh,20px);
  background:rgba(8,9,10,.94);padding:var(--lb-pad)}
.lb[hidden]{display:none}
/* contain, never cover: a viewer that crops the photo defeats the point of opening it.
   max-height leaves room for the caption/counter so nothing is ever hidden behind them. */
.lb-img{max-width:100%;width:auto;height:auto;object-fit:contain;
  display:block;margin:0 auto;border-radius:4px;box-shadow:0 8px 40px rgba(0,0,0,.5)}
.lb-cap{position:fixed;left:0;right:0;bottom:0;padding:12px 16px;text-align:center;
  color:#e9ebe8;font-size:.9rem;background:linear-gradient(to top,rgba(8,9,10,.75),transparent)}
.lb-count{display:block;font-variant-numeric:tabular-nums;color:#cfd4da;font-size:.85rem}
/* 44px hit areas - a control you have to aim at precisely is a bad control on a phone */
.lb-btn{position:fixed;display:grid;place-items:center;width:44px;height:44px;border-radius:50%;
  border:1px solid rgba(255,255,255,.5);background:rgba(8,9,10,.6);color:#fff;font-size:1.5rem;
  line-height:1;cursor:pointer;padding:0}
.lb-btn:hover{background:rgba(8,9,10,.9);border-color:#fff}
.lb-btn:focus-visible{outline:2px solid #fff;outline-offset:2px}
.lb-close{top:12px;right:12px}
.lb-prev{left:12px;top:50%;transform:translateY(-50%)}
.lb-next{right:12px;top:50%;transform:translateY(-50%)}
/* on a phone the arrows move under the image: at 390px wide they would sit on top of it and
   hide part of the photo, which is the one thing a viewer must not do */
@media(max-width:699px){.lb-prev{left:auto;bottom:14px;right:50%;transform:translateX(-70%)}
  .lb-next{right:50%;transform:translateX(70%);top:auto;bottom:14px}}

/* Photo page (no-JS fallback + shareable URL) and the viewer's comment panel. */
.photo-page{display:grid;gap:14px}
.ph-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.ph-full{width:100%;height:auto;object-fit:contain;border-radius:6px;background:var(--sunken)}
.ph-caps-h{margin:2px 0 0;font-size:.95rem}
.clist{list-style:none;margin:8px 0 0;padding:0;display:grid;gap:10px}
.cmt{display:flex;gap:9px;align-items:flex-start}
.cmt p{margin:2px 0;font-size:.95rem}

/* The comment column is sized by the IMAGE, not the viewport (founder's sketch): the row
   shrink-wraps the photo, so the panel begins at its right edge and matches its top and
   bottom exactly. Narrow the window -> the photo gets smaller -> the panel follows it down to
   the new bottom edge. flex:0 1 auto on the stage is what makes that possible; a stage that
   fills the row (flex:1) would make the panel viewport-tall instead, which is what shipped
   before and looked wrong in both screenshots. align-self:stretch then copies the image's
   height onto the panel, and overflow-y:auto keeps long threads inside it. */
.lb{gap:14px}
.lb-cap{position:absolute;left:0;right:0;bottom:0;background:none;padding:8px 16px}
.lb-stage{flex:0 1 auto;min-width:0;display:flex;align-items:center;justify-content:center}
.lb-side[hidden]{display:none}
/* No align-self here: it inherits align-items:center from .lb. The JS sets the panel's height
   to the image's measured height, and a stretched item with a definite height behaves as
   flex-start - which is how the two came to disagree at one width but not another. Centring
   both in a line sized by the taller of the two makes their top and bottom edges coincide at
   every window size, which is what "the comment box follows the image" asks for. */
.lb-side{flex:0 0 clamp(280px,24vw,380px);max-height:none;min-width:0;
  overflow-y:auto;background:var(--card);color:var(--ink);border-radius:var(--radius);
  padding:14px;display:grid;gap:12px;align-content:start}
.lb-side .c-form{display:flex;gap:8px;align-items:center;margin-top:auto;padding-top:8px}
.lb-side .c-form input{flex:1 1 auto;min-width:0;padding:9px 11px;border:1px solid var(--line);
  border-radius:8px;background:var(--field);color:var(--ink);font:inherit;font-size:.95rem}
.lb-side .reactions{display:flex;gap:6px;flex-wrap:wrap;align-items:center}
.lb-cmt{top:12px;left:12px}
.lb-load,.lb-err{margin:0;color:#cfd4da;font-size:.9rem}
/* the photo is bounded by BOTH axes so the row height is always the image's own height */
/* the image must fit INSIDE the padding box on both axes, otherwise centring offsets it.
   max-width is a percentage of .lb-stage (already shrunk by the panel), so width cannot
   cause overflow - height is the only axis that can, and this makes it impossible. */
/* ONE height rule for the image, derived from the container's own padding so the photo can
   never be taller than the box that centres it. Two other rules used to hard-code this
   (6.5rem and 8rem) and disagreed with the padding - that is what pushed the photo above
   its own container. The caption/counter are position:absolute overlays, so they cost no height. */
.lb-img{max-height:calc(100vh - (var(--lb-pad) * 2))}
@media(max-width:899px){
  /* no room for two columns: panel goes under the photo, reading order unchanged */
  .lb{flex-direction:column;align-items:stretch;overflow-y:auto}
  .lb-stage{flex:0 0 auto;min-height:52vh}
  .lb-side{flex:0 0 auto;width:100%;align-self:stretch;background:transparent;color:#e9ebe8}
  .lb-side .c-form input{background:rgba(255,255,255,.08);border-color:rgba(255,255,255,.35);color:#fff}
  .lb-side .pname,.lb-side .clist p{color:#e9ebe8}
  .lb-img{max-height:52vh}
}

/* .lb-fit is added by lightbox.js once it has measured the rendered image, and its whole job
   is to make that measurement authoritative: a flex row sizes to its tallest item, so pure CSS
   cannot give the panel the image's height (measured 677px vs 717px at 1776 wide, and taller
   than the image at 1200). With this rule the panel matches the photo's top and bottom exactly
   and follows it when the window is resized. When there is no room beside the photo the class
   is not added and the panel falls back to the stacked layout above. */
/* No align-self override here: the container centres both columns, so once fitPanel makes
   them the same height their top and bottom coincide. Pinning the panel to flex-start while
   the image stayed centred is what offset them by 37px at 1200 wide (measured: both boxes
   645px tall, one at T19, the other at T56). */

/* Photo tiles are now a wrapper with two siblings (a link and, where allowed, a delete form):
   HTML forbids a form inside an anchor, so the old <a class="shot"> alone could not carry a
   button. The grid still sizes .tile, and .shot fills it - every existing .grid/.album/.nine
   rule keeps working because the wrapper inherits the cell. */
.tile{position:relative;min-width:0}
.grid>*,.album>*,.nine>*{min-width:0}
.tile-del{position:absolute;top:6px;right:6px;margin:0;display:none}
/* shown on hover/focus at pointer widths, always visible on touch/narrow screens where there is
   no hover to discover it with */
@media(hover:hover) and (min-width:900px){.tile:hover .tile-del,.tile:focus-within .tile-del{display:block}}
@media(hover:none),(max-width:899px){.tile-del{display:block}}
.tile-del-btn{width:30px;height:30px;border-radius:50%;border:1px solid rgba(255,255,255,.7);
  background:rgba(8,9,10,.62);color:#fff;font-size:1.05rem;line-height:1;padding:0;cursor:pointer;
  display:grid;place-items:center}
.tile-del-btn:hover{background:var(--bad);border-color:#fff}
.tile-del-btn:focus-visible{outline:2px solid #fff;outline-offset:2px}

/* Confirm dialog (founder asked for a confirm window). Not native confirm(): Chrome suppresses
   it inside cross-origin iframes - the founder browses with an extension frame present, so a
   native prompt can silently never appear and a destructive delete would happen on one click.
   The forms still carry onsubmit=confirm(...) as the no-JS fallback; this takes over when JS is
   on. Colours fixed (not palette) because it must stay legible over any photo behind it. */
.cfm{position:fixed;inset:0;z-index:70;display:grid;place-items:center;padding:16px;
  background:rgba(8,9,10,.72)}
.cfm[hidden]{display:none}
.cfm-box{max-width:30rem;background:var(--card);color:var(--ink);border-radius:var(--radius);
  box-shadow:0 10px 40px rgba(0,0,0,.45);padding:18px;display:grid;gap:12px}
.cfm-title{margin:0;font-size:1.05rem}
.cfm-body{margin:0;color:var(--ink-soft);font-size:.95rem;line-height:1.45}
.cfm-actions{display:flex;gap:10px;justify-content:flex-end;flex-wrap:wrap}

/* ---------------------------------------------------------------- chat media -- */
/* Photos sent inside a conversation. They are real photo_tile() output - same markup as the
   feed, album and rail - so the viewer, its comments/reactions and the delete control all work
   here with no second implementation. Only the layout differs: a bubble is narrow and a chat
   photo should not be cropped to a square the way an album thumbnail is. */
.chat-photos{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px;margin:6px 0 4px}
.chat-photos.one{grid-template-columns:minmax(0,300px)}
/* .shot img is object-fit:cover at aspect-ratio 1/1 for grids. In a chat the photo IS the
   message, so show it whole: contain + natural ratio, capped so one tall phone photo cannot
   push the thread off screen. The link still opens the full master in the viewer. */
.chat-photos .shot img{height:auto;aspect-ratio:auto;object-fit:contain;
  max-height:min(60vh,420px);width:100%;background:var(--init-bg)}

/* Emoji to write with (founder). Hidden until <html> carries .js, which base.html sets from an
   inline script: without scripting the control is absent rather than inert, and typing an emoji
   from the OS keyboard still works - so this degrades instead of breaking. */
.emoji-wrap{position:relative;display:inline-flex}
.emoji-btn{flex:0 0 auto;font-size:1.05rem;line-height:1;padding:7px 10px}
html:not(.js) .emoji-wrap{display:none}
.emoji-pop{position:absolute;bottom:calc(100% + 8px);left:0;z-index:30;display:grid;
  grid-template-columns:repeat(5,auto);gap:2px;padding:6px;border:1px solid var(--line);
  border-radius:10px;background:var(--card);box-shadow:0 10px 30px rgba(0,0,0,.18)}
.emoji-pop[hidden]{display:none}
.emoji-opt{border:0;background:none;cursor:pointer;font-size:1.25rem;line-height:1;padding:6px;
  border-radius:8px}
.emoji-opt:hover{background:var(--tint-brand)}
.emoji-opt:focus-visible{outline:2px solid var(--focus);outline-offset:1px}

/* A chat form now carries three controls plus the field, so it must wrap instead of squeezing
   the text input to nothing at phone width. */
.chat-form{flex-wrap:wrap;align-items:center}
.chat-form input[name=body]{min-width:120px}

/* The viewer's delete control. It had no CSS at all, and below 900px .lb-side sets
   background:transparent over the near-black lightbox while .linkish.danger keeps
   color:var(--bad) - #a8322b on that backdrop measures 2.96:1, which fails WCAG AA for text
   (4.5 needed). Same class of defect as the count pill moving off --alert.
   The colour is fixed rather than a theme token ON PURPOSE: this backdrop is rgba(8,9,10,.94)
   whatever the UI theme is, so var(--bad-ink) would be #7c2620 in light theme - dark red on
   near-black, worse than what it replaced. #e0776d measures 6.59:1 there and matches the
   hardcoded values this media query already uses (#e9ebe8 text, white field borders). */
@media(max-width:899px){
  .lb-side .linkish.danger{color:#e0776d}
  .lb-side .rx-n,.lb-side .sub,.lb-side .hint{color:#cfd4da}
}

/* ------------------------------------------------------- post draft preview -- */
/* Shows the photo + text as it will read, before posting (founder: "as it is now you can only see
   the photo after you post"). The markup reuses .post-body/.grid/.tile/.shot, so almost all of
   this is container chrome - the layout itself comes from the real rules, which is the point: a
   preview built from its own class names would be a second design that quietly drifts from the
   first. */
/* Divider now sits BELOW the block: the preview comes first in the composer (founder's request),
   so it is the input that needs separating from what it produces. */
/* No padding here on purpose: .stack already puts a 12px gap between form items, so adding
   padding-bottom would double the space under the divider. */
.pv{border-bottom:1px solid var(--line)}
.pv-h{margin:0 0 8px;font-size:.78rem;font-weight:700;letter-spacing:.06em;
  text-transform:uppercase;color:var(--dim)}
/* The preview IS a post card, but nesting one .card inside another reads as two boxes. Drop the
   inner card's own frame and let it sit on the composer's surface instead. */
.pv-card{border:0;background:transparent;box-shadow:none;padding:0;border-radius:0}
/* The remove control is hover-only on real tiles (a stray click deletes a photo from someone's
   post). In a preview there is nothing to accidentally destroy - the photo has not been posted -
   and hiding it behind hover makes an unwanted pick impossible to get rid of on a phone or by
   keyboard, so it is always shown. */
.pv-del{display:block}

/* A file the browser cannot decode (HEIC in Chromium/Edge) still occupies its tile - it IS selected and
   will post - but an <img> that fails leaves only alt text, which reads as a broken upload. This shows a
   quiet placeholder with the real facts instead: no preview, file size, filename. Same box geometry as a
   normal tile so a multi-image draft does not shift when one entry is unpreviewable. */
.pv-noimg .shot{display:grid;place-items:center;background:var(--sunken);padding:8px;text-align:center}
.pv-noimg-t{font-size:.75rem;color:var(--dim);line-height:1.35;overflow-wrap:anywhere}

/* Comment boxes now carry an emoji button too (founder: "we are missing the similes option in the
   comments section"). Inputs have a default intrinsic width, so a flex row that gains a control can
   no longer shrink and overflows at phone width. min-width:0 lets the field give up space instead;
   .lb-side already did this for the viewer, this covers comments under a post. */
.c-form input[name=body]{min-width:0}

/* The emoji trigger is a cluster of four glyphs (founder: "maybe a group of 4 emojis"): one lone
   smile reads as "insert THIS emoji", a cluster reads as "a set to choose from". aria-hidden in the
   markup, so only the button's own name (chat.emoji) is announced.
   Measured rather than assumed (headless, container widths forced because --window-size=390 reports
   innerWidth=500): the control is 86px wide; the open popup is 207px and stays inside a 320px and a
   500px container. Overlapping the glyphs by 4px takes it from 98px to 86px - cosmetic only: at a
   ~320px composer row 'Post' wraps to the next line whether the cluster is 86px, 70px or 61px, so
   shrinking does NOT buy layout and must not be blamed for that wrap. */
.emoji-fan{display:inline-flex;align-items:center;line-height:1;font-size:.95rem;letter-spacing:0}
.emoji-fan > * + *{margin-left:-4px}

/* ------------------------------------------------- comment box, two rows (v2) -- */
/* Founder's mock: the field gets its own full-width row so there is room to write, and the emoji
   picker + Send sit on a second row beneath it. Scoped to .cform-v because .c-form is shared with
   the DM/group send rows, which are deliberately one row (field beside Send) and were not part of
   this request. Specificity has to beat ".lb-side .c-form", hence the paired selectors. */
.c-form.cform-v{display:grid;gap:8px}
.lb-side .c-form.cform-v{display:grid;gap:8px;margin-top:auto;padding-top:8px}
.cform-row{display:flex;align-items:center;justify-content:space-between;gap:8px}

/* One height for both controls (founder: "the same height as the smile box"). Two rules naming the
   same number drift - that is exactly how the viewer ended up with three different .lb-img caps.
   height, NOT min-height: measured first attempt gave 34 vs 38px because .btn's own padding pushed
   Send past a floor. With *{box-sizing:border-box} an explicit height makes them equal whatever
   their padding differs by (.btn is 10px/14px, .btn.sm 6px/10px, the picker 6px/10px), which is the
   whole point - one number, three different paddings, identical boxes. */
.cform-row{--cform-ctl-h:34px}
.cform-row .emoji-btn,
.cform-row button[type=submit]{height:var(--cform-ctl-h);border-radius:10px;
  display:inline-flex;align-items:center;justify-content:center;padding:6px 14px;line-height:1}

/* The field matches the buttons' radius. It used to be 8px in the viewer only (a one-off that
   agreed with nothing); every other input on the site is already 10px, so this removes a special
   case rather than adding one. */
.lb-side .c-form.cform-v input{border-radius:10px}

/* ------------------------------------------------------------- admin panel (v1) -- */
/* Overview is a grid of small stat blocks, not one long table: the founder asked for statistics
   "nicely displayed", and a 30-row table reads as a database dump. minmax() so it reflows at phone
   width without a second layout to keep in sync (the rule already used by .album). */
/* Overview tiles, in the shape of the founder's reference panel (a monitoring dashboard): one box per
   number, label small and muted ABOVE, the figure large below it. The previous version was four columns
   of "label .... value" rows; read across they run together into one sentence ("Samtals 11 Færslur 17
   Bíðandi vinabeiðnir 2") and long Icelandic words wrapped mid-word, which I first misread as broken
   strings. A tile cannot be misread that way: each figure is its own block with its own label.
   auto-fit + minmax means the grid reflows from 4 across to 1 with no breakpoint to maintain, and
   overflow-wrap:anywhere keeps a long single word inside its tile instead of widening the row. */
.tile-h{margin:16px 0 8px;font-size:.95rem;color:var(--dim)}
.tiles{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(190px,1fr))}
.tile{border:1px solid var(--line);border-radius:12px;padding:12px 14px;background:var(--card-alt);
  display:grid;gap:2px;min-width:0}
.tile-k{font-size:.8rem;color:var(--dim);text-transform:none}
.tile-n{font-size:1.75rem;font-weight:700;line-height:1.15;font-variant-numeric:tabular-nums;
  overflow-wrap:anywhere}
.tile-n u{font-size:.95rem;font-weight:600;text-decoration:none;color:var(--dim)}
.tile-s{font-size:.8rem;color:var(--dim);overflow-wrap:anywhere}
.tile.warn{border-color:rgba(168,50,43,.45)}
.tile.warn .tile-n{color:var(--bad)}
a.tile{text-decoration:none;color:inherit}
a.tile:hover{border-color:var(--brand);text-decoration:none}

/* Trash rows, built around a thumbnail (founder: "need to see preview of the files ... so I can see
   what I am deleting"). A photo row previously showed only its database id ("#8"), which names nothing.
   The image is leftmost and fixed-size; text takes the rest. Stacks on a phone because a 118px thumb
   beside a narrow column squeezes the text to unreadable. */
.row-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;flex-wrap:wrap}
.row-head h2{margin:0}
/* A heading and its action on one line, wrapping if the label is long. The bulk button belongs with the
   count it acts on - putting it at the bottom of a long list means scrolling to find it. */

/* Runtime switches (app/flags.py). One row: what it controls and its current state on the left, the
   action on the right. The button is .primary when paused (the next click OPENS it again - a calm verb)
   and .danger when open (the next click PAUSES - a consequential one), so colour says which direction
   is being taken rather than decorating it. */
.flagrow{display:flex;gap:14px;align-items:center;flex-wrap:wrap;padding:10px 0;
  border-top:1px solid var(--line)}
.flagrow>div:first-child{flex:1 1 220px;min-width:0}
.flag-k{margin:0;font-weight:700}
.flag-form{display:flex;gap:10px;align-items:center;flex-wrap:wrap;flex:1 1 320px}
.flag-form input[name=reason]{flex:1 1 220px;min-width:0}
@media(max-width:699px){.flag-form input[name=reason]{flex:none;width:100%}}

/* Reported posts (session 8). One card per report, everything an admin needs to decide on it in one
   place: the post text, who wrote it, that person's previous offenses, and the outcome form. Not a
   table with a "view" link per row - deciding requires the context, and making an admin click into each
   one to find out whether it deserves acting on is work pushed onto them. */
.rep{border:1px solid var(--line);border-radius:12px;padding:12px;background:var(--card);
  display:grid;gap:10px}
.rep + .rep{margin-top:12px}
.rep-head{display:flex;align-items:center;gap:10px}
/* A reason chip. Reuses the tint tokens rather than inventing colours, and is NOT .badge: that class is
   for roles and carries align-self:center for flex rows where it already sits (see the Friends-blob
   autopsie in HANDOFF). A tag next to a name should not inherit a row's stretch behaviour. */
.tag{display:inline-block;align-self:center;font-size:.75rem;line-height:1.6;padding:1px 8px;
  border-radius:999px;background:var(--tint-warn);color:var(--warn-ink);white-space:nowrap}
.rep-note,.rep-post{margin:0}
.rep-note{font-style:italic;color:var(--ink-soft);white-space:pre-wrap;overflow-wrap:anywhere}
.rep-post{border-left:3px solid var(--line);padding:10px 12px;background:var(--sunken);
  border-radius:0 10px 10px 0}
/* A long text post scrolls; a photo never makes its own preview scroll inside a clipped box. */
.rep-post .rep-text{margin:0 0 6px;white-space:pre-wrap;overflow-wrap:anywhere;
  max-height:260px;overflow:auto}
/* Thumbnails on a report card reuse .tprev from the trash rows - same size, same crop, one class.
   They are links into the viewer, so they get its hover border too. */
.rep-media{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:8px}
/* An admin deciding about a post that is already gone needs to know which way it went: 'delete' is the
   wrong button for something in the trash, and a hidden post wants un-hiding. */
/* --tint-bad/-ink exist in BOTH themes (style.css line 25 and the dark block), so no fallback is
   needed here - a var() with a default quietly hides a renamed token instead of showing it. */
.tag.bad{background:var(--tint-bad);color:var(--bad-ink)}
/* The post has been deleted or hidden since it was reported. Marked, not hidden: an admin deciding
   about a report needs to know the thing is already gone before choosing "delete". */
.rep-post.gone{border-left-color:var(--bad);opacity:.72}
.rep-post.hid{border-left-color:var(--warn)}
.rep-hist{border-top:1px solid var(--line);padding-top:8px}
.rep-off{list-style:none;margin:6px 0 0;padding:0;display:grid;gap:4px;font-size:.85rem}
.rep-form{display:flex;flex-wrap:wrap;gap:10px;align-items:end;border-top:1px solid var(--line);
  padding-top:10px}
.rep-form select{min-width:220px}
.rep-form input[name=note]{flex:1 1 200px;min-width:0}
@media(max-width:699px){.rep-form select,.rep-form input[name=note]{width:100%;flex:none}}

/* What a moderator did, and what they wrote. On their own lines: these are complete sentences, so inline
   they read as one run-on ("...your post A moderator removed your post."). The note is indented under
   the action because that is the order it was written in - what happened, then why. */
.mod-note{margin:4px 0 0;font-weight:600}
.mod-note.why{font-weight:400;color:var(--ink-soft);font-style:italic;padding-left:10px;
  border-left:2px solid var(--line)}
/* Snapshots (migration 019): a small copy of the post's own thumbnail, kept for moderation records.
   .snap is NOT clickable and has no hover border - there is no /photo/{id} left to open, and a link that
   404s is the bug the founder just reported. Same size as .tprev so a live photo strip and a snapshot
   line up in the same grid. */
.tprev.snap{cursor:default}
.tprev.snap:hover{border-color:var(--line)}
.snap-text{color:var(--ink-soft)}
.hist-thumb{display:inline-block;vertical-align:middle;margin-right:4px;border-radius:6px;
  border:1px solid var(--line);object-fit:cover;width:26px;height:26px}
.snap-cap{margin:8px 0 3px;font-size:.78rem;color:var(--dim)}
img.snap-img{width:150px;height:150px;object-fit:cover;border-radius:10px;border:1px solid var(--line);
  background:var(--sunken)}
/* An id we can no longer open, shown as text rather than a link (notifications + admin queue). */
.gone{color:var(--dim);font-variant-numeric:tabular-nums}
/* Handled reports. Same row shape as the trash list (.tprev thumbnail left, text right) on purpose:
   two layouts for "a record of something that was removed" is two things to keep in sync. */
.rephist{list-style:none;margin:0;padding:0;display:grid;gap:10px}
.rh-row{display:flex;gap:12px;align-items:flex-start;border:1px solid var(--line);border-radius:12px;
  padding:10px;background:var(--card)}
.rh-body{flex:1 1 auto;min-width:0}
.rh-body p{margin:0 0 4px}
@media(max-width:699px){.rh-row{flex-direction:column}.rh-row .tprev{width:100%;max-width:none;height:180px}}

.trashgrid{list-style:none;margin:0;padding:0;display:grid;gap:12px}
.trashgrid .trunc{-webkit-line-clamp:4;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}
.trow{display:flex;gap:14px;align-items:flex-start;border:1px solid var(--line);border-radius:12px;
  padding:12px;background:var(--card)}
.tbody{flex:1 1 auto;min-width:0}
.tbody p{margin:0 0 5px}
.ttext{white-space:pre-wrap;overflow-wrap:anywhere}
.tprev{flex:0 0 auto;display:block;width:118px;height:118px;border-radius:10px;overflow:hidden;
  background:var(--sunken);border:1px solid var(--line)}
.tprev img{width:100%;height:100%;object-fit:cover;display:block}
.tprev:hover{border-color:var(--brand)}
/* a post can carry several photos - they wrap in their own block rather than stretching the row */
.tprev-group{flex:0 1 auto;display:flex;flex-wrap:wrap;gap:6px;max-width:252px}
.tprev-group .tprev{width:78px;height:78px}
/* text-only kinds get a letter tile so every row lines up whether or not it has an image */
.tprev-text{display:grid;place-items:center;font-size:1.4rem;font-weight:700;color:var(--dim)}
@media(max-width:699px){.trow{flex-direction:column}.tprev,.tprev-group{width:100%;max-width:none}
  .tprev{height:180px}}

/* EVENTS AND OCCASIONS (founder, session 9). Layout as asked: "the closest will show up on top in the
   event box on profile" + "second one (small text below)" — so the nearest event is a card with a real
   title and date, and everything after it stays list-sized like the other rail sections. Nothing here
   invents a new visual language: .evt-hero borrows the radius/padding of .card, the small rows reuse
   .rlist exactly as birthdays do, and the button is the existing .btn sm. */
.rail-sec.events .evt-new{margin-top:10px}
.evt-hero{display:block;border:1px solid var(--line);border-radius:12px;padding:10px 11px;
  background:var(--field);text-decoration:none;color:var(--ink);margin-bottom:8px}
.evt-hero:hover{border-color:var(--focus)}
.evt-hero .evt-when{display:block;font-size:.78rem;font-weight:700;letter-spacing:.02em;
  text-transform:uppercase;color:var(--brand)}
.evt-hero .evt-when.today{color:var(--bad)}
.evt-hero .evt-title{display:block;font-size:1.02rem;font-weight:650;line-height:1.3;margin:2px 0;
  overflow-wrap:anywhere}
.evt-hero .evt-when-sub{display:block;font-size:.82rem;color:var(--dim)}
.evt-hero .evt-rep{display:inline-block;margin-top:6px;font-size:.74rem;color:var(--dim);
  border:1px solid var(--line);border-radius:999px;padding:2px 7px}
/* The rest: deliberately small text (founder's words). Reusing .rlist keeps it identical to the
   birthday rows above it, so "less important" is expressed by size that already means that here. */
.evt-rest{margin:0}
.evt-rest li a{text-decoration:none}

/* Event page + form. .fld-h heads one group of fields; note h3 has no rule anywhere else in this file
   (only h1,h2 at line ~95), so an unstyled h3 would fall back to browser defaults and come out LARGER
   than the styled h2 above it. Defining it here is what keeps the hierarchy reading top-down. */
.fld-h{margin:14px 0 4px;font-size:.8rem;font-weight:700;letter-spacing:.03em;
  text-transform:uppercase;color:var(--dim)}
.fld-row{display:flex;flex-wrap:wrap;gap:10px 12px;align-items:flex-end}
.fld-row label{min-width:9rem}
.evt-card .evt-when-line{font-size:1.15rem;font-weight:650;margin:0 0 4px}
.evt-card .evt-when-line.today{color:var(--bad)}
.evt-card .evt-place{margin:0 0 8px;color:var(--dim)}
.tag-owner{font-size:.74rem;color:var(--dim);border:1px solid var(--line);border-radius:999px;
  padding:2px 7px;margin-left:6px}

/* Phone width. The rail becomes one column below 1100px, so the hero must not rely on a wide box for
   its size — it grows by padding and type scale, both of which hold at 390px. */
@media (max-width:420px){
  .evt-hero{padding:9px 10px}
  .evt-hero .evt-title{font-size:.98rem}
}

/* EVENT THUMBNAIL + UPLOAD PREVIEW (founder, 2026-10-25). .evt-thumb was referenced by _rail.html in the
   previous revision with no rule at all — an unconstrained image inside a narrow rail column. */
.evt-hero .evt-thumb{display:block;width:100%;aspect-ratio:16/9;object-fit:cover;
  border-radius:8px;margin-top:8px;background:var(--init-bg)}

/* The preview box upload_preview.js fills. Mirrors .nine (3 columns, 5px gap) so a chosen photo looks
   like the photos members already see, and .shot's crop rules apply by class rather than by copying markup. */
.upv{display:grid;grid-template-columns:repeat(3,1fr);gap:5px}
.upv-tile{position:relative;display:block}
.upv-tile img{width:100%;height:100%;object-fit:cover;display:block;border-radius:8px;
  aspect-ratio:1/1;background:var(--init-bg)}
/* A format this browser cannot decode (HEIC in Chromium) gets text instead of an empty box, which would
   otherwise read as a failed upload. */
.upv-noimg{display:flex;align-items:center;justify-content:center;padding:8px;text-align:center;
  font-size:.72rem;color:var(--dim);border:1px dashed var(--line);border-radius:8px;
  aspect-ratio:1/1;overflow-wrap:anywhere}
.upv-more{display:flex;align-items:center;justify-content:center;font-size:.85rem;color:var(--dim);
  border:1px solid var(--line);border-radius:8px;aspect-ratio:1/1}

/* The control's own label after a pick. .file-label hides the real input (opacity:0, 1px) and styles the
   span, so without JS or a preview box this text change is the ONLY confirmation that a file was chosen —
   which matters most on the event form, where the photo is required. */
.file-label span.upv-chosen{border-style:solid;color:var(--ink);background:var(--card);
  max-width:min(100%,26rem);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

/* RSVP + invite picker (founder, 2026-10-25: "there should be a way to mark that you are going, and a list
   of people that are going"; "we need to be able to invite people when we create the event"). */
.rsvp-row{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:12px 0}
.rsvp-q{font-weight:650;font-size:.9rem}
/* The chosen answer is drawn as the pressed one — the state is on screen, not inferred. */
.btn.rsvp.on{background:var(--brand);border-color:var(--brand);color:var(--brand-ink)}
.rsvp-counts{display:flex;flex-wrap:wrap;gap:10px;margin:0 0 10px;font-size:.85rem;color:var(--dim)}
.rsvp-counts .c-yes{color:var(--ok,#2f7d4f);font-weight:650}
.rsvp-tag{font-size:.74rem;border:1px solid var(--line);border-radius:999px;padding:2px 8px;
  color:var(--dim);white-space:nowrap}
.rsvp-tag.r-yes{color:var(--ok,#2f7d4f);border-color:currentColor}
.rsvp-tag.r-no{color:var(--bad)}
.pick-list label.pick{display:flex;flex-direction:row;align-items:center;gap:9px;cursor:pointer;
  font-weight:400}
.pick-list input[type=checkbox]{width:auto;accent-color:var(--brand);margin:0}

/* EVENT CARD PHOTO (founder, 2026-10-25: "have a photo in there and make it look better"). Bounded banner,
   NOT the full-bleed cover of .profile-card — deliberately different, so the reasoning is written down.
   A profile is a person's space and their name sits ON the picture; an event page is mostly its guest list
   and posts, so a photo that text must be legible over would need plates for every line on the card. Here
   the image is a header and the words stay on the card surface: no contrast problem to solve at all.

   What IS copied from the profile card is the frame rule, because it fixed a reported bug ("the size of
   box changed after I uploaded the image"): aspect-ratio on the WRAPPER plus position:absolute on the
   image means the frame's height comes from its own ratio and never from the picture inside it. A 1920x1280
   photo and a 640x480 one produce exactly the same box, and a card with no photo simply has no header —
   nothing else moves. Without this the <img> renders at natural size: measured on event 13, a 1920px-wide
   image filled the whole viewport and pushed the title off screen. That is what the founder was looking at. */
.evt-head{position:relative;width:100%;aspect-ratio:16/9;border-radius:12px;overflow:hidden;
  margin-bottom:14px;background:var(--init-bg)}
.evt-cover{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block}
@media(max-width:699px){.evt-head{border-radius:10px;margin-bottom:12px}}

/* Unanswered is TEXT, not a pill with a dash in it. The dash read as a control and there is no route
   behind it — a mark shaped like a button that does nothing is worse than words that say what is true. */
.rsvp-none{font-size:.8rem;color:var(--dim)}
