/*=============================================================================
  Media Cloud — app.css
  Dark by default (data-theme="dark" on <html>); light is opt-in via the toggle.
  Responsive / mobile-first grid.
=============================================================================*/

:root {
	color-scheme: light;

	--bg:        #ffffff;
	--fg:        #1a1a1a;
	--muted:     #666666;
	--card:      #f2f2f4;
	--card-hi:   #e6e6ea;
	--accent:    #3b82f6;
	--bar:       #f8f8fa;
	--border:    #dddde2;
	--shadow:    rgba(0,0,0,0.15);
}

:root[data-theme="dark"] {
	color-scheme: dark;

	--bg:        #121316;
	--fg:        #e8e8ea;
	--muted:     #9a9aa2;
	--card:      #1e1f24;
	--card-hi:   #2a2b31;
	--accent:    #5b9dff;
	--bar:       #17181c;
	--border:    #2c2d33;
	--shadow:    rgba(0,0,0,0.5);
}

@media (prefers-color-scheme: dark) {
	:root:not([data-theme="light"]) { color-scheme: dark; }
}

* { box-sizing: border-box; }

html, body {
	margin: 0;
	padding: 0;
	background: var(--bg);
	color: var(--fg);
	font: 15px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}

#bar {
	position: sticky;
	top: 0;
	z-index: 10;
	display: flex;
	align-items: center;
	gap: 10px;
	padding: 10px 14px;
	background: var(--bar);
	border-bottom: 1px solid var(--border);
}

.spacer { flex: 1; }
.count  { color: var(--muted); font-size: 13px; }

.btn {
	background: var(--card);
	color: var(--fg);
	border: 1px solid var(--border);
	border-radius: 8px;
	padding: 6px 10px;
	cursor: pointer;
	font-size: 15px;
}
.btn:hover { background: var(--card-hi); }

#crumbs a {
	color: var(--accent);
	text-decoration: none;
	cursor: pointer;
}
#crumbs .sep { color: var(--muted); margin: 0 6px; }
#crumbs .here { color: var(--fg); font-weight: 600; }

#grid {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
	gap: 10px;
	padding: 14px;
}

@media (min-width: 700px) {
	#grid { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); }
}

.cell {
	position: relative;
	background: var(--card);
	border: 1px solid var(--border);
	border-radius: 10px;
	overflow: hidden;
	aspect-ratio: 1 / 1;
	cursor: pointer;
	display: flex;
	align-items: center;
	justify-content: center;
}
.cell:hover { border-color: var(--accent); }

.cell img {
	width: 100%;
	height: 100%;
	object-fit: cover;
	display: block;
	background: var(--card-hi);
}

.cell .label {
	position: absolute;
	left: 0; right: 0; bottom: 0;
	z-index: 1;                     /* above the thumbnail, below the filmstrip */
	padding: 6px 8px;
	font-size: 12px;
	color: #fff;
	background: linear-gradient(transparent, rgba(0,0,0,0.75));
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}

.cell.folder {
	flex-direction: column;
	gap: 6px;
	color: var(--fg);
}
.cell.folder .glyph { font-size: 48px; line-height: 1; }
.cell.folder .fname {
	font-size: 13px;
	padding: 0 8px;
	text-align: center;
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
	max-width: 100%;
}

.cell.other { color: var(--muted); font-size: 13px; padding: 8px; text-align: center; word-break: break-word; }

/*-----------------------------------------------------------------------------
  List view. The header and the rows share one column template so they line up.
-----------------------------------------------------------------------------*/

.listhead, .row {
	display: grid;
	grid-template-columns: 44px minmax(0, 1fr) 62px 84px 132px;
	align-items: center;
	gap: 10px;
}

.listhead {
	position: sticky;               /* top is set from the bar height in Init() */
	z-index: 9;
	padding: 8px 14px;
	background: var(--bar);
	border-bottom: 1px solid var(--border);
	color: var(--muted);
	font-size: 12px;
	text-transform: uppercase;
	letter-spacing: 0.04em;
}
.listhead.hidden { display: none; }

.sortbtn {
	background: none;
	border: 0;
	padding: 0;
	font: inherit;
	color: var(--muted);
	text-align: left;
	cursor: pointer;
	text-transform: inherit;
	letter-spacing: inherit;
}
.sortbtn:hover  { color: var(--fg); }
.sortbtn.active { color: var(--accent); font-weight: 600; }
.arrow { font-size: 10px; }

#grid.as-list {
	display: block;
	padding: 0;
}

.row {
	padding: 5px 14px;
	border-bottom: 1px solid var(--border);
	cursor: pointer;
	font-size: 14px;
	min-height: 46px;
}
.row:hover { background: var(--card); }

.row img.col-thumb {
	width: 40px;
	height: 40px;
	object-fit: cover;
	border-radius: 5px;
	background: var(--card-hi);
	display: block;
}
.row .col-thumb.glyph { font-size: 22px; text-align: center; color: var(--muted); }
.row .col-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.row.folder .col-name { font-weight: 600; }
.row .col-type, .row .col-size, .row .col-date { color: var(--muted); font-size: 12px; }
.row .col-size { text-align: right; }
.row .col-date { font-variant-numeric: tabular-nums; }

@media (max-width: 700px) {
	.listhead, .row { grid-template-columns: 44px minmax(0, 1fr) 118px; }
	.col-type, .col-size { display: none; }
}

.status { text-align: center; color: var(--muted); padding: 16px; min-height: 20px; }

.lightbox {
	position: fixed;
	inset: 0;
	z-index: 50;
	background: rgba(0,0,0,0.92);
	display: flex;
	align-items: center;
	justify-content: center;
	padding: 20px;
}
.lightbox.hidden { display: none; }

/* the figure catches its own clicks so only the backdrop closes the lightbox */
.lb-figure {
	margin: 0;
	max-width: 100%;
	max-height: 100%;
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 10px;
}

.lightbox img {
	max-width: 100%;
	max-height: calc(100vh - 100px);
	border-radius: 8px;
	box-shadow: 0 6px 40px var(--shadow);
	object-fit: contain;
}

