:root {
	--bg: #faf8f6;
	--surface: #ffffff;
	--surface-2: #f1ede8;
	--border: #e8e2db;
	--text: #2a2622;
	--text-muted: #766f66;
	--accent: #c1543a;
	--accent-hover: #a8432e;
	--error: #b00020;
	--radius: 10px;
	/* Pastel green "this is a group" signal (lightbox tag chips/suggestions,
	   Type column badge) — darker than the pre-existing #1e5e2e used by
	   .import-summary/the "New" import tag, since at chip/badge sizes that
	   shade read as a bit low-contrast against #e3f4e6. */
	--group-bg: #e3f4e6;
	--group-text: #14532d;
}

* {
	box-sizing: border-box;
}

body {
	margin: 0;
	background: var(--bg);
	color: var(--text);
	font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
	line-height: 1.5;
}

.container {
	margin: 4rem auto;
	padding: 0 1.5rem;
}

.container-sm { max-width: 420px; }
.container-md { max-width: 640px; }
.container-lg { max-width: 900px; }
/* Import page: filter panel + album tree side by side want more room. */
.container-xl { max-width: 1200px; }

h1 {
	font-size: 1.4rem;
	margin: 0 0 .25rem;
}

h2 {
	font-size: 1.1rem;
	color: var(--text-muted);
	font-weight: 600;
	margin: 0 0 1.5rem;
}

a {
	color: var(--accent);
}

a:hover {
	color: var(--accent-hover);
}

.muted {
	color: var(--text-muted);
}

.card {
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	padding: 1.5rem;
}

.error {
	color: var(--error);
	background: #fbe9e9;
	border: 1px solid #f2c6c6;
	border-radius: var(--radius);
	padding: .6rem .9rem;
	margin-bottom: 1rem;
	font-size: .9rem;
}

.upload-status-info {
	color: var(--accent-hover);
	background: var(--surface-2);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	padding: .6rem .9rem;
	margin-bottom: 1rem;
	font-size: .9rem;
}

label {
	display: block;
	font-size: .9rem;
	color: var(--text-muted);
	margin-bottom: 1rem;
}

input[type="text"],
input[type="email"],
input[type="password"],
input[type="number"],
input[type="date"],
input[type="time"],
textarea {
	display: block;
	width: 100%;
	margin-top: .35rem;
	padding: .6rem .7rem;
	font-size: 1rem;
	font-family: inherit;
	color: var(--text);
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: var(--radius);
}

/* Date + optional-time pair (album manual date-range editing) — date gets
   the bulk of the width, time is inherently shorter content. app.js wraps
   every date/time input in a .date-picker-wrap (to fit its canonical-
   format readout below it without breaking this flex row — the wrapper,
   not the bare input, is the actual flex item once that JS runs). */
.date-time-row {
	display: flex;
	gap: .5rem;
}

.date-time-row input[type="date"],
.date-time-row input[type="time"] {
	margin-top: 0;
}

.date-time-row .date-picker-wrap-date {
	flex: 2;
}

.date-time-row .date-picker-wrap-time {
	flex: 1;
}

.date-picker-wrap {
	display: flex;
	flex-direction: column;
}

/* Always ISO (yyyy-mm-dd / 24h HH:MM) — that's the whole point, so it
   reads as data, not prose. Reserves a line even when empty so the layout
   doesn't jump the moment a value is entered. */
.date-readout {
	display: block;
	min-height: 1em;
	margin-top: .2rem;
	font-family: ui-monospace, "SF Mono", Consolas, monospace;
	font-size: .75rem;
	color: var(--text-muted);
}

input:focus,
textarea:focus {
	outline: none;
	border-color: var(--accent);
}

/* Label-beside-control layout, scoped to opt-in rows (the contact
   add/edit form) rather than changing the global stacked `label` rule
   every other form on the site still relies on. */
.form-row {
	display: flex;
	align-items: center;
	gap: .75rem;
	margin-bottom: .9rem;
}

.form-row > label {
	flex: 0 0 70px;
	margin-bottom: 0;
}

.form-row > input,
.form-row > .group-picker {
	flex: 1;
	min-width: 0;
	margin-top: 0;
}

button,
.btn {
	display: inline-block;
	font-size: 1rem;
	font-family: inherit;
	font-weight: 600;
	padding: .65rem 1.3rem;
	border-radius: var(--radius);
	border: none;
	background: var(--accent);
	color: #fff;
	text-decoration: none;
	cursor: pointer;
}

button:hover,
.btn:hover {
	background: var(--accent-hover);
}

/* .btn is applied to <a> as well as <button> — the global `a:hover` rule
   (element + pseudo-class) outranks the plain-class `.btn` color rule
   above, so without this, hovering an <a class="btn"> turned its text the
   same --accent-hover as the background it's now sitting on, making it
   unreadable. .btn:hover (class + pseudo-class) has enough specificity to
   win back over `a:hover`. */
.btn:hover {
	color: #fff;
}

.btn-link {
	background: none;
	border: none;
	color: var(--accent);
	text-decoration: underline;
	cursor: pointer;
	padding: 0;
	font-weight: normal;
	font-size: .9rem;
}

.btn-link:hover {
	background: none;
	color: var(--accent-hover);
}

nav.top {
	display: flex;
	gap: 1rem;
	align-items: center;
	margin-bottom: 1.5rem;
	font-size: .9rem;
}

/* Deliberately *not* inside nav.top — that's exactly the spot every other
   page's "back" link sits, and sign-out is a different, one-way kind of
   action that shouldn't share a click target with muscle-memory "go
   back" clicks. Fixed to the corner so it stays out of that flow (and
   stays reachable) regardless of scroll position. */
.sign-out-link {
	position: fixed;
	top: 1rem;
	right: 1.5rem;
	padding: .4rem .8rem;
	border-radius: 999px;
	background: var(--surface);
	border: 1px solid var(--border);
	color: var(--text-muted);
	text-decoration: none;
	font-size: .85rem;
	box-shadow: 0 2px 8px rgba(0, 0, 0, .08);
	z-index: 10;
}

.sign-out-link:hover {
	color: var(--accent-hover);
	border-color: var(--accent-hover);
}

/* Pill / segmented control — real radios for accessibility, visually
   hidden, with the sibling <span> styled based on :checked state. */
.pill-group {
	display: inline-flex;
	background: var(--surface-2);
	border-radius: 999px;
	padding: 4px;
	gap: 4px;
	margin-bottom: 1.25rem;
}

.pill {
	position: relative;
	margin: 0;
}

.pill input {
	position: absolute;
	opacity: 0;
	width: 100%;
	height: 100%;
	margin: 0;
	cursor: pointer;
}

.pill span {
	display: block;
	padding: .5rem 1.1rem;
	border-radius: 999px;
	font-size: .9rem;
	color: var(--text-muted);
	transition: background .15s, color .15s;
	white-space: nowrap;
}

.pill input:checked + span {
	background: var(--accent);
	color: #fff;
}

.pill input:focus-visible + span {
	outline: 2px solid var(--accent);
	outline-offset: 2px;
}

.album-list {
	list-style: none;
	padding: 0;
}

.album-list li {
	padding: .6rem 0;
	border-bottom: 1px solid var(--border);
}

.album-grid {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
	gap: 1rem;
	margin: 1rem 0 2rem;
}

.album-card {
	display: block;
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	overflow: hidden;
	color: inherit;
	text-decoration: none;
}

.album-card:hover {
	border-color: var(--accent);
}

.album-card-thumbs {
	display: flex;
	height: 100px;
	background: var(--surface-2);
}

.album-card-thumbs img {
	flex: 1;
	height: 100%;
	object-fit: cover;
	min-width: 0;
}

/* A video thumbnail here is wrapped in .video-thumb-wrap (see below,
   for the play-icon overlay), so it's that wrapper — not the <video>
   itself — that needs to be the flex item; the video then just fills
   100% of whatever the wrapper ends up sized to. */
.album-card-thumbs .video-thumb-wrap {
	flex: 1;
	height: 100%;
	min-width: 0;
}

.album-card-thumbs .video-thumb-wrap .video-thumb {
	width: 100%;
	height: 100%;
	object-fit: cover;
}

.album-card-thumbs .thumb-fallback {
	flex: 1;
	display: flex;
	align-items: center;
	justify-content: center;
	color: var(--text-muted);
	font-size: .7rem;
	text-align: center;
	padding: .25rem;
}

.album-card-body {
	padding: .75rem .9rem;
}

.album-card-title {
	font-weight: 600;
	margin: 0 0 .25rem;
}

