/* splitNest webapp — design system and components.
   No build step: this file is served as written. Theming is custom
   properties; dark mode follows the system unless <html data-theme> pins it
   (assets/theme.js applies a stored choice before first paint). */

/* ---------- tokens ---------- */

:root {
  color-scheme: light dark;

  --bg: #f6f4ef;
  --surface: #ffffff;
  --surface-2: #f0ede5;
  --ink: #211f1a;
  --ink-2: #5b564c;
  --muted: #8a8478;
  --line: #e6e2d8;
  --line-strong: #d3cec1;

  --accent: #2e7d5b;
  --accent-strong: #266a4d;
  --accent-soft: #e2efe7;
  --accent-ink: #1d5940;

  --pos: #2e7d5b;
  --pos-soft: #e2efe7;
  --neg: #b3453a;
  --neg-soft: #f7e6e2;
  --warn: #a3730f;
  --warn-soft: #f5ecd3;
  --info-soft: #e7ecf4;

  /* Storage-section colours. See .quota for how these were chosen and
     why the blue is brighter than the rest of this palette. */
  --seg-0: #2e7d5b;
  --seg-1: #00b2ff;
  --seg-2: #e5982d;
  --seg-3: #703055;

  --shadow: 0 1px 2px rgb(35 31 24 / .05), 0 6px 24px rgb(35 31 24 / .06);
  --shadow-pop: 0 2px 6px rgb(35 31 24 / .08), 0 12px 40px rgb(35 31 24 / .14);

  --r-s: 8px;
  --r-m: 12px;
  --r-l: 16px;

  --sidebar-w: 264px;
  --font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
  --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #141419;
    --surface: #1d1c23;
    --surface-2: #27252e;
    --ink: #edeaf0;
    --ink-2: #b6b2bd;
    --muted: #8b8794;
    --line: #302e39;
    --line-strong: #3d3a48;

    --accent: #58b98a;
    --accent-strong: #6cc79a;
    --accent-soft: #22352c;
    --accent-ink: #8fd6b1;

    --pos: #58b98a;
    --pos-soft: #22352c;
    --neg: #e0705f;
    --neg-soft: #3a2723;
    --warn: #d9a33c;
    --warn-soft: #3a3120;
    --info-soft: #262b36;

    /* Storage-section colours; see .quota. */
    --seg-0: #58b98a;
    --seg-1: #0078ba;
    --seg-2: #ffc368;
    --seg-3: #ff8de3;

    --shadow: 0 1px 2px rgb(0 0 0 / .3), 0 6px 24px rgb(0 0 0 / .25);
    --shadow-pop: 0 2px 6px rgb(0 0 0 / .4), 0 16px 48px rgb(0 0 0 / .5);
  }
}

:root[data-theme="dark"] {
  --bg: #141419;
  --surface: #1d1c23;
  --surface-2: #27252e;
  --ink: #edeaf0;
  --ink-2: #b6b2bd;
  --muted: #8b8794;
  --line: #302e39;
  --line-strong: #3d3a48;

  --accent: #58b98a;
  --accent-strong: #6cc79a;
  --accent-soft: #22352c;
  --accent-ink: #8fd6b1;

  --pos: #58b98a;
  --pos-soft: #22352c;
  --neg: #e0705f;
  --neg-soft: #3a2723;
  --warn: #d9a33c;
  --warn-soft: #3a3120;
  --info-soft: #262b36;

  /* Storage-section colours; see .quota. */
  --seg-0: #58b98a;
  --seg-1: #0078ba;
  --seg-2: #ffc368;
  --seg-3: #ff8de3;

  --shadow: 0 1px 2px rgb(0 0 0 / .3), 0 6px 24px rgb(0 0 0 / .25);
  --shadow-pop: 0 2px 6px rgb(0 0 0 / .4), 0 16px 48px rgb(0 0 0 / .5);
}

/* ---------- base ---------- */

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

html { height: 100%; }

body {
  margin: 0;
  min-height: 100dvh;
  background: var(--bg);
  color: var(--ink);
  font: 15px/1.55 var(--font);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1, h2, h3, h4 { margin: 0; font-weight: 650; letter-spacing: -.015em; line-height: 1.25; }
h1 { font-size: 1.45rem; }
h2 { font-size: 1.1rem; }
h3 { font-size: .95rem; }
p { margin: 0; }
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
small { color: var(--muted); }
code { font-family: var(--mono); font-size: .85em; background: var(--surface-2); padding: .1em .35em; border-radius: 5px; }
hr { border: 0; border-top: 1px solid var(--line); margin: 1rem 0; }

::selection { background: color-mix(in srgb, var(--accent) 25%, transparent); }

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

button { font: inherit; color: inherit; }

.icon { flex: none; vertical-align: -3px; }
.spin { animation: spin 1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }

.muted { color: var(--muted); }
.dim { color: var(--ink-2); }
.small { font-size: .85rem; }
.tiny { font-size: .78rem; }
.strong { font-weight: 600; }
.center { text-align: center; }
.grow { flex: 1; min-width: 0; }
.nowrap { white-space: nowrap; }
.ellip { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; max-width: 100%; }
/* Inside a COLUMN flex container, min-width:0 does not stop a nowrap line
   from inflating the container's min-content width (cross-axis sizing), so a
   long unbroken string would force the whole page wide on a phone. width:0
   zeroes the intrinsic contribution; min-width:100% restores the real width
   once the column has one. */
.col > .ellip { width: 0; min-width: 100%; }
.mono { font-family: var(--mono); }

.row { display: flex; align-items: center; gap: .6rem; min-width: 0; }
.col { display: flex; flex-direction: column; gap: .35rem; min-width: 0; }
.wrap { flex-wrap: wrap; }
.spread { justify-content: space-between; }
.stack { display: flex; flex-direction: column; gap: 1rem; }
.stack-s { display: flex; flex-direction: column; gap: .5rem; }

/* ---------- money ---------- */

.money {
  font-variant-numeric: tabular-nums;
  font-weight: 620;
  white-space: nowrap;
}
.money.pos { color: var(--pos); }
.money.neg { color: var(--neg); }
.money.big { font-size: 1.35rem; letter-spacing: -.01em; }

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

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .45rem;
  padding: .52rem .95rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-s);
  background: var(--surface);
  color: var(--ink);
  font-weight: 560;
  font-size: .9rem;
  cursor: pointer;
  user-select: none;
  transition: background .12s, border-color .12s, opacity .12s;
  text-decoration: none;
}
.btn:hover { background: var(--surface-2); text-decoration: none; }
.btn:active { transform: translateY(.5px); }
.btn:disabled { opacity: .55; cursor: default; transform: none; }

