/* ===========================================================================
   Aile
   ---------------------------------------------------------------------------
   Design thesis: the interface has no color of its own. Every color on screen
   is borrowed from the photograph you are looking at -- the dominant color is
   measured at ingest and injected per card as --bloom, so the archive tints the
   UI rather than the other way round. Chrome stays in darkroom neutrals so the
   prints are the only saturated thing in the room.

   The one fixed accent is --imprint, the amber of a film date-stamp, used only
   at imprint scale in the mono face for metadata. It is a quotation, not a brand.

   Type is system-only by request: nothing loads from a third party, so opening
   the album leaks nothing to anyone but the family's own server.
   =========================================================================== */

:root {
  --ground: #0b0a09;
  --surface: #15120e;
  --surface-2: #1f1a14;
  --edge: #2c261e;
  --edge-bright: #453c30;
  /* Warm off-white rather than grey: this is an archive of paper, and a cold
     neutral reads wrong against sepia. The two quieter inks were genuinely too
     dim -- --ink-faint sat at 3.3:1 on the page, under the 4.5:1 anyone needs
     to read comfortably, and it is the most-used ink in the file. */
  --ink: #faf7f2;             /* 18.5:1 */
  --ink-dim: #d2cbc1;         /* 12.3:1, was 6.3 */
  --ink-faint: #a49c92;       /*  7.3:1, was 3.3 */
  --imprint: #f0a63c;
  --danger: #e2664f;
  --hover-edge: #f600ff;      /* grid hover outline -- reads against any photo */

  --bloom: #6b6258;                 /* overridden inline, per photo */

  /* Georgia carries the display voice: it ships nearly everywhere, and its
     old-style figures give dates and counts a period feel a UI sans cannot. */
  --font-display: Georgia, 'Iowan Old Style', 'Palatino Linotype', 'Times New Roman', serif;
  --font-ui: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  --font-mono: ui-monospace, 'SF Mono', 'Cascadia Mono', 'Segoe UI Mono', Consolas, Menlo, monospace;

  --gap: 10px;
  --bar-h: 58px;
  --lb-top: 62px;
  --lb-bottom: 132px;
  --radius: 3px;                    /* prints have corners, not pills */
  --ease: cubic-bezier(.22, .61, .36, 1);
}

* { box-sizing: border-box; }
[hidden] { display: none !important; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--font-ui);
  -webkit-font-smoothing: antialiased;
  overscroll-behavior-y: none;
}

body.no-scroll { overflow: hidden; }

a { color: inherit; }
a.btn, a.wordmark { text-decoration: none; }

:focus-visible {
  outline: 2px solid var(--imprint);
  outline-offset: 2px;
}

button {
  font: inherit;
  color: inherit;
  background: none;
  border: none;
  cursor: pointer;
}

/* --- the date-stamp imprint: the only fixed accent ------------------------ */
.imprint {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: .10em;
  text-transform: uppercase;
  color: var(--imprint);
}
.imprint-dim { color: var(--ink-faint); }
.imprint-meas { text-transform: none; letter-spacing: .06em; }

/* =========================== top bar ====================================== */
.bar {
  position: sticky;
  top: 0;
  z-index: 40;
  height: var(--bar-h);
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 0 14px;
  background: color-mix(in srgb, var(--ground) 86%, transparent);
  backdrop-filter: blur(14px) saturate(1.3);
  border-bottom: 1px solid var(--edge);
}

.wordmark {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 16px;
  letter-spacing: .015em;
  white-space: nowrap;
  text-decoration: none;
}
.wordmark span { color: var(--ink-faint); font-weight: 500; }

.bar-spacer { flex: 1; }

.search {
  flex: 0 1 260px;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 8px;
  height: 34px;
  padding: 0 11px;
  background: var(--surface);
  border: 1px solid var(--edge);
  border-radius: var(--radius);
}
.search input {
  flex: 1;
  min-width: 0;
  background: none;
  border: none;
  color: var(--ink);
  font: inherit;
  font-size: 13px;
  outline: none;
}
.search input::placeholder { color: var(--ink-faint); }

.btn {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  height: 34px;
  padding: 0 13px;
  border: 1px solid var(--edge);
  border-radius: var(--radius);
  background: var(--surface);
  font-size: 13px;
  font-weight: 500;
  white-space: nowrap;
  transition: border-color .15s, background .15s;
}
.btn:hover { border-color: var(--edge-bright); background: var(--surface-2); }
.btn[aria-pressed="true"], .btn.is-on {
  border-color: var(--imprint);
  color: var(--imprint);
}
.btn-ghost { background: none; border-color: transparent; }
.btn-danger:hover { border-color: var(--danger); color: var(--danger); }
/* States the photo IS hidden, rather than offering to hide it. */
.btn-hidden-on {
  background: var(--danger);
  border-color: var(--danger);
  color: #fff;
  font-weight: 700;
}
.btn-hidden-on:hover { background: #ef7b64; border-color: #ef7b64; color: #fff; }

.bar-tools { display: flex; align-items: center; gap: 14px; }
.sheet-head { display: none; }
.sheet-links { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
.sheet-links form { display: contents; }

.count {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--ink-dim);
  white-space: nowrap;
}

/* =========================== layout ======================================= */
.shell { display: flex; align-items: flex-start; }

.rail {
  position: sticky;
  top: var(--bar-h);
  flex: 0 0 232px;
  height: calc(100vh - var(--bar-h));
  overflow-y: auto;
  /* No top padding: it belongs to .rail-actions instead. A sticky box is pinned
     relative to the scrollport but cannot rise above its containing block's
     content box, so padding here parked the buttons 18px down and left a band
     of scrolling list showing above them. */
  padding: 0 14px 60px;
  border-right: 1px solid var(--edge);
  scrollbar-width: thin;
}

/* No vertical padding: the pagers sit directly against the bar and the
   bottom of the page, which is where they are reached for. */
.main { flex: 1; min-width: 0; padding: 0 var(--gap); }

.facet { margin-bottom: 22px; }
/* Print size, Color, Shape -- the same kind of label as the tag kinds below
   them, so they take the same colour and weight rather than reading as a
   quieter, separate class of heading. */
.facet h3 {
  margin: 0 0 9px;
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--imprint);
  opacity: .75;
}

.opt {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 5px 7px;
  border-radius: var(--radius);
  font-size: 13px;
  text-align: left;
  color: var(--ink-dim);
  transition: background .12s, color .12s;
}
.opt:hover { background: var(--surface); color: var(--ink); }
.opt input[type="checkbox"] {
  flex: 0 0 auto;
  width: 15px;
  height: 15px;
  margin: 0;
  accent-color: var(--imprint);
  cursor: pointer;
}
.opt:has(input:checked) { background: var(--surface-2); color: var(--ink); }
.opt-label { flex: 1; }
.opt-n { font-family: var(--font-mono); font-size: 10px; color: var(--ink-faint); }

.rail-actions {
  position: sticky;
  top: 0;
  z-index: 2;
  display: flex;
  gap: 6px;
  /* The painted slab has to reach the rail's edges, not just sit inside its
     padding. A sticky box cannot rise above its containing block's content box,
     so the rail's 18px top padding used to pin these buttons 18px down and the
     list scrolled visibly through the gap above them; the 12px margin below did
     the same underneath. Negative margins pull the background out to the rail's
     own edges, and the matching padding puts the buttons back where they were. */
  margin: 0 -14px;
  padding: 18px 14px 12px;
  background: var(--ground);
}
.rail-actions .btn { flex: 1; justify-content: center; }
.btn-apply {
  background: var(--surface);
  border-color: var(--edge);
  color: var(--ink-faint);
}
.btn-apply.is-dirty {
  background: var(--imprint);
  border-color: var(--imprint);
  color: #17120a;
  font-weight: 700;
}
/* The sheet pads itself differently from the rail, so the same slab needs its
   own numbers to reach ITS edges. */
.sheet .rail-actions {
  background: var(--surface);
  margin: 0 -16px;
  padding: 10px 16px 12px;
}

.facet-search {
  width: 100%;
  height: 34px;
  margin-bottom: 8px;
  padding: 0 11px;
  background: var(--ground);
  border: 1px solid var(--edge);
  border-radius: var(--radius);
  color: var(--ink);
  font: inherit;
  font-size: 13px;
  outline: none;
}
.facet-search:focus { border-color: var(--imprint); }
.facet-none { margin: 4px 2px 8px; font-size: 12.5px; color: var(--ink-faint); }