.lb-caption {
	display: flex;
	align-items: center;
	gap: 14px;
	flex-wrap: wrap;
	justify-content: center;
	color: #d8d8dc;
	font-size: 13px;
}
.lb-orig { color: #7fb3ff; text-decoration: none; }
.lb-orig:hover { text-decoration: underline; }

.lb-close {
	position: absolute;
	top: 10px;
	right: 14px;
	width: 44px;
	height: 44px;
	font-size: 30px;
	line-height: 1;
	color: #e8e8ea;
	background: rgba(0,0,0,0.45);
	border: 1px solid rgba(255,255,255,0.25);
	border-radius: 50%;
	cursor: pointer;
}
.lb-close:hover { background: rgba(0,0,0,0.75); }

body.noscroll { overflow: hidden; }


/*-----------------------------------------------------------------------------
  Admin: selection, the action bar, dialogs, toasts.
-----------------------------------------------------------------------------*/

.btn.danger  { color: #ff6b6b; border-color: #7a3a3a; }
.btn.danger:hover { background: #3a2020; }
.btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn:disabled { opacity: 0.4; cursor: not-allowed; }

.actions {
	position: sticky;
	z-index: 11;                    /* above the list header */
	display: flex;
	align-items: center;
	gap: 8px;
	flex-wrap: wrap;
	padding: 8px 14px;
	background: var(--card);
	border-bottom: 1px solid var(--border);
}
.actions.hidden { display: none; }
.selcount { font-size: 13px; font-weight: 600; }

/* a checkbox on every tile and row, always visible so it works on touch too */
.pick {
	position: absolute;
	top: 6px;
	left: 6px;
	z-index: 2;
	width: 18px;
	height: 18px;
	accent-color: var(--accent);
	cursor: pointer;
	opacity: 0.45;                 /* recedes until it means something */
	transition: opacity 120ms;
}
.pick:hover,
.pick:focus-visible,
.pick:checked        { opacity: 1; }
.cell:hover .pick,
.row:hover .pick     { opacity: 1; }

@media (prefers-reduced-motion: reduce) { .pick { transition: none; } }

/* A THUMB IS NOT A MOUSE POINTER.
   Joe: "it is really, really hard to SELECT MEDIA in folder view with my thumb
   or finger: I keep opening the lightbox view of one photo instead. The
   checkbox is just too small and not in a good place (top left corner, right
   on the edge)."

   Three separate faults, all of them invisible on a desktop:

     18px against Apple's 44pt minimum, so the target was a sixth of the area
     a thumb actually lands on;

     flush into the corner, where the tile's own edge and the screen bezel
     both compete for the same press;

     opacity 0.45 lifted by :hover, and there is no hover on a touch screen,
     so it stayed faint permanently.

   The pointer:coarse query is the honest test: it asks about the input device
   rather than guessing from screen width, so a touch laptop gets this too and
   a narrow desktop window does not. */
@media (pointer: coarse) {
	.pick {
		width: 30px;
		height: 30px;
		top: 10px;
		left: 10px;
		opacity: 0.9;
		/* the box is 30px; the press target is 44 */
		box-shadow: 0 0 0 7px rgba(0,0,0,0.001);
		border-radius: 6px;
	}
	.cell.picked .pick { opacity: 1; }
}
.row .pick { position: static; margin: 0; }
.cell.picked { outline: 3px solid var(--accent); outline-offset: -3px; }
.row.picked  { background: var(--card-hi); }

.listhead, .row { grid-template-columns: 24px 44px minmax(0, 1fr) 62px 84px 132px; }
@media (max-width: 700px) {
	.listhead, .row { grid-template-columns: 24px 44px minmax(0, 1fr) 118px; }
}

.crumb-edit {
	background: none;
	border: 0;
	color: var(--muted);
	cursor: pointer;
	font-size: 13px;
	padding: 0 4px;
}
.crumb-edit:hover { color: var(--accent); }

.modal {
	position: fixed;
	inset: 0;
	z-index: 60;
	background: rgba(0,0,0,0.6);
	display: flex;
	align-items: center;
	justify-content: center;
	padding: 20px;
}
.modal.hidden { display: none; }

.modal-box {
	background: var(--bg);
	border: 1px solid var(--border);
	border-radius: 12px;
	box-shadow: 0 10px 50px var(--shadow);
	width: min(520px, 100%);
	max-height: 80vh;
	/* see .modal-box.wide below: a sort plan is a wall of paths and 520px wraps
	   every one of them */
	display: flex;
	flex-direction: column;
	padding: 16px;
	gap: 12px;
}
.modal-box h3 { margin: 0; font-size: 16px; }
#modal-body   { overflow: auto; }
.modal-foot   { display: flex; justify-content: flex-end; gap: 8px; }

.modal input[type=text] {
	width: 100%;
	padding: 9px 10px;
	font: inherit;
	color: var(--fg);
	background: var(--card);
	border: 1px solid var(--border);
	border-radius: 8px;
}

.pickpath { color: var(--muted); font-size: 13px; margin-bottom: 8px; word-break: break-all; }
.picklist { list-style: none; margin: 0; padding: 0; }
.picklist li {
	padding: 8px 10px;
	border-radius: 8px;
	cursor: pointer;
	font-size: 14px;
}
.picklist li:hover { background: var(--card); }
.picklist li.up { color: var(--muted); }

.toast {
	position: fixed;
	left: 50%;
	bottom: 24px;
	transform: translateX(-50%);
	z-index: 70;
	max-width: min(560px, 92vw);
	padding: 11px 16px;
	border-radius: 10px;
	background: var(--card);
	border: 1px solid var(--border);
	box-shadow: 0 6px 30px var(--shadow);
	font-size: 14px;
	white-space: pre-wrap;
}
.toast.hidden { display: none; }
.toast.bad { border-color: #7a3a3a; color: #ff9d9d; }


/*-----------------------------------------------------------------------------
  Media kinds: videos carry a keyframe plus a play badge; the kinds we cannot
  thumbnail (RAW, audio, documents) get a legible chip instead of a blank tile.
-----------------------------------------------------------------------------*/

/* running time, bottom right, the way every video player labels a thumbnail */
.playbadge {
	position: absolute;
	right: 6px;
	bottom: 6px;
	z-index: 1;
	display: inline-flex;
	align-items: center;
	gap: 4px;
	padding: 2px 6px;
	border-radius: 4px;
	background: rgba(0,0,0,0.78);
	color: #fff;
	font-size: 12px;
	font-weight: 600;
	font-variant-numeric: tabular-nums;
	letter-spacing: 0.02em;
	line-height: 1.5;
	pointer-events: none;
}

.cell.kindcell, .cell.nothumb {
	flex-direction: column;
	gap: 8px;
	color: var(--muted);
}

/* a line icon for the kind, plus the file's own extension underneath — the
   extension is what actually identifies an unfamiliar type */
.cell .glyph {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 7px;
	color: var(--muted);
}
.cell .glyph svg { width: 42px; height: 42px; }

.exttag {
	font-size: 11px;
	font-weight: 700;
	letter-spacing: 0.09em;
	padding: 2px 7px;
	border: 1px solid var(--border);
	border-radius: 4px;
	color: var(--muted);
	background: var(--bg);
}

.row .glyph svg { width: 22px; height: 22px; display: block; margin: 0 auto; }
.row .col-thumb.glyph { color: var(--muted); }

/* list-view thumbnails share the grid derivative and mark video the same way */
.rowthumb { position: relative; display: block; width: 40px; height: 40px; }
.rowthumb img {
	width: 40px; height: 40px;
	object-fit: cover;
	border-radius: 5px;
	background: var(--card-hi);
	display: block;
}
.rowthumb.isvideo::after {
	content: "\25B6";
	position: absolute;
	right: -2px; bottom: -2px;
	width: 16px; height: 16px;
	border-radius: 50%;
	background: rgba(0,0,0,0.7);
	color: #fff;
	font-size: 8px;
	line-height: 16px;
	text-align: center;
	padding-left: 1px;
}
.rowthumb.nothumb { background: var(--card-hi); border-radius: 5px; }
.row .col-thumb.glyph.kind-raw, .row .col-thumb.glyph.kind-doc { font-size: 9px; padding: 3px 4px; }

/* the lightbox plays video in place rather than navigating away */
.lightbox video {
	max-width: 100%;
	max-height: calc(100vh - 100px);
	border-radius: 8px;
	background: #000;
	box-shadow: 0 6px 40px var(--shadow);
}
.lightbox video.hidden, .lightbox img.hidden { display: none; }

a.btn { text-decoration: none; display: inline-flex; align-items: center; line-height: 1; }


/* A quarter-turned video is drawn rotated, so its box has to swap width and
   height or the player is letterboxed into the wrong shape. */
.lightbox video.turned {
	max-width: calc(100vh - 100px);
	max-height: 100vw;
}


/*-----------------------------------------------------------------------------
  The trash is somewhere else. Deleting from here is permanent, so the room
  should not look like every other room — a warm terracotta ground, in both
  themes, so you always know where you are standing.
-----------------------------------------------------------------------------*/

body.in-trash {
	--bg:      #fdf4f1;
	--fg:      #3b1f18;
	--muted:   #8a5f52;
	--card:    #f6e3dc;
	--card-hi: #eed3c9;
	--bar:     #f4ded6;
	--border:  #e0bdb0;
}

:root[data-theme="dark"] body.in-trash,
body.in-trash {
	background: var(--bg);
}

@media (prefers-color-scheme: dark) {
	:root:not([data-theme="light"]) body.in-trash {
		--bg:      #1d1210;
		--fg:      #f0ddd6;
		--muted:   #b08c80;
		--card:    #2b1a16;
		--card-hi: #3a241e;
		--bar:     #2a1714;
		--border:  #4d2f26;
	}
}
:root[data-theme="dark"] body.in-trash {
	--bg:      #1d1210;
	--fg:      #f0ddd6;
	--muted:   #b08c80;
	--card:    #2b1a16;
	--card-hi: #3a241e;
	--bar:     #2a1714;
	--border:  #4d2f26;
}

/* the chip stops being a door once you are through it */
#trash.here {
	background: #a8402f;
	border-color: #a8402f;
	color: #fff;
	cursor: default;
	font-weight: 600;
}
#trash.here:hover { background: #a8402f; }


/*-----------------------------------------------------------------------------
  Folder icons draw in currentColor, so they carry the same weight as the text
  beside them in either theme. The emoji they replaced rendered as a fixed
  light-grey glyph that all but vanished on a dark ground.
-----------------------------------------------------------------------------*/

.cell .glyph.folder-icon svg { width: 46px; height: 46px; }
.cell.folder .glyph.folder-icon { color: var(--accent); }

.row .col-thumb.glyph.folder-icon { color: var(--accent); }
.row .col-thumb.glyph.folder-icon svg { width: 24px; height: 24px; }


/* shift-click sweeps a range, and a browser's instinct is to paint a text
   selection across everything it passes over */
.cell, .row { user-select: none; -webkit-user-select: none; }


/*-----------------------------------------------------------------------------
  Search sits in the bar, scoped to wherever you are standing.
-----------------------------------------------------------------------------*/

.searchform { display: flex; align-items: center; gap: 9px; flex: 0 1 auto; }

#q {
	width: clamp(140px, 26vw, 300px);
	padding: 6px 10px;
	font: inherit;
	font-size: 14px;
	color: var(--fg);
	background: var(--card);
	border: 1px solid var(--border);
	border-radius: 8px;
}
#q:focus { outline: 2px solid var(--accent); outline-offset: -1px; }

/* Two stacked radios. A checkbox that renamed itself when ticked gave no way to
   see what the other state was; a radio group shows both choices at rest. */
.scopes {
	display: flex;
	flex-direction: column;
	gap: 1px;
	flex: 0 0 auto;
	font-size: 12px;
	line-height: 1.25;
}

.scoperow {
	display: flex;
	align-items: center;
	gap: 5px;
	color: var(--muted);
	white-space: nowrap;
	cursor: pointer;
}
.scoperow:has(input:checked) { color: var(--fg); font-weight: 600; }
.scoperow input {
	accent-color: var(--accent);
	cursor: pointer;
	width: 13px;
	height: 13px;
	margin: 0;
}
.scoperow input:disabled { cursor: not-allowed; opacity: 0.4; }
.scoperow:has(input:disabled) { opacity: 0.5; cursor: not-allowed; }

.searchcrumb { color: var(--accent); }

/*-----------------------------------------------------------------------------
  THE SEARCH ROW ON A PHONE.

  #bar wraps and hands the whole form its own row, and that was where the
  fixing stopped: the FORM itself did not wrap. Six controls on one line -- the
  box, the help dot, Photos/Videos/Both, and two scope radios -- needed 382
  pixels in a 370 pixel row, so "everything" was sliced in half at the screen
  edge. Twelve pixels, and it made the whole header look broken.

  Two lines now. The box takes the first with the help dot beside it; the kind
  buttons and the scope radios share the second, and the radios drop to a third
  line rather than being cut if the phone is narrower still.
-----------------------------------------------------------------------------*/
@media (max-width: 700px) {
	#bar { flex-wrap: wrap; }
	.searchform {
		order: 5;
		flex-basis: 100%;
		flex-wrap: wrap;
		row-gap: 8px;
	}
	#q      { flex: 1 1 140px; width: auto; min-width: 0; }
	.qhelp  { flex: 0 0 auto; }
	.kinds  { flex: 1 1 auto; }
	.scopes { flex: 0 0 auto; }
}

/*-----------------------------------------------------------------------------
  THE TOP BAR ON A PHONE. Joe: "top nav a mess (almost unusable)".

  It was, and the cause was one element: #crumbs had no width limit, so
  "HOME > soccer/1999-Matthew/2017-Aztecs-BU17/2017-05-21-Sun > River" wrapped
  onto two lines, shoved the kebab onto a row of its own, and squeezed the
  search box down to about 40 pixels showing a single letter.

  Fixed by giving the breadcrumb its own row and letting it scroll sideways
  rather than wrap. Scroll, not ellipsis: the END of a path is the part that
  says where you are, and truncating from the right hides exactly that.
-----------------------------------------------------------------------------*/
@media (max-width: 700px) {
	#crumbs {
		flex: 1 1 100%;
		order: 1;
		min-width: 0;
		white-space: nowrap;
		overflow-x: auto;
		overscroll-behavior-x: contain;
		-webkit-overflow-scrolling: touch;
		scrollbar-width: none;
		font-size: 14px;
		padding-bottom: 2px;
	}
	#crumbs::-webkit-scrollbar { display: none; }

	/* the logo, the count and the two buttons share one tidy row */
	#bar     { gap: 8px; padding: 8px 10px; row-gap: 6px; }
	.count   { order: 2; font-size: 12px; }
	.spacer  { order: 3; }

	/* A SEARCH BOX THAT CANNOT COLLAPSE. It had width:100% and still came out
	   at one character wide, because a flex item's automatic minimum size is
	   its content, and everything else in the row had already claimed the
	   space. min-width:0 on the form plus a floor on the input fixes both
	   halves of that. */
	.searchform { min-width: 0; display: flex; gap: 6px; align-items: center; }
	#q          { min-width: 7rem; font-size: 16px; }

	/* 16px is not a style choice: iOS zooms the whole page when a focused
	   input is smaller, which is why the page lurched sideways when the
	   keyboard came up. */

	/* touch targets in the bar */
	.btn, .kind { min-height: 40px; }
	#menu       { min-width: 44px; min-height: 44px; }
}


/*-----------------------------------------------------------------------------
  "Show in folder" — a search hit arrives with no surroundings, and the
  surroundings are usually the reason you were looking.
-----------------------------------------------------------------------------*/

.lb-in { display: inline-flex; align-items: center; gap: 5px; }
.lb-in.hidden { display: none; }
.lb-folder-icon { font-size: 12px; opacity: 0.75; }
#lightbox-where { max-width: 46ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* on a result tile the folder identifies the hit better than a camera serial */
.label.wherelabel { font-style: italic; }

#scopename { max-width: 16ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }


/*-----------------------------------------------------------------------------
  Photos / Videos / Both. A segmented control rather than three radio buttons:
  it takes a third of the width in a bar that is already busy, and the chosen
  state is legible at a glance rather than needing a filled dot to be found.
-----------------------------------------------------------------------------*/

.kinds {
	display: inline-flex;
	border: 1px solid var(--border);
	border-radius: 8px;
	overflow: hidden;
	flex: 0 0 auto;
}

.kind {
	background: var(--card);
	border: 0;
	border-right: 1px solid var(--border);
	padding: 6px 11px;
	font: inherit;
	font-size: 12.5px;
	color: var(--muted);
	cursor: pointer;
	white-space: nowrap;
}
.kind:last-child { border-right: 0; }
.kind:hover { background: var(--card-hi); color: var(--fg); }
.kind.on {
	background: var(--accent);
	color: #fff;
	font-weight: 600;
}

@media (max-width: 700px) {
	.kind { padding: 6px 9px; font-size: 12px; }
}


/*-----------------------------------------------------------------------------
  KLOUD. One letter, set in the accent, ahead of the breadcrumbs — a mark rather
  than a banner, because the bar is a working surface and every pixel it takes
  is a pixel of somebody's photographs.
-----------------------------------------------------------------------------*/

/* The anchor is now the flex child, so the divider and the spacing belong to
   it. Left on the <img>, the hit area would be the artwork alone sitting inside
   an anchor with no size, and the 9px of padding would not be clickable. */
.wordmark-link {
	flex: 0 0 auto;
	display: flex;
	align-items: center;
	padding-right: 9px;
	margin-right: 2px;
	border-right: 1px solid var(--border);
	border-radius: 3px;
	text-decoration: none;
}
.wordmark-link:hover .wordmark { opacity: 0.72; }

/* A visible focus ring, because this is now a keyboard stop and it is the
   first one on the page. */
.wordmark-link:focus-visible {
	outline: 2px solid var(--accent, #4a9eff);
	outline-offset: 2px;
}

.wordmark {
	flex: 0 0 auto;
	width: 38px;
	height: 38px;
	object-fit: contain;
	user-select: none;
	cursor: pointer;
	transition: opacity 0.12s ease;
	/* Joe's mark is drawn in black line on transparency, so on a dark ground it
	   has to be inverted rather than redrawn — his artwork, both themes. */
	filter: none;
}

@media (prefers-color-scheme: dark) {
	:root:not([data-theme="light"]) .wordmark { filter: invert(1); }
}
:root[data-theme="dark"] .wordmark { filter: invert(1); }

@media (max-width: 700px) {
	.wordmark { width: 30px; height: 30px; }
	.wordmark-link { padding-right: 7px; }
}

/* the lightbox takes focus so Escape reaches us rather than the video player;
   it should not therefore grow a focus ring */
.lightbox:focus { outline: none; }


/* the frame position, shown only while you are stepping */
.lb-when {
	position: absolute;
	top: 14px;
	left: 50%;
	transform: translateX(-50%);
	z-index: 2;
	padding: 5px 13px;
	border-radius: 999px;
	background: rgba(0,0,0,0.78);
	color: #fff;
	font-size: 13px;
	font-variant-numeric: tabular-nums;
	letter-spacing: 0.02em;
	pointer-events: none;
}
.lb-when.hidden { display: none; }

.lb-hint { color: var(--muted); font-size: 12px; }
.lb-hint.hidden { display: none; }

/*-----------------------------------------------------------------------------
  Empty-folder call to action.

  Sorting a folder empties it but leaves it standing. Rather than make the user
  climb to the parent, find the child again in a list of forty and delete the
  right one, the offer is made where they already are.

  Deliberately quiet: this is a soft delete into the parent's trash, so it gets
  a plain card and an ordinary button, not a red alarm.
-----------------------------------------------------------------------------*/
#emptycta {
	max-width: 30rem;
	margin: 8vh auto 3rem;
	padding: 28px 26px;
	text-align: center;
	background: var(--card);
	border: 1px solid var(--border);
	border-radius: 10px;
}

#emptycta .ec-title {
	margin: 0;
	font-size: 17px;
	font-weight: 600;
}