.btn.primary {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}
.btn.primary:hover { background: var(--accent-strong); border-color: var(--accent-strong); }

.btn.danger { color: var(--neg); border-color: color-mix(in srgb, var(--neg) 35%, var(--line-strong)); }
.btn.danger:hover { background: var(--neg-soft); }
.btn.danger.solid { background: var(--neg); border-color: var(--neg); color: #fff; }
.btn.danger.solid:hover { background: color-mix(in srgb, var(--neg) 88%, black); }

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

.btn.small { padding: .3rem .6rem; font-size: .82rem; border-radius: 7px; }
.btn.big { padding: .7rem 1.2rem; font-size: 1rem; border-radius: var(--r-m); }
.btn.wide { width: 100%; }

.btn.iconbtn { padding: .45rem; border-radius: var(--r-s); }
.btn.iconbtn.small { padding: .3rem; }

/* ---------- inputs ---------- */

.field { display: flex; flex-direction: column; gap: .3rem; min-width: 0; }
.field > label,
/* A field whose control is a group of buttons cannot use <label for>, so its
   heading is a span named through aria-labelledby. It must still look like a
   label. */
.field > .fieldlabel { font-size: .82rem; font-weight: 600; color: var(--ink-2); }
.field .hint { font-size: .78rem; color: var(--muted); }
.field .err { font-size: .8rem; color: var(--neg); }

.input, select.input, textarea.input {
  width: 100%;
  padding: .52rem .7rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-s);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  transition: border-color .12s;
}
.input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent); }
.input::placeholder { color: var(--muted); }
.input.invalid { border-color: var(--neg); }
.input:disabled { opacity: .6; background: var(--surface-2); }
textarea.input { resize: vertical; min-height: 4.2rem; }
select.input { appearance: auto; }

.input-row { display: flex; gap: .5rem; }
.input-row > .input { min-width: 0; }

/* the big amount input on the expense screen */
.amount-input {
  display: flex;
  align-items: stretch;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-m);
  background: var(--surface);
  overflow: hidden;
}
.amount-input:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent); }
.amount-input input {
  flex: 1;
  min-width: 0;
  border: 0;
  background: transparent;
  color: var(--ink);
  font-size: 1.7rem;
  font-weight: 650;
  font-variant-numeric: tabular-nums;
  padding: .55rem .9rem;
  outline: none;
}
.amount-input select {
  border: 0;
  border-left: 1px solid var(--line);
  background: var(--surface-2);
  color: var(--ink-2);
  font: inherit;
  font-weight: 600;
  padding: 0 .7rem;
  outline: none;
  cursor: pointer;
}

/* toggle switch */
.switch { position: relative; display: inline-flex; width: 40px; height: 24px; flex: none; cursor: pointer; }
.switch input { position: absolute; opacity: 0; inset: 0; margin: 0; cursor: pointer; }
.switch .knob {
  position: absolute; inset: 0;
  border-radius: 999px;
  background: var(--line-strong);
  transition: background .15s;
}
.switch .knob::after {
  content: "";
  position: absolute; top: 3px; left: 3px;
  width: 18px; height: 18px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 1px 3px rgb(0 0 0 / .25);
  transition: transform .15s;
}
.switch input:checked + .knob { background: var(--accent); }
.switch input:checked + .knob::after { transform: translateX(16px); }
.switch input:focus-visible + .knob { outline: 2px solid var(--accent); outline-offset: 2px; }

/* segmented control */
.seg {
  display: flex;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--r-s);
  padding: 3px;
  gap: 2px;
}
.seg button {
  flex: 1;
  border: 0;
  background: transparent;
  padding: .38rem .5rem;
  border-radius: 6px;
  font-size: .85rem;
  font-weight: 560;
  color: var(--ink-2);
  cursor: pointer;
  white-space: nowrap;
}
.seg button.on {
  background: var(--surface);
  color: var(--ink);
  box-shadow: 0 1px 2px rgb(0 0 0 / .08);
}

/* checkbox row (participants) */
.checkline { display: flex; align-items: center; gap: .6rem; cursor: pointer; }
.checkline input[type="checkbox"] { width: 16px; height: 16px; accent-color: var(--accent); cursor: pointer; }

input[type="checkbox"] { accent-color: var(--accent); }
input[type="radio"] { accent-color: var(--accent); }

/* ---------- cards, lists ---------- */

.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-l);
  box-shadow: var(--shadow);
}
.card.pad { padding: 1.1rem 1.2rem; }
.card.flat { box-shadow: none; }

.card-title {
  font-size: .78rem;
  font-weight: 650;
  text-transform: uppercase;
  letter-spacing: .07em;
  color: var(--muted);
}