.taggroup { margin-bottom: 10px; }
.taggroup h4 {
  margin: 8px 0 4px;
  font-family: var(--font-mono);
  font-size: 9px;
  font-weight: 700;
  letter-spacing: .11em;
  text-transform: uppercase;
  color: var(--imprint);
  opacity: .75;
}

.facet-more {
  width: 100%;
  padding: 6px 7px;
  margin-top: 2px;
  border-radius: var(--radius);
  font-size: 12px;
  text-align: left;
  color: var(--ink-faint);
}
.facet-more:hover { color: var(--imprint); background: var(--surface); }

/* =========================== grid ========================================= */
/* Justified rows. Real aspect ratios are preserved because print size is the
   organising principle of this archive -- a 9x13 must not be squared off into
   looking like a 10x15. */
.grid { display: flex; flex-direction: column; gap: var(--gap); }
.row { display: flex; gap: var(--gap); }

.card {
  position: relative;
  container-type: inline-size;
  display: block;
  flex: 0 0 auto;
  padding: 0;
  border-radius: var(--radius);
  overflow: hidden;
  background-color: var(--bloom);
  background-size: cover;
  background-position: center;
  cursor: pointer;
  transform: translateZ(0);
  transition: transform .32s var(--ease), box-shadow .32s var(--ease);
  box-shadow: 0 1px 2px rgba(0,0,0,.5),
              0 14px 34px -16px color-mix(in srgb, var(--bloom) 70%, transparent);
}
.card img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity .5s var(--ease);
}
.card img.loaded { opacity: 1; }
.card img.rotated {
  position: absolute;
  top: 50%;
  left: 50%;
  transform-origin: center;
  object-fit: cover;
}

@media (hover: hover) {
  .card:hover {
    transform: translateY(-3px);
    box-shadow: 0 2px 4px rgba(0,0,0,.55),
                0 26px 60px -18px color-mix(in srgb, var(--bloom) 95%, transparent);
  }
  .card:hover .card-meta { opacity: 1; }

  /* In edit mode the keyboard acts on whatever the cursor is over, so the card
     has to say plainly which one that is. A lift is too soft a signal when the
     next keystroke will turn the photo; drop it for a hard outline instead. */
  body.edit-mode .card:hover { transform: none; }
}

/* Driven by a class, not :hover, so it survives the relayout after a rotation.
   Outside the animation too -- a scaled outline would thin out mid-flight. */
body.edit-mode .card.is-hovered {
  transform: none;
  outline: 3px solid var(--hover-edge);
  outline-offset: -3px;
  box-shadow: 0 2px 4px rgba(0,0,0,.55),
              0 0 0 1px rgba(0,0,0,.6),
              0 18px 44px -16px color-mix(in srgb, var(--bloom) 80%, transparent);
}

.card-meta {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  display: flex;
  justify-content: space-between;
  gap: 8px;
  padding: 16px 9px 7px;
  /* No scrim -- the label carries its own shadow instead of dimming the photo. */
  opacity: 0;
  transition: opacity .2s;
  pointer-events: none;
  text-shadow: 0 1px 3px rgba(0,0,0,.95), 0 0 8px rgba(0,0,0,.7);
}

.badge-back {
  position: absolute;
  top: 7px; right: 7px;
  width: 20px; height: 20px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: rgba(10,9,8,.72);
  backdrop-filter: blur(4px);
  color: var(--imprint);
  font-family: var(--font-mono);
  font-size: 9px;
  font-weight: 700;
}

/* --- edit mode: correct a whole pile without opening each photo --------- */
.cardedit {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  /* Two children pushed apart: the arrow pair, then the colour marker. Absolute
     positioning let them overlap on narrow cards; this cannot. */
  justify-content: space-between;
  padding: 8px;
  /* No scrim: the buttons carry their own backdrop, and a gradient across the
     bottom of every card hid the part of the photo you are judging. */
}
.cardbtn {
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  padding: 0;
  border: 1px solid rgba(255,255,255,.34);
  border-radius: 50%;
  background: rgba(12,10,9,.72);
  color: var(--ink);
  backdrop-filter: blur(3px);
  transition: background .14s, border-color .14s, transform .14s;
}
.cardbtn:hover {
  background: rgba(12,10,9,.95);
  border-color: var(--imprint);
  color: var(--imprint);
  transform: translateY(-1px);
}
.cardbtn[disabled] { opacity: .5; }
.cardbtn svg { width: 22px; height: 22px; display: block; }
.cardedit-rot { display: flex; gap: 9px; }
.cardbtn-col { padding: 4px; }
.cardbtn-col svg { width: 24px; height: 24px; border-radius: 50%; }

/* Colour is shown as the thing itself: a full spectrum wheel. */
/* Small prints get small controls; a wallet-sized card is only ~126px wide and
   three full-size buttons simply do not fit. */
@container (max-width: 210px) {
  .cardbtn { width: 30px; height: 30px; }
  .cardbtn svg { width: 17px; height: 17px; }
  .cardbtn-col svg, .colorwheel { width: 19px; height: 19px; }
  .cardedit-rot { gap: 5px; }
  .cardedit { padding: 6px; }
}
@container (max-width: 150px) {
  .cardbtn { width: 25px; height: 25px; }
  .cardbtn svg { width: 14px; height: 14px; }
  .cardbtn-col svg, .colorwheel { width: 15px; height: 15px; }
  .cardedit-rot { gap: 3px; }
  .cardedit { padding: 4px; }
}

.colorwheel {
  display: block;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  border: 1px solid rgba(255,255,255,.5);
  background: conic-gradient(#ff4d4d, #ffd24d, #6fe36f, #4dd2ff, #6a6aff, #e04dff, #ff4d4d);
}

/* On a phone the controls need to be reachable, not decorative. */
@media (max-width: 760px) {
  .cardbtn { width: 42px; height: 42px; }
  .cardbtn svg { width: 24px; height: 24px; }
  .cardedit-rot { gap: 11px; }
}

/* A 2px amber line was easy to miss against a photograph. Six pixels of a
   turning gradient is not.
   Drawn as a ring rather than an overlay: the box is masked so only its own
   padding paints, which leaves the photograph underneath untouched. A plain
   border cannot hold a gradient that turns, and covering the middle with a
   second layer would hide the picture. */
@property --sel-a {
  syntax: "<angle>";
  initial-value: 0deg;
  inherits: false;
}
.card.is-sel::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 3;
  padding: 6px;                       /* the thickness of the ring */
  border-radius: var(--radius);
  background: conic-gradient(from var(--sel-a),
    var(--imprint), #f5e6c8, var(--danger), #5ec8e8, var(--imprint));
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
  animation: sel-turn 3.2s linear infinite;
  pointer-events: none;
}
@keyframes sel-turn { to { --sel-a: 360deg; } }
/* Where a custom property cannot be animated the gradient still paints, so the
   ring is there and simply does not turn. */
@media (prefers-reduced-motion: reduce) {
  .card.is-sel::before { animation: none; }
}
/* A hidden photo stays in the owner's grid, dimmed and badged, so it can still
   be found and put back. Family members never receive it at all. */
.card.is-hidden-photo { opacity: .5; }
.card.is-hidden-photo:hover { opacity: .85; }
.badge-hidden {
  position: absolute;
  top: 7px; left: 7px;
  z-index: 3;
  display: grid;
  place-items: center;
  width: 24px; height: 24px;
  border-radius: 50%;
  background: var(--danger);
  color: #fff;
  box-shadow: 0 2px 6px rgba(0,0,0,.6);
}
.badge-hidden svg { width: 15px; height: 15px; display: block; }

.pager {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 10px;
  padding: 10px 2px;
}
#pager-bottom { padding-bottom: 40px; }
.pager-range {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--ink-dim);
}
/* nowrap: the row used to break with the last button stranded on its own line.
   The number of pages shown narrows on a phone (see renderPagers) so it fits;
   overflow-x is the backstop for a very narrow screen. */
.pager-nav {
  display: flex;
  align-items: center;
  gap: 4px;
  flex-wrap: nowrap;
  max-width: 100%;
  overflow-x: auto;
  scrollbar-width: none;
}
.pager-nav::-webkit-scrollbar { display: none; }
.pagebtn, .pager-gap { flex: 0 0 auto; }
.pagebtn {
  min-width: 34px;
  height: 34px;
  padding: 0 9px;
  border: 1px solid var(--edge);
  border-radius: var(--radius);
  background: var(--surface);
  font-size: 13px;
  font-variant-numeric: tabular-nums;
}
.pagebtn:hover:not([disabled]) { border-color: var(--edge-bright); background: var(--surface-2); }
.pagebtn.is-current {
  background: var(--imprint);
  border-color: var(--imprint);
  color: #17120a;
  font-weight: 700;
}
.pagebtn[disabled] { opacity: .35; cursor: default; }
.pager-gap { color: var(--ink-faint); padding: 0 2px; }