#emptycta .ec-note,
#emptycta .ec-refuse {
	margin: 10px 0 0;
	font-size: 13.5px;
	line-height: 1.45;
	color: var(--muted);
}

/* a refusal has to say why: a missing button with no explanation reads as a bug */
#emptycta .ec-refuse {
	color: var(--fg);
	background: var(--bg);
	border-left: 3px solid #b8863b;
	border-radius: 0 5px 5px 0;
	padding: 9px 12px;
	text-align: left;
}

#emptycta .ec-go {
	margin-top: 18px;
	padding: 9px 20px;
	font: 600 14px/1 inherit;
	font-family: inherit;
	color: #fff;
	background: var(--accent);
	border: 0;
	border-radius: 7px;
	cursor: pointer;
}

#emptycta .ec-go:hover  { filter: brightness(1.08); }
#emptycta .ec-go:disabled { opacity: .5; cursor: default; }
#emptycta .ec-go:focus-visible {
	outline: 2px solid var(--fg);
	outline-offset: 2px;
}

#emptycta .ec-fine {
	margin: 12px 0 0;
	font-size: 12px;
	color: var(--muted);
}

/*-----------------------------------------------------------------------------
  A generic .hidden.

  There were nine rules for it — .listhead.hidden, .lightbox.hidden, and so on —
  one per element that happened to need it, and NONE that matched a button. So
  the JS that hides the "empty trash" button outside the trash, and the trash
  chip when the trash is empty, added a class that no rule acted on. Both
  toggles read as correct in the source and did nothing on screen.

  Kept last and !important so it beats the display a component sets for itself,
  whichever stylesheet loads after this one.
-----------------------------------------------------------------------------*/
.hidden { display: none !important; }