.list { display: flex; flex-direction: column; }
.list > * + * { border-top: 1px solid var(--line); }

.item {
  display: flex;
  align-items: center;
  gap: .75rem;
  padding: .7rem 1.1rem;
  min-width: 0;
  color: inherit;
  text-align: left;
  background: none;
  border-block: 0;
  border-inline: 0;
  width: 100%;
}
a.item:hover, button.item:hover { background: var(--surface-2); text-decoration: none; }
a.item, button.item { cursor: pointer; }

.badge {
  display: inline-flex;
  align-items: center;
  gap: .3rem;
  padding: .12rem .5rem;
  border-radius: 999px;
  font-size: .72rem;
  font-weight: 650;
  background: var(--surface-2);
  color: var(--ink-2);
  white-space: nowrap;
}
.badge.green { background: var(--pos-soft); color: var(--accent-ink); }
.badge.red { background: var(--neg-soft); color: var(--neg); }
.badge.amber { background: var(--warn-soft); color: var(--warn); }
.badge.line { background: transparent; border: 1px solid var(--line-strong); }

/* notices */
.notice {
  display: flex;
  gap: .6rem;
  padding: .75rem .9rem;
  border-radius: var(--r-m);
  font-size: .88rem;
  align-items: flex-start;
}
.notice .icon { margin-top: .1rem; }
.notice.info { background: var(--info-soft); color: var(--ink-2); }
.notice.warn { background: var(--warn-soft); color: var(--warn); }
.notice.error { background: var(--neg-soft); color: var(--neg); }
.notice.ok { background: var(--pos-soft); color: var(--accent-ink); }

/* ---------- avatars ---------- */

.avatar {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  font-weight: 650;
  color: #fff;
  background: var(--a-bg, var(--accent));
  user-select: none;
  letter-spacing: .02em;
}
.avatar-stack { display: flex; }
.avatar-stack .avatar { box-shadow: 0 0 0 2px var(--surface); }
.avatar-stack .avatar + .avatar { margin-left: -8px; }

.grouptile {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--r-m);
  background: var(--a-bg, var(--accent-soft));
  color: #fff;
}

.cat {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 38px; height: 38px;
  border-radius: var(--r-m);
  background: var(--accent-soft);
  color: var(--accent-ink);
}

/* ---------- shell / navigation ---------- */

.shell { display: grid; grid-template-columns: var(--sidebar-w) 1fr; min-height: 100dvh; }
/* Grid items default to min-width:auto and refuse to shrink below their
   content, which on a phone would widen the layout viewport instead of
   truncating a long line. */
.shell > * { min-width: 0; }

.sidebar {
  position: sticky;
  top: 0;
  height: 100dvh;
  display: flex;
  flex-direction: column;
  border-right: 1px solid var(--line);
  background: var(--surface);
  padding: 1rem .8rem;
  gap: .25rem;
  overflow-y: auto;
}

.brand {
  display: flex;
  align-items: center;
  gap: .55rem;
  padding: .35rem .55rem .9rem;
  font-size: 1.12rem;
  font-weight: 700;
  letter-spacing: -.02em;
  color: var(--ink);
}
.brand:hover { text-decoration: none; }
.brand .logo {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px; height: 30px;
  border-radius: 9px;
  background: var(--accent);
  color: #fff;
}

.navlink {
  display: flex;
  align-items: center;
  gap: .6rem;
  padding: .48rem .55rem;
  border-radius: var(--r-s);
  color: var(--ink-2);
  font-weight: 560;
  font-size: .92rem;
}
.navlink:hover { background: var(--surface-2); text-decoration: none; color: var(--ink); }
.navlink.on { background: var(--accent-soft); color: var(--accent-ink); }

.navhead {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 1rem .55rem .3rem;
  font-size: .72rem;
  font-weight: 650;
  text-transform: uppercase;
  letter-spacing: .07em;
  color: var(--muted);
}

.sidebar .foot { margin-top: auto; padding-top: .8rem; border-top: 1px solid var(--line); }

.main { min-width: 0; padding: 1.4rem clamp(1rem, 3.5vw, 2.5rem) 4.5rem; }
.page { max-width: 880px; margin: 0 auto; display: flex; flex-direction: column; gap: 1.1rem; }
.page.narrow { max-width: 620px; }

/* A group's banner: a wide image with the group's own header sitting on it.

   object-fit: cover with a fixed height rather than an aspect-ratio box,
   because the header inside it has to stay legible at every width - a box that
   kept the 4:1 ratio would be 90px tall on a phone, which is not enough room
   for a name, a chip and a button.

   The scrim is what makes white text safe on every design in the catalogue.
   The palettes are all dark enough at the bottom-left without it, but an
   UPLOADED banner is somebody's holiday photograph and could be anything at
   all, so the guarantee has to come from here rather than from the picture. */
.banner {
  position: relative;
  border-radius: var(--r-l);
  overflow: hidden;
  margin-bottom: 1rem;
  background: var(--b-tint, var(--accent-soft));
  min-height: 132px;
  display: flex;
  align-items: flex-end;
}
.banner > img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.banner-scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(to top,
    rgba(0, 0, 0, .62) 0%, rgba(0, 0, 0, .34) 42%, rgba(0, 0, 0, .06) 100%);
}
.banner-body { position: relative; width: 100%; padding: .9rem 1rem; }

/* Inside a banner the header is always on a dark ground, whatever the app's
   theme is, so its colours are pinned rather than inherited. Without this the
   light theme writes near-black text onto a dark photograph. */