/* Photo size, next to the count. Two buttons rather than a pinch: Safari's page
   pinch-zoom cannot be handed to a page script, so the gesture never arrived. */
.zoomctl {
  display: inline-flex;
  flex: 0 0 auto;
  border: 1px solid var(--edge);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--surface);
}
.zoombtn {
  width: 32px;
  height: 30px;
  display: grid;
  place-items: center;
  border: 0;
  background: none;
  color: var(--ink-dim);
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
}
.zoombtn + .zoombtn { border-left: 1px solid var(--edge); }
.zoombtn:hover:not([disabled]) { background: var(--surface-2); color: var(--imprint); }
.zoombtn[disabled] { opacity: .3; cursor: default; }

@media (max-width: 560px) {
  /* Bigger targets on a phone, where this is the only way to resize. */
  .zoombtn { width: 38px; height: 34px; font-size: 16px; }
}

@media (max-width: 560px) {
  .pager { justify-content: center; }
  .pager-range { width: 100%; text-align: center; }
  .pagebtn { min-width: 38px; height: 38px; }
}

.empty {
  padding: 90px 20px;
  text-align: center;
  color: var(--ink-faint);
}
.empty h2 {
  margin: 0 0 8px;
  font-family: var(--font-display);
  font-weight: 400;
  font-style: italic;
  font-size: 21px;
  color: var(--ink-dim);
}

/* The status line ("Loading...", a connection error) carries .empty too, but
   that class pads by 90px so a real "nothing matches" block can sit centred in
   the page. On a one-line status that is blank almost all the time it reserved
   180px of dead space under every grid, so give it its own size -- and take it
   out of the flow entirely when it has nothing to say. */
#status {
  padding: 12px 20px;
  font-size: 13px;
}
#status:empty {
  display: none;
}

/* =========================== lightbox ===================================== */
.lb {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: none;
  background: var(--ground);
  /* The neighbouring frames are parked a screen-width away in each direction;
     without this they would extend the scrollable area. */
  overflow: hidden;
}
.lb.open { display: block; }

/* The ambient wash the archive is built around: a blurred, over-saturated copy
   of the photograph itself, so each image floods the screen with its own light.
   Two layers so navigation cross-fades instead of flickering. */
.ambient {
  position: absolute;
  inset: -12%;
  background-size: cover;
  background-position: center;
  filter: blur(70px) saturate(1.75);
  opacity: 0;
  transform: scale(1.12);
  transition: opacity .65s var(--ease);
  will-change: opacity;
}
.ambient.on { opacity: .52; }
.lb-vignette {
  position: absolute;
  inset: 0;
  background: radial-gradient(ellipse at 50% 45%,
              transparent 30%, rgba(6,5,5,.55) 78%, rgba(6,5,5,.88) 100%);
  pointer-events: none;
}

.lb-stage {
  position: absolute;
  inset: 0;
  /* Our own hand rather than the system's. The system one is drawn at whatever
     size the desktop is set to, which over a photograph is a lot of pointer;
     this one is about two thirds of that. The keyword after it is the fallback
     for anything that will not take an image. */
  cursor: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2222%22%20height%3D%2222%22%20viewBox%3D%220%200%2032%2032%22%3E%3Cg%20fill%3D%22%23fff%22%20stroke%3D%22%23111%22%20stroke-width%3D%221.6%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M11%2020V7.5a1.9%201.9%200%200%201%203.8%200V14V6a1.9%201.9%200%200%201%203.8%200v8V7.6a1.9%201.9%200%200%201%203.8%200V16v-3.4a1.9%201.9%200%200%201%203.8%200V21c0%204.4-3.2%208-7.6%208h-2.2c-2%200-3.4-.8-4.6-2.3L7%2021.4c-.9-1.2-.6-2.6.5-3.3%201-.6%202.2-.3%202.9.7z%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E') 11 8, grab;
  display: grid;
  place-items: center;
  padding: var(--lb-top) 14px var(--lb-bottom);
  touch-action: none;
}

/* Loading indicator over the picture. The first open of a photo is a cold fetch
   of a file that can run to several megabytes, and switching to the original
   scan fetches a second file that was never preloaded. Without this the viewer
   just sits there and the click that caused it looks ignored. */
.lb-loading {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  z-index: 3;
  /* Must never swallow a pan or a tap meant for the photo underneath. */
  pointer-events: none;
  color: var(--ink-dim);
  font-size: 11px;
  letter-spacing: .10em;
  text-transform: uppercase;
}
/* display:flex above would otherwise defeat the hidden attribute. */
.lb-loading[hidden] { display: none; }

.lb-spinner {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  border: 2px solid rgba(240,166,60,.22);
  border-top-color: var(--imprint);
  animation: lb-spin .8s linear infinite;
}

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

/* The same signal on the button that started the load, so the press registers
   even before the picture can change. */
.btn.is-busy { color: var(--imprint); border-color: var(--imprint); }
.btn.is-busy::before {
  content: '';
  display: inline-block;
  width: 11px;
  height: 11px;
  margin-right: 7px;
  vertical-align: -1px;
  border-radius: 50%;
  border: 2px solid rgba(240,166,60,.3);
  border-top-color: var(--imprint);
  animation: lb-spin .8s linear infinite;
}

@media (prefers-reduced-motion: reduce) {
  /* Slowed rather than removed: the spin IS the feedback, so stopping it would
     put us back to a viewer that looks frozen. */
  .lb-spinner, .btn.is-busy::before { animation-duration: 2.4s; }
}

/* the flip: turning the print over to read the back */
.flipper {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  transform-style: preserve-3d;
  transition: transform .72s var(--ease);
  max-width: 100%;
}
/* Pan, zoom and the flip are composed into one transform on .flipper by JS.
   Precedence matters: the long ease is the flip; button-driven zoom gets a short
   ease; a live gesture gets none at all, or each wheel tick animates against the
   next and the zoom stutters. */
.lb.zoomed .flipper { transition: transform .2s var(--ease); }
.lb.dragging .flipper { transition: none; }
.lb.zoomed .flipper, .lb.dragging .flipper { will-change: transform; }

/* Swiping between photos. .dragging (above) kills the transition while a finger
   is down so the picture tracks it exactly; these put the transition back for
   the settle and the page-turn. Declared after .dragging so that if both classes
   are ever set at once, the animation wins. */
.lb.swipe-anim .flipper,
.lb.swipe-anim .lb-peeks {
  /* Duration is set per gesture from the finger's speed (see glide() in
     viewer.js); the fallback only applies if that ever fails to land. */
  transition: transform var(--swipe-dur, 200ms) cubic-bezier(.22, .61, .36, 1);
}
/* While a finger is down the strip must track it exactly, with no easing. */
.lb.dragging .lb-peeks { transition: none; }

/* The photographs either side, parked one frame away in each direction so a
   swipe drags the next one into view instead of swapping it in afterwards. */
.lb-peeks {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}
.peekcell {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 100%;
  display: grid;
  place-items: center;
  /* Same box as .lb-stage, so a neighbour lands exactly where the current
     photograph sits rather than shifting as it arrives. */
  padding: var(--lb-top) 14px var(--lb-bottom);
}
.peekcell-prev { left: calc(-100% - 16px); }
.peekcell-next { left: calc(100% + 16px); }
.peek[hidden] { display: none; }

@media (prefers-reduced-motion: reduce) {
  /* Go straight to the next photograph rather than sliding it. */
  .lb.swipe-anim .flipper, .lb.swipe-anim .lb-peeks { transition: none; }
}

.face {
  display: block;
  width: auto;
  height: auto;
  max-width: 100%;
  /* Percentages would resolve against an auto-height parent and be ignored. */
  max-height: calc(100vh - var(--lb-top) - var(--lb-bottom));
  max-height: calc(100dvh - var(--lb-top) - var(--lb-bottom));
  object-fit: contain;
  backface-visibility: hidden;
  -webkit-user-drag: none;
  user-select: none;
  transform-origin: center center;
  border-radius: 2px;
  box-shadow: 0 30px 80px -24px rgba(0,0,0,.9), 0 0 0 1px rgba(255,255,255,.05);
  transition: transform .2s var(--ease);
}
/* A quarter-turned element keeps its original layout box, so the height limit
   has to become the width limit or the picture paints outside the stage and looks
   misaligned once you zoom. */
