/* The app shell: source-list sidebar (pointer) / tab bar (phone), the conversation list, toolbars and page
   scaffolding, login, 我, and the overlays this area owns (alert, toasts, share sheet, connection strip).

   Rows here are navigation, so they share one set of states:
     hover     only where there is a pointer that hovers (never sticks after a tap)
     pressed   at once (0s), released over 180ms
     selected  --selection, a clear step darker than hover
     focus     an inset 2px tint ring (rows live in clipped scrollers) */

.app { position: relative; display: grid; grid-template-columns: var(--side-w) minmax(0, 1fr); height: 100%; background: var(--bg); }
.app.app-in { animation: fade var(--t-base) ease; }   /* after a sign-in only, never on first paint */
.main { position: relative; display: flex; flex-direction: column; min-width: 0; min-height: 0; overflow: hidden; background: var(--bg); }
/* one screen; a second one overlaps it only for the length of a transition */
.pane { position: absolute; inset: 0; display: flex; flex-direction: column; min-width: 0; min-height: 0; background: var(--bg); }
.tabbar { display: none; }

/* between screens with a pointer: the new one dissolves in over the old, which stays put until it is covered
   (two panes fading through each other would show the background for a moment) */
.fade-enter-active { z-index: 1; transition: opacity var(--t-out) ease; }
.fade-enter-from { opacity: 0; }
.fade-leave-active { transition: opacity var(--t-out); }   /* no change of its own: it only keeps the old pane that long */

/* a change of appearance is not animated (main.js sets the attribute for the frame in which the theme changes) */
:root[data-theme-switch] *:not(.seg-thumb), :root[data-theme-switch] *::before, :root[data-theme-switch] *::after { transition: none !important; }

/* the spinner shown while the session loads only appears if that takes long enough to notice */
.boot .spinner { animation: spin .7s linear infinite, fade var(--t) ease 300ms backwards; }

/* rows are not links to the system: no callout, no text selection, no drag ghost */
.tab, .conv, .nav-item, .nav-row, .push-bar a, .sb-me, .sr, .cl-go {
  -webkit-touch-callout: none; -webkit-user-select: none; user-select: none; -webkit-user-drag: none;
}

/* ------------------------------------------------------------------ sidebar: an opaque source list
   One leading edge at 18px: brand mark, search glyph, dashboard icons, section heading, avatars, account. */
.sidebar {
  display: flex; flex-direction: column; min-width: 0; min-height: 0; position: relative; z-index: 2; background: var(--bg-side);
  --row-hover: var(--fill-3); --row-press: var(--fill);   /* on the grey list the white-pane row colours would vanish */
}
/* the edge against the content pane is drawn over the list, so nothing inside it (a sticky heading that runs the
   full width, a selected row) can interrupt the line */
.sidebar::after { content: ""; position: absolute; top: 0; right: 0; bottom: 0; z-index: 3; width: var(--hair); background: var(--hairline); pointer-events: none; }
.sb-brand { display: flex; align-items: center; gap: 8px; flex: none; height: var(--bar-h); padding: 0 10px 0 18px; }
.sb-brand img { width: 22px; height: 22px; border-radius: calc(var(--radius-sm) - 2px); flex: none; }
.sb-brand b { flex: 1; min-width: 0; font-size: var(--fs-md); font-weight: 600; letter-spacing: var(--track-title); }
.new-menu { position: relative; flex: none; }
.new-menu .menu { right: 0; top: calc(100% + 6px); transform-origin: calc(100% - 14px) top; }   /* grows out of its button */
.sb-sec { display: flex; align-items: baseline; padding: 16px 8px 5px; font-size: var(--fs-xs); font-weight: 600; color: var(--text-3); letter-spacing: .01em; }
.sb-sec span { margin-left: auto; font-weight: 500; font-variant-numeric: tabular-nums; }

/* dashboards as source-list rows: accent icon, label, live status on the trailing side */
.nav { display: flex; flex-direction: column; gap: 1px; }
.nav-item {
  display: flex; align-items: center; gap: 8px; min-height: 30px; padding: 0 8px; border-radius: var(--radius-sm);
  color: var(--text); font-size: var(--fs-md); transition: background-color var(--t);
}
.nav-item:hover { text-decoration: none; }
.nav-item .icon { color: var(--link); flex: none; transition: color 150ms; }
.nav-item:active { background: var(--row-press); transition-duration: 0s; }
.nav-item.active { background: var(--selection); }
.nav-item:focus-visible, .sb-me:focus-visible, .conv:focus-visible, .sr:focus-visible, .cl-go:focus-visible { box-shadow: inset 0 0 0 2px var(--tint); }
.nav-t { flex: 1; line-height: 1.3; }
/* The status column is calm and written one way: a P&L figure in the colour of its sign; every other state — a
   breach, a count of warnings, instructions waiting — as a small dot in the colour of the state and the words
   in ink (the way 今日 writes 「● 需处理」). Never a coloured word beside a dotted one, no capsules. */
.nav-s { display: inline-flex; align-items: center; gap: 5px; flex: none; font-size: var(--fs-sm); color: var(--text-3); font-variant-numeric: tabular-nums; }
.nav-s.pos { color: var(--pos); font-weight: 600; }
.nav-s.neg { color: var(--neg); font-weight: 600; }
.nav-s.flat, .nav-s.stale { color: var(--text-2); font-weight: 600; }   /* a P&L figure with no sign, or too old to colour */
.nav-s.level { color: var(--text); font-weight: 600; }                  /* a level (net / gross): ink, whatever its sign */
.nav-s.danger, .nav-s.warn, .nav-s.wait { color: var(--text-2); }
.nav-s .dot { width: 6px; height: 6px; }