.banner-body .pagehead h1,
.banner-body .pagehead .muted,
.banner-body .pagehead .tiny { color: #fff; }
.banner-body .pagehead h1 { text-shadow: 0 1px 6px rgba(0, 0, 0, .45); }
.banner-body .pagehead .muted,
.banner-body .pagehead .tiny { opacity: .92; text-shadow: 0 1px 4px rgba(0, 0, 0, .5); }
.banner-body .grouptile,
.banner-body .avatar { box-shadow: 0 0 0 2px rgba(255, 255, 255, .5); }
.banner-body .chip {
  background: rgba(255, 255, 255, .22);
  color: #fff;
  border-color: rgba(255, 255, 255, .35);
}

@media (max-width: 620px) {
  .banner { min-height: 116px; }
  .banner-body { padding: .75rem .8rem; }
}

/* The banner picker. Wide tiles, because a banner is a wide picture and a
   square swatch of one says nothing about how it will look behind a header.

   The icon picker reuses .iconpick below rather than defining a second grid:
   choosing a group's icon and choosing your own are the same interaction with
   a different catalogue behind them. */
.bannerpick {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: .45rem;
}
.bannerpick-item {
  position: relative;
  aspect-ratio: 3 / 1;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--r-m);
  background: var(--surface-2);
  cursor: pointer;
  overflow: hidden;
}
.bannerpick-item img { width: 100%; height: 100%; object-fit: cover; display: block; }
.bannerpick-item.on { box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px var(--accent); }
.bannerpick-item:disabled { opacity: .5; cursor: default; }

.pagehead { display: flex; align-items: center; gap: .8rem; min-height: 38px; }
.pagehead .actions { margin-left: auto; display: flex; gap: .5rem; align-items: center; }

.topbar, .bottomnav { display: none; }

.mobile-only { display: none; }