.album-card-desc {
	font-size: .85rem;
	color: var(--text-muted);
	margin: 0 0 .4rem;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.album-card-meta {
	font-size: .75rem;
	color: var(--text-muted);
	margin: 0;
}

/* Deliberately distinct from .album-card-meta's "updated" timestamp above
   it — this is when the photos were actually taken, not when the album
   record itself last changed. */
.album-card-range {
	font-size: .75rem;
	color: var(--text-muted);
	margin: .2rem 0 0;
}

.photo-grid {
	display: flex;
	flex-wrap: wrap;
	gap: .75rem;
}

.table-scroll {
	max-height: 320px;
	overflow-y: auto;
	border: 1px solid var(--border);
	border-radius: var(--radius);
}

table.contacts {
	width: 100%;
	border-collapse: collapse;
	font-size: .9rem;
}

table.contacts th {
	position: sticky;
	top: 0;
	background: var(--surface-2);
	text-align: left;
	padding: .6rem .75rem;
	font-weight: 600;
	color: var(--text-muted);
	border-bottom: 1px solid var(--border);
}

table.contacts th:first-child,
table.contacts td:first-child {
	width: 2.5rem;
	text-align: right;
	color: var(--text-muted);
}

table.contacts td {
	padding: .6rem .75rem;
	border-bottom: 1px solid var(--border);
	vertical-align: top;
}

.col-sort {
	background: none;
	border: none;
	padding: 0;
	margin: 0;
	font: inherit;
	font-weight: 600;
	color: inherit;
	cursor: pointer;
	white-space: nowrap;
}

.col-sort:hover,
.col-sort.active {
	color: var(--text);
}

table.contacts tr:last-child td {
	border-bottom: none;
}

table.contacts tr.editing-row td {
	background: #fbf0ec;
}

/* Shared by the zip-cache and performance-metrics admin pages — same
   plain data-table look, kept under one selector list rather than
   duplicated per page. */
table.zip-cache,
table.admin-table {
	width: 100%;
	border-collapse: collapse;
	font-size: .9rem;
	margin-bottom: 1.5rem;
}

table.zip-cache th,
table.admin-table th {
	text-align: left;
	padding: .6rem .75rem;
	font-weight: 600;
	color: var(--text-muted);
	border-bottom: 1px solid var(--border);
}

table.zip-cache td,
table.admin-table td {
	padding: .6rem .75rem;
	border-bottom: 1px solid var(--border);
	vertical-align: top;
}

table.zip-cache tr:last-child td,
table.admin-table tr:last-child td {
	border-bottom: none;
}

/* --- Admin performance page ------------------------------------------------ */

.metrics-stat-row {
	display: flex;
	flex-wrap: wrap;
	gap: .75rem;
	margin-bottom: 1.5rem;
}

.metrics-stat {
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	padding: .6rem 1rem;
	min-width: 90px;
}

.metrics-stat span {
	display: block;
	font-size: .75rem;
	color: var(--text-muted);
}

.metrics-stat strong {
	display: block;
	font-size: 1.15rem;
	margin-top: .1rem;
}

.tag {
	display: inline-block;
	background: var(--surface-2);
	color: var(--text-muted);
	border-radius: 999px;
	padding: .1rem .6rem;
	font-size: .75rem;
	margin: 0 .25rem .25rem 0;
}

/* A "?" badge that carries its full explanation in a custom tooltip
   (data-tooltip, wired up in app.js) instead of inline muted text —
   keeps a label from wrapping onto several lines just to fit an aside. */
.info-hint {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 15px;
	height: 15px;
	border-radius: 50%;
	border: 1px solid var(--text-muted);
	color: var(--text-muted);
	font-size: .65rem;
	font-weight: 600;
	cursor: help;
	user-select: none;
	vertical-align: middle;
}

/* The bubble app.js shows/positions on hover or focus of any element
   carrying a data-tooltip attribute (the "?" info badges, thumbnail
   position hints, ...). Starts hidden and off-screen-irrelevant
   (display:none) rather than at 0×0 in the corner, so nothing flashes at
   (0,0) before the first real position is computed. */
.hover-tooltip {
	display: none;
	position: absolute;
	max-width: 260px;
	background: var(--text);
	color: var(--surface);
	font-size: .75rem;
	line-height: 1.4;
	padding: .5rem .65rem;
	border-radius: 6px;
	box-shadow: 0 4px 12px rgba(0, 0, 0, .18);
	z-index: 1000;
	pointer-events: none;
}

/* Follow-cursor tooltips (the thumbnail "X of Y" readout) — the app's own
   accent color instead of the "?" badge's neutral dark grey, so this
   reads as its own distinct kind of hint at a glance, plus a snugger
   padding to suit its short, numeric content ("3 of 24") rather than the
   longer prose the badge tooltip usually carries. */
.hover-tooltip-cursor {
	background: var(--accent);
	color: var(--surface);
	padding: .25rem .4rem;
}

/* Status, not membership — outlined rather than filled so it doesn't
   read as just another group tag. */
.tag-archived {
	background: transparent;
	border: 1px solid var(--text-muted);
	text-transform: uppercase;
	letter-spacing: .02em;
	font-weight: 600;
	font-size: .65rem;
}

/* --- Groups management (contacts page) ------------------------------- */

/* Collapsed by default (see contacts.html) so it doesn't sit between the
   contacts table and the add/edit form on every visit — group management
   is occasional, not the primary task on this page. Styled as its own
   card (not just a bare <details>) so it reads as a distinct block with
   its own margin, rather than a heading that happens to be collapsible. */
.groups-section {
	margin-bottom: 1.5rem;
}

.groups-section summary {
	display: flex;
	align-items: baseline;
	gap: .6rem;
	cursor: pointer;
	list-style: none;
}

.groups-section[open] summary {
	margin-bottom: .75rem;
}

.groups-section summary::-webkit-details-marker {
	display: none;
}

.groups-section summary h2 {
	display: inline-block;
	margin: 0;
}

.groups-section summary h2::before {
	content: "▶";
	display: inline-block;
	font-size: .65em;
	margin-right: .5rem;
	color: var(--text-muted);
	transition: transform .15s;
}

.groups-section[open] summary h2::before {
	transform: rotate(90deg);
}

.group-create {
	display: flex;
	gap: .5rem;
	margin-top: 1rem;
	max-width: 400px;
}

.group-create input {
	flex: 1;
	margin-top: 0;
}

.group-create-error {
	max-width: 400px;
}

.group-list-empty-note {
	margin: 0 0 .5rem;
}

.groups-list {
	display: flex;
	flex-direction: column;
	gap: .5rem;
}

.group-item {
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	padding: .3rem .9rem;
}

/* Otherwise every card's border is the same pale grey and an expanded
   group visually runs into its neighbors — this gives the open one a
   clearly distinct edge, same accent color already used for focus rings. */
.group-item[open] {
	border-color: var(--accent);
}

.group-item summary {
	cursor: pointer;
	padding: .5rem 0;
	list-style: none;
	display: flex;
	align-items: baseline;
	gap: .6rem;
}

.group-item summary::-webkit-details-marker {
	display: none;
}

.group-item-name {
	font-weight: 600;
}

.group-item-body {
	padding: 0 0 .9rem;
	border-top: 1px solid var(--border);
	margin-top: -.1rem;
	padding-top: .75rem;
}

.group-rename-form {
	display: flex;
	gap: .5rem;
	margin: .75rem 0 .5rem;
}

.group-rename-form input {
	flex: 1;
}

.group-member-chips {
	display: flex;
	flex-wrap: wrap;
	gap: .35rem;
	margin-bottom: .5rem;
}

.group-empty-note {
	margin-bottom: .5rem;
}

.group-add-member {
	margin: .75rem 0;
	max-width: 260px;
}

/* --- Group picker (contact add/edit form) ----------------------------- */

.group-picker {
	margin-bottom: 1rem;
}

.group-picker-chips {
	display: flex;
	flex-wrap: wrap;
	gap: .35rem;
	margin-bottom: .4rem;
}

.tag-chip {
	display: inline-flex;
	align-items: center;
	gap: .3rem;
	background: var(--surface-2);
	color: var(--text);
	border-radius: 999px;
	padding: .15rem .5rem .15rem .7rem;
	font-size: .8rem;
}

.tag-chip button {
	background: none;
	border: none;
	color: var(--text-muted);
	opacity: .8;
	cursor: pointer;
	font-size: .9rem;
	line-height: 1;
	padding: 0;
}

.tag-chip button:hover {
	opacity: 1;
}

.group-picker-input-wrap {
	position: relative;
}

.group-picker-suggestions {
	position: absolute;
	top: 100%;
	left: 0;
	right: 0;
	margin-top: .25rem;
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: 8px;
	box-shadow: 0 8px 24px rgba(0, 0, 0, .15);
	max-height: 180px;
	overflow-y: auto;
	z-index: 1;
}

.group-picker-suggestion {
	padding: .45rem .7rem;
	font-size: .85rem;
	color: var(--text);
	cursor: pointer;
}

.group-picker-suggestion:hover,
.group-picker-suggestion.highlighted {
	background: var(--surface-2);
}

.group-picker-create {
	color: var(--accent);
	font-style: italic;
}

.toolbar {
	display: flex;
	gap: .75rem;
	align-items: flex-start;
	margin: 1rem 0 1.5rem;
	flex-wrap: wrap;
}

/* Native <details>/<summary> dropdown — no JS required. */
.dropdown {
	position: relative;
	display: inline-block;
}

.dropdown summary {
	display: inline-block;
	list-style: none;
}

.dropdown summary::-webkit-details-marker {
	display: none;
}

.dropdown-menu {
	position: absolute;
	top: calc(100% + 4px);
	left: 0;
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	box-shadow: 0 4px 12px rgba(0, 0, 0, .08);
	min-width: 200px;
	z-index: 10;
	overflow: hidden;
}

/* The download menu's item text (see .dropdown-item below) is now
   nowrap, so this needs room to actually grow to fit it — its generated
   labels ("Part 2 of 3 — ~340 MB — click to prepare") run well past the
   200px default. Capped rather than left unbounded so one long album
   title in the "Manifest (...)" entry can't push it past the viewport on
   a narrow screen. */
.dropdown-menu[data-download-status-url] {
	max-width: min(92vw, 440px);
}

.dropdown-menu a {
	display: block;
	padding: .6rem .9rem;
	color: var(--text);
	text-decoration: none;
	font-size: .9rem;
}

.dropdown-menu a:hover {
	background: var(--surface-2);
}

/* One row of a download dropdown's part/manifest listing (see
   download.js) — `.dropdown-item` covers both a real link (a part that's
   already ready) and a `<button>` (not ready yet — clicking it starts
   tracking that one part in the download tray below), since only the
   former also picks up `.dropdown-menu a`'s hover/link styling above. The
   button reset (border/background/font/alignment) is needed because
   `<button>` doesn't inherit any of that the way `<a>` does. */
.dropdown-item {
	display: block;
	width: 100%;
	padding: .6rem .9rem;
	font-size: .9rem;
	font-family: inherit;
	color: var(--text);
	text-decoration: none;
	white-space: nowrap;
	text-align: left;
	background: none;
	border: none;
}

button.dropdown-item {
	cursor: pointer;
}

button.dropdown-item:hover {
	background: var(--surface-2);
}

.dropdown-item-pending {
	color: var(--text-muted);
}

/* Persistent bottom-right tray tracking whatever parts the user has
   actually clicked to prepare — deliberately not a modal: any number can
   be tracked at once, independently, and the rest of the page stays fully
   usable while they're in flight. Only exists in the DOM (well, only
   visible — see ensureTray's own comment on why `display` is toggled via
   inline style rather than the `hidden` attribute) once there's at least
   one row. */
.download-tray {
	position: fixed;
	right: 1.25rem;
	bottom: 1.25rem;
	width: 300px;
	max-width: calc(100vw - 2.5rem);
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	box-shadow: 0 8px 28px rgba(0, 0, 0, .18);
	z-index: 1000;
	overflow: hidden;
}

.download-tray-header {
	padding: .6rem .9rem;
	font-size: .85rem;
	font-weight: 600;
	background: var(--surface-2);
	border-bottom: 1px solid var(--border);
}

.download-tray-items {
	max-height: 260px;
	overflow-y: auto;
}

.download-tray-item {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: .75rem;
	padding: .6rem .9rem;
	border-bottom: 1px solid var(--border);
}

.download-tray-item:last-child {
	border-bottom: none;
}

.download-tray-item-text {
	min-width: 0;
}

.download-tray-item-label {
	font-size: .85rem;
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}

.download-tray-item-status {
	font-size: .8rem;
	color: var(--text-muted);
}

.download-tray-item-action {
	flex-shrink: 0;
}

.import-summary {
	background: #eaf5ec;
	border: 1px solid #cfe8d4;
	border-radius: var(--radius);
	padding: .6rem .9rem;
	margin-bottom: 1rem;
	font-size: .9rem;
	color: #1e5e2e;
}

.photo-thumb {
	width: 160px;
	height: 160px;
	border-radius: var(--radius);
	object-fit: cover;
	display: block;
	background: var(--surface-2);
}

/* Combined with .photo-thumb/.reorder-thumb (sizing/object-fit/background
   all already apply the same to a <video> as to an <img>) — this only
   adds what's specific to it being a <video>. The actual frame only ever
   shows up because app.js seeks a moment into the video once its metadata
   loads — `preload="metadata"` alone doesn't reliably decode/paint one on
   its own, in Chrome or anywhere else. The `poster` attribute (an inline
   SVG data URI, no extra asset/request) is what shows until that seek
   completes, or permanently in a browser where it doesn't work.
   `pointer-events: none` keeps a click landing on the wrapping trigger/
   label exactly like it would for a plain <img> — a bare <video> has no
   controls here, but this removes any doubt across browsers. */
.video-thumb {
	pointer-events: none;
}

/* Wraps every .video-thumb (see the templates) purely so the play-icon
   badge below has somewhere to sit — <video>, like <img>, is a replaced
   element, and ::before/::after generated content doesn't apply to those
   in any browser, so the badge can't just be a pseudo-element on the
   video itself. inline-block + line-height:0 makes this wrapper shrink to
   exactly the video's own footprint (whatever .photo-thumb/.reorder-thumb
   size that ends up being) with no extra gap underneath — the usual
   inline-replaced-element quirk line-height:0 is the standard fix for. */
.video-thumb-wrap {
	position: relative;
	display: inline-block;
	line-height: 0;
}

/* A dark wash across the *whole* thumbnail (not just a badge behind the
   icon) + a centered pure-CSS triangle (the classic border trick — no
   image/SVG needed) — shown at all times over the frame once it's
   decoded, not just as a loading placeholder, so a video thumbnail reads
   as "video" regardless of whether the frame render worked out in a
   given browser. `inset:0` fills whatever size .video-thumb-wrap ends up
   being (160px grid thumb, 48px reorder-row thumb, ...) without needing
   its own fixed dimensions. */
.video-thumb-play {
	position: absolute;
	inset: 0;
	background: rgba(0, 0, 0, .35);
	display: flex;
	align-items: center;
	justify-content: center;
	pointer-events: none;
}

/* Matches whichever thumbnail shape this particular video sits in —
   .photo-thumb/.reorder-thumb's own border-radius values, since this is
   a separate absolutely-positioned sibling rather than a child the video
   itself could clip. Left un-rounded (the default) for the gallery
   album-card row (a bare .video-thumb, no .photo-thumb/.reorder-thumb) —
   there, the *card's* own overflow:hidden + border-radius is what rounds
   the row's outer corners; individual thumbnails in that flex row have
   square edges against each other already, so rounding this one would
   look wrong rather than matching anything. */
.video-thumb-wrap:has(.photo-thumb) .video-thumb-play {
	border-radius: var(--radius);
}

.video-thumb-wrap:has(.reorder-thumb) .video-thumb-play {
	border-radius: 6px;
}

.video-thumb-play::after {
	content: "";
	width: 0;
	height: 0;
	border-style: solid;
	border-width: 7px 0 7px 12px;
	border-color: transparent transparent transparent #fff;
	margin-left: 3px; /* optical centering — a triangle's own bounding box reads off-center otherwise */
}

/* 2x the base size specifically in the main album/share photo-grid (the
   160px .photo-thumb context) — left at the default size in the album
   edit page's reorder rows (.reorder-thumb, 48px), where the default
   already reads fine and this much bigger would be cramped. */
.video-thumb-wrap:has(.photo-thumb) .video-thumb-play::after {
	border-width: 14px 0 14px 24px;
	margin-left: 6px;
}

/* Owner-only "this is hidden from shares" badge — same dark-wash-across-
   the-whole-thumbnail treatment as .video-thumb-play, just with the
   lightbox private-toggle button's own crossed-eye icon (same reddish
   --error color, same slash direction) instead of a play triangle.
   .thumb-private only gets position:relative (everything else here
   already has no layout of its own to preserve) so a non-private
   thumbnail's markup/CSS is completely untouched. */
.thumb-trigger.thumb-private {
	position: relative;
	display: inline-block;
	line-height: 0;
}

.private-thumb-overlay {
	position: absolute;
	inset: 0;
	background: rgba(0, 0, 0, .67);
	border-radius: var(--radius);
	color: var(--error);
	display: flex;
	align-items: center;
	justify-content: center;
	pointer-events: none;
}

.private-thumb-overlay svg {
	width: 56px;
	height: 56px;
	flex-shrink: 0;
}

/* Same 160px-grid-vs-48px-reorder-row split as the video play icon
   above — smaller icon, and the reorder row's own corner radius, in that
   context. */
.thumb-trigger.thumb-private:has(.reorder-thumb) .private-thumb-overlay {
	border-radius: 6px;
}

.thumb-trigger.thumb-private:has(.reorder-thumb) .private-thumb-overlay svg {
	width: 32px;
	height: 32px;
}

.photo-thumb-fallback {
	width: 160px;
	height: 160px;
	border-radius: var(--radius);
	background: var(--surface-2);
	color: var(--text-muted);
	display: flex;
	align-items: center;
	justify-content: center;
	text-align: center;
	padding: .5rem;
	font-size: .8rem;
	word-break: break-word;
}

/* Toast — driven by a `?toast=`/`?toast_kind=` query param pair on
   redirect (see app.js), or called directly for a client-side-only event
   with no navigation involved. Confirms an action actually happened for
   plain server-rendered forms that would otherwise redirect with no
   visible feedback. Full-width rather than a small floating pill, and
   anchored to the top rather than the bottom, specifically because it
   always appears right after a full page navigation — the one moment a
   small corner chip is easiest to miss, since the viewer's attention is
   already reorienting to a freshly loaded page rather than parked on one
   spot waiting for a reaction.

   Three dispositions, each a diagonal-striped bar with the message in its
   own solid pill on top (so the text stays legible regardless of where it
   lands on the stripes): success (moss green) for a completed upload,
   warning (amber/rust) for a cancelled one, error (terracotta) for a
   failure. `.toast-message` carries the pill; `.toast` itself is just the
   positioning/animation shell plus whichever stripe background the kind
   class sets. */
.toast {
	position: fixed;
	top: 0;
	left: 0;
	right: 0;
	display: flex;
	justify-content: center;
	padding: .85rem 1rem;
	transform: translateY(-100%);
	opacity: 0;
	transition: opacity .25s ease, transform .25s ease;
	z-index: 1000;
	pointer-events: none;
}

.toast-visible {
	opacity: 1;
	transform: translateY(0);
}

.toast-message {
	max-width: 90%;
	padding: .55rem 1.3rem;
	border-radius: 999px;
	font-size: .95rem;
	font-weight: 500;
	box-shadow: 0 4px 14px rgba(0, 0, 0, .22);
}

.toast-error {
	background: repeating-linear-gradient(135deg,
		var(--accent) 0px, var(--accent) 16px,
		var(--accent-hover) 16px, var(--accent-hover) 32px);
}

.toast-error .toast-message {
	background: var(--text);
	color: var(--bg);
}

.toast-warning {
	background: repeating-linear-gradient(135deg,
		#e0983f 0px, #e0983f 16px,
		var(--accent-hover) 16px, var(--accent-hover) 32px);
}

.toast-warning .toast-message {
	background: var(--bg);
	color: #5a2e14;
}

.toast-success {
	/* Swapped the dark-green stripe for option B's own pastel orange
	   (#e0983f) — keeping the pastel green (#6b8f5e) — so success shares a
	   warmer note with warning instead of reading as a colder, more
	   isolated green against the rest of the site. */
	background: repeating-linear-gradient(135deg,
		#e0983f 0px, #e0983f 16px,
		#6b8f5e 16px, #6b8f5e 32px);
}

.toast-success .toast-message {
	background: var(--bg);
	color: #26401f;
}

table.reorder-table {
	width: 100%;
	border-collapse: collapse;
	font-size: .9rem;
	margin-bottom: .25rem;
}

table.reorder-table th {
	text-align: left;
	padding: .5rem .6rem;
	font-weight: 600;
	color: var(--text-muted);
	border-bottom: 1px solid var(--border);
}

table.reorder-table td {
	padding: .4rem .6rem;
	border-bottom: 1px solid var(--border);
	vertical-align: middle;
}

table.reorder-table tr[draggable="true"] {
	cursor: grab;
}

table.reorder-table tr.dragging {
	opacity: .4;
	background: var(--surface-2);
}

.rename-input {
	width: 160px;
	margin-right: .4rem;
	padding: .2rem .4rem;
	font-size: .9rem;
}

.star-col {
	width: 1.5rem;
	text-align: center;
}

.star-toggle {
	background: none;
	border: none;
	cursor: pointer;
	font-size: 1.2rem;
	line-height: 1;
	color: var(--text-muted);
	padding: 0;
}

.star-toggle.starred {
	color: #d4a017;
}

.star-toggle:hover {
	color: #d4a017;
}

.drag-handle {
	color: var(--text-muted);
	width: 1.5rem;
	text-align: center;
	user-select: none;
}

.reorder-thumb {
	width: 48px;
	height: 48px;
	border-radius: 6px;
	object-fit: cover;
	display: block;
	background: var(--surface-2);
}

.reorder-thumb-fallback {
	display: flex;
	align-items: center;
	justify-content: center;
	color: var(--text-muted);
	font-size: .65rem;
	text-align: center;
}

/* Also a lightbox trigger (see album_edit.html) — a <span>, unlike an
   <a>, doesn't get a pointer cursor for free. */
.filename-display {
	cursor: pointer;
}

/* --- Drag-reorder chrome (Manage Albums list, photo-order table) ------ */
/* Shared by any drag-reorder list on the site — the border/highlight/
   save-row mechanics are identical regardless of what's being reordered;
   only the row content itself (.album-manage-row vs table.reorder-table's
   own rows) is feature-specific. See STYLE_GUIDE.md's "Drag-and-drop
   reordering" section. */

.reorder-list {
	display: flex;
	flex-direction: column;
	border: 2px solid var(--border);
	border-radius: var(--radius);
	overflow: hidden;
	transition: border-color .15s ease;
}

/* A list wrapping a <table> (rather than row divs directly) wants a little
   breathing room between the border and the table's own edge-to-edge
   cells/header. */
.reorder-list-table {
	padding: .4rem;
}

/* Unsaved drag-reorder — set/cleared by comparing the live row order
   against the order the page loaded with, not just "has a drag ever
   happened". The two .reorder-save-row elements (top and bottom) get this
   same class toggled on them directly by that same check, so both stay in
   sync without either depending on DOM position relative to this element. */
.reorder-list.dirty {
	border-color: var(--accent);
}

.album-manage-row {
	display: flex;
	align-items: center;
	gap: .75rem;
	padding: .5rem .75rem;
	background: var(--surface);
	border-bottom: 1px solid var(--border);
	cursor: grab;
}

.album-manage-row:last-child {
	border-bottom: none;
}

.album-manage-row.dragging {
	opacity: .4;
	background: var(--surface-2);
}

.album-manage-thumb-link {
	flex-shrink: 0;
	line-height: 0;
}

.album-manage-thumb {
	width: 56px;
	height: 56px;
	border-radius: 6px;
	object-fit: cover;
	display: block;
	background: var(--surface-2);
}

.album-manage-thumb-fallback {
	/* Same box, just no image — an empty or video/other-only album. */
}

.album-manage-info {
	flex: 1;
	min-width: 0;
}

.album-manage-title {
	display: block;
	font-weight: 600;
	color: var(--text);
	text-decoration: none;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.album-manage-title:hover {
	color: var(--accent);
}

.album-manage-meta {
	margin: .15rem 0 0;
	font-size: .8rem;
	color: var(--text-muted);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

/* Only rendered at all once there's an unsaved reorder — see .dirty
   below — rather than staying present but visually inert. Both buttons
   inside only ever exist while there's something to act on, so there's
   nothing to gate their own clickability on separately. */
.reorder-save-row {
	display: none;
	align-items: center;
	justify-content: center;
	gap: .6rem;
	/* Straddles the list's border on its own side (top row overlaps the
	   top edge, bottom row the bottom edge) rather than floating near it —
	   relative + z-index so each row's (opaque) buttons reliably paint
	   over that border segment regardless of which one is earlier in DOM
	   order (only true by default for the bottom row otherwise). */
	position: relative;
	z-index: 1;
}

.reorder-save-row.dirty {
	display: flex;
}

.reorder-save-row-top {
	margin-bottom: -1.05rem;
}

.reorder-save-row-bottom {
	margin-top: -1.05rem;
}

.reorder-dirty-note {
	font-size: .8rem;
	font-weight: 600;
	color: var(--accent);
}

.reorder-save-btn {
	background: var(--accent);
	border: 2px solid var(--accent);
	color: #fff;
	cursor: pointer;
	transition: background .15s ease, border-color .15s ease;
}

.reorder-save-btn:hover {
	background: var(--accent-hover);
	border-color: var(--accent-hover);
}

/* Opens the revert-confirm popup (see reorderlist.js) — outlined rather
   than filled so "Save order" still reads as the primary action of the
   pair. */
.reorder-revert-btn {
	background: var(--surface);
	border: 2px solid var(--border);
	color: var(--text-muted);
	cursor: pointer;
	transition: border-color .15s ease, color .15s ease;
}

.reorder-revert-btn:hover {
	border-color: var(--text-muted);
	color: var(--text);
}

.lightbox-overlay {
	display: none;
	position: fixed;
	inset: 0;
	background: rgba(20, 16, 12, .92);
	z-index: 2000;
	align-items: center;
	justify-content: center;
	/* Nothing in here is meant to be text-selected — most relevantly, this
	   keeps a double-click on blank space from highlighting the nearest
	   text/image instead of just toggling the popup menu. */
	-webkit-user-select: none;
	-moz-user-select: none;
	user-select: none;
}

.lightbox-tag-input,
.lightbox-caption-input,
.lightbox-caption {
	/* The two actual text inputs are a deliberate exception to the
	   overlay-wide no-select rule above — and so is the read-only caption
	   display text itself, which has no reason to resist a drag-select
	   just because the blank backdrop around it does. */
	-webkit-user-select: text;
	-moz-user-select: text;
	user-select: text;
}

.lightbox-overlay.lightbox-open {
	display: flex;
}

.lightbox-media {
	display: flex;
	align-items: center;
	justify-content: center;
}

.lightbox-media img,
.lightbox-media video {
	max-width: 92vw;
	/* min() with a fixed-rem floor, not just 88vh alone — a pure vh cap
	   left only 6vh (12vh split top+bottom) for the bottom-center caption
	   bar on a tall/portrait photo, which is what overlapped it. The
	   calc() term guarantees at least ~3.5rem of clearance above/below
	   regardless of viewport height, while 88vh still wins (and this has
	   no effect) on any photo that isn't already hugging the height cap. */
	max-height: min(88vh, calc(100vh - 7rem));
	border-radius: var(--radius);
}

/* Alternate layout for a photo that ends up hugging the full allowed
   width (see updateNavLayout in lightbox.js) — the default flex-row
   layout below (prev/media/next side by side) leaves no room for the nav
   buttons beside a photo like that, so this moves them below it instead.
   Grid, not flex — lets the two buttons share one row of their own,
   centered as a pair, without a wrapping element around just those two:
   media spans both columns in row 1, each button takes one column in
   row 2. Absolutely-positioned siblings (.lightbox-toolbar, .lightbox-
   counter, ...) are unaffected either way, same as the default flex
   layout — switching the container's own display mode doesn't change
   how they resolve their position. */
.lightbox-overlay.lightbox-nav-below {
	display: grid;
	grid-template-columns: auto auto;
	column-gap: 1.5rem;
	row-gap: .5rem;
	justify-content: center;
	align-content: center;
}

.lightbox-overlay.lightbox-nav-below .lightbox-media {
	grid-column: 1 / -1;
	grid-row: 1;
}

.lightbox-overlay.lightbox-nav-below .lightbox-prev {
	grid-column: 1;
	grid-row: 2;
	justify-self: end;
}

.lightbox-overlay.lightbox-nav-below .lightbox-next {
	grid-column: 2;
	grid-row: 2;
	justify-self: start;
}

/* Extra vertical room reserved for that button row (~4rem more than the
   default budget) — mirrored exactly by fitConstraints' navBelow branch
   in lightbox.js, so the thumbnail placeholder box never renders a
   different size than the full-res image that goes on to replace it. */
.lightbox-overlay.lightbox-nav-below .lightbox-media img,
.lightbox-overlay.lightbox-nav-below .lightbox-media video {
	max-height: min(88vh, calc(100vh - 11rem));
}

.lightbox-media img {
	transform-origin: center center;
	/* No CSS transition on transform: wheel/drag-driven zoom and pan need
	   to track the input immediately, not ease toward it. */
	/* Matters specifically while the low-res thumbnail is standing in for
	   the full-res image (see loadPhotoProgressively/computeFitSize in
	   lightbox.js) — that JS sets an explicit width/height on the box
	   itself (max-width/max-height alone can only ever shrink an
	   oversized image, never grow an undersized one), and object-fit:
	   contain is what actually stretches the thumbnail's much-smaller
	   bitmap to fill that box rather than leaving it pinned to its own
	   native (small) pixel size. A no-op once the full-res image has
	   loaded, since the explicit width/height gets cleared then and the
	   image's own intrinsic size already matches the box exactly. */
	object-fit: contain;
}

.lightbox-toolbar {
	position: absolute;
	top: 1rem;
	right: 1.25rem;
	display: flex;
	align-items: center;
	gap: .25rem;
	/* Same reasoning as .lightbox-nav's z-index: a zoomed photo's own
	   `transform` gives it a stacking context too (transform != none does
	   that automatically), putting it on equal footing with this
	   absolutely-positioned toolbar — and since the image comes later in
	   the markup, DOM-order tie-breaking would let it paint (and swallow
	   clicks) over the toolbar once panned underneath it. */
	z-index: 1;
}

/* The caption-editing toggle's own home — bottom-right, near where the
   caption itself is actually edited (.lightbox-info, bottom-center),
   rather than the top toolbar it used to share with buttons that aren't
   related to the caption at all. Same zoomed-image z-index reasoning as
   .lightbox-toolbar above. */
.lightbox-bottom-toolbar {
	position: absolute;
	bottom: .75rem;
	right: 1.25rem;
	z-index: 1;
	display: flex;
	align-items: center;
	gap: .25rem;
}

.lightbox-close,
.lightbox-download,
.lightbox-star,
.lightbox-private-toggle,
.lightbox-caption-mode,
.lightbox-tags-toggle,
.lightbox-delete-photo {
	display: flex;
	align-items: center;
	justify-content: center;
	width: 2.25rem;
	height: 2.25rem;
	background: none;
	border: none;
	border-radius: 8px;
	color: #fff;
	line-height: 1;
	cursor: pointer;
	opacity: .85;
}

.lightbox-close {
	font-size: 1.8rem;
}

.lightbox-download,
.lightbox-star,
.lightbox-caption-mode,
.lightbox-tags-toggle {
	font-size: 1.3rem;
}

/* An inline <svg>'s own width/height HTML attributes aren't reliable
   inside a flex container — as a flex item its layout box can collapse to
   zero width (Chrome/Firefox both do this) unless CSS explicitly sizes it;
   the attribute is only an intrinsic-size hint, not something flex sizing
   actually respects. */
.lightbox-tags-toggle svg,
.lightbox-delete-photo svg,
.lightbox-private-toggle svg {
	width: 18px;
	height: 18px;
	flex-shrink: 0;
}

.lightbox-star.starred {
	color: #f4c542;
	opacity: 1;
}

/* Same prohibition-sign language as .lightbox-counter-date.date-hidden —
   a reddish diagonal slash across the icon, here instead of an oval
   border since the icon itself (not a variable-width text pill) is what
   needs circumscribing. */
.lightbox-private-toggle {
	position: relative;
}

.lightbox-private-toggle.is-private {
	color: var(--error);
	opacity: 1;
}

.lightbox-private-toggle.is-private::after {
	content: "";
	position: absolute;
	left: 20%;
	right: 20%;
	top: 50%;
	border-top: 1.5px solid var(--error);
	transform: translateY(-50%) rotate(-45deg);
}

.lightbox-caption-mode.active,
.lightbox-tags-toggle.active {
	background: rgba(255, 255, 255, .18);
	opacity: 1;
}

.lightbox-close:hover,
.lightbox-download:hover,
.lightbox-star:hover,
.lightbox-private-toggle:hover,
.lightbox-caption-mode:hover,
.lightbox-tags-toggle:hover {
	opacity: 1;
	background: rgba(255, 255, 255, .12);
}

/* Its own (red-tinted) hover rather than joining the group above — the
   one destructive action in this toolbar, worth visually saying so. */
.lightbox-delete-photo:hover {
	opacity: 1;
	background: rgba(176, 0, 32, .35);
	color: #ff8a80;
}

/* Bottom-left — pairs with the top-right toolbar and the bottom-right
   caption-mode button so every corner carries one piece of chrome,
   rather than stacking two of them (this and the tags-toggle tab) into
   the same top-left corner. */
.lightbox-counter {
	position: absolute;
	bottom: .75rem;
	left: 1.25rem;
	color: #fff;
	max-width: 55vw;
	/* Same zoomed-image-stacking-context reasoning as .lightbox-toolbar. */
	z-index: 1;
}

.lightbox-counter-index {
	display: block;
	font-size: .85rem;
	opacity: .8;
}

.lightbox-counter-filename-row {
	display: flex;
	align-items: baseline;
	gap: .5rem;
	margin-top: .15rem;
	max-width: 100%;
}

.lightbox-counter-filename {
	flex: 1 1 auto;
	font-size: .75rem;
	opacity: .6;
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
	/* Lets the filename (not the date, which should always stay fully
	   readable — see .lightbox-counter-date's flex: none below) be the
	   one that shrinks/ellipsizes when the two together outgrow the
	   available width — a flex child's default min-width is auto (its
	   own content size), which would otherwise stop it from shrinking
	   below that at all. */
	min-width: 0;
}

.lightbox-counter-date {
	flex: 0 0 auto;
	font: inherit;
	font-size: .75rem;
	opacity: .6;
	padding: 0;
	background: none;
	border: none;
	color: inherit;
	cursor: default;
	white-space: nowrap;
}

.lightbox-counter-date-interactive {
	cursor: pointer;
}

.lightbox-counter-date-interactive:hover {
	opacity: .9;
}

/* The "hidden from share links" state — a reddish oval circumscribing
   the date with a diagonal slash through it, same prohibition-sign
   language as a "no" icon, applied directly to the text itself rather
   than a separate icon glyph. */
.lightbox-counter-date.date-hidden {
	position: relative;
	opacity: 1;
	padding: .1rem .55rem;
	border: 1.5px solid var(--error);
	border-radius: 999px;
	color: var(--error);
}

.lightbox-counter-date.date-hidden::after {
	content: "";
	position: absolute;
	left: -6%;
	right: -6%;
	top: 50%;
	border-top: 1.5px solid var(--error);
	transform: translateY(-50%) rotate(-25deg);
}

/* Top-center — clear of the top-right toolbar and the tags-panel tab
   poking out at top-left. Shown while the thumbnail (or the previous
   photo, if there's no thumbnail) is standing in for a full-res image
   still loading — see loadPhotoProgressively's setFullResLoading calls.
   Same zoomed-image-stacking-context reasoning as .lightbox-toolbar/
   .lightbox-counter for the z-index. Pill + CSS-only spinner, cross-
   checked against the CPG199 portal's own lightbox, which uses the same
   treatment for the same purpose. */
.lightbox-loading {
	display: none;
	position: absolute;
	top: 1rem;
	left: 50%;
	transform: translateX(-50%);
	z-index: 1;
	align-items: center;
	gap: .45rem;
	background: rgba(0, 0, 0, .55);
	color: #fff;
	font-size: .8rem;
	padding: .35rem .9rem;
	border-radius: 999px;
	pointer-events: none;
}

.lightbox-loading::before {
	content: "";
	width: 11px;
	height: 11px;
	border-radius: 50%;
	border: 2px solid rgba(255, 255, 255, .35);
	border-top-color: #fff;
	animation: lightbox-loading-spin .7s linear infinite;
}

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

/* Same top-center pill treatment as .lightbox-loading, and safe to share
   the exact position — the two are mutually exclusive in practice (zoom
   always resets on every photo navigation, which is exactly when
   .lightbox-loading might be showing instead). Shows the current zoom as
   a percentage of the photo's own native resolution — see
   updateZoomReadout in lightbox.js — for as long as a photo (not a
   video) is on screen. A real <button> (the zoom-to-100%/back-to-fit
   toggle — see toggleZoomTo100Percent), so this resets the usual button
   chrome rather than the pointer-events:none a passive pill like
   .lightbox-loading gets away with. */
.lightbox-zoom-readout {
	display: none;
	position: absolute;
	top: 1rem;
	left: 50%;
	transform: translateX(-50%);
	z-index: 1;
	align-items: center;
	background: rgba(0, 0, 0, .55);
	color: #fff;
	font-size: .8rem;
	font-family: inherit;
	padding: .35rem .9rem;
	border-radius: 999px;
	border: none;
	cursor: pointer;
}

.lightbox-zoom-readout:hover,
.lightbox-zoom-readout:focus {
	background: rgba(0, 0, 0, .72);
	outline: none;
}

/* Subtly de-emphasized relative to the percentage it sits beside — an
   aside, not the headline figure. */
.lightbox-zoom-readout-fit-tag {
	opacity: .65;
	margin-left: .35rem;
	font-size: .72rem;
}

.lightbox-menu {
	position: fixed;
	display: flex;
	flex-direction: column;
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	box-shadow: 0 8px 24px rgba(0, 0, 0, .35);
	overflow: hidden;
	z-index: 2001;
	min-width: 170px;
}

.lightbox-menu button {
	background: none;
	border: none;
	text-align: left;
	padding: .6rem .9rem;
	font-size: .9rem;
	color: var(--text);
	cursor: pointer;
}

.lightbox-menu button:hover,
.lightbox-menu button:focus {
	background: var(--surface-2);
	outline: none;
}

.lightbox-menu button + button {
	border-top: 1px solid var(--border);
}

/* Back to normal flex flow beside .lightbox-media (hugging the image's
   own edges) rather than pinned to fixed screen positions — that made
   the reach between them too far and put the left one over the tags
   panel. The center-collapse-while-loading problem this was chasing is
   now solved at the source instead: show()/loadPhotoProgressively keep
   the previous photo's element in place (and thus this row's width
   stable) until the new one is actually ready to swap in, rather than
   clearing mediaContainer up front and leaving it briefly empty. */
.lightbox-nav {
	position: relative;
	/* A zoomed-and-panned image can visually bleed well past its own box;
	   without this, that overflow (unpositioned, so painted in plain DOM
	   order) would paint over these buttons and swallow their clicks. */
	z-index: 1;
	background: none;
	border: none;
	color: #fff;
	font-size: 2.5rem;
	line-height: 1;
	cursor: pointer;
	padding: 0 1rem;
	opacity: .8;
}

.lightbox-nav:hover {
	opacity: 1;
}

/* Explicit and unconditional (not :focus-visible) — a click leaves the
   button focused, and Enter/Space then repeats that direction natively,
   so the highlight should show immediately on click, not only once the
   browser's focus-visible heuristic decides a keyboard interaction
   happened. */
.lightbox-nav:focus {
	opacity: 1;
	outline: none;
	background: rgba(255, 255, 255, .18);
	border-radius: 8px;
}

/* Bottom-center, between the two bottom-corner buttons. Centered via
   left:50%/translateX(-50%), which centers on this box's *own* width —
   the status readout used to be a flex sibling counted in that width,
   which pulled the whole bar's centerpoint away from the caption box's
   own center (true centering needs the caption/input to be the only
   thing this centering math accounts for; see .lightbox-caption-status
   below for where the status readout actually lives now). */
.lightbox-info {
	position: absolute;
	bottom: .75rem;
	left: 50%;
	transform: translateX(-50%);
	/* Wide enough to not clip the caption input below, which is allowed
	   to get about as wide as the photo itself. */
	max-width: 92vw;
	color: #fff;
	z-index: 1;
}

/* Its own backdrop (unlike the plain white text this used to be) —
   legible over whatever the photo underneath happens to look like,
   which matters now that a tall photo's bottom edge can end up close by
   (see the max-height calc() on .lightbox-media img/video). Hidden
   entirely when there's no caption (existing display:none toggle in
   renderCaption), so there's never an empty pill floating here. */
.lightbox-caption {
	font-size: .95rem;
	background: rgba(20, 16, 12, .75);
	padding: .225rem .9rem;
	border-radius: var(--radius);
}

/* Wider ("a longer, single line" — rows="1" in the markup) and no
   longer manually resizable, since a single line is the whole point;
   already had its own background, so no legibility change needed here
   the way the caption view text above did. Capped at 960px (the 92vw
   term only matters below ~1040px viewport width, as a narrow-window
   fallback). Centered text (not left-aligned) so the cursor starts in
   the middle of the box rather than pinned to its far left edge, which
   is where the width increase would otherwise leave it stranded. */
.lightbox-info textarea.lightbox-caption-input {
	width: min(92vw, 960px);
	text-align: center;
	background: rgba(0, 0, 0, .35);
	border: 1px solid rgba(255, 255, 255, .25);
	border-radius: 8px;
	color: #fff;
	font-family: inherit;
	font-size: .9rem;
	padding: .25rem .6rem;
	resize: none;
}

.lightbox-info textarea.lightbox-caption-input::placeholder {
	color: rgba(255, 255, 255, .5);
}

/* Floats just past the caption box's right edge — taken out of normal
   flow entirely (rather than a flex sibling counted in .lightbox-info's
   own width) so its changing content ("...", "Saved", the longer error
   text, or nothing at all) can never shift the caption box's own
   position or true-centering, only its own. .lightbox-info establishes
   the containing block this positions against (it's already
   position:absolute, which does that regardless of display type). */
.lightbox-caption-status {
	position: absolute;
	left: 100%;
	top: 50%;
	transform: translateY(-50%);
	margin-left: .6rem;
	font-size: .75rem;
	opacity: .65;
	white-space: nowrap;
}

.lightbox-tags-panel {
	position: fixed;
	top: 0;
	left: 0;
	height: 100%;
	width: 280px;
	max-width: 80vw;
	background: rgba(20, 16, 12, .96);
	border-right: 1px solid rgba(255, 255, 255, .1);
	padding-top: 4.5rem;
	box-sizing: border-box;
	transform: translateX(-100%);
	transition: transform .2s ease;
	z-index: 1;
	display: flex;
	flex-direction: column;
}

.lightbox-tags-panel.open {
	transform: translateX(0);
}

/* The toggle button lives inside the panel and rides along with its
   slide transform instead of sitting in the top toolbar — it's
   positioned just past the panel's own right edge, which — because a
   translateX(-100%) shift moves everything inside the panel (including
   this child) by exactly the panel's own width — lands it right back at
   the screen's top-left corner once closed. No separate open/closed
   position logic needed: the same translateX toggle that already opens
   and closes the panel carries the button along with it. */
.lightbox-tags-panel > .lightbox-tags-toggle {
	position: absolute;
	top: 1rem;
	right: -3.5rem;
}

.lightbox-tags-panel-top {
	flex: 0 0 auto;
	padding: 0 1.1rem 1rem;
}

.lightbox-tags-panel-header {
	color: #fff;
	font-size: .85rem;
	font-weight: 600;
	opacity: .8;
	margin-bottom: .75rem;
}

.lightbox-tags-missing-note {
	color: #ff9b9b;
	font-size: .78rem;
	margin: -.4rem 0 .6rem;
}

/* The deliberate-browse counterpart to the type-ahead combobox above it —
   every contact, checkbox-toggleable, in a fixed alphabetical order that
   never reshuffles as you check things off (a row jumping position the
   instant you click it would defeat the point of a scan-through list). Its
   own scroll region, separate from the panel as a whole, so the combobox
   stays reachable without scrolling back up through a long roster. */
.lightbox-contacts-table {
	flex: 1 1 auto;
	overflow-y: auto;
	padding: .5rem 1.1rem 1.1rem;
	border-top: 1px solid rgba(255, 255, 255, .1);
}

/* Sticky within .lightbox-contacts-table's own scroll region, so the
   column labels (and the ability to re-sort) stay reachable while
   scrolling through a long roster. */
.lightbox-contacts-headerrow {
	position: sticky;
	top: 0;
	margin: -.5rem -1.1rem .4rem;
	padding: .5rem 1.1rem .4rem;
	display: flex;
	gap: .55rem;
	background: rgba(20, 16, 12, .96);
	border-bottom: 1px solid rgba(255, 255, 255, .15);
	z-index: 1;
}

.lightbox-contacts-sort {
	background: none;
	border: none;
	color: #fff;
	opacity: .65;
	font-size: .75rem;
	font-weight: 600;
	text-transform: uppercase;
	letter-spacing: .02em;
	cursor: pointer;
	padding: 0;
	text-align: left;
	white-space: nowrap;
}

.lightbox-contacts-sort:hover,
.lightbox-contacts-sort.active {
	opacity: 1;
}

.lightbox-contacts-sort-tagged {
	/* Not narrowed to the checkbox column's width — "Tagged" (plus its sort
	   arrow once active) doesn't fit in that little space, and a fixed
	   width just clips/overlaps the text instead of wrapping it. */
	flex-shrink: 0;
}

.lightbox-contacts-sort-name {
	flex: 1;
}

.lightbox-contacts-sort-type {
	flex-shrink: 0;
}

.lightbox-contact-row {
	display: flex;
	align-items: center;
	gap: .55rem;
	padding: .35rem 0;
	border-bottom: 1px solid rgba(255, 255, 255, .06);
	/* Row selection (independent of the checkbox) reads as text selection
	   to a browser by default — same reasoning as the overlay-wide rule,
	   just called out again here since drag-to-select is exactly the
	   gesture that would otherwise paint a native selection highlight. */
	user-select: none;
}

.lightbox-contact-row:last-child {
	border-bottom: none;
}

.lightbox-contact-row.selected {
	background: rgba(120, 170, 255, .18);
}

/* Deliberately only the checkbox itself is the click target — not wrapped
   in a <label> spanning the row — so scrolling past or reading a name
   can't register as an accidental toggle. */
.lightbox-contact-row input[type="checkbox"] {
	width: auto;
	margin: 0;
	flex-shrink: 0;
	cursor: pointer;
}

.lightbox-contact-name {
	flex: 1;
	min-width: 0;
	color: #fff;
	font-size: .85rem;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.lightbox-contact-type {
	flex-shrink: 0;
	font-size: .75rem;
	color: rgba(255, 255, 255, .5);
}

/* Same pastel green family already used for "positive"/"new" signals
   elsewhere (.import-summary, the "New" tag on the import-confirm page) —
   reused here (via --group-bg/--group-text, a slightly darker text shade
   for contrast at this size) rather than introducing a second green, per
   the badge/pill approach (not a full-row tint) agreed for distinguishing
   groups. */
.lightbox-contact-type-group {
	background: var(--group-bg);
	color: var(--group-text);
	border-radius: 999px;
	padding: .1rem .55rem;
	font-weight: 600;
}

/* Fixed height, reserved up front, rather than sized to content — a
   checkbox click in the table below shouldn't shift the table (and the row
   you just clicked) by however tall the newly-added chip happens to be.
   align-content packs wrapped chip rows toward the bottom, so with few
   chips the reserved space sits empty at the *top* instead of pushing the
   combobox down. The `safe` keyword matters: plain `flex-end` is "unsafe"
   alignment per the CSS box-alignment spec — once chips overflow the fixed
   height, the overflow happens off the *top* (opposite the packing
   direction), which most browsers then render as unreachable via scroll
   even with overflow-y: auto set (no scrollbar, content just clipped).
   `safe flex-end` keeps the bottom-packed look while everything fits, and
   automatically falls back to top-packed (properly scrollable) the moment
   it wouldn't. */
.lightbox-tag-chips {
	display: flex;
	flex-wrap: wrap;
	align-content: safe flex-end;
	gap: .35rem;
	height: 20vh;
	overflow-y: auto;
	margin-bottom: .75rem;
}

.lightbox-tag-chip {
	display: inline-flex;
	align-items: center;
	gap: .3rem;
	background: rgba(255, 255, 255, .15);
	color: #fff;
	border-radius: 999px;
	padding: .2rem .5rem .2rem .7rem;
	font-size: .8rem;
}

.lightbox-tag-chip button {
	background: none;
	border: none;
	color: #fff;
	opacity: .75;
	cursor: pointer;
	font-size: .9rem;
	line-height: 1;
	padding: 0;
}

.lightbox-tag-chip button:hover {
	opacity: 1;
}

.lightbox-tag-chip-group {
	background: var(--group-bg);
	color: var(--group-text);
	font-weight: 600;
}

.lightbox-tag-chip-group button {
	color: var(--group-text);
}

.lightbox-tag-picker {
	position: relative;
	margin-bottom: .6rem;
}

.lightbox-tag-input {
	width: 100%;
}

.lightbox-tag-suggestions {
	position: absolute;
	top: 100%;
	left: 0;
	right: 0;
	margin-top: .25rem;
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: 8px;
	box-shadow: 0 8px 24px rgba(0, 0, 0, .35);
	max-height: 160px;
	overflow-y: auto;
	/* Higher than .lightbox-contacts-headerrow's z-index: 1 — with equal
	   z-index, the sticky table header wins on DOM order alone (it comes
	   later in the markup than the combobox), so it was painting over the
	   dropdown wherever the two visually overlapped. */
	z-index: 2;
}

.lightbox-tag-suggestion {
	padding: .45rem .7rem;
	font-size: .85rem;
	color: var(--text);
	cursor: pointer;
}

.lightbox-tag-suggestion:hover,
.lightbox-tag-suggestion.highlighted {
	background: var(--surface-2);
}

.lightbox-tag-suggestion-group {
	background: var(--group-bg);
	color: var(--group-text);
	font-weight: 600;
}

.lightbox-tag-suggestion-group:hover,
.lightbox-tag-suggestion-group.highlighted {
	background: #cdeacf;
}

.upload-progress {
	height: 8px;
	background: var(--surface-2);
	border-radius: 999px;
	overflow: hidden;
	margin: .75rem 0;
}

.upload-cancel {
	display: block;
	margin: -.5rem 0 .75rem;
}

.upload-progress-bar {
	height: 100%;
	width: 0%;
	background: var(--accent);
	transition: width .15s ease;
}

/* Distinguishes the "server processing" phase (thumbnail generation, after
   the upload itself has finished sending) from the upload-sending phase. */
.upload-progress-bar.processing {
	background: var(--accent-hover);
}

.quota-label {
	font-size: .85rem;
	color: var(--text-muted);
	margin-bottom: .5rem;
}

.upload-progress-bar.quota-bar-warning {
	background: var(--error);
}

/* --- Import-from-another-album page ----------------------------------- */

.import-action-bar {
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: 1rem;
	margin-bottom: 1rem;
}

/* Inline beside the submit button, unlike the global block-with-space label. */
.import-order-label {
	display: flex;
	align-items: center;
	gap: .4rem;
	margin-bottom: 0;
}

.import-order-label select {
	font-family: inherit;
	font-size: .9rem;
	padding: .35rem .45rem;
	border: 1px solid var(--border);
	border-radius: 6px;
	background: var(--surface);
	color: var(--text);
}

.import-clear {
	background: none;
	border: none;
	color: var(--accent);
	cursor: pointer;
	font-size: .85rem;
	font-weight: normal;
	padding: 0;
	text-decoration: underline;
}

.import-clear:hover {
	background: none;
	color: var(--accent-hover);
}

.import-hidden-note {
	color: var(--text-muted);
	font-size: .85rem;
}

.import-layout {
	display: flex;
	align-items: flex-start;
	gap: 1.25rem;
}

/* Sticky, bounded to the viewport, and a flex column: the top section
   (chips + combobox + mode toggle) stays put while the tag table below
   gets the remaining height as its own scroll region — mirroring the
   lightbox's tags panel, where the combobox and in-play chips stay visible
   no matter how far down the table you've scrolled. The tree on the right
   scrolls independently of all of it. */
.import-filter-panel {
	flex: 0 0 320px;
	position: sticky;
	top: 1rem;
	max-height: calc(100vh - 2rem);
	display: flex;
	flex-direction: column;
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: var(--radius);
}

/* Positioned + raised so everything in the top section that floats
   (the combobox's suggestions dropdown) stacks above the table section
   below it — the sticky header row down there carries z-index: 1, and
   both being at 1 would let the later-in-DOM header paint over the
   dropdown. */
.import-filter-top {
	position: relative;
	z-index: 2;
	padding: 1rem 1rem .25rem;
}

.import-filter-panel h3 {
	margin: 0 0 .4rem;
}

.import-filter-hint {
	font-size: .8rem;
	margin: 0 0 .6rem;
}

/* Fixed height, reserved up front, same reasoning as .lightbox-tag-chips:
   a checkbox click in the table below shouldn't shift the table (and the
   row you just clicked) by however tall a newly-added chip happens to be.
   Bottom-packed with `safe` alignment so overflowing chips stay reachable
   by scroll — see the .lightbox-tag-chips comment for the full story. */
.import-filter-chips {
	display: flex;
	flex-wrap: wrap;
	align-content: safe flex-end;
	gap: .35rem;
	height: 11vh;
	overflow-y: auto;
	margin-bottom: .5rem;
}

/* Same pastel "this is a group" signal as the lightbox's chips, on the
   light-theme .tag-chip base the contacts page already uses. */
.tag-chip-group {
	background: var(--group-bg);
	color: var(--group-text);
	font-weight: 600;
}

.tag-chip-group button {
	color: var(--group-text);
}

.group-picker-suggestion.import-tag-suggestion-group {
	color: var(--group-text);
	font-weight: 600;
}

.import-filter-mode {
	font-size: .85rem;
	color: var(--text-muted);
	margin: .6rem 0;
}

.import-filter-mode label {
	display: flex;
	align-items: center;
	gap: .35rem;
	margin: .2rem 0 0;
	font-size: .85rem;
	color: var(--text);
}

/* The table is its own scroll region (the flex remainder of the panel),
   with the column headers sticky inside it — same structure as
   .lightbox-contacts-table, in this page's light theme. */
.import-tag-table {
	flex: 1 1 auto;
	min-height: 0;
	max-height: 400px;
	overflow-y: auto;
	padding: .5rem 1rem 1rem;
	border-top: 1px solid var(--border);
}

/* Sticky within .import-tag-table's own scroll region, so the column
   labels (and the ability to re-sort) stay reachable while scrolling
   through a long roster. */
.import-tag-headerrow {
	position: sticky;
	top: 0;
	margin: -.5rem -1rem .4rem;
	padding: .5rem 1rem .4rem;
	display: flex;
	gap: .55rem;
	background: var(--surface);
	border-bottom: 1px solid var(--border);
	z-index: 1;
}

.import-tag-sort {
	background: none;
	border: none;
	color: var(--text-muted);
	font-size: .75rem;
	font-weight: 600;
	text-transform: uppercase;
	letter-spacing: .02em;
	cursor: pointer;
	padding: 0;
	text-align: left;
	white-space: nowrap;
}

.import-tag-sort:hover,
.import-tag-sort.active {
	background: none;
	color: var(--text);
}

.import-tag-sort-filter {
	flex-shrink: 0;
}

.import-tag-sort-name {
	flex: 1;
}

.import-tag-sort-type {
	flex-shrink: 0;
}

.import-tag-row {
	display: flex;
	align-items: center;
	gap: .55rem;
	padding: .35rem 0;
	border-bottom: 1px solid var(--border);
	font-size: .85rem;
	/* Drag-to-select is exactly the gesture that would otherwise paint a
	   native text-selection highlight. */
	user-select: none;
}

.import-tag-row:last-child {
	border-bottom: none;
}

.import-tag-row.selected {
	background: rgba(120, 170, 255, .22);
}

/* Only the checkbox itself is the filter toggle target — the row body is
   the selection surface (same split as the lightbox table). */
.import-tag-row input[type="checkbox"] {
	width: auto;
	margin: 0;
	flex-shrink: 0;
	cursor: pointer;
}

.import-tag-name {
	flex: 1;
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.import-tag-type {
	font-size: .7rem;
	color: var(--text-muted);
}

.import-tag-type-group {
	background: var(--group-bg);
	color: var(--group-text);
	border-radius: 999px;
	padding: .05rem .45rem;
}

.import-tree {
	flex: 1;
	min-width: 0;
}

/* No overflow: hidden here (despite the rounded corners) — the per-photo
   tags popup positions itself outside its row and must not get clipped at
   the card's edge. Nothing inside paints its own background over the
   corners anyway. */
.import-album {
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	margin-bottom: .6rem;
}

.import-album-row {
	display: flex;
	align-items: center;
	gap: .6rem;
	padding: .55rem .75rem;
}

.import-album-expander {
	background: none;
	border: none;
	color: var(--text-muted);
	cursor: pointer;
	font-size: 1.2rem;
	line-height: 1;
	padding: 0 .2rem;
	transition: transform .15s;
}

.import-album-expander:hover {
	background: none;
	color: var(--text);
}

.import-album.expanded .import-album-expander {
	transform: rotate(90deg);
}

.import-album-title {
	font-weight: 600;
	cursor: pointer;
}

.import-album-count {
	font-size: .85rem;
	margin-left: auto;
}

.import-photo-list {
	border-top: 1px solid var(--border);
	padding: .35rem .75rem .5rem 2.4rem;
}

/* A label (click anywhere on the row toggles its checkbox), but laid out
   as a row — undo the global block-label spacing/coloring. */
.import-photo-row {
	display: flex;
	align-items: center;
	gap: .6rem;
	margin: 0;
	padding: .25rem 0;
	font-size: .9rem;
	color: var(--text);
	cursor: pointer;
}

.import-photo-name {
	min-width: 0;
	overflow-wrap: anywhere;
}

.import-photo-note {
	font-size: .8rem;
}

/* Right-aligned cluster at the end of a photo row: tags readout,
   "already in this album" note, and the lightbox preview link — one
   wrapper carries the margin-left: auto so the cluster stays right-aligned
   whichever of its members happen to be present. */
.import-photo-row-right {
	display: flex;
	align-items: center;
	gap: .6rem;
	margin-left: auto;
	flex-shrink: 0;
}

/* Per-photo tags readout: a one-line summary that ellipses when long;
   hovering it reveals the full set as chips in a floating panel.
   Position: relative on the outer span (not overflow: hidden — the popup
   has to escape it), ellipsis on the inner summary. */
.import-photo-tags {
	position: relative;
}

.import-photo-tags-summary {
	display: block;
	max-width: 220px;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
	font-size: .8rem;
}

.import-photo-tags-full {
	display: none;
	position: absolute;
	right: 0;
	top: calc(100% + .25rem);
	z-index: 5;
	width: max-content;
	max-width: 300px;
	flex-wrap: wrap;
	gap: .3rem;
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	box-shadow: 0 8px 24px rgba(0, 0, 0, .15);
	padding: .5rem .6rem;
}

.import-photo-tags:hover .import-photo-tags-full {
	display: flex;
}

.import-open-link {
	font-size: .85rem;
	white-space: nowrap;
}

.import-lightbox-link {
	font-size: .8rem;
	white-space: nowrap;
}

.import-photo-in-target {
	opacity: .55;
	cursor: default;
}

/* --- Sharing center ----------------------------------------------------- */

.sharing-recipient-card,
.sharing-isolated-card {
	margin-bottom: 1rem;
}

.sharing-recipient-heading {
	display: flex;
	align-items: center;
	gap: .5rem;
	margin: 0 0 .75rem;
}

/* Same pastel "this is a group" signal used everywhere else (lightbox tag
   chips, import filter table) — plain surface-2 pill for a contact. */
.sharing-kind-badge {
	font-size: .7rem;
	font-weight: 600;
	text-transform: uppercase;
	letter-spacing: .02em;
	color: var(--text-muted);
	background: var(--surface-2);
	border-radius: 999px;
	padding: .1rem .55rem;
}

.sharing-kind-badge-group {
	color: var(--group-text);
	background: var(--group-bg);
}

/* Admin dashboard's environment reference — neutral for the real
   production instance, the same warning orange as .toast-warning
   otherwise, so a test/dev instance's admin page can't be mistaken for
   the live one at a glance. */
.env-badge {
	font-size: .7rem;
	font-weight: 600;
	text-transform: uppercase;
	letter-spacing: .02em;
	color: var(--text-muted);
	background: var(--surface-2);
	border-radius: 999px;
	padding: .1rem .55rem;
}

.env-badge-nonlive {
	color: #5a2e14;
	background: #e0983f;
}

.sharing-item-list {
	display: flex;
	flex-direction: column;
	gap: .5rem;
	margin-bottom: .75rem;
}

.sharing-summary-thumbs {
	display: flex;
	flex-wrap: wrap;
	gap: .4rem;
	margin-top: .6rem;
}

.sharing-summary-thumb {
	width: 44px;
	height: 44px;
	font-size: .75rem;
	font-weight: 600;
}

.sharing-item-row {
	display: flex;
	align-items: center;
	gap: .6rem;
}

.sharing-item-title {
	flex: 1;
	min-width: 0;
	overflow-wrap: anywhere;
}

.sharing-links-label {
	margin: .75rem 0 .4rem;
	border-top: 1px solid var(--border);
	padding-top: .75rem;
}

.sharing-link-list {
	display: flex;
	flex-direction: column;
	gap: .5rem;
}

.sharing-link-row {
	display: flex;
	align-items: center;
	gap: .6rem;
	flex-wrap: wrap;
}

.sharing-link-description {
	font-style: italic;
	font-size: .85rem;
	margin: .35rem 0 .75rem;
}

.sharing-link-input {
	flex: 1 1 260px;
	min-width: 0;
	margin: 0;
	font-size: .8rem;
	font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
	padding: .35rem .5rem;
	color: var(--text-muted);
	background: var(--surface-2);
}

/* A <form> is block-level by default, which would drop the revoke button
   onto its own line inside an otherwise-inline row. */
.sharing-revoke-form {
	display: inline-block;
	margin: 0;
}

/* --- Sharing: content-selection tree (extends the import-page classes) - */

/* An album whose true, total membership ended up fully selected (see
   compute_share_items on the server) — reuses the import-album-count slot
   this text already lives in, just recolored to call out the state
   change from a plain item count. */
.import-album-count.sharing-select-full {
	color: var(--accent);
	font-weight: 600;
}

.sharing-options-card {
	margin-top: 1.5rem;
	max-width: 480px;
}

.sharing-expiry-label {
	margin: 1rem 0;
}

/* --- Sharing: recipient link results (extends sharing-center classes) -- */

.sharing-email-known {
	color: var(--accent);
	font-size: .8rem;
	font-weight: 600;
}

.sharing-email-unknown {
	font-size: .8rem;
}

.sharing-result-expander {
	display: block;
	margin-top: .5rem;
}

.sharing-member-table {
	margin-top: .5rem;
	border-top: 1px solid var(--border);
	padding-top: .5rem;
}

.sharing-member-row {
	display: flex;
	justify-content: space-between;
	gap: 1rem;
	padding: .25rem 0;
	font-size: .85rem;
}

/* --- Sharing: results-page content summary ------------------------------ */

.sharing-content-summary {
	max-height: 260px;
	overflow-y: auto;
	display: flex;
	flex-direction: column;
	gap: .5rem;
}

/* --- Public share-viewing page ------------------------------------------ */

.share-collapsible {
	background: var(--surface);
	border: 1px solid var(--border);
	border-radius: var(--radius);
	margin: .75rem 0;
	overflow: hidden;
}

.share-collapsible-header {
	display: flex;
	align-items: center;
	gap: .6rem;
	width: 100%;
	background: none;
	border: none;
	color: var(--text);
	font-family: inherit;
	font-weight: 600;
	font-size: 1rem;
	text-align: left;
	padding: .75rem 1rem;
	cursor: pointer;
}

.share-collapsible-header:hover {
	background: var(--surface-2);
}

.share-collapsible-chevron {
	color: var(--text-muted);
	transition: transform .15s;
}

.share-collapsible.expanded .share-collapsible-chevron {
	transform: rotate(90deg);
}

.share-collapsible-title {
	flex: 1;
	min-width: 0;
	overflow-wrap: anywhere;
}

/* Fixed (not flex: 1) with a max-width cap — without the cap, flexbox
   would rather shrink this (its natural, unconstrained size) than the
   title (flex: 1, min-width: 0, so it has no size floor to protect it),
   letting a long hint squeeze the title down instead of wrapping itself.
   The cap forces this to wrap once it outgrows it, keeping the title at
   its natural width. font-weight: normal against the header's own bold,
   so the hint doesn't visually compete with the title. */
.share-collapsible-hint {
	flex: 0 0 auto;
	max-width: 45%;
	text-align: right;
	font-weight: normal;
	font-size: .85rem;
	line-height: 1.3;
}

.share-collapsible-body {
	padding: 0 1rem 1rem;
}

/* Right-aligned so the Download button sits under the "X items" readout
   at the far end of the header row above it, rather than under the
   title at the left. .dropdown-menu's own default (left:0, growing
   rightward) would run the menu off the edge of the page from way out
   here on the right, so it's flipped to grow leftward from the button's
   right edge instead — scoped to just this row rather than changing the
   shared .dropdown-menu default every other dropdown in the app relies on. */
.share-download-row {
	text-align: right;
	margin: 0 0 .75rem;
}

.share-download-row .dropdown-menu {
	left: auto;
	right: 0;
}

/* --- Admin members page -------------------------------------------------- */

.admin-quota-form {
	display: flex;
	align-items: center;
	gap: .4rem;
}

.admin-quota-form input {
	width: 70px;
}

/* --- Sortable admin tables (members, ...) — see sortable-table.js -------- */

table.admin-table th.sortable-column {
	cursor: pointer;
	user-select: none;
}

table.admin-table th.sortable-column:hover {
	color: var(--text);
}

table.admin-table th.sorted-asc::after {
	content: " \25B2";
}

table.admin-table th.sorted-desc::after {
	content: " \25BC";
}