/*-----------------------------------------------------------------------------
  Folder names are anchors now.

  A div with an onclick gives the browser nothing to work with: no cmd-click for
  a new tab, no middle-click, no URL in the status bar, no "Open Link in New
  Tab" in the context menu. A real href gives all four away for free.

  These rules only undo what the browser adds to a link. The folders must look
  exactly as they did -- the whole tile was already clickable, so underlining
  the name now would suggest the rest had stopped working.
-----------------------------------------------------------------------------*/
.cell.folder .fname,
.row.folder  .col-name {
	color: inherit;
	text-decoration: none;
	display: block;          /* an inline anchor cannot do text-overflow */
}

.cell.folder .fname:focus-visible,
.row.folder  .col-name:focus-visible {
	outline: 2px solid var(--accent);
	outline-offset: 2px;
	border-radius: 3px;
}

/* the nested case still draws the empty folder tiles, so the card sits nearer */
#emptycta.has-tiles { margin-top: 3vh; }

/*-----------------------------------------------------------------------------
  Failure panel.

  A failed search used to say "error: HTTP 503" in small grey text in the corner
  where the file count goes. That is not an explanation -- it does not say what
  broke, whether it will come back, or what to do. The server knows all three
  and says so in `detail`; this is where that lands, at a readable size, in the
  space the results would have filled.
-----------------------------------------------------------------------------*/
#failure {
	max-width: 34rem;
	margin: 8vh auto 3rem;
	padding: 24px 26px;
	text-align: center;
	background: var(--card);
	border: 1px solid var(--border);
	border-left: 3px solid #c2703c;
	border-radius: 10px;
}

#failure .f-title {
	margin: 0;
	font-size: 17px;
	font-weight: 600;
}

#failure .f-body {
	margin: 10px 0 0;
	font-size: 14px;
	line-height: 1.5;
	color: var(--fg);
	text-align: left;
}

#failure .f-retry {
	margin-top: 18px;
	padding: 8px 18px;
	font: 600 14px/1 inherit;
	font-family: inherit;
	color: #fff;
	background: var(--accent);
	border: 0;
	border-radius: 7px;
	cursor: pointer;
}
#failure .f-retry:hover { filter: brightness(1.08); }
#failure .f-retry:focus-visible { outline: 2px solid var(--fg); outline-offset: 2px; }

#failure .f-fine {
	margin: 14px 0 0;
	font-size: 11.5px;
	color: var(--muted);
	font-variant-numeric: tabular-nums;
}

/*-----------------------------------------------------------------------------
  Capture-year range, and the camera's own record under a photograph.
-----------------------------------------------------------------------------*/
.years {
	display: flex;
	align-items: center;
	gap: 4px;
	flex: none;
}

.years input {
	width: 4.6rem;
	padding: 5px 7px;
	font: inherit;
	font-size: 13px;
	color: var(--fg);
	background: var(--card);
	border: 1px solid var(--border);
	border-radius: 6px;
	text-align: center;
	font-variant-numeric: tabular-nums;
	-moz-appearance: textfield;
}
.years input::-webkit-outer-spin-button,
.years input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.years input::placeholder { color: var(--muted); }
.years input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.ydash { color: var(--muted); font-size: 13px; }

@media (max-width: 700px) { .years { display: none; } }

.lb-exif {
	color: var(--muted);
	font-size: 12px;
	font-variant-numeric: tabular-nums;
}
.lb-exif.hidden { display: none; }

/*-----------------------------------------------------------------------------
  Present to the form, invisible to the reader.

  Used for the search form's submit button, which exists so that pressing Enter
  works at all -- see the comment in index.html. Deliberately NOT display:none,
  which would stop it being the form's default button.
-----------------------------------------------------------------------------*/
.visually-hidden {
	position: absolute;
	width: 1px;
	height: 1px;
	margin: -1px;
	padding: 0;
	border: 0;
	overflow: hidden;
	clip: rect(0 0 0 0);
	clip-path: inset(50%);
	white-space: nowrap;
}

/*-----------------------------------------------------------------------------
  Delete from inside the player.

  TOP-LEFT WAS WRONG, TWICE OVER. It read as a primary action -- a filled red
  button, the first thing the eye meets -- for something wanted on roughly one
  photograph in a thousand. And the corner it sat in is the corner the
  browser's own Back button sits in: a missed Back landed on Delete.

  So: the far bottom-right, the one corner with nothing else in it. Save, Share
  and the kebab are top-right, the close cross is top-right, Back and Reload
  are the browser's top-left. Nothing of ours is within reach of this.

  And QUIET until touched. Dim grey on almost nothing, taking the red only on
  hover and focus, so it states what it is at the moment somebody has actually
  aimed at it and not before.
-----------------------------------------------------------------------------*/
.lb-del {
	position: absolute;
	right: 18px;
	bottom: 16px;
	z-index: 3;
	padding: 6px 12px;
	font: 500 12px/1 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
	color: rgba(255, 255, 255, 0.4);
	background: rgba(0, 0, 0, 0.35);
	border: 1px solid rgba(255, 255, 255, 0.15);
	border-radius: 7px;
	cursor: pointer;
	backdrop-filter: blur(4px);
	transition: color .12s ease, background .12s ease, border-color .12s ease;
}
.lb-del:hover, .lb-del:focus-visible {
	color: #ffd9d9;
	background: rgba(122, 34, 34, 0.9);
	border-color: rgba(255, 120, 120, 0.5);
}
.lb-del:disabled { opacity: .5; cursor: default; }
.lb-del:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }

/* On a phone the metadata panel is the full width of the screen, so it would
   run underneath the button. Give it room to end above it. */
@media (max-width: 560px) {
	.lb-del  { right: 12px; bottom: 12px; }
	.lb-meta { padding-bottom: 42px; }
}