/* the account row at the foot of the sidebar; its second line is also where connection trouble is first mentioned */
.sb-foot { flex: none; padding: 6px 10px 8px; }
.sb-me { display: flex; align-items: center; gap: 9px; width: 100%; padding: 5px 8px; border-radius: var(--radius-sm); color: var(--text); transition: background-color var(--t); }
.sb-me:hover { text-decoration: none; }
.sb-me:active { background: var(--row-press); transition-duration: 0s; }
.sb-me.active { background: var(--selection); }
.sb-me-t { display: flex; flex-direction: column; flex: 1; min-width: 0; gap: 1px; line-height: 1.2; }
.sb-me-t b { font-size: var(--fs-md); font-weight: 500; }
.sb-me-t span { font-size: var(--fs-xs); color: var(--text-3); }
.sb-me > .icon { color: var(--text-4); flex: none; }
@media (hover: hover) {
  .nav-item:not(.active):hover, .sb-me:not(.active):hover { background: var(--row-hover); transition-duration: var(--t-fast); }
  .nav-item:not(.active):active, .sb-me:not(.active):active { background: var(--row-press); transition-duration: 0s; }
}
/* a window in the background lets go of its accent colour (main.js sets data-inactive) */
:root[data-inactive] .nav-item .icon, :root[data-inactive] .dash-h .icon-btn { color: var(--text-3); }
:root[data-inactive] .sb-brand .icon-btn { color: var(--text-3); }

/* ------------------------------------------------------------------ conversation list
   search field · (sidebar) the dashboards, which never scroll away · the scroller with the conversations */
.cl { display: flex; flex-direction: column; flex: 1; min-height: 0; }
.cl-search { display: flex; align-items: center; flex: none; padding: 0 10px 8px; }
.search {
  display: flex; align-items: center; gap: 6px; flex: 1; min-width: 0; height: var(--ctl-h); padding: 0 8px; border-radius: var(--radius-ctl);
  background: var(--fill-2); color: var(--text-2); cursor: text; transition: box-shadow var(--t-fast), background-color var(--t-fast);
}
.search:focus-within { background: var(--bg-elev); box-shadow: var(--ring); }
.search input { flex: 1; min-width: 0; height: 100%; border: 0; background: none; outline: none; font-size: var(--fs-md); color: var(--text); padding: 0; }
.search input:focus-visible { box-shadow: none; border-radius: 0; }   /* the ring belongs to the capsule, not to the bare input inside it */
.search input::placeholder { color: var(--text-3); }
.search-x { display: flex; align-items: center; justify-content: center; width: 16px; height: 16px; border-radius: 50%; background: var(--text-3); color: var(--bg-side); flex: none; position: relative; }
.search-x::after { content: ""; position: absolute; inset: -6px; }
.search-x { transition: background-color var(--t-fast), opacity var(--t); }
.search-x:active { opacity: .6; transition-duration: 0s; }
@media (hover: hover) { .search-x:hover { background: var(--text-2); } }
.search-cancel { display: none; }
.cl-nav { flex: none; max-height: 52%; overflow-y: auto; padding: 0 10px; overscroll-behavior: contain; }
/* In a short window the dashboards scroll: where there is more of them, above or below, the list fades out
   instead of ending in the middle of a row (v-scrolled sets the classes; list.js keeps them true to the
   window's size). */