@media (max-width: 900px) {
  .shell { grid-template-columns: 1fr; }
  .sidebar { display: none; }

  .topbar {
    position: sticky;
    top: 0;
    z-index: 20;
    display: flex;
    align-items: center;
    gap: .6rem;
    padding: .6rem .9rem;
    padding-top: calc(.6rem + env(safe-area-inset-top));
    background: color-mix(in srgb, var(--bg) 88%, transparent);
    backdrop-filter: blur(10px);
    border-bottom: 1px solid var(--line);
  }
  .topbar .title { font-weight: 650; font-size: 1rem; }

  .main { padding: 1rem .9rem calc(5.2rem + env(safe-area-inset-bottom)); }

  .bottomnav {
    position: fixed;
    bottom: 0; left: 0; right: 0;
    z-index: 20;
    display: flex;
    justify-content: space-around;
    background: color-mix(in srgb, var(--surface) 92%, transparent);
    backdrop-filter: blur(10px);
    border-top: 1px solid var(--line);
    padding: .3rem .4rem calc(.4rem + env(safe-area-inset-bottom));
  }
  /* Five items have to fit a 320px phone, so they share the width evenly and
     the labels are allowed to shrink rather than wrap or push a tab off the
     edge. flex-basis:0 with grow makes every tab the same width whatever its
     label says, so "Recurring" does not steal room from "Home". */
  .bottomnav a {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: .1rem;
    padding: .25rem .3rem;
    border-radius: var(--r-s);
    color: var(--muted);
    font-size: .68rem;
    font-weight: 600;
    text-align: center;
  }
  .bottomnav a span,
  .bottomnav a > :not(svg) {
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  @media (max-width: 380px) {
    .bottomnav a { font-size: .62rem; }
  }
  .bottomnav a:hover { text-decoration: none; }
  .bottomnav a.on { color: var(--accent-ink); }

  /* Shown only at this width. For an action that belongs in a page's own
     header on a phone and is already reachable from the sidebar on a desktop,
     where repeating it would just be a second button doing the same thing. */
  .mobile-only { display: inline-flex; }
}

/* ---------- tabs ---------- */

.tabs {
  display: flex;
  gap: .15rem;
  border-bottom: 1px solid var(--line);
  overflow-x: auto;
  scrollbar-width: none;
}
.tabs::-webkit-scrollbar { display: none; }
.tabs button {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  border: 0;
  background: none;
  padding: .55rem .8rem;
  font-size: .9rem;
  font-weight: 580;
  color: var(--muted);
  cursor: pointer;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  white-space: nowrap;
}
.tabs button:hover { color: var(--ink); }
.tabs button.on { color: var(--accent-ink); border-bottom-color: var(--accent); }

/* ---------- dialogs ---------- */

dialog.modal {
  border: 1px solid var(--line);
  border-radius: var(--r-l);
  background: var(--surface);
  color: var(--ink);
  padding: 0;
  width: min(94vw, 460px);
  box-shadow: var(--shadow-pop);
}
dialog.modal.wide { width: min(94vw, 620px); }
dialog.modal::backdrop { background: rgb(15 14 12 / .45); backdrop-filter: blur(2px); }
.modal-head {
  display: flex;
  align-items: center;
  gap: .6rem;
  padding: 1rem 1.2rem .2rem;
}
.modal-head h2 { flex: 1; font-size: 1.05rem; }
.modal-body { padding: .8rem 1.2rem 1.2rem; display: flex; flex-direction: column; gap: .9rem; }
.modal-foot { display: flex; justify-content: flex-end; gap: .5rem; padding-top: .3rem; }

@media (max-width: 560px) {
  dialog.modal {
    width: 100vw;
    max-width: 100vw;
    margin: auto 0 0;
    border-radius: var(--r-l) var(--r-l) 0 0;
    border-bottom: 0;
  }
}

/* ---------- toasts ---------- */

/* The container is a manual popover so it reaches the TOP LAYER. A <dialog>
   opened with showModal() outranks every z-index in the document, so without
   this a toast raised over a modal renders under the dialog's backdrop -
   dimmed and unreadable, which is exactly the moment a message matters most.

   Everything below the position line undoes a UA popover default: the browser
   centres a popover with inset/margin, and gives it its own border, padding
   and background. `display` is deliberately NOT left to :popover-open, so a
   browser without popover support still shows toasts exactly as before. */
.toasts {
  position: fixed;
  z-index: 60;
  inset: auto;
  margin: 0;
  border: 0;
  padding: 0;
  overflow: visible;
  background: transparent;
  color: inherit;
  bottom: calc(1rem + env(safe-area-inset-bottom));
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  gap: .5rem;
  width: min(94vw, 380px);
  pointer-events: none;
}
.toast {
  pointer-events: auto;
  display: flex;
  align-items: flex-start;
  gap: .55rem;
  background: var(--ink);
  color: var(--bg);
  border-radius: var(--r-m);
  padding: .65rem .9rem;
  font-size: .88rem;
  box-shadow: var(--shadow-pop);
  animation: toast-in .18s ease-out;
}
.toast .icon { margin-top: .12rem; }
.toast.error .icon { color: #ff9d8f; }
.toast.ok .icon { color: #7ed6a9; }
@keyframes toast-in { from { opacity: 0; transform: translateY(8px); } }
@media (max-width: 900px) {
  .toasts { bottom: calc(5.4rem + env(safe-area-inset-bottom)); }
}

/* ---------- menus (small popovers) ---------- */

.menuwrap { position: relative; }
.menu {
  position: absolute;
  right: 0;
  top: calc(100% + 4px);
  z-index: 30;
  min-width: 180px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-m);
  box-shadow: var(--shadow-pop);
  padding: .3rem;
  display: flex;
  flex-direction: column;
}
.menu button {
  display: flex;
  align-items: center;
  gap: .55rem;
  border: 0;
  background: none;
  text-align: left;
  padding: .48rem .6rem;
  border-radius: var(--r-s);
  font-size: .88rem;
  cursor: pointer;
  color: var(--ink);
}
.menu button:hover { background: var(--surface-2); }
.menu button.danger { color: var(--neg); }
.menu .sep { border-top: 1px solid var(--line); margin: .25rem .3rem; }

/* ---------- auth screens ---------- */

.authwrap {
  min-height: 100dvh;
  display: grid;
  place-items: center;
  padding: 1.2rem;
  background:
    radial-gradient(60rem 30rem at 110% -10%, color-mix(in srgb, var(--accent) 9%, transparent), transparent 60%),
    radial-gradient(50rem 26rem at -20% 110%, color-mix(in srgb, var(--warn) 7%, transparent), transparent 60%),
    var(--bg);
}
.authcard {
  width: min(94vw, 400px);
  display: flex;
  flex-direction: column;
  gap: 1.1rem;
}
.authcard .card { padding: 1.5rem 1.4rem; }
.authbrand {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: .6rem;
  font-size: 1.35rem;
  font-weight: 700;
  letter-spacing: -.02em;
}
.authbrand .logo {
  display: inline-flex; align-items: center; justify-content: center;
  width: 38px; height: 38px; border-radius: 12px;
  background: var(--accent); color: #fff;
}
.authfoot { text-align: center; font-size: .88rem; color: var(--muted); }

/* ---------- dashboard ---------- */

.hero-balances { display: grid; grid-template-columns: 1fr 1fr; gap: .9rem; }
@media (max-width: 560px) { .hero-balances { grid-template-columns: 1fr; } }
.hero-balances .card { padding: .9rem 1.1rem; display: flex; flex-direction: column; gap: .15rem; }

.groupgrid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: .9rem;
}
.groupcard {
  display: flex;
  align-items: center;
  gap: .8rem;
  padding: .95rem 1rem;
  color: inherit;
}
.groupcard:hover { text-decoration: none; border-color: var(--line-strong); box-shadow: var(--shadow-pop); }

/* ---------- expense rows ---------- */

.exp-date {
  flex: none;
  width: 2.6rem;
  text-align: center;
  color: var(--muted);
  line-height: 1.15;
}
.exp-date .d { font-size: 1.05rem; font-weight: 650; color: var(--ink-2); }
.exp-date .m { font-size: .68rem; text-transform: uppercase; letter-spacing: .05em; }

.exp-side { text-align: right; flex: none; }
.exp-side .lbl { font-size: .7rem; color: var(--muted); }

/* ---------- balances ---------- */

.balrow { display: flex; align-items: center; gap: .75rem; padding: .55rem 1.1rem; }
.balbar {
  position: relative;
  flex: 1;
  height: 8px;
  border-radius: 999px;
  background: var(--surface-2);
  overflow: hidden;
  min-width: 60px;
}
.balbar .fill { position: absolute; top: 0; bottom: 0; border-radius: 999px; }
.balbar .fill.pos { background: var(--pos); }
.balbar .fill.neg { background: var(--neg); }

/* ---------- timeline (edit history) ---------- */

.timeline { display: flex; flex-direction: column; padding: .3rem 0; }
.tl-item { display: flex; gap: .8rem; padding: 0 1.1rem; }
.tl-rail { display: flex; flex-direction: column; align-items: center; flex: none; width: 26px; }
.tl-dot {
  flex: none;
  width: 26px; height: 26px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--surface-2);
  color: var(--ink-2);
  margin-top: .15rem;
}
.tl-item.created .tl-dot { background: var(--pos-soft); color: var(--accent-ink); }
.tl-item.deleted .tl-dot { background: var(--neg-soft); color: var(--neg); }
.tl-line { flex: 1; width: 2px; background: var(--line); min-height: 10px; }
.tl-body { flex: 1; min-width: 0; padding: .25rem 0 1rem; }
.tl-changes { display: flex; flex-wrap: wrap; gap: .35rem; margin-top: .35rem; }