/* A thumbnail, or the inline blur-up from the payload, standing in until the
   screen-size file arrives. Blurred deliberately: at full size it would
   otherwise just read as a bad scan. The transition lives on .face so that
   removing the class resolves the blur on the real photograph rather than
   cutting to it. */
.face { transition: filter .2s linear; }
.face.is-placeholder { filter: blur(12px); }

.face.rot90 {
  max-width: calc(100vh - var(--lb-top) - var(--lb-bottom));
  max-width: calc(100dvh - var(--lb-top) - var(--lb-bottom));
  max-height: calc(100vw - 28px);
}
/* Closed while the hand is down -- at any zoom. It used to need .zoomed as
   well, so an ordinary press on a photograph never closed the hand at all. */
.lb.dragging .lb-stage,
.lb-stage:active {
  cursor: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2222%22%20height%3D%2222%22%20viewBox%3D%220%200%2032%2032%22%3E%3Cg%20fill%3D%22%23fff%22%20stroke%3D%22%23111%22%20stroke-width%3D%221.6%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M10.5%2017.5v-2.2a1.8%201.8%200%200%201%203.6%200v.9-2.1a1.8%201.8%200%200%201%203.6%200v1.2V14a1.8%201.8%200%200%201%203.6%200v1.5.2a1.8%201.8%200%200%201%203.5.5V21c0%204.4-3.2%208-7.6%208h-2.1c-2%200-3.5-.8-4.6-2.3L6.8%2021.4c-.9-1.2-.6-2.6.5-3.3%201-.6%202.2-.3%202.9.7z%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E') 11 8, grabbing;
}
.face-back {
  position: absolute;
  inset: 0;
  margin: auto;
  transform: rotateY(180deg);
}

.lb-top {
  position: absolute;
  top: 0; left: 0; right: 0;
  z-index: 3;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 14px;
  background: linear-gradient(rgba(6,5,5,.72), transparent);
}
.lb-title { display: flex; flex-direction: column; gap: 3px; min-width: 0; }

/* Says the full-resolution file is on its way. It is several megabytes and a
   thirty-megapixel decode, so the wait is real and worth naming -- unnamed, a
   zoomed print just looks soft. */
/* Solid rather than a tinted outline. It sits over a photograph -- which may be
   a bright sky -- and a faint amber pill on a faint gradient was easy to miss,
   which defeats the point of saying anything at all. */
.lb-detail {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0;
  padding: 0 15px;
  height: 32px;
  border-radius: 99px;
  background: var(--imprint);
  color: #1a1206;
  font: 700 12.5px/1 var(--font-ui);
  letter-spacing: .01em;
  white-space: nowrap;
  box-shadow: 0 2px 16px color-mix(in srgb, var(--imprint) 50%, transparent),
              0 0 0 1px rgba(6,5,5,.35);
}
/* The words themselves do the work: "Loading detail", then a dot, then two,
   then three. No icon -- a small dark ring on amber read as a gap where an
   icon should have been.
   The box is a fixed three characters wide whatever is showing, so the pill
   keeps one width instead of breathing in and out; the dots are revealed by
   clipping rather than by being added, which is what keeps it still. */
/* Three dots, always there, brightening in turn. Revealing them one at a time
   worked but read as text being typed; a cycle reads as waiting. */