.cl-nav { --fade-top: #000; --fade-end: #000; }
.cl-nav.is-scrolled { --fade-top: transparent; }
.cl-nav:not(.is-end) { --fade-end: transparent; }
.cl-nav.is-scrolled, .cl-nav:not(.is-end) {
  -webkit-mask-image: linear-gradient(var(--fade-top), #000 24px, #000 calc(100% - 24px), var(--fade-end));
  mask-image: linear-gradient(var(--fade-top), #000 24px, #000 calc(100% - 24px), var(--fade-end));
}
.cl-body { position: relative; display: flex; flex-direction: column; flex: 1; min-height: 0; }
.cl-scroll { flex: 1; min-height: 0; overflow-y: auto; padding: 0 10px 12px; overscroll-behavior: contain; }
/* the scroller's edges are drawn only while something is scrolled under them (v-scrolled sets the classes) */
.cl-edge { position: absolute; left: 0; right: 0; height: var(--hair); background: var(--hairline); opacity: 0; pointer-events: none; transition: opacity var(--t); z-index: 2; }
.cl-edge.top { top: 0; } .cl-edge.end { bottom: 0; }
.cl-scroll.is-scrolled ~ .cl-edge.top, .cl-scroll:not(.is-end) ~ .cl-edge.end { opacity: 1; }
/* a heading stays at the top of the scroller while its rows pass beneath (full width, so rows never peek out beside it) */
.cl-scroll > .sb-sec { position: sticky; top: 0; z-index: 1; margin: 0 -10px; padding: 14px 18px 6px; background: var(--bg-side); }
.cl-hint { padding: 14px 12px; text-align: center; font-size: var(--fs-sm); color: var(--text-3); }

/* "search the messages for …": the row Return presses */
.cl-go {
  display: flex; align-items: center; gap: 8px; width: 100%; min-height: 34px; padding: 0 8px; border-radius: var(--radius-sm);
  text-align: left; font-size: var(--fs-md); color: var(--text); transition: background-color var(--t);
}
.cl-go:active { background: var(--row-press, var(--bg-active)); transition-duration: 0s; }
.cl-go-i { display: flex; align-items: center; justify-content: center; flex: none; width: 22px; height: 22px; border-radius: 50%; background: var(--fill-2); color: var(--text-2); }
.cl-go kbd { flex: none; min-width: 20px; height: 18px; padding: 0 5px; border-radius: var(--radius-xs); box-shadow: inset 0 0 0 var(--hair) var(--hairline-strong); font: 500 var(--fs-xs)/18px var(--font); color: var(--text-3); text-align: center; }
.cl-go .chev { color: var(--text-4); flex: none; }
.cl-go .spinner { width: 14px; height: 14px; border-width: 1.5px; }
.cl-go:disabled { color: var(--text-2); }

/* A conversation row. Its state is in the type — unread: name 600, preview in full ink, time in the link colour —
   and its height is fixed, so no state can change it. */
.conv {
  display: flex; align-items: center; gap: 10px; height: 56px; padding: 0 8px; border-radius: var(--radius-sm);
  color: var(--text); position: relative; transition: background-color var(--t);
}
.conv:hover { text-decoration: none; }
.conv:active { background: var(--row-press, var(--bg-active)); transition-duration: 0s; }
.conv.active { background: var(--selection); }
/* the online dot is cut out of the avatar in the colour of the row it sits on — also when that row is selected
   (with a sidebar only: a phone's list has no selected row) or hovered (the veil over the list's own colour) */
.conv .avatar { --dot-ring: var(--bg-side); }
@media (min-width: 768px) { .conv.active .avatar { --dot-ring: var(--selection); } }
@media (hover: hover) { .conv:not(.active):hover .online-dot { box-shadow: 0 0 0 2px var(--row-hover, var(--bg-hover)), 0 0 0 2px var(--dot-ring); } }
.conv-m { display: flex; flex-direction: column; flex: 1; min-width: 0; gap: 2px; }
.conv-1, .conv-2 { display: flex; align-items: center; gap: 5px; min-width: 0; }
.conv-1 b { flex: 0 1 auto; font-size: var(--fs-md); font-weight: 500; line-height: var(--lh-md); }
.conv-1 time { margin-left: auto; padding-left: 6px; flex: none; font-size: var(--fs-xs); color: var(--text-3); font-variant-numeric: tabular-nums; }
.conv-flag { color: var(--text-3); flex: none; }
.conv-2 { min-height: 18px; }
.conv-p { flex: 1; font-size: var(--fs-sm); line-height: var(--lh-sm); color: var(--text-3); }
.conv-p em { font-style: normal; color: var(--link); font-weight: 600; }   /* 「@我」: the count beside it keeps the red */
.conv.unread .conv-1 b { font-weight: 600; }
.conv.unread .conv-1 time { color: var(--link); font-weight: 600; }
.conv.unread .conv-p { color: var(--text); }

/* message search results: who said it, where and when on the trailing side, two lines of the message */
.sr { display: flex; align-items: flex-start; gap: 8px; width: 100%; padding: 7px 8px; border-radius: var(--radius-sm); text-align: left; transition: background-color var(--t); }
.sr:active { background: var(--row-press, var(--bg-active)); transition-duration: 0s; }
.sr .avatar { margin-top: 1px; }
.sr-m { display: flex; flex-direction: column; flex: 1; min-width: 0; gap: 1px; }
.sr-h { display: flex; align-items: baseline; gap: 6px; min-width: 0; }
.sr-h b { flex: 0 1 auto; font-size: var(--fs-md); font-weight: 600; line-height: var(--lh-md); }
.sr-g { flex: 0 1 auto; margin-left: auto; padding-left: 4px; max-width: 55%; color: var(--text-3); font-size: var(--fs-xs); text-align: right; }
.sr-h time { flex: none; color: var(--text-3); font-size: var(--fs-xs); font-variant-numeric: tabular-nums; }
.sr-t { font-size: var(--fs-sm); line-height: var(--lh-sm); color: var(--text-2); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; overflow-wrap: anywhere; }
.sr-t mark { background: color-mix(in srgb, var(--warn-fill) 20%, transparent); color: var(--text); font-weight: 600; border-radius: var(--radius-xs); padding: 0 1px; }

@media (hover: hover) {
  /* neighbouring rows keep a sliver of air, so a hovered row next to the selected one reads as two rows */
  .cl-scroll .conv + .conv, .cl-scroll .sr + .sr { margin-top: 2px; }
  .conv:not(.active):hover, .sr:hover, .cl-go:not(:disabled):hover { background: var(--row-hover, var(--bg-hover)); transition-duration: var(--t-fast); }
  .conv:not(.active):active, .sr:active, .cl-go:not(:disabled):active { background: var(--row-press, var(--bg-active)); transition-duration: 0s; }
}

/* ------------------------------------------------------------------ pages (everything that is not a conversation)
   A page starts with a toolbar: title and one line of status on the leading side, actions on the trailing side.
   It is a translucent bar the content scrolls under. */
.page { flex: 1; min-height: 0; overflow-y: auto; padding: 0 var(--page-pad) 48px; overscroll-behavior: contain; }
/* (dialogs opened from a page are fixed overlays: they must not be narrowed with the content) */
.page > :not(.modal-mask) { max-width: var(--page-max); margin-left: auto; margin-right: auto; }
.page.page-narrow > :not(.modal-mask) { max-width: 640px; }
/* The bar itself runs edge to edge; its contents share the page's column, so a title sits exactly above the
   cards' leading edge and the last control above their trailing edge at every window width. Its hairline is
   drawn only once content has scrolled beneath (.is-scrolled, set by the shell). */
.page > .dash-h {
  position: sticky; top: 0; z-index: 10; display: flex; align-items: center; gap: 6px 12px; flex-wrap: wrap;
  max-width: none; min-height: var(--bar-h); margin: 0 calc(-1 * var(--page-pad)) 18px;
  padding: 8px calc(var(--page-pad) + max(0px, (100% - var(--page-max)) / 2));
  background: var(--material-bar); -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur);
  box-shadow: inset 0 calc(-1 * var(--hair)) 0 transparent; transition: box-shadow var(--t);
}
/* (a reading page narrows its column, not its bar: the title starts where it starts on every other page, so it
   does not jump when one page replaces another) */
.page.page-narrow > .dash-h { max-width: none; }
.page.is-scrolled > .dash-h { box-shadow: inset 0 calc(-1 * var(--hair)) 0 var(--hairline); }
.dash-h-t { flex: 1; min-width: 200px; display: flex; flex-direction: column; justify-content: center; gap: 1px; }
.dash-h h1 { font: 600 var(--fs-headline)/1.25 var(--font); letter-spacing: var(--track-title); }
.dash-h-sub { display: flex; gap: 0 10px; flex-wrap: wrap; min-width: 0; color: var(--text-2); font-size: var(--fs-sm); line-height: var(--lh-sm); }
.dash-h-sub > span { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }   /* one line of status, never two */
.page-narrow .dash-h-sub > span { white-space: normal; }   /* (a reading page's lead-in is a sentence, not a status) */
/* controls in a toolbar share one height, and their labels one size */
.dash-h-a { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
.dash-h-a .btn, .dash-h-a .select, .dash-h-a .seg { height: var(--ctl-h); }
.dash-h-a .btn { font-size: var(--fs-md); padding: 0 10px; border-radius: var(--radius-ctl); }
.dash-h-a .select { width: auto; min-width: 150px; max-width: 420px; font-size: var(--fs-md); }
.dash-h-a .seg { flex-wrap: nowrap; }
/* a page that is only a message: centred in the pane */
.page.page-center { display: flex; align-items: center; justify-content: center; }
.empty-acts { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; margin-top: 10px; }
.empty-acts .btn { margin-top: 0; }

/* the top bar of a phone screen (back button, inline title); see the phone block */
.push-bar { display: none; }

/* connection trouble, once it has lasted: a thin strip laid over the pane (nothing moves), with a way to retry */
.conn-strip {
  position: absolute; top: var(--bar-h); left: 0; right: 0; z-index: 20; display: flex; align-items: center; justify-content: center; gap: 10px;
  height: 24px; background: linear-gradient(var(--warn-soft), var(--warn-soft)), var(--bg-elev); color: var(--warn);
  font-size: var(--fs-xs); font-weight: 500; box-shadow: 0 var(--hair) 0 var(--hairline);
}
.conn-strip.ok { background: linear-gradient(var(--ok-soft), var(--ok-soft)), var(--bg-elev); color: var(--ok); }
.conn-strip button { position: relative; color: inherit; font-weight: 600; text-decoration: underline; text-underline-offset: 2px; }
.conn-strip button::after { content: ""; position: absolute; inset: -10px -12px; }   /* a thin strip, a full-size target */
.conn-strip button:active { opacity: .6; }
.strip-enter-active { transition: opacity var(--t) ease; } .strip-leave-active { transition: opacity var(--t-out) ease; }
.strip-enter-from, .strip-leave-to { opacity: 0; }
/* (never over a conversation, which has its own banner: main.js does not render the strip there)
   Under a toolbar the strip is exactly as tall as the gap between the bar and the first card, so it covers no
   card. A pane without a toolbar (我, an empty or a not-found pane) has it on its top edge instead: 52px down it
   would run through whatever is there — on 我, the avatar, which starts at 36px. While two panes cross-fade, the
   one that is arriving decides (the leaving one is :not()-ed out), so the strip is in place before the fade ends. */
@media (min-width: 768px) {
  .conn-strip { height: 18px; }
  .main:not(:has(> .pane:not(.fade-leave-active) .dash-h)) .conn-strip { top: 0; }
}

/* embedded intranet pages: the usual toolbar, then the frame; what is said while it loads lies over the frame */
.embed { flex: 1; min-height: 0; display: flex; flex-direction: column; background: var(--bg-content); }
.embed > .dash-h {
  display: flex; align-items: center; gap: 6px 12px; flex: none; min-height: var(--bar-h); padding: 8px var(--page-pad);
  box-shadow: inset 0 calc(-1 * var(--hair)) 0 var(--hairline);
}
.embed-body { position: relative; display: flex; flex: 1; min-height: 0; }
/* the embedded sites are light pages: keep the frame's own canvas light so a dark app does not show through them */
.embed iframe { flex: 1; width: 100%; min-width: 0; border: 0; background: var(--bg-content); color-scheme: light; }
.embed-state { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; gap: 10px; background: var(--bg-content); color: var(--text-3); font-size: var(--fs-md); }
.embed-state.wait { animation: fade var(--t) 150ms both; }   /* a page that arrives at once never shows a spinner */

/* ------------------------------------------------------------------ login */
.login { height: 100%; overflow: auto; display: flex; align-items: center; justify-content: center; padding: 24px; background: var(--bg-side); }
.login-card { width: 100%; max-width: 340px; padding: 8px 4px; }
.login-card.rise { animation: sheet-in var(--t-base) var(--ease-snappy); }   /* after a sign-out only, never on first paint */
.login-card h2 { margin-bottom: 18px; text-align: center; }
.login-brand { display: flex; flex-direction: column; align-items: center; gap: 12px; margin-bottom: 28px; text-align: center; }
.login-brand img { width: 68px; height: 68px; border-radius: var(--radius-xl); box-shadow: 0 10px 28px color-mix(in srgb, var(--tint) 26%, transparent), 0 0 0 var(--hair) var(--hairline); }
.login-brand h1 { font-size: var(--fs-large); line-height: var(--lh-large); }
.login-brand p { color: var(--text-2); font-size: var(--fs-md); margin-top: -8px; }
/* the fields are one grouped block: the ring goes around the group, the field in use is tinted */
.login-fields { background: var(--bg-elev); border-radius: var(--radius-lg); box-shadow: var(--shadow-card), 0 0 0 var(--hair) var(--hairline); overflow: hidden; margin-bottom: 10px; transition: box-shadow var(--t-fast); }   /* (the edge: in dark the block is only one step lighter than its ground) */
.login-fields:focus-within { box-shadow: var(--shadow-card), var(--ring); }
.login-fields input { display: block; width: 100%; height: 46px; padding: 0 14px; border: 0; border-radius: 0; background: none; font-size: var(--fs-headline); color: var(--text); outline: none; transition: background-color var(--t-fast); }
.login-fields input + input { box-shadow: inset 0 var(--hair) 0 var(--hairline); }
.login-fields input::placeholder { color: var(--text-4); }
.login-fields input:focus { background: var(--accent-soft); }
.login-fields input:focus-visible { border-radius: 0; box-shadow: none; }
.login-fields input + input:focus-visible { box-shadow: inset 0 var(--hair) 0 var(--hairline); }
/* the error line is always there, so the button below never jumps when a message appears */
.login-err { min-height: var(--lh-md); margin: 0 4px 10px; color: var(--danger); font-size: var(--fs-md); line-height: var(--lh-md); text-align: center; }
.login-note { margin-top: 16px; text-align: center; font-size: var(--fs-sm); color: var(--text-3); }
.login-note.lead { margin: 0 0 14px; }
.login-alt { margin-top: 8px; }
/* the button is the same shape as the block of fields it sits under (and keeps it when it has the focus) */
.login .btn.lg, .login .btn.lg:focus-visible { height: 44px; border-radius: var(--radius-lg); }
.btn.primary .spinner { border-color: color-mix(in srgb, var(--accent-text) 35%, transparent); border-top-color: var(--accent-text); }
.pw-acts { display: flex; align-items: center; justify-content: flex-end; gap: 8px; }

/* ------------------------------------------------------------------ 我 */
.page.me { padding-top: 36px; }
.me-head { display: flex; flex-direction: column; align-items: center; gap: 3px; margin-bottom: 26px; text-align: center; }
.me-head h1 { margin-top: 10px; font: 600 var(--fs-title1)/var(--lh-title1) var(--font-display); letter-spacing: var(--track-title); }
.me-sub { color: var(--text-2); font-size: var(--fs-md); }
/* the 资料 heading always has room for 还原 / 保存, so editing a field moves nothing */
.me-h { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: var(--ctl-h-sm); margin-bottom: 5px; }
.me-acts { display: inline-flex; align-items: center; gap: 6px; }   /* (保存 ends on the same edge as the values below it) */
.me-acts-enter-active { transition: opacity var(--t) ease; } .me-acts-leave-active { transition: opacity var(--t-out) ease; }
.me-acts-enter-from, .me-acts-leave-to { opacity: 0; }
.me-out { margin-top: 28px; }
/* changing the password: the three fields are a well inside the row, not a second card */
.list-row.stacked .login-fields { background: var(--bg-sunken); border-radius: var(--radius); box-shadow: inset 0 0 0 var(--hair) var(--hairline-strong); margin-bottom: 8px; }
.list-row.stacked .login-fields:focus-within { box-shadow: var(--ring); }
.list-row.stacked .login-fields input { height: 36px; padding: 0 12px; font-size: var(--fs-body); }
.list-row.stacked .login-err { text-align: left; margin: 0 2px 8px; }
.list-row.stacked { scroll-margin: 24px; }   /* opened at the foot of the page, it is scrolled into view with air around it */
/* a label here is as wide as its words (the shared 96px column would push 主题's control out of the row on a
   narrow phone); the spacer / value still ends on the trailing edge */
.me .list-row .list-l { min-width: 0; }

/* ------------------------------------------------------------------ overlays this area owns */
/* alert (confirmDialog): a question, at most a line about the consequence, two answers.
   It is mounted once, at the start, so in <body> it comes before every dialog opened later and would be painted
   under the very dialog that asked: it has a layer of its own (above the dialogs at 100, below the image viewer
   at 200 and the toasts at 300). */
.modal-mask.alert-mask { align-items: center; padding: 24px; animation: none; z-index: 150; }
/* Over a dialog it only dims a little more, and that dialog keeps its own dimmer — base.css gives the lighter
   one to "the later sibling", which here is the dialog underneath (its ground would flash as the alert opens). */
.modal-mask.alert-mask:has(~ .modal-mask) { background: color-mix(in srgb, var(--overlay) 60%, transparent); }
.modal-mask.alert-mask ~ .modal-mask:not(.modal-mask:not(.alert-mask) ~ .modal-mask) { background: var(--overlay); }
.alert {
  width: 320px; max-width: 100%; padding: 20px; border-radius: var(--radius-xl); background: var(--bg-elev); box-shadow: var(--shadow-sheet);
  text-align: center;
}
.alert-t { font: 600 var(--fs-headline)/var(--lh-headline) var(--font); letter-spacing: var(--track-title); text-wrap: balance; overflow-wrap: anywhere; }
.alert-b { margin-top: 6px; font-size: var(--fs-md); line-height: var(--lh-md); color: var(--text-2); white-space: pre-line; text-wrap: balance; overflow-wrap: anywhere; }
.alert-f { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 18px; }
.alert-f .btn { width: 100%; min-width: 0; }
/* The answer that destroys something is red, and never the default: a red label on the plain button ground —
   not a filled red button, which would be the loudest control of the alert — while 取消, where the focus
   starts, carries the weight. */
.alert-f .btn.danger { background: var(--fill-2); box-shadow: none; }
.alert-f:has(.btn.danger) .alert-no { font-weight: 600; }
.alert-enter-active { transition: opacity var(--t) ease; }
.alert-enter-active .alert { transition: transform var(--t) var(--ease-snappy); }
.alert-enter-from { opacity: 0; }
.alert-enter-from .alert { transform: scale(1.04); }
.alert-leave-active { transition: opacity var(--t-out) ease; }
.alert-leave-to { opacity: 0; }

/* toasts: the glyph on a filled disc; a notice fades up and away, the ones below close the gap */
.toast-i { display: inline-flex; align-items: center; justify-content: center; flex: none; width: 16px; height: 16px; border-radius: 50%; background: var(--tint); color: var(--accent-text); }
.toast.ok .toast-i { background: var(--ok-fill); }
.toast.danger .toast-i { background: var(--danger-fill); }
.toast a { font-weight: 600; }
/* (overlays.js pins a leaving notice where it stands; out of the flow, it lets the others move up at once) */
.toast-leave-active { position: absolute; max-width: none; transition: opacity var(--t) ease, transform var(--t) ease; }
.toast-leave-to { opacity: 0; transform: translateY(-8px) scale(.96); }
.toast-move:not(.toast-leave-active) { transition: transform var(--t-base) var(--ease-snappy); }   /* (the one that leaves keeps its own fade) */

/* image viewer and menus leave the way they came, only quicker */
.lb-leave-active { transition: opacity var(--t-out) ease; }
.lb-leave-to { opacity: 0; }
.lightbox-n { opacity: .6; }
/* More than one picture: previous / next stand beside it on the dark ground, never on the picture — many
   pictures are white charts, on which a white chevron cannot be seen. (A margin, not padding: a zoomed picture
   scrolls inside the body and must not pass under the arrows either.) */
.lightbox:has(.lightbox-nav) .lightbox-body { margin-inline: 72px; padding-inline: 0; }
/* A picture already shown at its full size has nothing to zoom into: no zoom cursor (overlays.js does not toggle
   it either). A zoomed one that is smaller than the box along one axis stays centred on that axis — auto margins
   take the free space, and where it overflows they are zero, so it still scrolls from its top-left corner. */
.lightbox-body.fit img { cursor: default; }
.lightbox-body.zoomed img { margin: auto; }

/* share sheet: where it goes, what goes, a remark */
.share-to { gap: 8px; }
.share-to-h { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.share-to-h label { font-size: var(--fs-sm); color: var(--text-3); font-weight: 500; }
.share-to .pick-list { max-height: 188px; }
.share-to .pick-item { width: 100%; text-align: left; }
.share-on { color: var(--link); flex: none; }
.share-none { padding: 12px; font-size: var(--fs-sm); color: var(--text-3); }
.share-note { margin-bottom: 0; }
.share-note textarea { resize: none; }   /* (a sheet has no resize grip: dragging it would change the dialog's height) */
/* 发送 was pressed with no group chosen (overlays.js): the reminder turns red (base.css .field.invalid .hint) and
   the red edge goes around the list, where the choice is missing — not around the filter field above it. */
.share-to.invalid .input { box-shadow: inset 0 0 0 var(--hair) var(--hairline-strong); }
.share-to.invalid .input:focus { box-shadow: var(--ring); }
.share-to.invalid .pick-list { box-shadow: inset 0 0 0 1px var(--danger); }
/* Until a group is chosen 发送 looks unavailable, but it is only aria-disabled: it still takes the press, which
   is answered by the picker above. It does not move or shade under the finger, since it does not act. */
.modal:has(.share-to) .modal-f .btn[aria-disabled="true"] { opacity: .4; }
.modal:has(.share-to) .modal-f .btn[aria-disabled="true"]:active { transform: none; }
.modal:has(.share-to) .modal-f .btn[aria-disabled="true"]::after { content: none; }
.modal:has(.share-to) .modal-f .btn.primary[aria-disabled="true"]:hover { background: var(--accent); }

/* A filled button's own inset highlight outweighs the shared :focus-visible ring (base.css), so the keyboard
   focus would not show on exactly the buttons Return presses: the filled buttons of this area draw the ring. */
.login .btn.primary:focus-visible, .pw-acts .btn.primary:focus-visible, .me-acts .btn.primary:focus-visible,
.empty-acts .btn.primary:focus-visible, .alert-f .btn:focus-visible,
.modal:has(.share-to) .modal-f .btn.primary:focus-visible { box-shadow: var(--ring); }

/* ------------------------------------------------------------------ tablet with a touch screen: same layout, larger targets */
@media (hover: none) and (min-width: 768px) {
  .nav-item { min-height: 40px; }
  .search { height: 36px; }
  .sb-brand .icon-btn { width: 36px; height: 36px; }
}

/* ------------------------------------------------------------------ phone: an iOS app */
@media (max-width: 767px) {
  .dash-h-a .select { font-size: 16px; }   /* (under 16px iOS zooms the page when the field takes the focus) */
  .app { display: block; }
  .sidebar { display: none; }
  .main { height: 100%; }

  /* A deeper screen is pushed in from the trailing edge over the one it came from, which slides a little way
     out and dims; going back is the same film in reverse. */
  .push-enter-active, .push-leave-active, .pop-enter-active, .pop-leave-active { transition: transform var(--t-slow) var(--ease-sheet), opacity var(--t-slow) var(--ease-sheet); }
  .push-enter-active, .pop-leave-active { z-index: 2; box-shadow: 0 0 0 var(--hair) var(--hairline); }
  .push-enter-from, .pop-leave-to { transform: translateX(100%); }
  .push-leave-to, .pop-enter-from { transform: translateX(-28%); opacity: .6; }

  /* The tab bar lies over the app (material: lists and pages scroll beneath it), so every scroller ends with
     room for it. It slides away while a conversation is open. */
  .tabbar {
    position: absolute; left: 0; right: 0; bottom: 0; z-index: 30; display: flex; height: calc(var(--tab-h) + var(--safe-b)); padding: 0 4px var(--safe-b);
    background: var(--material-tab); -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur);
    box-shadow: inset 0 var(--hair) 0 var(--hairline);
    transition: transform var(--t-slow) var(--ease-sheet), visibility 0s;
  }
  .app.no-tabs .tabbar { transform: translateY(100%); visibility: hidden; transition: transform var(--t-slow) var(--ease-sheet), visibility 0s var(--t-slow); }
  /* (the label's line is as tall as a caption's: with the body's 22px line the glyph was pushed up against the
     bar's top edge, and the unread badge on its corner stuck out above the bar) */
  .tab { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; padding-top: 2px; color: var(--text-3); font-size: var(--fs-xs); line-height: var(--lh-xs); font-weight: 500; }
  .tab:hover { text-decoration: none; }
  .tab.active { color: var(--tint); }
  .tab:focus-visible { box-shadow: inset 0 0 0 2px var(--tint); }
  .tab-i { position: relative; display: flex; transition: transform var(--t), opacity var(--t); }
  .tab:active .tab-i { transform: scale(.9); opacity: .6; transition-duration: 0s; }
  .tab-i .badge { position: absolute; top: -4px; left: 15px; box-shadow: none; }   /* on the glyph's corner, 2px inside the bar: it never crosses the top hairline */
  .tab-dot { position: absolute; top: 0; right: -6px; width: 6px; height: 6px; border-radius: 50%; background: var(--text-3); }

  /* 消息 tab: the list is the page, with a large title */
  .cl-page { flex: 1; min-height: 0; display: flex; flex-direction: column; background: var(--bg-content); padding-top: var(--safe-t); }
  .cl-page-h { display: flex; align-items: center; gap: 10px; padding: 8px 10px 2px 16px; transition: margin-top var(--t-base) var(--ease-snappy), opacity var(--t), visibility 0s; }
  .cl-page-h h1 { flex: none; }
  /* While the search field is in use the large title folds away (by its own height: the title's line plus the
     row's padding), so the field and 取消 are the top of the screen and the results start right below. */
  .cl-page:has(.cl-search.on) .cl-page-h {
    margin-top: calc(-1 * (var(--lh-large) + 10px)); opacity: 0; visibility: hidden;
    transition: margin-top var(--t-base) var(--ease-snappy), opacity var(--t), visibility 0s var(--t);
  }
  .cl-page:has(.cl-search.on) .cl-search { padding-top: 8px; }
  .cl-conn { flex: 1; min-width: 0; align-self: flex-end; margin-bottom: 9px; font-size: var(--fs-sm); color: var(--text-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .cl-page-h > .new-menu { margin-left: auto; }
  .cl-page-h > .new-menu > .icon-btn { width: 40px; height: 40px; }   /* the compose button only: buttons in the sheets it opens keep their own size */
  .cl-page-h > .new-menu > .menu { transform-origin: calc(100% - 20px) top; }
  .cl-search { padding: 6px 16px 8px; }
  .search { height: 36px; }
  .search:focus-within { box-shadow: none; background: var(--fill-2); }   /* the keyboard and the caret say "focused"; a ring is a desktop idiom */
  .search input { font-size: var(--fs-headline); }
  .search-x { width: 18px; height: 18px; color: var(--bg-content); }
  .search-x::after { inset: -13px; }   /* 18px glyph, 44px target */
  /* 取消 slides in beside the field while it is in use */
  .search-cancel {
    display: block; flex: none; width: 0; overflow: hidden; opacity: 0; color: var(--link); font-size: var(--fs-headline); text-align: right; white-space: nowrap;
    height: 44px; margin-block: -4px; line-height: 44px;   /* a full-size target; the row keeps the field's height */
    transition: width var(--t-base) var(--ease-snappy), opacity var(--t);
  }
  .cl-search.on .search-cancel { width: calc(2em + 14px); opacity: 1; }
  .search-cancel:active { opacity: .5; }
  .cl-scroll { padding: 0 0 calc(var(--tab-h) + var(--safe-b) + 16px); }
  .cl-edge.end { display: none; }   /* (the tab bar draws that edge) */
  .cl-scroll > .sb-sec { margin: 0; padding: 14px 16px 6px; background: var(--bg-content); font-size: var(--fs-sm); }   /* (the size of every list heading on a phone, .list-h) */
  .cl-go { min-height: var(--row-h); padding: 0 16px; gap: 12px; border-radius: 0; font-size: var(--fs-body); }
  .cl-go-i { width: 28px; height: 28px; }
  .cl-hint { padding: 28px 16px; font-size: var(--fs-md); }
  /* rows run edge to edge; the hairline starts at the text (after the avatar) and there is none after the last */
  .conv { gap: 12px; height: 72px; padding: 0 16px; border-radius: 0; }
  .conv .avatar { --dot-ring: var(--bg-content); }
  .conv .avatar .online-dot { width: 12px; height: 12px; }   /* (28% of a 50px avatar would be a blob beside the name) */
  .conv + .conv::before { content: ""; position: absolute; top: 0; left: 78px; right: 0; height: var(--hair); background: var(--hairline); }
  .conv-m { gap: 3px; }
  .conv-1 b { font-size: var(--fs-headline); line-height: var(--lh-headline); }
  .conv-1 time { font-size: var(--fs-md); }
  .conv-2 { min-height: 20px; }
  .conv-p { font-size: var(--fs-md); line-height: var(--lh-md); }
  .conv .badge:not(.dot) { height: 20px; min-width: 20px; padding: 0 6px; border-radius: var(--pill); font-size: var(--fs-sm); }
  .conv .badge.dot { width: 10px; min-width: 10px; height: 10px; padding: 0; margin-right: 5px; }
  .conv.active { background: none; }
  .conv:active, .conv.active:active { background: var(--bg-active); }
  .sr { gap: 12px; padding: 10px 16px; border-radius: 0; position: relative; }
  .sr + .sr::before { content: ""; position: absolute; top: 0; left: 56px; right: 0; height: var(--hair); background: var(--hairline); }
  .sr-h b { font-size: var(--fs-body); line-height: var(--lh-body); }
  .sr-g, .sr-h time { font-size: var(--fs-sm); }
  .sr-t { font-size: var(--fs-md); line-height: var(--lh-md); }

  /* pages: large titles, no toolbar */
  .page { padding: calc(10px + var(--safe-t)) var(--page-pad) 28px; }
  .pane > .page, .adm > .page { padding-bottom: calc(var(--tab-h) + var(--safe-b) + 16px); }
  .page > .dash-h, .page.page-narrow > .dash-h, .page.is-scrolled > .dash-h {
    position: static; min-height: 0; margin: 0 0 14px; padding: 0; background: none; -webkit-backdrop-filter: none; backdrop-filter: none; box-shadow: none; align-items: flex-end;
  }
  .dash-h-t { min-width: 0; flex: 1 1 calc(100% - 60px); gap: 2px; }   /* (room beside it for one glyph, see below) */
  .dash-h h1 { font: 700 var(--fs-large)/var(--lh-large) var(--font-display); letter-spacing: var(--track-hero); }
  .dash-h-sub > span { white-space: normal; }
  /* A page's actions (refresh, share, the original report …) are drawn one way on a phone: glyphs in the link
     colour on the header's trailing edge — the place and the colour of the actions in the bar of an embedded
     page or of 管理 (.push-a) and of 消息's compose button. A button that has an icon and a label keeps only the
     icon (the label stays for a screen reader). One glyph sits beside the title's status line; more take the row below,
     and a picker or a segmented control takes that row's width with the glyphs at its end. The last glyph is
     set out by its own margin, so the mark itself — not its empty box — ends on the cards' trailing edge.
     (These selectors out-rank a page's own toolbar rules on purpose: this is the one rule.) */
  .page > .dash-h > .dash-h-a { margin-left: auto; justify-content: flex-end; }
  .page > .dash-h > .dash-h-a:has(> .select, > .seg, > .sk-ctl) { width: 100%; }
  .page > .dash-h > .dash-h-a > :is(.seg, .sk-ctl) { flex: 1 1 auto; }
  .page > .dash-h > .dash-h-a > :is(.icon-btn, .btn:has(> .icon):has(> span)) {
    width: var(--ctl-h); height: var(--ctl-h); padding: 0; gap: 0; border-radius: var(--radius-ctl); background: none; box-shadow: none; color: var(--link);
  }
  .page > .dash-h > .dash-h-a > :is(.icon-btn, .btn:has(> .icon):has(> span)):active { transform: scale(.94); background: var(--fill-2); }
  /* (the hit area: 44px tall, and as wide as the glyph's box plus its share of the gap, so neighbours never
     overlap; it follows its button — nothing to hit while that is disabled or busy) */
  .page > .dash-h > .dash-h-a > :is(.icon-btn, .btn:has(> .icon):has(> span))::after { content: ""; position: absolute; inset: -4px -3px; border-radius: 0; background: none; pointer-events: inherit; }
  .page > .dash-h > .dash-h-a > :is(.icon-btn, .btn:has(> .icon):has(> span)):last-child { margin-right: calc((18px - var(--ctl-h)) / 2); }
  .page > .dash-h > .dash-h-a > .btn:has(> .icon):has(> span) > .icon { width: 18px; height: 18px; }
  .page > .dash-h > .dash-h-a > .btn:has(> .icon):has(> span) > span { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
  .page.me { padding-top: calc(24px + var(--safe-t)); }
  .embed { padding-bottom: calc(var(--tab-h) + var(--safe-b)); }

  /* The top bar. On a pushed page it lies over the scroller: clear while the large title is in view, material
     (with its hairline) as soon as something is beneath, and the title inline once the large one has gone.
     .auto (a tab's own page: nothing to go back to) only exists in that last state. .solid (an embedded site,
     which cannot scroll beneath anything) is a plain bar in the flow. */
  .push-bar { display: flex; align-items: center; flex: none; position: relative; z-index: 12; min-height: calc(44px + var(--safe-t)); padding: var(--safe-t) 6px 0; }
  .push-bar > a:not(.icon-btn) { display: inline-flex; align-items: center; gap: 1px; min-height: 44px; padding: 0 8px 0 2px; font-size: var(--fs-headline); color: var(--link); transition: opacity var(--t); }
  .push-bar > a:hover { text-decoration: none; }
  .push-bar > a:not(.icon-btn):active { opacity: .5; transition-duration: 0s; }
  .push-t {
    position: absolute; left: 50%; bottom: 11px; max-width: calc(100% - 196px); transform: translate(-50%, 4px); opacity: 0; pointer-events: none;
    font-size: var(--fs-headline); line-height: var(--lh-headline); font-weight: 600; transition: opacity var(--t), transform var(--t);
  }
  .push-bar.is-past .push-t, .push-bar.solid .push-t { opacity: 1; transform: translate(-50%, 0); }
  .push-bar.over {
    position: absolute; top: 0; left: 0; right: 0; -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur);
    box-shadow: inset 0 calc(-1 * var(--hair)) 0 transparent; transition: background-color var(--t), box-shadow var(--t);
  }
  .push-bar.over.is-scrolled { background: var(--material-bar); box-shadow: inset 0 calc(-1 * var(--hair)) 0 var(--hairline); }
  /* .auto: until the title has handed over, the bar is only a cover for the status-bar inset (::before, in the
     page's own colour) — whatever scrolls up never shows under the clock. Then the material (::after) fades in
     over it with the title; the two cross-fade, so the bar never blinks. */
  .push-bar.over.auto { background: none; -webkit-backdrop-filter: none; backdrop-filter: none; box-shadow: none; pointer-events: none; }
  .push-bar.over.auto::before, .push-bar.over.auto::after { content: ""; position: absolute; top: 0; left: 0; right: 0; transition: opacity var(--t); }
  .push-bar.over.auto::before { height: var(--safe-t); background: var(--bg); }
  .push-bar.over.auto::after {
    bottom: 0; z-index: -1; opacity: 0; background: var(--material-bar); -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur);
    box-shadow: inset 0 calc(-1 * var(--hair)) 0 var(--hairline);
  }
  .push-bar.over.auto.is-past { pointer-events: auto; }
  .push-bar.over.auto.is-past::before { opacity: 0; }
  .push-bar.over.auto.is-past::after { opacity: 1; }
  .push-bar.over:not(.auto) + .page { padding-top: calc(50px + var(--safe-t)); }
  .push-bar.solid { background: var(--bg-content); box-shadow: inset 0 calc(-1 * var(--hair)) 0 var(--hairline); }
  .push-a { display: flex; align-items: center; margin-left: auto; }
  .push-a .icon-btn { width: 40px; height: 40px; color: var(--link); }

  /* on the tab bar's top edge (it is not shown where there is no tab bar: an open conversation) */
  .conn-strip { top: auto; bottom: calc(var(--tab-h) + var(--safe-b)); box-shadow: 0 calc(-1 * var(--hair)) 0 var(--hairline); }

  /* 看板 tab: a grouped list. A tile's colour is the app's (a red tile would read as that dashboard's identity,
     not as a breach); a dashboard's state is written on the trailing side exactly as in the sidebar — a dot in
     the colour of the state, the words in ink. */
  .nav-page { padding-bottom: 8px; }
  .nav-row { align-items: flex-start; gap: 12px; padding-block: 10px; }
  .nav-row + .nav-row::before { left: 58px; }
  .nav-row-ico { display: flex; align-items: center; justify-content: center; width: 30px; height: 30px; margin-top: 2px; border-radius: calc(var(--radius) - 3px); background: var(--tint); color: var(--accent-text); flex: none; }
  .nav-row-ico.plain { background: var(--fill-2); color: var(--text-2); }
  .nav-row-t { display: block; font-size: var(--fs-headline); line-height: var(--lh-headline); }
  .nav-row-d { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; margin-top: 1px; font-size: var(--fs-sm); line-height: var(--lh-sm); color: var(--text-2); }
  .nav-row .nav-s { align-self: center; gap: 6px; font-size: var(--fs-md); font-weight: 400; color: var(--text-2); }
  /* (a P&L figure keeps its sign's colour and its weight, as in the sidebar: the rule above would win over them) */
  .nav-row .nav-s.pos { color: var(--pos); font-weight: 600; }
  .nav-row .nav-s.neg { color: var(--neg); font-weight: 600; }
  .nav-row .nav-s.flat, .nav-row .nav-s.stale { font-weight: 600; }
  .nav-row .nav-s.level { color: var(--text); font-weight: 600; }
  .nav-row .nav-s .dot { width: 8px; height: 8px; }   /* (in proportion to the larger words) */
  .nav-row .chev { align-self: center; }

  /* 我 */
  .me-acts .btn::before { content: ""; position: absolute; inset: -7px -2px; }   /* 30px buttons in a heading line, 44px targets */
  .list-row.stacked .login-fields input { height: 44px; }
  .list-row.stacked { scroll-margin-bottom: calc(var(--tab-h) + var(--safe-b) + 16px); }   /* (clear of the tab bar) */
  .login { background: var(--bg); }
  .login-fields input { height: 50px; padding: 0 16px; }
  .login .btn.lg, .login .btn.lg:focus-visible { height: var(--ctl-h-lg); }

  /* alert: the answers are stacked, full width; the way out is the lower one (nearest the thumb). The answer
     that is the default carries the weight: the filled one, or 取消 when the other one destroys something. */
  .alert { width: 300px; padding: 20px 16px 16px; }
  .alert-f { grid-template-columns: 1fr; margin-top: 16px; }
  .alert-f .btn, .alert-f .btn:focus-visible { height: var(--ctl-h-lg); border-radius: var(--radius-lg); font-size: var(--fs-headline); font-weight: 600; }
  .alert-f .alert-no { order: 2; font-weight: 500; }
  .alert-f .btn.danger { font-weight: 500; }

  .share-to .pick-list { max-height: 30vh; }

  /* image viewer: there is no room beside a picture, so previous / next sit side by side under it (a swipe on
     the picture does the same, overlays.js) */
  .lightbox:has(.lightbox-nav) .lightbox-body { margin: 0 0 calc(76px + var(--safe-b)); padding: 0 12px; }
  .lightbox-nav { top: auto; bottom: calc(16px + var(--safe-b)); margin-top: 0; }
  .lightbox-nav.prev { left: calc(50% - 60px); }
  .lightbox-nav.next { right: calc(50% - 60px); }
}