/* ---------- category picker ---------- */

.catgrid { display: flex; flex-direction: column; gap: .9rem; max-height: 55vh; overflow-y: auto; padding-right: .2rem; }
.catgrid .grp { display: flex; flex-direction: column; gap: .4rem; }
.catgrid .cells { display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); gap: .4rem; }
.catcell {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .3rem;
  padding: .6rem .3rem .5rem;
  border: 1px solid var(--line);
  border-radius: var(--r-m);
  background: var(--surface);
  cursor: pointer;
  font-size: .74rem;
  color: var(--ink-2);
  text-align: center;
}
.catcell:hover { background: var(--surface-2); }
.catcell.on { border-color: var(--accent); background: var(--accent-soft); color: var(--accent-ink); }

/* ---------- attachments ---------- */

.attgrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); gap: .6rem; }
.att {
  position: relative;
  /* Without this the button sizes to its CONTENT, so a tile showing a
     thumbnail (which has intrinsic width) filled its cell while a tile showing
     only a format icon collapsed to the size of the glyph. Every tile is the
     same square now, whether or not the browser can preview what is in it. */
  width: 100%;
  border: 1px solid var(--line);
  border-radius: var(--r-m);
  overflow: hidden;
  aspect-ratio: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--surface-2);
  color: var(--muted);
  cursor: pointer;
  padding: 0;
}
.att img { width: 100%; height: 100%; object-fit: cover; }

/* The format chip. Sits over the thumbnail because the whole point is that it
   is readable when there IS a thumbnail; a PDF or an undecodable HEIC shows
   the large icon behind it instead. White on a saturated fill so it stays
   legible over a photograph of a receipt, which is usually pale. */
.att-badge {
  position: absolute;
  top: 4px;
  left: 4px;
  padding: 1px 5px;
  border-radius: 4px;
  font-size: .58rem;
  font-weight: 700;
  letter-spacing: .04em;
  color: #fff;
  /* A receipt photo can be any colour; the outline keeps the chip readable
     against a light one without needing a drop shadow. */
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .18);
  pointer-events: none;
}
.att .name {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  padding: .8rem .4rem .25rem;
  font-size: .68rem;
  color: #fff;
  background: linear-gradient(transparent, rgb(0 0 0 / .65));
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ---------- tables (admin) ---------- */

/* A table wider than its card scrolls sideways inside it. The gradients are
   a scroll shadow: they sit on the scroller itself (background-attachment
   local for the pair that scrolls away, scroll for the pair that stays), so a
   soft edge appears only on the side there is more table. Without it a phone
   shows a column of descriptions with the figures apparently missing, and
   nothing suggests they are one swipe away.

   --surface is the card behind it, so the "cover" gradients erase the shadow
   when the table is scrolled fully to that end, in either theme. */
.tablewrap {
  overflow-x: auto;
  background:
    linear-gradient(to right, var(--surface) 30%, transparent) left center,
    linear-gradient(to left, var(--surface) 30%, transparent) right center,
    radial-gradient(farthest-side at 0 50%, rgb(35 31 24 / .13), transparent) left center,
    radial-gradient(farthest-side at 100% 50%, rgb(35 31 24 / .13), transparent) right center;
  background-repeat: no-repeat;
  background-size: 28px 100%, 28px 100%, 14px 100%, 14px 100%;
  background-attachment: local, local, scroll, scroll;
}
table.table { width: 100%; border-collapse: collapse; font-size: .9rem; }
.table th {
  text-align: left;
  font-size: .72rem;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--muted);
  font-weight: 650;
  padding: .55rem 1.1rem;
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
}
.table td { padding: .6rem 1.1rem; border-bottom: 1px solid var(--line); vertical-align: middle; }
/* A column of money is a column because the decimal points line up, which
   only happens if it is right-aligned - header included, or the heading
   floats away from the figures it names. */
.table th.num, .table td.num { text-align: right; font-variant-numeric: tabular-nums; }

/* Report tables restack on a narrow screen.
   A report is read, not administered: the figures ARE the content, so leaving
   them one horizontal swipe off-screen behind a scroll shadow is not good
   enough here. Below the breakpoint each row becomes a block - the first cell
   as its heading, every other cell as a label/value pair taken from
   data-label - so nothing needs scrolling to be read.
   Scoped to .rtable so the admin tables, which are wide by nature and are
   worked with rather than read, keep their horizontal scroll. */
@media (max-width: 620px) {
  .rtable, .rtable tbody, .rtable tr, .rtable td { display: block; width: 100%; }
  .rtable thead { display: none; }
  .rtable tr {
    display: flex;
    flex-direction: column;
    padding: .55rem 0;
    border-bottom: 1px solid var(--line);
  }
  .rtable tr:last-child { border-bottom: 0; }
  .rtable td {
    display: flex;
    justify-content: space-between;
    gap: 1rem;
    padding: .12rem 0;
    border: 0;
    text-align: right;
  }
  .rtable td::before {
    content: attr(data-label);
    color: var(--muted);
    font-size: .78rem;
    text-align: left;
    white-space: nowrap;
  }
  /* One cell per row is marked as its subject and reads as a heading rather
     than as another label/value pair. It is marked explicitly rather than
     taken as the first cell, because the subject of an expense is its
     description while the first COLUMN is the date - and stacking a row under
     "Aug 19, 2026" buries the thing it is about. */
  .rtable td.subject {
    /* Hoisted to the top of the block whatever column it sits in, so a row
       leads with what it is about rather than with its date. */
    order: -1;
    font-weight: 620;
    padding-bottom: .2rem;
    text-align: left;
  }
  .rtable td.subject::before { content: none; }
  /* A cell with nothing in it would otherwise show a lone dangling label. */
  .rtable td:empty { display: none; }
}
.table tr:last-child td { border-bottom: 0; }