/*-----------------------------------------------------------------------------
  Folder menu, and the sort dry run.

  The plan is deliberately plain monospace, one file per line: 447 lines exist
  to be scanned for the one that looks wrong, and proportional text with pretty
  cards would make that harder, not easier.
-----------------------------------------------------------------------------*/
.kebab { position: relative; }

.menupop {
	position: absolute;
	top: calc(100% + 6px);
	right: 0;
	z-index: 40;
	min-width: 15rem;
	padding: 6px;
	background: var(--bar);
	border: 1px solid var(--border);
	border-radius: 9px;
	box-shadow: 0 8px 24px var(--shadow);

	/* IT HAS TO FIT, AND SCROLL WHEN IT DOES NOT.
	   Fourteen items is taller than an iPhone below the bar it hangs from, so
	   the last three sat under the browser chrome with no way to reach them.
	   dvh rather than vh: Safari's vh is the height with the URL bar HIDDEN,
	   so a vh-based cap still overflows while the bar is showing -- which is
	   exactly when somebody is opening a menu. */
	max-height: calc(100dvh - 96px);
	overflow-y: auto;
	overscroll-behavior: contain;          /* do not scroll the grid behind it */
	-webkit-overflow-scrolling: touch;
}
@supports not (height: 100dvh) {
	.menupop { max-height: calc(100vh - 150px); }
}
/*-----------------------------------------------------------------------------
  THE FOLDER MENU ON A PHONE.

  It hangs off the kebab with `right: 0`, so it grows LEFTWARDS from wherever
  that button happens to be. On a desktop the button is at the right-hand end
  of a wide bar and there is nothing but room. On a phone the bar wraps, the
  kebab lands about 340 pixels from the left, and a menu wider than that runs
  straight off the edge of the screen: Joe got "...ark", "...folder...",
  "...es in this folder...", every label beheaded at the left margin.

  So on a phone it stops hanging off the button and spans the screen instead,
  eight pixels from each edge, hung under the bar. It cannot run off either
  side because it is no longer anchored to anything that can move.

  #menupop only. The lightbox has its own menu, and that one opens from a
  button already at the right-hand edge with room to spare.
-----------------------------------------------------------------------------*/
@media (max-width: 700px) {
	#menupop {
		position: fixed;
		top: calc(var(--barh, 56px) + 6px);
		left: 8px;
		right: 8px;
		width: auto;
		min-width: 0;
		max-height: calc(100dvh - var(--barh, 56px) - 90px);
	}
}

.menupop.hidden { display: none; }

.menupop .mi {
	display: block;
	width: 100%;
	padding: 9px 12px;
	font: inherit;
	font-size: 14px;
	text-align: left;
	color: var(--fg);
	background: none;
	border: 0;
	border-radius: 6px;
	cursor: pointer;
}
.menupop .mi:hover:not(:disabled) { background: var(--card-hi); }
.menupop .mi:disabled { color: var(--muted); cursor: default; }
.menupop .misep { height: 1px; margin: 6px 4px; background: var(--border); }

.sortsum {
	margin: 0 0 12px;
	font-size: 13.5px;
	color: var(--muted);
	font-variant-numeric: tabular-nums;
}

.sortplan {
	margin: 0;
	max-height: 58vh;
	overflow: auto;
	padding: 14px 16px;
	font: 12.5px/1.55 ui-monospace, SFMono-Regular, Menlo, monospace;
	color: var(--fg);
	background: var(--card);
	border: 1px solid var(--border);
	border-radius: 8px;
	white-space: pre;
}

/*-----------------------------------------------------------------------------
  Save and Share, top-right of the player, left of the close cross.

  These replace Chrome's own overflow menu, which cannot open: keeping focus out
  of the controls is what makes Escape work, and that menu needs the focus it can
  no longer have. Rather than leave a button that does nothing, the menu is
  suppressed and the two things anybody wanted from it are buttons.
-----------------------------------------------------------------------------*/
.lb-tools {
	position: absolute;
	top: 14px;
	right: 68px;
	z-index: 3;
	display: flex;
	gap: 8px;
}

.lb-tool {
	padding: 7px 14px;
	font: 600 13px/1 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
	color: #e8e8ea;
	background: rgba(0, 0, 0, 0.45);
	border: 1px solid rgba(255, 255, 255, 0.25);
	border-radius: 7px;
	cursor: pointer;
	backdrop-filter: blur(4px);
}
.lb-tool:hover { background: rgba(0, 0, 0, 0.75); color: #fff; }
.lb-tool:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }

@media (max-width: 560px) {
	.lb-tools { right: 60px; gap: 6px; }
	.lb-tool  { padding: 6px 10px; font-size: 12px; }
}

/* the player's own menu, opening leftwards from the ⋮ beside Save and Share */
.lb-kebab { position: relative; }
.lb-kebab .menupop { top: calc(100% + 8px); right: 0; min-width: 13rem; }
.menupop .mi.hidden { display: none; }

/*-----------------------------------------------------------------------------
  Chrome's overflow button in the media controls.

  controlsList removes the ITEMS from that menu but not the button itself, so it
  sat there opening nothing -- and it cannot open, because keeping focus out of
  the controls is what makes Escape work. A control that can do nothing should
  not be drawn, so it is removed outright. Everything it would have offered is
  in our own menu beside Save and Share.
-----------------------------------------------------------------------------*/
#lightbox-vid::-webkit-media-controls-overflow-button,
#lightbox-vid::-webkit-media-controls-overflow-menu-list,
video::-webkit-media-controls-overflow-button { display: none !important; }

/* the tile you were just watching, so closing the player does not lose it */
.justwatched {
	outline: 3px solid var(--accent);
	outline-offset: -3px;
	border-radius: 4px;
}

.menupop .misep.hidden { display: none; }

/*-----------------------------------------------------------------------------
  A wide modal, for the sort plan.

  Those lines are full paths -- M2/M2-2005/2005-11-November/2005-11-05-Sat/ --
  and at 520px every one wrapped mid-path, which is exactly where a reader
  cannot afford to lose their place. The whole point of the dry run is that it
  can be SCANNED for the line that looks wrong.
-----------------------------------------------------------------------------*/
.modal-box.wide {
	width: min(1180px, 96vw);
	max-height: 88vh;
}

.modal-box.wide .sortplan {
	max-height: 68vh;
	font-size: 13px;
	line-height: 1.5;
}

/* People, the trash and empty-trash live in the menu now, so they take the
   menu's shape rather than a toolbar button's */