.lb-detail-dots { flex: 0 0 auto; display: inline-flex; }
.lb-detail-dots i {
  font-style: normal;
  opacity: .3;
  animation: detail-dot 1.2s ease-in-out infinite;
}
.lb-detail-dots i:nth-child(2) { animation-delay: .2s; }
.lb-detail-dots i:nth-child(3) { animation-delay: .4s; }
@keyframes detail-dot {
  0%, 60%, 100% { opacity: .3; }
  30%           { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .lb-detail-dots i { animation: none; opacity: .75; }
}
.lb-counter {
  flex: 0 0 auto;
  padding: 5px 11px;
  border: 1px solid var(--edge-bright);
  border-radius: 99px;
  background: rgba(10,9,8,.7);
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: .04em;
  color: var(--ink);
}
.lb-size {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 15px;
  letter-spacing: .01em;
}

.lb-bottom {
  position: absolute;
  bottom: 0; left: 0; right: 0;
  z-index: 3;
  /* The top of this padding is space taken off the photograph, so it is kept
     tight; the gradient does the work of separating the controls from it. */
  padding: 12px 14px 14px;
  background: linear-gradient(transparent, rgba(6,5,5,.8));
}
.lb-controls {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 8px;
}

.nav-arrow {
  position: absolute;
  top: 50%;
  z-index: 3;
  width: 46px; height: 46px;
  margin-top: -23px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: rgba(10,9,8,.5);
  backdrop-filter: blur(6px);
  border: 1px solid var(--edge);
  transition: background .15s, border-color .15s;
}
/* The glyph used to be a text arrow, which sits where its own font metrics put
   it -- close to the middle of the circle but never in it. An SVG is centred by
   its box, so it actually is. */
.nav-arrow svg { width: 20px; height: 20px; display: block; }
.nav-next svg { transform: rotate(180deg); }
.nav-arrow:hover { background: rgba(10,9,8,.85); border-color: var(--edge-bright); }
.nav-prev { left: 12px; }
.nav-next { right: 12px; }
@media (max-width: 760px) { .nav-arrow { display: none; } }

@media (max-width: 760px) {
  /* Tighter control rows on a phone. --lb-bottom is the space the stage keeps
     clear for them, so shrinking the rows without shrinking it would just leave
     a gap; lowering both hands the difference back to the photograph. */
  :root { --lb-bottom: 112px; }
  .lb-bottom { padding: 14px 12px 10px; }
  .lb-controls { gap: 6px; }
  .lb-controls .btn { height: 30px; padding: 0 10px; }
  .tagbar { gap: 6px; margin-bottom: 8px; }
}

/* Close sits at the right-hand end of the top bar and reads as a real button
   rather than bare text -- on a phone it is the one control people hunt for. */
.lb-close {
  flex: 0 0 auto;
  height: 40px;
  padding: 0 18px;
  border-color: var(--edge-bright);
}
.lb-close:hover { border-color: var(--imprint); color: var(--imprint); }

/* Copy-link, immediately left of the photograph count. Icon only: the top bar
   is the one place where width is genuinely short on a phone. */
/* The round-ish icon buttons in the lightbox bar. Named for the shape rather
   than for the share button that happened to be the first one, so that asking
   for ".btn-share" does not also find the others. */
.lb-icon {
  flex: 0 0 auto;
  width: 40px;
  height: 40px;
  padding: 0;
  justify-content: center;
  border-color: var(--edge-bright);
}
.lb-icon svg { width: 18px; height: 18px; display: block; }
/* In the header bar it is an icon among words, so it takes the icon's shape
   rather than a button's padding. */
.bar .btn-full { width: 38px; padding: 0; justify-content: center; }
.bar .btn-full svg { width: 18px; height: 18px; display: block; }
.btn-full[aria-pressed="true"] { border-color: var(--imprint); color: var(--imprint); }
.lb-icon:hover { border-color: var(--imprint); color: var(--imprint); }
/* Held briefly after a successful copy, so the press has a visible result even
   though nothing on the page changes. */
.btn-share.is-copied { border-color: var(--imprint); color: var(--imprint); }

/* Big previous/next for touch. The side arrows above are hidden at this width
   because they land under your thumbs in the middle of the picture, which used
   to leave swiping as the only way through the archive. */
.lb-navrow { display: none; }

@media (max-width: 760px) {
  .lb-close { height: 46px; padding: 0 20px; font-size: 14px; }
  .lb-icon { width: 46px; height: 46px; }
  .lb-icon svg { width: 20px; height: 20px; }

  .lb-navrow {
    display: flex;
    gap: 10px;
    /* .lb-bottom already pads the sides; siblings space themselves with
       margin-bottom, so match that rather than double up on padding. */
    margin-bottom: 6px;
  }
  .navbig {
    flex: 1 1 0;
    /* 44px is the minimum comfortable touch target; anything more here is taken
       straight out of the photograph. */
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    border: 1px solid var(--edge-bright);
    border-radius: var(--radius);
    background: rgba(10,9,8,.6);
    backdrop-filter: blur(6px);
    color: var(--ink);
    font-size: 11px;
    letter-spacing: .08em;
    text-transform: uppercase;
  }
  .navbig svg { width: 17px; height: 17px; flex: 0 0 auto; }
  /* :active, not :hover -- a phone has no hover, and a stuck highlight after a
     tap looks like the button is still busy. */
  .navbig:active {
    background: rgba(240,166,60,.16);
    border-color: var(--imprint);
    color: var(--imprint);
  }
}

/* tags */
.end-panel {
  position: absolute;
  inset: 0;
  z-index: 6;
  display: grid;
  place-items: center;
  padding: 20px;
  background: rgba(6,5,5,.7);
}
.end-card {
  width: 100%;
  max-width: 340px;
  padding: 22px 20px 16px;
  text-align: center;
  background: var(--surface);
  border: 1px solid var(--edge-bright);
  border-radius: 10px;
  box-shadow: 0 30px 70px -20px rgba(0,0,0,.85);
}
.end-card h2 {
  margin: 0 0 6px;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 21px;
}
.end-card p { margin: 0 0 18px; font-size: 13.5px; color: var(--ink-dim); }
.end-actions { display: flex; flex-direction: column; gap: 8px; }
.end-actions .btn { justify-content: center; height: 40px; }
.end-dismiss {
  margin-top: 12px;
  font-size: 12.5px;
  color: var(--ink-faint);
}
.end-dismiss:hover { color: var(--imprint); }

.pin-box {
  position: fixed;
  z-index: 120;
  transform: translate(-50%, -130%);
}
.pin-box::after {
  content: '';
  position: absolute;
  left: 50%; bottom: -5px;
  width: 9px; height: 9px;
  margin-left: -4px;
  background: var(--surface-2);
  border-right: 1px solid var(--imprint);
  border-bottom: 1px solid var(--imprint);
  transform: rotate(45deg);
}
.pin-input {
  width: 190px;
  height: 36px;
  padding: 0 11px;
  background: var(--surface-2);
  border: 1px solid var(--imprint);
  border-radius: var(--radius);
  color: var(--ink);
  font: inherit;
  font-size: 14px;
  outline: none;
  box-shadow: 0 10px 26px -8px rgba(0,0,0,.8);
}

.modal-backdrop {
  position: fixed;
  inset: 0;
  z-index: 150;
  display: grid;
  place-items: center;
  padding: 16px;
  background: rgba(6,5,5,.45);
}
.modal {
  width: 100%;
  max-width: 370px;
  max-height: 88vh;
  max-height: 88dvh;
  overflow-y: auto;
  padding: 14px 16px;
  background: var(--surface);
  border: 1px solid var(--edge-bright);
  border-radius: 10px;
  box-shadow: 0 30px 70px -20px rgba(0,0,0,.85);
}
.modal h2 {
  margin: 0 0 10px;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 17px;
}
.fieldset { margin-bottom: 10px; }
.fieldset > h3 {
  margin: 0 0 4px;
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 400;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.tagfield {
  display: block;
  width: 100%;
  height: 34px;                    /* mobile gets 44px back, further down */
  margin-bottom: 4px;
  padding: 0 10px;
  background: var(--ground);
  border: 1px solid var(--edge);
  border-radius: var(--radius);
  color: var(--ink);
  font: inherit;
  font-size: 14px;
  outline: none;
}
.tagfield:focus { border-color: var(--imprint); }
.addrow {
  padding: 3px 2px;
  font-size: 12px;
  color: var(--ink-faint);
}
.addrow:hover { color: var(--imprint); }
.modal-actions {
  position: sticky;
  bottom: -14px;
  display: flex;
  gap: 8px;
  padding: 10px 0 0;
  margin-top: 2px;
  background: var(--surface);
  border-top: 1px solid var(--edge);
}
.modal-actions .btn { flex: 1; justify-content: center; height: 36px; }
.modal-actions .btn-ok {
  background: var(--imprint);
  border-color: var(--imprint);
  color: #17120a;
  font-weight: 700;
}
.modal-actions .btn-ok:hover { background: #ffb955; border-color: #ffb955; }

/* These fields are type=search purely to keep Safari's Contacts autofill away
   (see hardenField in viewer.js). Strip the appearance that comes with it so
   they still look like the text boxes they are. */
.tagfield,
.pin-input {
  -webkit-appearance: none;
  appearance: none;
}
.tagfield::-webkit-search-decoration,
.tagfield::-webkit-search-cancel-button,
.tagfield::-webkit-search-results-button,
.pin-input::-webkit-search-decoration,
.pin-input::-webkit-search-cancel-button,
.pin-input::-webkit-search-results-button {
  -webkit-appearance: none;
  appearance: none;
  display: none;
}

@media (max-width: 560px) {
  /* Full screen rather than a centred card. Safari's floating toolbars clip a
     centred dialog, and the on-screen keyboard shoves what is left out of view;
     a full-screen sheet has nothing to clip and the fields stay where they are. */
  .modal-backdrop {
    padding: 0;
    place-items: stretch;
    background: var(--ground);
  }
  .modal {
    width: 100%;
    max-width: none;
    max-height: none;
    /* dvh, not vh: on iOS vh is the viewport at its TALLEST, so the bottom of
       the sheet would sit underneath Safari's toolbar. */
    height: 100vh;
    height: 100dvh;
    border: 0;
    border-radius: 0;
    box-shadow: none;
    padding: calc(14px + env(safe-area-inset-top)) 16px
             calc(14px + env(safe-area-inset-bottom));
  }
  /* Keep OK and Cancel on screen however long the field list gets. */
  .modal-actions {
    bottom: calc(-14px - env(safe-area-inset-bottom));
    padding-bottom: calc(env(safe-area-inset-bottom));
  }
}

@media (max-width: 560px) {
  .end-panel {
  position: absolute;
  inset: 0;
  z-index: 6;
  display: grid;
  place-items: center;
  padding: 20px;
  background: rgba(6,5,5,.7);
}
.end-card {
  width: 100%;
  max-width: 340px;
  padding: 22px 20px 16px;
  text-align: center;
  background: var(--surface);
  border: 1px solid var(--edge-bright);
  border-radius: 10px;
  box-shadow: 0 30px 70px -20px rgba(0,0,0,.85);
}
.end-card h2 {
  margin: 0 0 6px;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 21px;
}
.end-card p { margin: 0 0 18px; font-size: 13.5px; color: var(--ink-dim); }
.end-actions { display: flex; flex-direction: column; gap: 8px; }
.end-actions .btn { justify-content: center; height: 40px; }
.end-dismiss {
  margin-top: 12px;
  font-size: 12.5px;
  color: var(--ink-faint);
}
.end-dismiss:hover { color: var(--imprint); }

.pin-box {
  position: fixed;
  z-index: 120;
  transform: translate(-50%, -130%);
}
.pin-box::after {
  content: '';
  position: absolute;
  left: 50%; bottom: -5px;
  width: 9px; height: 9px;
  margin-left: -4px;
  background: var(--surface-2);
  border-right: 1px solid var(--imprint);
  border-bottom: 1px solid var(--imprint);
  transform: rotate(45deg);
}
.pin-input {
  width: 190px;
  height: 36px;
  padding: 0 11px;
  background: var(--surface-2);
  border: 1px solid var(--imprint);
  border-radius: var(--radius);
  color: var(--ink);
  font: inherit;
  font-size: 14px;
  outline: none;
  box-shadow: 0 10px 26px -8px rgba(0,0,0,.8);
}

.modal-backdrop { place-items: end center; padding: 0; }
  .modal {
    max-width: none;
    max-height: 92dvh;
    border-radius: 14px 14px 0 0;
    border-bottom: none;
  }
}

/* Show back earns color: 470 prints have writing on the reverse and it is the
   single most rewarding thing in the archive to discover. */
.btn-back-present {
  border-color: var(--imprint);
  color: var(--imprint);
  background: color-mix(in srgb, var(--imprint) 14%, transparent);
}
.btn-back-present:hover {
  background: color-mix(in srgb, var(--imprint) 26%, transparent);
  border-color: var(--imprint);
}
.btn-back-present[aria-pressed="true"] {
  background: var(--imprint);
  border-color: var(--imprint);
  color: #17120a;
  font-weight: 700;
}

/* Holds the front face and its pin layer together. It is one side of the 3D
   flip, so it keeps backface-visibility; pan and zoom live on .flipper. */
.facewrap {
  position: relative;
  display: block;
  max-width: 100%;
  backface-visibility: hidden;
}

/* --- pinned tags on the picture ---------------------------------------- */
.pinlayer {
  position: absolute;
  inset: 0;
  pointer-events: none;
  overflow: hidden;
}
.pin {
  position: absolute;
  transform: translate(-50%, -100%);
  pointer-events: auto;
  cursor: pointer;
  display: none;
  filter: drop-shadow(0 2px 4px rgba(0,0,0,.7));
}
.pins-on .pin { display: block; }
.pin-dot {
  width: 17px;
  height: 17px;
  border: 2px solid #17120a;
  border-radius: 50% 50% 50% 0;
  transform: rotate(-45deg);
  background: var(--imprint);
}
.pin-person .pin-dot { background: #6fd08c; }
.pin-place .pin-dot { background: #6bb6ff; }
.pin-label {
  position: absolute;
  left: 50%;
  bottom: 22px;
  transform: translateX(-50%);
  padding: 3px 8px;
  border-radius: 4px;
  background: rgba(10,9,8,.92);
  border: 1px solid var(--edge-bright);
  color: var(--ink);
  font-size: 12px;
  white-space: nowrap;
  opacity: 0;
  transition: opacity .14s;
  pointer-events: none;
}
.pin:hover .pin-label, .pin:focus-visible .pin-label { opacity: 1; }

.lb.tagging .lb-stage { cursor: crosshair; }
.lb.tagging .face { cursor: crosshair; }
.tag-hint {
  position: absolute;
  top: 64px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 4;
  padding: 6px 14px;
  border-radius: 99px;
  background: var(--imprint);
  color: #17120a;
  font-size: 12.5px;
  font-weight: 700;
}

/* --- autocomplete ------------------------------------------------------- */
.ac-wrap { position: relative; }
.ac-list {
  position: absolute;
  left: 0; right: 0; top: calc(100% - 4px);
  z-index: 5;
  max-height: 190px;
  overflow-y: auto;
  background: var(--surface-2);
  border: 1px solid var(--edge-bright);
  border-radius: var(--radius);
  box-shadow: 0 12px 28px -8px rgba(0,0,0,.7);
}
.ac-item {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  width: 100%;
  padding: 9px 12px;
  text-align: left;
  font-size: 14px;
  background: none;
}
.ac-item:hover, .ac-item.is-active { background: var(--surface); color: var(--imprint); }
.ac-item .ac-n { font-family: var(--font-mono); font-size: 10px; color: var(--ink-faint); }

/* --- draggable dialog --------------------------------------------------- */
.modal-head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: -14px -16px 10px;
  padding: 9px 16px;
  border-bottom: 1px solid var(--edge);
}
.modal-head h2 { margin: 0; flex: 1; }
@media (min-width: 561px) {
  .modal-head { cursor: grab; user-select: none; }
  .modal-head:active { cursor: grabbing; }
  .modal-backdrop.dragged { place-items: start; }
  .modal-backdrop.free { background: rgba(6,5,5,.35); pointer-events: none; }
  .modal-backdrop.free .modal { pointer-events: auto; }
}
.drag-hint {
  font-family: var(--font-mono);
  font-size: 9px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
@media (max-width: 560px) { .drag-hint { display: none; } }

.zoomgroup {
  display: inline-flex;
  align-items: center;
  border: 1px solid var(--edge);
  border-radius: var(--radius);
  background: var(--surface);
  overflow: hidden;
}
.zoomgroup .btn {
  border: none;
  border-radius: 0;
  background: none;
  padding: 0 11px;
  font-size: 15px;
  line-height: 1;
}
.zoomgroup .btn:hover { background: var(--surface-2); }
.zoom-level {
  min-width: 52px;
  padding: 0 4px;
  font-family: var(--font-mono);
  font-size: 11px;
  text-align: center;
  color: var(--ink-dim);
  border-left: 1px solid var(--edge);
  border-right: 1px solid var(--edge);
  height: 34px;
  line-height: 34px;
}
.zoom-level:hover { color: var(--imprint); }

/* --- tagging ---------------------------------------------------------- */
/* "Show back" now sits at the end of the tag row rather than on a line of its
   own. It keeps the look it had there -- a solid button among pill-shaped tag
   chips, and amber whenever this print actually has a back worth turning to --
   because it is an action on the photograph, not another label about it. */
.btn-back {
  flex: 0 0 auto;                 /* .taglist does not wrap; it must not squash */
  padding: 0 18px;
  /* Set apart from "Show tags" next to it: this one turns the print over, the
     others are about labels, and at the row's 6px gap they read as one group. */
  margin-left: 14px;
}
/* Directly after the base rule, not up with the other phone overrides: a media
   query carries no extra specificity, so the later declaration wins regardless.
   Sitting earlier in the file is why this sizing never actually applied. */
@media (max-width: 760px) {
  .btn-back { height: 30px; padding: 0 14px; }
}

.tagbar { display: flex; flex-direction: column; align-items: center; gap: 8px; margin-bottom: 12px; }

.taglist {
  display: flex;
  flex-wrap: nowrap;              /* one row, as asked; it scrolls if it overflows */
  align-items: center;
  gap: 6px;
  max-width: 100%;
  overflow-x: auto;
  scrollbar-width: none;
  padding-bottom: 2px;
}
.taglist::-webkit-scrollbar { display: none; }
.chip, .btn-addtag { flex: 0 0 auto; }
.btn-addtag {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  height: 30px;
  padding: 0 12px;
  border: 1px dashed var(--edge-bright);
  border-radius: 99px;
  background: rgba(10,9,8,.6);
  font-size: 12px;
  color: var(--ink-dim);
}
/* "Stay logged in" sits between the password and the submit button, where it is
   read before the decision to sign in is made rather than after. */
.remember {
  display: flex;
  align-items: center;
  gap: 9px;
  /* A 44px row, so the label itself is the tap target, not just the box. */
  min-height: 44px;
  margin: 2px 0 6px;
  font-size: 13px;
  color: var(--ink-dim);
  cursor: pointer;
  user-select: none;
}
.remember input[type="checkbox"] {
  width: 18px;
  height: 18px;
  flex: 0 0 auto;
  accent-color: var(--imprint);
  cursor: pointer;
}

.btn-addtag:hover { border-style: solid; border-color: var(--imprint); color: var(--imprint); }

/* "Tag by click" and "Show tags" sit in the tag row beside "+ Add tag". They
   are toggles rather than actions, so they take a solid border -- the dashed
   outline stays reserved for the one button that adds something new. */
.btn-tagtool { border-style: solid; border-color: var(--edge-bright); }
.btn-tagtool[aria-pressed="true"] {
  border-color: var(--imprint);
  color: var(--imprint);
  background: rgba(240,166,60,.14);   /* --imprint at 14% */
}
.chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 9px;
  border: 1px solid var(--edge-bright);
  border-radius: 99px;
  background: rgba(10,9,8,.6);
  font-size: 12px;
}
.chip-x { color: var(--ink-faint); font-size: 13px; line-height: 1; }
.chip-x:hover { color: var(--danger); }
.chip-cat-person { border-color: color-mix(in srgb, var(--imprint) 55%, var(--edge)); }

/* The controls stay put while zoomed -- you need them most when you are in
   close on a face and want to flip, tag, or zoom back out. */
.lb-top, .lb-bottom, .nav-arrow { transition: opacity .25s; }

/* =========================== sheet (mobile filters) ======================= */
.sheet-backdrop {
  position: fixed; inset: 0; z-index: 60;
  background: rgba(6,5,5,.7);
  backdrop-filter: blur(3px);
  opacity: 0; pointer-events: none;
  transition: opacity .25s;
}
.sheet-backdrop.open { opacity: 1; pointer-events: auto; }

.sheet {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 61;
  max-height: 82vh;
  overflow-y: auto;
  padding: 8px 16px 30px;
  background: var(--surface);
  border-top: 1px solid var(--edge-bright);
  border-radius: 14px 14px 0 0;
  transform: translateY(101%);
  transition: transform .32s var(--ease);
}
.sheet.open { transform: translateY(0); }
.sheet-grab {
  width: 34px; height: 4px;
  margin: 6px auto 14px;
  border-radius: 99px;
  background: var(--edge-bright);
}

/* Your own pointer, shown on the one page where you can see it. */
.cursor-preview {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 16px;
  padding: 10px 12px;
  border: 1px solid var(--edge);
  border-radius: var(--radius);
  background: var(--ground);
}
.cursor-preview .lookcursor-dot { position: static; }

/* Pick your own. Radios, so a keyboard and a screen reader get a real choice;
   the input itself is hidden behind the colour it stands for. */
.swatches { display: flex; flex-wrap: wrap; gap: 8px; border: 0; padding: 0; margin-bottom: 16px; }
.swatches legend {
  padding: 0;
  margin-bottom: 8px;
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--imprint);
  opacity: .75;
}
.swatch { cursor: pointer; line-height: 0; }
.swatch input { position: absolute; opacity: 0; width: 0; height: 0; }
.swatch-dot {
  display: block;
  width: 26px;
  height: 26px;
  border-radius: 2px 50% 50% 50%;   /* the pointer it will become */
  background: var(--sw);
  box-shadow: 0 0 0 1.5px rgba(6,5,5,.6);
  transition: box-shadow .15s, transform .15s var(--ease);
}
.swatch:hover .swatch-dot { transform: scale(1.08); }
.swatch input:checked + .swatch-dot { box-shadow: 0 0 0 2px var(--ground), 0 0 0 4px var(--sw); }
.swatch input:focus-visible + .swatch-dot { outline: 2px solid var(--ink); outline-offset: 3px; }
.visually-hidden {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.cursor-note { font-size: 11.5px; color: var(--ink-faint); }

/* =========================== auth pages =================================== */
.auth-wrap {
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 24px;
}
.auth-card {
  width: 100%;
  max-width: 350px;
  padding: 30px 26px 26px;
  background: var(--surface);
  border: 1px solid var(--edge);
  border-radius: 6px;
}
.auth-card h1 {
  margin: 0 0 4px;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 22px;
  letter-spacing: .01em;
}
.auth-sub { margin: 0 0 22px; font-size: 13px; color: var(--ink-dim); }
.field { margin-bottom: 13px; }
.field label {
  display: block;
  margin-bottom: 5px;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.field input {
  width: 100%;
  height: 40px;
  padding: 0 12px;
  background: var(--ground);
  border: 1px solid var(--edge);
  border-radius: var(--radius);
  color: var(--ink);
  font: inherit;
  font-size: 14px;
  outline: none;
}
.field input:focus { border-color: var(--imprint); }
.btn-primary {
  width: 100%;
  height: 42px;
  margin-top: 8px;
  justify-content: center;
  background: var(--ink);
  border-color: var(--ink);
  color: var(--ground);
  font-weight: 700;
}
.btn-primary:hover { background: #fff; border-color: #fff; }
.alert {
  padding: 9px 11px;
  margin-bottom: 14px;
  border: 1px solid color-mix(in srgb, var(--danger) 50%, transparent);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--danger) 12%, transparent);
  font-size: 12.5px;
  color: #f0b5a8;
}

/* A confirmation, not a problem: same shape as .alert, opposite meaning. */
.alert-ok {
  /* .alert is tinted with --danger throughout, so all three have to be undone,
     not just the text -- a saved confirmation on a red panel reads as a failure. */
  border-color: color-mix(in srgb, var(--imprint) 55%, var(--edge));
  background: color-mix(in srgb, var(--imprint) 12%, transparent);
  color: var(--imprint);
}


/* =========================== toast ======================================== */
.toast {
  position: fixed;
  left: 50%; bottom: 26px;
  z-index: 200;
  padding: 9px 16px;
  border: 1px solid var(--edge-bright);
  border-radius: 99px;
  background: var(--surface-2);
  font-size: 13px;
  transform: translate(-50%, 20px);
  opacity: 0;
  pointer-events: none;
  transition: opacity .25s, transform .25s var(--ease);
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }

/* =========================== admin pages ================================== */
.page { max-width: 1100px; margin: 0 auto; padding: 26px 18px 70px; }
.page h1 {
  margin: 0 0 6px;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 25px;
  letter-spacing: .01em;
}
.page p.lede { margin: 0 0 26px; color: var(--ink-dim); font-size: 14px; max-width: 62ch; }

.cluster {
  display: flex;
  gap: 12px;
  flex-wrap: wrap;
  align-items: flex-start;
  padding: 14px;
  margin-bottom: 12px;
  background: var(--surface);
  border: 1px solid var(--edge);
  border-radius: 5px;
}
.cluster img { height: 132px; border-radius: 2px; display: block; }
.cluster figure { margin: 0; }
.cluster figcaption { margin-top: 6px; }

.table { width: 100%; border-collapse: collapse; font-size: 13px; }

/* --- tag editor ---------------------------------------------------------- */
.tagsearch { margin: 0 0 16px; max-width: 320px; }
.table .num { text-align: right; white-space: nowrap; }
.table th.num { text-align: right; }
/* The name box is the primary control on the row, so let it take the slack. */
.tagname { width: 100%; min-width: 140px; }
.tagcat, .tagmerge { height: 32px; padding: 0 8px; }
.tagmerge { max-width: 190px; }
.tagactions {
  display: flex;
  gap: 8px;
  align-items: center;
  flex-wrap: wrap;
}
.tagactions .btn { height: 32px; }

@media (max-width: 760px) {
  /* A four-column table does not fit a phone; let it scroll rather than
     squeezing the name box down to nothing. */
  .page .table { min-width: 640px; }
  .tagtable-wrap { overflow-x: auto; }
}
.table th, .table td {
  padding: 9px 10px;
  text-align: left;
  border-bottom: 1px solid var(--edge);
}
.table th {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-faint);
  font-weight: 400;
}

/* =========================== responsive =================================== */
@media (max-width: 1023px) {
  /* On a phone the bar keeps only what a family member needs: where they are,
     how much is here, and the way in to filters. Everything else moves into the
     sheet, where it has room to be legible. */
  .rail { display: none; }
  .main { padding-block: var(--gap); }

  /* The zoom control lives inside .bar-tools now, so it can sit beside the
     search box on a desktop. On a phone it is the only way to resize the grid,
     so the container stays and its other children go instead -- and `order`
     puts it back beside the count, where a thumb already expects it. */
  .bar-tools > *:not(.zoomctl) { display: none; }
  .bar-tools { gap: 0; }
  .bar-spacer, #lookstrip, #btn-filter, #btn-edit { order: 1; }
  .sheet-head { display: block; margin-bottom: 18px; }
  .sheet-head .search { flex-basis: auto; width: 100%; height: 40px; }
  #btn-filter { margin-left: auto; }

  /* Filter rows are checkboxes on a touch screen, so give them a real target.
     The desktop rail can stay compact; a thumb cannot. */
  .sheet .opt {
    min-height: 44px;
    padding: 8px 8px;
    font-size: 14.5px;
  }
  .sheet .opt input[type="checkbox"] { width: 20px; height: 20px; }
  .sheet .facet-search { height: 42px; font-size: 15px; }
  .sheet .facet h3 { margin-top: 14px; }
  .sheet .facet-more { min-height: 40px; font-size: 13.5px; }

  /* The dialog is compact on desktop; a thumb still needs room. */
  .modal .tagfield { height: 44px; font-size: 15px; margin-bottom: 6px; }
  .modal-actions .btn { height: 44px; }
  .pin-input { height: 44px; font-size: 15px; }
}
@media (max-width: 760px) {
  :root { --lb-bottom: 150px; }
}
@media (min-width: 1024px) {
  .sheet, .sheet-backdrop, #btn-filter { display: none; }
  :root { --gap: 12px; }
}

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

/* ============================== look together =============================
   The strip of everyone else in the archive, in the header bar. */
.lookstrip {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 0 0 auto;
  min-width: 0;
  padding: 4px 6px;
  border: 1px solid var(--edge);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--surface) 92%, transparent);
}

.look-title {
  font: 11px/1 var(--font-ui);
  color: var(--ink-faint);
  text-transform: uppercase;
  letter-spacing: .08em;
  white-space: nowrap;
  padding-left: 4px;
}

/* One person: a thumbnail of what they are looking at, their name, and what
   they are doing. The thumbnail is the point -- it turns a list of names into
   something worth glancing at. */
.look-chip {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 3px 8px 3px 3px;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: none;
  color: var(--ink-dim);
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: border-color .2s var(--ease), background .2s var(--ease);
}
.look-chip:hover { border-color: var(--edge-bright); background: var(--surface-2); }
.look-chip.is-sharing { border-color: var(--imprint); }

/* Asked, and waiting on an answer. Dimmed rather than disabled so it still
   reads as a person rather than a dead control. */
.look-chip.is-waiting { opacity: .6; }
.look-chip.is-waiting .look-sub { color: var(--imprint); }
.look-chip.is-busy { opacity: .45; cursor: default; }
.look-chip.is-busy:hover { border-color: transparent; background: none; }

.look-face {
  flex: 0 0 auto;
  width: 30px;
  height: 30px;
  border-radius: var(--radius);
  border: 1px solid var(--edge-bright);
  background: var(--surface-2) center/cover no-repeat;
  color: var(--ink-dim);
  font: 600 10px/28px var(--font-ui);
  text-align: center;
  letter-spacing: .04em;
}
.look-face.has-thumb { border-color: var(--edge); }

.look-meta { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.look-name {
  /* See .share-label: line-height 1 plus overflow hidden cuts the tail off a
     "ş" or a "g". */
  font: 600 12px/1.5 var(--font-ui);
  /* Their colour, so the name in the strip and the pointer on the photograph
     read as the same person. */
  color: var(--cur, var(--ink));
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 11ch;
}
.look-sub {
  font: 10px/1 var(--font-ui);
  color: var(--cur, var(--ink-faint));
  opacity: .7;
  white-space: nowrap;
}

/* The row that holds the loading indicator and the sharing banner, centred at
   the top of the window. Inert except where a child is, so it never swallows a
   click aimed at the photograph beneath it. */
.looktop {
  position: fixed;
  z-index: 120;                    /* .lb is 100 */
  top: max(10px, env(safe-area-inset-top));
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 8px;
  max-width: calc(100vw - 20px);
  pointer-events: none;
}
.looktop > * { pointer-events: auto; }
.looktop:empty { display: none; }

/* --- the banner, while a screen is shared -------------------------------- */
/* It sits in the bar, immediately right of the people who are here -- floating
   it over them was covering the very thing it is about. */
.lookbanner {
  display: flex;
  align-items: center;
  gap: 10px;
  /* Never the thing that gives way. On an admin's bar there is not much room
     left, and "Sharing sc..." is not worth saying. The search box shrinks
     instead -- it has a placeholder, not a fact. */
  flex: 0 0 auto;
  padding: 5px 5px 5px 11px;
  border: 1px solid var(--imprint);
  border-radius: 99px;
  background: color-mix(in srgb, var(--imprint) 10%, transparent);
}
/* Over a full-screen photograph there is no bar to sit in, so it lifts out and
   floats instead. The element is moved rather than duplicated. */
/* In the row above a photograph it needs its own ground, since there is no bar
   behind it there. Position comes from .looktop. */
.lookbanner.is-floating {
  min-width: 0;
  background: color-mix(in srgb, var(--ground) 92%, transparent);
  backdrop-filter: blur(14px) saturate(1.3);
  box-shadow: 0 8px 26px rgba(0,0,0,.55);
}
.share-dot {
  width: 8px; height: 8px;
  flex: 0 0 auto;
  border-radius: 50%;
  background: var(--imprint);
  animation: share-pulse 2.4s var(--ease) infinite;
}
@keyframes share-pulse { 50% { opacity: .35; } }
.share-label {
  /* Not 1: the box would be exactly as tall as the type, and with the overflow
     hidden that the ellipsis needs, every descender and every cedilla is sliced
     off -- which in this family's names is most of them. */
  font: 12px/1.5 var(--font-ui);
  color: var(--imprint);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.share-exit { height: 26px; padding: 0 12px; font-size: 12px; border-radius: 99px; }

/* --- the ask ------------------------------------------------------------- */
.lookprompt {
  position: fixed;
  inset: 0;
  z-index: 130;
  display: grid;
  place-items: center;
  padding: 20px;
  background: rgba(6,5,5,.72);
}
.prompt-card {
  width: min(380px, 100%);
  padding: 20px;
  border: 1px solid var(--edge-bright);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: 0 20px 60px rgba(0,0,0,.6);
}
.prompt-text { margin: 0 0 8px; font-family: var(--font-display); font-size: 17px; }
.prompt-text strong { color: var(--imprint); font-weight: 400; }
.prompt-note { margin: 0 0 18px; font-size: 12.5px; color: var(--ink-dim); }
.prompt-row { display: flex; align-items: stretch; gap: 8px; }
/* .btn-primary is shaped for the sign-in forms -- full width, taller, and
   nudged down by a margin. Side by side with "Not now" that read as two
   different buttons at two different heights, so both are normalised here. */
.prompt-row .btn {
  flex: 1 1 0;
  width: auto;
  height: 40px;
  margin: 0;
  justify-content: center;
}
/* The quiet option still needs an edge, or it reads as a label rather than a
   choice. */
.prompt-row .btn-ghost { border-color: var(--edge-bright); }

@media (prefers-reduced-motion: reduce) {
  .share-dot { animation: none; }
}

/* --- other people's pointers, over the whole archive -------------------- */
/* Fixed and inert: it has to reach over the grid, the rail and the bar, and it
   must never be the thing a click lands on. */
.lookcursors {
  position: fixed;
  inset: 0;
  z-index: 125;                    /* under the ask, over everything else */
  pointer-events: none;
  overflow: hidden;
}
/* Placed by window fraction because this screen has nothing by the name they
   sent -- a rail too narrow to show, most often. Dimmed rather than hidden: it
   is roughly right, and pretending otherwise is worse than saying so. */
.lookcursor.is-adrift { opacity: .45; }

/* --- ...and on the photograph itself ------------------------------------ */
/* Same coordinate space as the tag pins: inset over the image and carried by
   the flipper's transform, so a point named at 40% across stays on the same
   face however either person has zoomed. */
.cursorlayer {
  position: absolute;
  inset: 0;
  pointer-events: none;
  overflow: hidden;
}
.lookcursor {
  position: absolute;
  top: 0;
  left: 0;
  display: flex;
  align-items: flex-start;
  gap: 6px;
  /* The anchor is this box's top-left, and the arrow's point is drawn there,
     so the two coincide -- a pointer points from its tip, the way the real one
     under your hand does. */
  transform-origin: 0 0;
  white-space: nowrap;
  /* No CSS transition: JS moves these every frame. A transition here would
     finish before the next position arrived and the pointer would visibly stop
     between them, which is the choppiness this replaced. */
  will-change: transform;
}
.lookcursor-dot {
  width: 14px;
  height: 14px;
  flex: 0 0 auto;
  /* Sharp at the top-left, round elsewhere: an arrowhead, not a bullet. The
     order is top-left, top-right, bottom-right, bottom-left. */
  border-radius: 2px 50% 50% 50%;
  background: var(--cur, var(--imprint));
  box-shadow: 0 0 0 1.5px rgba(6,5,5,.6), 0 2px 8px rgba(0,0,0,.55);
}
/* The name, not a label on the name: a box would sit on the photograph like a
   sticker. Colour alone says whose it is, and the shadow keeps it readable
   over a bright sky as well as a dark interior. */
.lookcursor-name {
  color: var(--cur, var(--imprint));
  font: 600 11px/14px var(--font-ui);
  letter-spacing: .01em;
  text-shadow: 0 1px 3px rgba(6,5,5,.95), 0 0 2px rgba(6,5,5,.8);
}
/* Somebody pressed. A ring rather than a filled blob, so it reads as an event
   at a point and does not hide whatever was pressed. It lives in the window
   overlay, which is why it is the same size at any zoom and shows just as well
   over a button as over a photograph.
   Linear, deliberately. An ease-out curve reaches most of its size in the first
   fifth of its life and then creeps, so however long the animation is set to it
   still reads as a flash. Growing at a steady rate over a full second is what
   makes it look like a circle expanding rather than something blinking. It also
   stays bright until near the end -- fading early made it faint for most of the
   time it was on screen. */
.lookripple {
  position: absolute;
  width: 28px;
  height: 28px;
  margin: -14px 0 0 -14px;
  border-radius: 50%;
  border: 5px solid var(--cur, var(--imprint));
  /* Dark either side of the ring so it holds against a bright sky as well as a
     dark interior. */
  box-shadow: 0 0 0 1.5px rgba(6,5,5,.5), inset 0 0 0 1.5px rgba(6,5,5,.4);
  pointer-events: none;
  animation: press-ripple 1.1s linear forwards;
}
@keyframes press-ripple {
  0%   { transform: scale(.22); opacity: 0;   border-width: 5px; }
  7%   { opacity: 1; }
  70%  { opacity: .85; }
  100% { transform: scale(7);   opacity: 0;   border-width: 2px; }
}
@media (prefers-reduced-motion: reduce) {
  .lookripple { animation: press-ripple-quiet .6s linear forwards; }
}
/* Still a ring that opens, because a circle that only fades is the "small
   thing flashed" this was meant to stop being -- but a shorter, gentler one. */
@keyframes press-ripple-quiet {
  0%   { transform: scale(.5); opacity: .95; }
  100% { transform: scale(3.4); opacity: 0; }
}

@media (max-width: 760px) {
  /* On a phone the names and captions would push the strip off the edge; the
     thumbnails are the part worth keeping. */
  .look-meta { display: none; }
  .look-title { display: none; }
  .share-label { font-size: 11px; }
}