/* ---------- misc ---------- */

.empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .5rem;
  padding: 2.4rem 1rem;
  text-align: center;
  color: var(--muted);
}
.empty .icon { color: var(--line-strong); }
.empty h3 { color: var(--ink-2); }

.loading { display: flex; justify-content: center; padding: 2.2rem; color: var(--muted); }

.kv { display: grid; grid-template-columns: auto 1fr; gap: .3rem 1.2rem; font-size: .9rem; }
.kv dt { color: var(--muted); }
.kv dd { margin: 0; min-width: 0; }

.chips { display: flex; flex-wrap: wrap; gap: .4rem; }
.chip {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  padding: .28rem .6rem .28rem .3rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  font-size: .84rem;
}

/* A chip that is a control rather than a label: tappable, and showing when it
   is the one in force. */
.chip.pick {
  padding: .28rem .7rem;
  cursor: pointer;
  color: var(--ink-2);
  font-weight: 600;
}
.chip.pick:hover { border-color: var(--accent); }
.chip.pick.on {
  background: var(--accent-soft);
  border-color: var(--accent);
  color: var(--accent-ink);
}

.formgrid { display: grid; grid-template-columns: 1fr 1fr; gap: .9rem; }
@media (max-width: 560px) { .formgrid { grid-template-columns: 1fr; } }
.formgrid .full { grid-column: 1 / -1; }

.divider-label {
  display: flex;
  align-items: center;
  gap: .7rem;
  color: var(--muted);
  font-size: .78rem;
  font-weight: 650;
  text-transform: uppercase;
  letter-spacing: .06em;
}
.divider-label::after { content: ""; flex: 1; border-top: 1px solid var(--line); }

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

/* ---------- account: icon picker, quota, recovery codes, QR ---------- */

.iconpick {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(44px, 1fr));
  gap: .4rem;
}
.iconpick-item {
  aspect-ratio: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--line);
  border-radius: var(--r-m);
  background: var(--a-bg, var(--surface-2));
  color: #fff;
  cursor: pointer;
}
.iconpick-item.on {
  /* An outline rather than a colour change: the tile's own colour is the
     thing being chosen, so recolouring it to show selection would hide what
     is being selected. */
  box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px var(--accent);
}
.iconpick-item:disabled { opacity: .5; cursor: default; }

/* The storage bar and its key.
 *
 * SEG_COLOURS below are not decoration, and they were measured rather than
 * chosen by eye. Each is checked with CIEDE2000 against the others under
 * normal vision AND under simulated deuteranopia, protanopia and tritanopia,
 * because a bar whose segments are only distinguishable to some readers is
 * only doing half its job. The two sections that exist today score above 40
 * in normal vision and above 37 under both red-green deficiencies; the pair
 * they replaced scored 9.5, which is why they looked identical.
 *
 * Two things about them will look wrong and are not:
 *
 *  - --seg-1 is BRIGHTER than the rest of this deliberately calm palette.
 *    That is load-bearing. Under tritanopia the blue-yellow axis collapses
 *    and a blue is told from a green only by lightness, so the calmer blues
 *    that were tried scored 6.7 and 7.5 - effectively identical - while this
 *    one scores 24. Muting it undoes the accessibility.
 *  - There are FOUR, not six. Six categorical colours that survive all three
 *    deficiencies do not exist in this gamut: every attempt collapsed some
 *    pair to single digits, one to 1.6. Four is the honest ceiling. A fifth
 *    kind of stored data will reuse a colour, and the key beneath the bar -
 *    which names every section in words - is what keeps that unambiguous.
 *    Adding a fifth colour means redoing the measurement, not guessing one.
 */
.quota {
  display: flex;
  height: 8px;
  border-radius: 999px;
  background: var(--surface-2);
  overflow: hidden;
}
/* One segment per kind of stored data. The bar is a flex row rather than one
   filled div so a future section is another segment, not another rule. */
.quota-bar { height: 100%; background: var(--seg-0); }
.quota-bar + .quota-bar { border-left: 1px solid var(--surface); }
.quota-bar[data-slot="1"] { background: var(--seg-1); }
.quota-bar[data-slot="2"] { background: var(--seg-2); }
.quota-bar[data-slot="3"] { background: var(--seg-3); }

/* Over quota is a state somebody has to act on, so it stops being decorative.
   The RING carries it, not the segments: recolouring them all red would leave
   the key underneath naming sections in colours the bar no longer uses, and
   the whole point of the key is that it matches. The percentage beside the
   bar already turns red too, so the state is on two channels, neither of
   which is the segment colours. */
.quota.over {
  outline: 2px solid var(--neg);
  outline-offset: 2px;
}

/* The key under the bar, so a segment can be told from its neighbour. */
.quota-key { display: flex; flex-wrap: wrap; gap: .25rem .9rem; margin: 0; padding: 0; }
.quota-key li {
  display: flex;
  align-items: center;
  gap: .35rem;
  list-style: none;
  font-size: .8rem;
  color: var(--muted);
}
.quota-key .dot {
  width: 8px; height: 8px; border-radius: 999px;
  background: var(--seg-0);
  flex: none;
}
.quota-key .dot[data-slot="1"] { background: var(--seg-1); }
.quota-key .dot[data-slot="2"] { background: var(--seg-2); }
.quota-key .dot[data-slot="3"] { background: var(--seg-3); }