.menupop a.mi { text-decoration: none; }
.menupop .mi.danger { color: #ff8b8b; }
.menupop .mi.danger:hover:not(:disabled) { background: rgba(122, 34, 34, 0.35); }
.menupop .mi.here { color: var(--muted); }

/*-----------------------------------------------------------------------------
  Metadata under a thumbnail.

  For auditing the face index, not for browsing: it is deliberately quiet and
  deliberately off by default, because a wall of captions under 471 thumbnails
  is tiring to look at when you only came to find a photograph.
-----------------------------------------------------------------------------*/
.meta-block {
	width: 100%;
	padding: 4px 7px 5px;
	/* tight: the leaders are blue and the values grey, so the colour separates
	   the columns and the line spacing does not have to */
	font: 11px/1.2 ui-monospace, SFMono-Regular, Menlo, monospace;
	color: var(--muted);
	background: var(--card);
	border-top: 1px solid var(--border);
	text-align: left;
	word-break: break-word;
}

/* A CAPTION ROW MUST SURVIVE ITS CONTENTS.
   Joe: "the labels on the meta data keep getting clipped. only the first photo
   looks ok." XIF rendered as "IF", VID as "ID", FIL as "IL" -- the first
   character of every label eaten, on every tile whose EXIF held a
   several-hundred-character C2PA signature.

   The signature is fixed at the source (Media.CameraName), but the layout
   should never have been breakable by a value in the first place. A base64
   blob offers no space to wrap at, so the browser let it run past the row and
   took the label with it. overflow-wrap:anywhere gives it permission to break
   mid-token; min-width:0 stops a long child claiming the row's whole width. */
.meta-row {
	display: block;
	min-width: 0;
	overflow-wrap: anywhere;
	word-break: break-word;
}
.meta-row.faces { color: var(--fg); }

.meta-tag {
	display: inline-block;
	min-width: 3.1rem;
	/* the label is a fixed column and never gives ground to its value */
	flex: 0 0 auto;
	color: var(--accent);
	font-weight: 600;
	letter-spacing: .04em;
}

/* A CAPTIONED TILE IS A COLUMN, not a square.
   .cell is a centring flex ROW holding one image, so appending the block put it
   BESIDE the photograph in a 40px column -- "FACI / Marg / unna", every line
   clipped. With a caption the tile becomes a column: picture on top, words
   underneath, and the fixed 1:1 aspect ratio released so the words have room. */
.cell:has(.meta-block) {
	aspect-ratio: auto;
	flex-direction: column;
	align-items: stretch;
	justify-content: flex-start;
}

.cell:has(.meta-block) img {
	aspect-ratio: 1 / 1;
	width: 100%;
	object-fit: cover;
}

.cell:has(.meta-block) .meta-block { margin-top: auto; }

/* in list view it spans the whole row, under the columns */
.row:has(.meta-block) { height: auto; }
.row .meta-block { grid-column: 1 / -1; margin-top: 4px; }

/*-----------------------------------------------------------------------------
  Video tiles wear a filmstrip.

  A clip and a photograph looked nearly identical when the keyframe happened to
  be a still-looking room -- the only difference was a duration badge in one
  corner, which the eye does not pick up while scanning a grid.

  The perforations are ROUNDED and INSET from both sides of the band, which is
  what makes them read as film. The first attempt used a repeating linear
  gradient, and a gradient cannot round a corner: it produced hard-edged blocks
  spanning the full width of the band, which read as a stripe pattern rather
  than as holes. This is a real shape instead, tiled down the edge.
-----------------------------------------------------------------------------*/
.cell.video::before,
.cell.video::after {
	content: "";
	position: absolute;
	top: 0;
	bottom: 0;
	width: 13px;
	z-index: 2;
	pointer-events: none;
	background: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2213%22%20height%3D%2217%22%3E%3Crect%20width%3D%2213%22%20height%3D%2217%22%20fill%3D%22%230d0d0f%22%2F%3E%3Crect%20x%3D%223.2%22%20y%3D%224.2%22%20width%3D%226.6%22%20height%3D%228.6%22%20rx%3D%222.1%22%20fill%3D%22%23e6e6e8%22%2F%3E%3C%2Fsvg%3E") repeat-y left top / 13px 17px;
	opacity: .92;
}
.cell.video::before { left: 0; }
.cell.video::after  { right: 0; }

/* THE STRIP RUNS THE FULL HEIGHT, SO THE CAPTION HAS TO CLEAR IT.
   Joe, twice: "the labels on the meta data keep getting clipped." XIF rendered
   as "IF", VID as "ID", FIL as "IL" -- but only on VIDEO tiles, which is the
   clue I missed the first time. The one clean tile in his screenshot was the
   only photograph among them.

   The perforated band is absolutely positioned top:0 bottom:0 with z-index 2,
   so it spans the whole cell INCLUDING the caption, painting an opaque 13px
   column over it. The caption's own padding is 8px, which puts the first
   character underneath.

   Nothing was ever clipped in the DOM sense -- the text was all there, and a
   headless check of scrollWidth against clientWidth reported everything fine.
   It was simply painted over, which no measurement of the text box can see.

   The band staying full height is correct: real film has sprockets all the way
   down. So the caption clears it instead, and sits above it. */
.cell.video .label,
.cell.video .meta-block {
	padding-left: 16px;
	padding-right: 16px;
}

/* BOTH, because the cell has one or the other and never both.
   ItemCell builds a floating .label with the filename, and then, if there is
   metadata to show, calls oLabel.remove() and appends .meta-block in its
   place -- deliberately removed rather than hidden, so a class that fails to
   apply cannot leave the name printed across the metadata.

   My first fix styled only .label, which is exactly the element that is not
   there when metadata is on. The rule loaded, matched nothing, and the
   measurement kept reporting 8px against a rule that says 16. The browser
   said so plainly when finally asked: "no .label in it". */

/* the list view keeps its small thumbnail, so mark that too, at half scale */
.row.video .col-thumb { position: relative; }
.row.video .col-thumb::before,
.row.video .col-thumb::after {
	content: "";
	position: absolute;
	top: 0;
	bottom: 0;
	width: 6px;
	pointer-events: none;
	background: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2213%22%20height%3D%2217%22%3E%3Crect%20width%3D%2213%22%20height%3D%2217%22%20fill%3D%22%230d0d0f%22%2F%3E%3Crect%20x%3D%223.2%22%20y%3D%224.2%22%20width%3D%226.6%22%20height%3D%228.6%22%20rx%3D%222.1%22%20fill%3D%22%23e6e6e8%22%2F%3E%3C%2Fsvg%3E") repeat-y left top / 6px 8px;
	opacity: .9;
}
.row.video .col-thumb::before { left: 0; }
.row.video .col-thumb::after  { right: 0; }

/* the filename inside the caption: same column as the other leaders, but it is
   the thing you actually read, so it gets the foreground colour */
/*-----------------------------------------------------------------------------
  A FOLDER SAYS WHAT IS IN IT.

  Joe, on the legacy /dir he is asking us to beat: "By giving the user a hint
  about the contents, it saves an enormous amount of manual traversal."

  Five small frames and a phrase. The frames are 96px sources drawn at 34px, so
  they stay sharp on a 2x display without asking for a hundred 400px
  derivatives to paint one listing.
-----------------------------------------------------------------------------*/
.peek {
	display: inline-flex;
	align-items: center;
	gap: 3px;
	margin-left: 10px;
	vertical-align: middle;
	flex: 0 0 auto;
}

.peek-img {
	width: 34px;
	height: 34px;
	object-fit: cover;
	border-radius: 3px;
	background: var(--card);
	/* a preview is a hint, not the subject: it must not out-shout the name */
	opacity: 0.85;
	position: relative;
	transition: transform .12s ease, opacity .12s ease, box-shadow .12s ease;
}
.row:hover .peek-img { opacity: 1; }

/*-----------------------------------------------------------------------------
  MAGNIFY UNDER THE POINTER. Joe: "on all filmstrips, can we do a rollover
  effect that magnifies the filmstrip icon as we pass the mouse over it? they
  are TINY!!!"

  They are: 34 pixels, which is enough to say "green field" and not enough to
  say which field. The derivative behind them is 96 pixels, so scaling by three
  lands exactly on the pixels that already exist and stops there -- going
  further would only enlarge the blur.

  It costs no layout and no request. transform does not reflow, so the row does
  not jump and the other four frames do not move, and the bytes are already in
  the page.

  Not on a touch screen: :hover on a phone latches on tap and sticks, so a
  finger would leave a magnified thumbnail behind it. hover:hover asks whether
  the device actually has a pointer.
-----------------------------------------------------------------------------*/
@media (hover: hover) {
	.peek-img:hover {
		transform: scale(3);
		opacity: 1;
		z-index: 30;
		box-shadow: 0 6px 22px rgba(0, 0, 0, .65);
		border-radius: 2px;
	}
}

/* the magnified frame has to be allowed OUT of the row. col-name clips for the
   sake of the name's ellipsis, which would shear the top and bottom off it. */
.row.folder .col-name.folder-col { overflow: visible; }

.peek-more {
	color: var(--muted);
	font-size: 13px;
	padding: 0 2px;
}

.peek-count {
	margin-left: 10px;
	color: var(--muted);
	font-size: 12px;
	white-space: nowrap;
	vertical-align: middle;
}

/* in the grid the count sits under the folder name, on its own line */
.cell.folder .peek-count { margin: 2px 0 0; font-size: 11px; }

/* the name column holds the name, the strip and the count, so it has to be
   allowed to lay them out rather than clip them */
.row.folder .folder-col {
	display: flex;
	align-items: center;
	min-width: 0;
}
/* the name gives way first: a long folder name should ellipsise rather than
   push the hint off the row, because the hint is the reason the row is here */
.row.folder .fname-link {
	flex: 0 1 auto;
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

/*-----------------------------------------------------------------------------
  THREE COLUMNS ABOVE A PHONE, the way the Places page does it. Joe: "I
  really like the 3-column layout and how the filmstrip is in its own
  column... Can we make regular folders follow this same precedent?"

  It was a flex row, so the strip started wherever the folder name happened to
  end -- HOME has names from "K/" to "SORT-ME-VIDEOS/" and the strips stepped
  in and out down the page, with the counts stepping again behind them.

  GRID, not flex with fixed widths, for one reason: some folders have no
  previews at all. With flex, a row like "unpublished/" would pull its count
  left into the space where every other row carries a filmstrip, and the
  column of counts would break exactly where the eye is scanning it. A grid
  leaves that cell empty and everything after it stays put, which is why each
  of the three is pinned to its own column rather than left to auto-placement.
-----------------------------------------------------------------------------*/
@media (min-width: 701px) {
	.row.folder .folder-col {
		display: grid;
		grid-template-columns: 14rem 13rem 1fr;
		align-items: center;
		gap: 10px;
	}
	.row.folder .fname-link { grid-column: 1; }
	.row.folder .peek       { grid-column: 2; margin-left: 0; }
	.row.folder .peek-count { grid-column: 3; margin-left: 0; }
}

/*-----------------------------------------------------------------------------
  THE FOLDER ROW ON A PHONE. Joe: "no folder names visible".

  He was right, and it was this file's fault. The name, the filmstrip and the
  count sat on ONE line inside a column 146 pixels wide, needing up to 241.
  The name is the only one of the three that was allowed to shrink, so the name
  is what vanished -- measured at a width of exactly ZERO on nine of the first
  ten rows. A listing of folders with no folder names on it.

  The strip was supposed to be the hint and the name the subject; squeezed onto
  one line it became the reverse.

  So on a phone they stack. The name gets a line to itself, at the full width
  of the column, and the hint goes underneath where it can be as wide as it
  likes. Two lines per row costs vertical space, which a phone has, instead of
  horizontal space, which it does not.
-----------------------------------------------------------------------------*/
@media (max-width: 700px) {
	.peek-img:nth-child(n+4) { display: none; }

	/* WRAP, not column. A column gives the count a third line of its own and
	   a row 200 pixels tall, so seven folders fill the screen out of 53. The
	   name claims a whole line by taking 100% of the width; the strip and the
	   count then flow together onto the second. */
	.row.folder .folder-col {
		flex-wrap: wrap;
		align-items: center;
		row-gap: 3px;
	}
	.row.folder .fname-link { flex: 1 1 100%; max-width: 100%; }

	.peek       { margin-left: 0; }
	.peek-count { margin-left: 8px; font-size: 11px; }
}

/* Narrower still, and the date column is worth less than the name it is
   taking room from: a folder called 2026-09-12-Sat carries its own date. */
@media (max-width: 560px) {
	.listhead, .row { grid-template-columns: 24px 44px minmax(0, 1fr); }
	.col-date { display: none; }
}

.meta-row.fname-row { color: var(--fg); word-break: break-all; }
.cell .label.hidden { display: none; }

/* a name in the caption is a link into the People page; an unnamed face is an
   invitation to fix it, which is the only way this index ever gets better */
.meta-name {
	color: var(--accent);
	text-decoration: none;
	border-bottom: 1px dotted currentColor;
	cursor: pointer;
}
.meta-name:hover { text-decoration: none; border-bottom-style: solid; }

button.meta-name {
	font: inherit;
	background: none;
	border: 0;
	border-bottom: 1px dotted currentColor;
	padding: 0;
	color: var(--muted);
}
button.meta-name.unnamed:hover { color: var(--accent); }

.meta-namebox {
	font: inherit;
	width: 11rem;
	max-width: 100%;
	padding: 1px 5px;
	color: var(--fg);
	background: var(--bg);
	border: 1px solid var(--accent);
	border-radius: 4px;
}

/* With a caption the tile is taller than its picture, so the running time was
   landing over the words. Put it at the top of the thumbnail instead, opposite
   the tick box. */
.cell:has(.meta-block) .playbadge {
	top: 6px;
	bottom: auto;
}

/*-----------------------------------------------------------------------------
  Still loading.

  A line of flat grey "loading..." at the foot of an infinite scroll reads as
  "you have reached the end" -- it is the same weight and colour as the "— end —"
  that genuinely means it. Something MOVING says the opposite without needing to
  be read.
-----------------------------------------------------------------------------*/
.spinner {
	display: flex;
	justify-content: center;
	gap: 7px;
	padding: 14px 0 26px;
}
.spinner.hidden { display: none; }

.spinner i {
	width: 9px;
	height: 9px;
	border-radius: 50%;
	background: var(--accent);
	animation: bounce 1.05s infinite ease-in-out both;
}
.spinner i:nth-child(1) { animation-delay: -.26s; }
.spinner i:nth-child(2) { animation-delay: -.13s; }

@keyframes bounce {
	0%, 70%, 100% { transform: translateY(0);     opacity: .35; }
	35%           { transform: translateY(-7px);  opacity: 1;   }
}

/* somebody who has asked for less movement gets a steady pulse instead */
@media (prefers-reduced-motion: reduce) {
	.spinner i { animation: fade 1.4s infinite ease-in-out both; }
	@keyframes fade { 0%, 100% { opacity: .3; } 50% { opacity: 1; } }
}

/* the metadata panel under an open photograph: the same block as a tile
   caption, but there is room here, so it can breathe */
.lb-meta {
	width: min(46rem, 100%);
	align-self: center;
	flex: 0 0 auto;
}
.lb-meta.hidden { display: none; }
.lb-meta .meta-block {
	font-size: 12.5px;
	line-height: 1.5;
	padding: 9px 12px 10px;
	border: 1px solid rgba(255, 255, 255, .14);
	border-radius: 8px;
	background: rgba(0, 0, 0, .5);
	text-align: left;
}
.lb-meta .meta-tag { min-width: 4.2rem; }
.lb-meta .meta-namebox { width: 14rem; }

/* The figure has to give the panel its room. Without this the image keeps its
   full height and the panel is pushed off the bottom of the window, where it
   reads as not having rendered at all. */
.lb-figure { overflow-y: auto; }
.lightbox.has-meta img#lightbox-img,
.lightbox.has-meta video#lightbox-vid { max-height: calc(100vh - 260px); }

/* the empty state on the scoped pages: a zero result has to explain itself,
   or it reads as "nothing wrong here" when it may mean "nothing checked" */
.emptystate {
	max-width: 44rem;
	margin: 3rem auto;
	padding: 1.6rem 1.8rem 1.8rem;
	border: 1px solid var(--border);
	border-radius: 10px;
	background: var(--panel, rgba(255,255,255,.03));
	text-align: left;
}
.emptystate h2 {
	margin: 0 0 .7rem;
	font-size: 1.15rem;
	font-weight: 600;
}
.emptybody p { margin: 0 0 .7rem; line-height: 1.55; }
.emptybody p.emptynote { color: var(--dim); font-size: .92rem; }
.emptybody ul.emptylist {
	margin: 0 0 .8rem;
	padding-left: 1.2rem;
	color: var(--dim);
	font-size: .92rem;
	line-height: 1.55;
}
.emptybody ul.emptylist li { margin-bottom: .35rem; }
.emptybody code {
	font-size: .88em;
	padding: 1px 4px;
	border-radius: 3px;
	background: rgba(127,127,127,.18);
}
.emptyactions { margin: 1.1rem 0 0; font-size: .95rem; }

/* separates the "who is here" list from the "name this one" action, which
   otherwise reads as another name in the list */
.meta-sep { color: var(--dim); opacity: .55; margin: 0 .45em 0 .3em; }

/* an unnamed face in the lightbox: the crop, so you can see who you are
   naming, and the action right beside it */
.facechip {
	display: inline-flex;
	align-items: center;
	gap: .4rem;
	margin: 0 .5rem .25rem 0;
	padding: 2px 6px 2px 2px;
	border: 1px solid var(--border);
	border-radius: 999px;
	vertical-align: middle;
}
.facechip img {
	width: 34px;
	height: 34px;
	border-radius: 50%;
	object-fit: cover;
	display: block;
	background: rgba(127,127,127,.2);
}
.facechip .meta-name { font-size: 12px; }

/* a NAMED face chip reads settled; an unnamed one reads as a job to do */
.facechip.unnamed { border-style: dashed; }
.facechip .meta-name:not(.unnamed) {
	color: var(--link, #7fb3ff);
	border-bottom: 1px dotted currentColor;
}

/* "search instead for the topic june" — the other readings of a query, offered
   only when they would actually find something */
.also-bar {
	font-size: 12.5px;
	color: var(--dim);
	padding: 2px 0 0;
}
.also-bar.hidden { display: none; }
.also-link { color: var(--link, #7fb3ff); text-decoration: none; border-bottom: 1px dotted currentColor; }
.also-link:hover { border-bottom-style: solid; }

/* the "i" beside the search box, and what it opens */
.qhelp {
	background: none; border: 1px solid var(--border); color: var(--dim);
	border-radius: 50%; width: 20px; height: 20px; line-height: 1;
	font-size: 12px; font-style: italic; font-family: Georgia, serif;
	cursor: pointer; flex: 0 0 auto; margin-left: -6px;
}
.qhelp:hover { color: var(--fg); border-color: var(--link, #7fb3ff); }

.helpbox {
	position: fixed; z-index: 60; top: 3.2rem; left: 50%;
	transform: translateX(-50%);
	width: min(38rem, calc(100vw - 2rem));
	max-height: calc(100vh - 5rem); overflow-y: auto;
	background: var(--panel, #16181d); color: var(--fg);
	border: 1px solid var(--border); border-radius: 10px;
	padding: 1rem 1.2rem 1.2rem; box-shadow: 0 12px 40px rgba(0,0,0,.55);
	font-size: 13.5px; line-height: 1.5;
}
.helpbox.hidden { display: none; }
.helpbox h3 { font-size: 12px; letter-spacing: .12em; text-transform: uppercase;
              color: var(--dim); margin: 1rem 0 .4rem; }
.helpbox h3:first-of-type { margin-top: .2rem; }
.helpbox p { margin: 0 0 .6rem; }
.helpbox code { font-family: ui-monospace, Menlo, monospace; font-size: .92em;
                background: rgba(127,127,127,.18); padding: 1px 5px; border-radius: 3px; }
table.helptab { border-collapse: collapse; margin: 0 0 .7rem; width: 100%; }
table.helptab td { padding: .2rem .8rem .2rem 0; vertical-align: baseline; }
table.helptab td:last-child { color: var(--dim); }
.helpnote { color: var(--dim); font-size: 12.5px; }
.helpclose { position: absolute; top: .5rem; right: .6rem; background: none;
             border: 0; color: var(--dim); font-size: 20px; cursor: pointer; }

/* THE TWO SORTING DECISIONS, above the plan they change.
 *
 * They sit inside the dry-run dialog because they are not settings -- they are
 * the two questions that determine every destination in the list underneath,
 * and the list redraws when either changes. */
.sortchoice {
	display: flex;
	flex-wrap: wrap;
	gap: 1rem;
	margin: 0 0 1rem;
}
.sortchoice fieldset {
	flex: 1 1 18rem;
	min-width: 0;
	margin: 0;
	padding: .7rem .9rem .8rem;
	border: 1px solid var(--line, #333);
	border-radius: .45rem;
}
.sortchoice legend {
	padding: 0 .35rem;
	font-size: .7rem;
	text-transform: uppercase;
	letter-spacing: .07em;
	color: var(--dim, #888);
}
/* the warning belongs to the choice, not to a toast that appears after it */
.sortchoice .cnote {
	margin: 0 0 .5rem;
	font-size: .74rem;
	line-height: 1.45;
	color: var(--dim, #888);
}
.sortchoice .cradio {
	display: flex;
	align-items: baseline;
	gap: .45rem;
	padding: .22rem 0;
	cursor: pointer;
	font-size: .84rem;
}
.sortchoice .cradio input { margin: 0; flex: 0 0 auto; }
/* the example path is the actual explanation, so it gets its own line and
   wraps rather than being clipped */
.sortchoice .cradio code {
	display: block;
	margin-left: auto;
	font-size: .72rem;
	color: var(--dim, #888);
	overflow-wrap: anywhere;
}
.sortchoice .cradio.off { opacity: .5; cursor: not-allowed; }

/* The pairing code: read on one screen, typed into another by somebody who is
   not enjoying it. Big, spaced, unambiguous. */
.paircode {
	margin: .8rem 0 1.2rem;
	padding: .9rem;
	text-align: center;
	font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
	font-size: 2rem;
	letter-spacing: .35em;
	text-indent: .35em;          /* letter-spacing pads the right; put it back */
	border: 1px solid var(--line, #333);
	border-radius: .5rem;
	background: var(--sunk, rgba(255,255,255,.04));
	cursor: pointer;
	user-select: all;
}
ul.devlist { list-style: none; margin: .4rem 0 0; padding: 0; font-size: .85rem; }
ul.devlist li {
	display: flex; align-items: center; gap: .5rem;
	padding: .35rem 0; border-top: 1px solid var(--line, #333);
}
ul.devlist li .tinybtn { margin-left: auto; }
ul.devlist li.none { color: var(--dim, #888); border-top: 0; }
/* A token is long and must survive being copied exactly. Selectable in one
   tap, wrapping rather than clipped: a truncated token fails later, quietly. */
.pairtoken {
	margin: .5rem 0 1rem;
	padding: .7rem;
	font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
	font-size: .8rem;
	line-height: 1.5;
	overflow-wrap: anywhere;
	border: 1px solid var(--line, #333);
	border-radius: .45rem;
	background: var(--sunk, rgba(255,255,255,.04));
	cursor: pointer;
	user-select: all;
}

/* A clip that cannot play yet. Sized and centred like the picture it replaces,
   so the lightbox does not jump when one follows the other. */
.lb-recode
{
	max-width: 34rem;
	margin: 0 auto;
	padding: 2.5rem 2rem;
	text-align: center;
	color: #ddd;
	background: rgba(255, 255, 255, 0.04);
	border: 1px solid rgba(255, 255, 255, 0.10);
	border-radius: 12px;
}
.lb-recode-mark  { font-size: 2.6rem; line-height: 1; margin-bottom: 0.6rem; }
.lb-recode h3    { margin: 0 0 0.7rem; font-size: 1.15rem; font-weight: 600; }
.lb-recode p     { margin: 0 0 0.6rem; line-height: 1.5; }
.lb-recode-note  { color: #999; font-size: 0.9rem; }

/* THE ARRIVAL BAND. Above the grid, quiet, and absent when there is nothing to
   say. TWO bars rather than one blended percentage: photographs finish in
   minutes and videos at roughly 27 clips a minute, so a single number would sit
   still for twenty minutes and never mention that the photographs were in. */
.band
{
	margin: 0 0 0.9rem;
	padding: 0.65rem 0.85rem;
	border: 1px solid var(--border);
	border-left: 3px solid var(--accent);
	border-radius: 8px;
	background: rgba(127, 127, 127, 0.06);
	font-size: 0.92rem;
}
.band.done    { border-left-color: #3fb950; }
.band.stopped { border-left-color: #e3883a; }

.band-row   { display: flex; align-items: baseline; gap: 0.55rem; flex-wrap: wrap; }
.band-text  { font-weight: 600; }
.band-phase { color: #8b949e; font-size: 0.86rem; }

/* A dot that breathes while work is happening and holds still when it is not.
   Reduced motion drops the animation and keeps the colour, which is the part
   carrying the meaning. */
.band-dot
{
	width: 8px; height: 8px; border-radius: 50%;
	background: var(--accent);
	flex: 0 0 auto;
	animation: band-pulse 1.6s ease-in-out infinite;
}
.band.done    .band-dot { background: #3fb950; animation: none; }
.band.stopped .band-dot { background: #e3883a; animation: none; }

@keyframes band-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
@media (prefers-reduced-motion: reduce) { .band-dot { animation: none; } }

.band-bars { display: flex; gap: 0.5rem; margin-top: 0.5rem; }
.band-bar
{
	flex: 1 1 0;
	height: 4px;
	border-radius: 2px;
	background: rgba(127, 127, 127, 0.22);
	overflow: hidden;
}
.band-bar i
{
	display: block; height: 100%; width: 0;
	background: var(--accent);
	transition: width 0.4s ease;
}
.band.done .band-bar i { background: #3fb950; }

/* The pencil beside a name in the lightbox. Small, quiet, and a real target:
   the name itself now searches, so this is the only way to correct one and it
   has to be findable without being loud. 24px of hit area on a 10px glyph. */
.meta-fix
{
	/* A <button> in the lightbox chips and an <a> in the text caption, so the
	   button defaults have to be undone or it arrives with a grey box and a
	   border. */
	background: none;
	border: 0;
	cursor: pointer;
	font-family: inherit;

	margin-left: 0.4rem;
	padding: 0.1rem 0.3rem;
	font-size: 1.15em;
	line-height: 1;
	color: #8b949e;
	text-decoration: none;
	opacity: 0.75;
	transition: opacity 0.12s ease, color 0.12s ease;
}
.meta-fix:hover,
.meta-fix:focus-visible { opacity: 1; color: var(--accent); }
.meta-fix:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