.codes {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: .35rem;
}
.codes code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .82rem;
  padding: .3rem .45rem;
  background: var(--surface-2);
  border-radius: var(--r-s);
  text-align: center;
}
.input.mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: .04em; }

/* White behind the QR regardless of theme: a dark background inverts the
   contrast the code depends on and phone cameras stop reading it. */
.qr {
  align-self: center;
  background: #fff;
  padding: 8px;
  border-radius: var(--r-m);
}

.pill {
  font-size: .7rem;
  font-weight: 700;
  padding: .12rem .45rem;
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--muted);
}
.pill.ok { background: color-mix(in srgb, var(--accent) 18%, transparent); color: var(--accent); }

/* The instance's tagline under its name on the sign-in page. Quiet, because
   it is orientation rather than content. */
.authtag {
  text-align: center;
  color: var(--muted);
  font-size: .85rem;
  margin: -.4rem 0 .9rem;
}

/* A file strip whose previews are shown WHOLE.
 *
 * The receipt strip on an expense downloads the full image and lets the tile
 * crop it, which is fine for a photograph. A task file's preview is generated
 * by the server, fitted inside a 320px box precisely so nothing is cut off -
 * a quote or an invoice is a tall column of text, and the middle of the page
 * identifies nothing. Cropping it in CSS would throw that away at the one
 * place it matters, which is picking the right file out of a row of them. */
.attgrid.whole .att img { object-fit: contain; }

/* ---------- task board ---------- */

.taskrow {
  display: flex;
  align-items: flex-start;
  gap: .6rem;
  padding: .7rem .9rem;
  border-bottom: 1px solid var(--line);
}
.taskrow:last-child { border-bottom: 0; }

/* The tick box. A real button rather than a checkbox: it has three states, not
   two - open, done and cancelled - and a checkbox cannot show the third. */
.taskmark {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  margin-top: .1rem;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  background: var(--surface);
  color: var(--muted);
  cursor: pointer;
}
.taskmark:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.taskmark:disabled { cursor: default; opacity: .7; }
.taskmark.done {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}
.taskmark.cancelled {
  background: var(--surface-2);
  border-color: var(--line-strong);
  color: var(--muted);
}

.taskbody {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: .25rem;
  border: 0;
  background: none;
  padding: 0;
  text-align: left;
  color: inherit;
  cursor: pointer;
  font: inherit;
}
.tasktitle { font-weight: 580; }
/* Struck through only once the task is CLOSED, and in both closed states: a
   cancelled task is as finished as a completed one, and leaving it looking
   live is what makes a board people stop trusting. */
.taskrow.done .tasktitle,
.taskrow.cancelled .tasktitle { color: var(--muted); text-decoration: line-through; }

.taskmeta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .45rem;
  font-size: .75rem;
  color: var(--muted);
}
.taskmeta .due {
  display: inline-flex;
  align-items: center;
  gap: .25rem;
  white-space: nowrap;
}
/* Overdue is the one thing on this row worth interrupting for, so it is the
   only thing that gets a colour of its own. "Today" is amber rather than red
   because it is not late yet. */
.taskmeta .due.overdue { color: var(--neg); font-weight: 600; }
.taskmeta .due.today { color: var(--warn); font-weight: 600; }
.taskclosed { display: block; }

.tasknotes {
  margin: 0;
  /* The text was typed into a textarea, so its line breaks are meaningful.
     Rendering it as flowed HTML would run a numbered list into one paragraph. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  color: var(--ink-2);
  font-size: .92rem;
}

/* Priority. Three steps, and only the top one is loud: a board where every
   chip shouts is a board where none of them do. */
.chip.prio {
  padding: .1rem .5rem;
  gap: .25rem;
  font-size: .7rem;
  font-weight: 650;
  letter-spacing: .01em;
}
.chip.prio.low { background: var(--surface-2); color: var(--muted); border-color: var(--line); }
.chip.prio.medium { background: var(--info-soft); color: var(--ink-2); border-color: var(--line); }
.chip.prio.high { background: var(--neg-soft); color: var(--neg); border-color: var(--neg-soft); }

/* ---------- message board ---------- */

.noteboard {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: .6rem;
  align-items: start;
}
.note {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-m);
  padding: .7rem .8rem .8rem;
  box-shadow: var(--shadow);
}
.notebody {
  margin: .55rem 0 0;
  /* Same reasoning as .tasknotes: what somebody typed keeps its line breaks,
     and a long unbroken string - a URL, usually - wraps rather than widening
     the whole column. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  font-size: .92rem;
  color: var(--ink);
}

/* ---------- lightbox ---------- */

/* A plain overlay rather than a <dialog>, because it opens from a full page
   with no modal underneath to stack against. z-index 50 is below .toasts, so
   an error raised while it is open is still readable over it - the same
   ordering the dialogs use. */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 2rem 1rem 3rem;
  background: rgb(15 14 12 / .82);
  backdrop-filter: blur(3px);
}
.lightbox img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  border-radius: var(--r-m);
  box-shadow: var(--shadow-pop);
  cursor: default;
}
.lightbox-x {
  position: absolute;
  top: calc(.75rem + env(safe-area-inset-top));
  right: .75rem;
  color: #fff;
}
.lightbox-x:hover { background: rgb(255 255 255 / .14); }
.lightbox-name {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  bottom: calc(1rem + env(safe-area-inset-bottom));
  max-width: 80vw;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: #fff;
  font-size: .8rem;
  opacity: .85;
}
