/* =========================================================================
   Photography portfolio
   Design system + all styles. Light, minimal, photo-first. No build step.
   ========================================================================= */

/* ---- Design tokens ---------------------------------------------------- */
:root {
  /* Brand colors (overridable at runtime from manifest.site.theme).
     primary  drives links, highlights, primary buttons
     secondary drives hover/secondary accents
     accent   drives small details (dots, focus)
     The starter colors are a neutral slate, nobody's brand (QA T7); keep them
     equal to DEFAULT_THEME in manifest.js (a test checks). */
  --brand-primary:   #5a7385;
  --brand-secondary: #4a5866;
  --brand-accent:    #8b9daa;
  --accent: var(--brand-primary);

  /* Palette (near-white canvas, ink text) */
  --bg:          #faf9f7;
  --bg-2:        #f2f0ec;   /* elevated surface / hover wash */
  --ink:         #14130f;   /* primary text */
  --ink-2:       #4a463f;   /* secondary text */
  --ink-3:       #716c63;   /* muted text / meta — ≥4.5:1 on every surface (WCAG AA) */
  --line:        #e3e0d9;   /* hairline borders */
  --white:       #ffffff;
  --surface:     #ffffff;   /* inputs, menus, raised panels */
  --photo-bg:    #ebe8e2;   /* behind a photo while it loads */
  --on-photo:    #fff;      /* text and lines over a photo; the same on every theme (QA U41) */
  --photo-band:  rgba(0, 0, 0, 0.7); /* the band behind a video tile's length: white text stays readable on every theme (B44) */
  --photo-shade: rgba(0, 0, 0, 0.45); /* Outline's wash under its text: white on any photo stays AA */
  --danger:      #b23a2e;
  --ok:          #2f7d4f;
  --shadow:      0 18px 60px -24px rgba(20, 19, 15, 0.28);

  /* Type — `--serif` is the heading/display font (overridable via manifest.site.fonts). */
  --sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --serif: "Jost", "Inter", sans-serif;

  /* Layout (photo-forward: tight side padding, minimal image gaps) */
  --container: 1720px;
  --gutter: clamp(12px, 2.4vw, 32px);
  --gap: 4px;         /* hairline space between images */
  --gap-large: clamp(12px, 1.5vw, 24px); /* Spacing: Large (QA U40) */
  --space-section: clamp(56px, 9vh, 120px);
  --radius: 0px;      /* photo-forward: square corners, edge to edge */
  --nav-h: 60px;      /* the nav bar's exact height; content below it offsets by this */

  /* Motion */
  --ease: cubic-bezier(0.22, 1, 0.36, 1);
  --dur: 0.7s;
  --dur-drawer: 0.32s; /* drawers + side panels slide (with --ease), never snap */
}

/* ---- Themes -------------------------------------------------------------
   Light ("Studio") is the default everywhere. A page opts into following the
   device's light/dark setting with `class="theme-auto"` on <html> (the admin
   does; the public site stays light, the photos' backdrop). `data-theme=
   "light" | "dark"` on <html> forces one. Dark = "Darkroom": neutral greys so
   photos carry the color. Components use the tokens, so they follow along. */
@media (prefers-color-scheme: dark) {
  :root.theme-auto:not([data-theme="light"]) {
    --bg: #151515; --bg-2: #262626; --surface: #1c1c1c; --photo-bg: #232323;
    --ink: #ececea; --ink-2: #bdbdba; --ink-3: #94948f; --line: #2e2e2c;
    --danger: #e0786c; --ok: #6cc28f;
    --shadow: 0 18px 50px -20px rgba(0, 0, 0, 0.7);
    color-scheme: dark;
  }
}
:root[data-theme="dark"] {
  --bg: #151515; --bg-2: #262626; --surface: #1c1c1c; --photo-bg: #232323;
  --ink: #ececea; --ink-2: #bdbdba; --ink-3: #94948f; --line: #2e2e2c;
  --danger: #e0786c; --ok: #6cc28f;
  --shadow: 0 18px 50px -20px rgba(0, 0, 0, 0.7);
  color-scheme: dark;
}

/* ---- Reset / base ----------------------------------------------------- */
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 16px;
  line-height: 1.65;
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}
img { max-width: 100%; display: block; }
[hidden] { display: none !important; }
/* Read by screen readers, not shown (e.g. a table caption, "to Client" after "Upgrade"). */
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
/* Keyboard focus ring for everything interactive (mouse clicks don't show it).
   Form fields use their own ring (see "Form fields"). */
:is(a, button, [role="button"], [tabindex]):focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px;
}
.photo-open:focus-visible { outline-offset: -3px; } /* inside the edge-to-edge tile */
.lightbox :is(button, a):focus-visible { outline-color: #fff; } /* `hidden` always wins over component display rules */
a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; cursor: pointer; }
h1, h2, h3, h4 { margin: 0; font-weight: 400; line-height: 1.08; }
p { margin: 0; }

/* Utility: a wide content column */
.wrap {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* Full-bleed band for image grids — spans the whole viewport, edge to edge. */
.bleed { width: 100%; padding-inline: 0; }

/* Serif display + elegant italic accent */
.display {
  font-family: var(--serif);
  font-optical-sizing: auto;
  letter-spacing: -0.01em;
}
.display em { font-style: italic; color: var(--accent); } /* kept: accent-color italic for headings (theme color hook) */

/* Eyebrow label — spaced small caps. Not used on a page yet; kept as a
   design-system utility for future templates/tenants. */
.eyebrow {
  font-size: 0.72rem;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--ink-3);
  font-weight: 500;
}

/* ---- Site nav --------------------------------------------------------- */
.nav {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* One fixed height in every state, so the cover and page headers can offset
     by exactly --nav-h (and nothing shifts when the bar turns solid). */
  height: var(--nav-h);
  padding: 0 var(--gutter);
  mix-blend-mode: difference;   /* stays legible over any photo */
  color: #fff;
  transition: transform 0.45s var(--ease), background 0.4s var(--ease);
  will-change: transform;
}
.nav a { color: inherit; }
/* Hidden off the top of the screen (scroll-down / resting state mid-page). */
.nav.hidden { transform: translateY(-102%); }
/* Revealed mid-page on a deliberate upward scroll: solid, legible chrome. */
.nav.solid {
  mix-blend-mode: normal;
  color: var(--ink);
  background: rgba(250, 249, 247, 0.9);
  backdrop-filter: saturate(1.4) blur(14px);
  border-bottom: 1px solid var(--line);
}
.wordmark {
  font-family: var(--serif);
  font-size: 1.15rem;
  letter-spacing: 0.02em;
  font-weight: 500;
}
.wordmark-img { height: 26px; width: auto; display: block; }

/* When a logo image is used, drop the difference blend and keep everything
   legible with shadows over photos (removed once the solid bar takes over). */
.nav.has-logo { mix-blend-mode: normal; }
.nav.has-logo:not(.solid) { color: #fff; }
.nav.has-logo:not(.solid) .nav-links a { text-shadow: 0 1px 8px rgba(0,0,0,0.55); }
.nav.has-logo:not(.solid) .nav-toggle span { box-shadow: 0 1px 6px rgba(0,0,0,0.4); }

/* Logo variant switching: light logo over the transparent hero, default on the
   solid bar. With only one logo, the default shows everywhere (with a shadow). */
.wordmark-img--light { display: none; }
.nav.has-logo.has-logo-light:not(.solid) .wordmark-img--default { display: none; }
.nav.has-logo.has-logo-light:not(.solid) .wordmark-img--light { display: block; }
/* Single-logo fallback: shadow it over photos for legibility. */
.nav.has-logo:not(.has-logo-light):not(.solid) .wordmark-img--default {
  filter: drop-shadow(0 1px 8px rgba(0,0,0,0.55));
}
.nav-links {
  display: flex;
  gap: clamp(18px, 3vw, 40px);
  align-items: center;
  font-size: 0.82rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.nav-links a { position: relative; padding-block: 4px; }
.nav-links a::after {
  content: "";
  position: absolute;
  left: 0; bottom: 0;
  width: 100%; height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.4s var(--ease);
}
.nav-links a:hover::after,
.nav-links a[aria-current="page"]::after { transform: scaleX(1); }

/* Hamburger toggle — hidden on desktop, shown on mobile */
.nav-toggle {
  display: none;
  width: 26px; height: 18px; position: relative;
  background: none; border: none; padding: 0; cursor: pointer;
}
.nav-toggle span {
  position: absolute; left: 0; width: 100%; height: 2px;
  background: currentColor; transition: transform 0.35s var(--ease), opacity 0.2s var(--ease);
}
.nav-toggle span:nth-child(1) { top: 0; }
.nav-toggle span:nth-child(2) { top: 8px; }
.nav-toggle span:nth-child(3) { top: 16px; }
body.menu-open .nav-toggle span:nth-child(1) { transform: translateY(8px) rotate(45deg); }
body.menu-open .nav-toggle span:nth-child(2) { opacity: 0; }
body.menu-open .nav-toggle span:nth-child(3) { transform: translateY(-8px) rotate(-45deg); }

/* Full-screen mobile menu */
.mobile-menu {
  position: fixed; inset: 0; z-index: 45;
  background: var(--bg);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 8px;
  opacity: 0; visibility: hidden; transform: translateY(-8px);
  transition: opacity 0.35s var(--ease), transform 0.35s var(--ease), visibility 0.35s;
}
.mobile-menu.open { opacity: 1; visibility: visible; transform: none; }
.mobile-menu a {
  font-family: var(--serif); font-size: 2rem; font-weight: 300; color: var(--ink);
  padding: 10px 0;
}

@media (max-width: 700px) {
  .nav-links { display: none; }
  .nav-toggle { display: block; }
}

/* ---- UI components (design system) ------------------------------------
   Shared by the admin and available to any page. Token-based, so they work in
   both themes. Icons come from icons.js (`icon("name")` → <svg class="i">). */
svg.i {
  width: 18px; height: 18px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 1.75; stroke-linecap: round; stroke-linejoin: round;
}

/* Buttons: .btn (outlined), .primary (filled, one per view), .quiet (no
   border), .danger (destructive, quiet). Icons go first: <svg class="i">. */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  height: 34px; padding: 0 14px; border: 1px solid var(--line); border-radius: 4px;
  background: var(--surface); color: var(--ink); font: inherit; font-size: 0.875rem;
  white-space: nowrap; cursor: pointer; transition: border-color 0.15s, background 0.15s, opacity 0.15s;
}
.btn:hover:not(:disabled) { border-color: var(--ink-3); }
.btn .i { width: 16px; height: 16px; }
.btn.primary { background: var(--ink); border-color: var(--ink); color: var(--bg); }
.btn.primary:hover:not(:disabled) { opacity: 0.86; }
.btn.quiet { background: none; border-color: transparent; color: var(--ink-2); }
.btn.quiet:hover:not(:disabled) { background: var(--bg-2); color: var(--ink); }
.btn.danger { background: none; border-color: transparent; color: var(--danger); }
.btn.danger:hover:not(:disabled) { background: color-mix(in srgb, var(--danger) 10%, transparent); }
.btn.small { height: 28px; padding: 0 10px; font-size: 0.8125rem; }
.btn:disabled { opacity: 0.4; cursor: default; }
/* "There's something to publish": a soft accent ring on the primary button. */
.btn.ready { box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 35%, transparent); }

/* Call to action on public pages (e.g. "Back to home"): an outlined link in
   spaced capitals, icon after the words; fills with ink on hover. */
.cta {
  display: inline-flex; align-items: center; gap: 1.75em;
  padding: 1.15em 2.5em; border: 1px solid var(--ink); color: var(--ink);
  font-size: 0.8125rem; font-weight: 500; letter-spacing: 0.24em; text-transform: uppercase; line-height: 1;
  transition: background-color 0.2s var(--ease), color 0.2s var(--ease);
}
.cta:hover { background-color: var(--ink); color: var(--bg); }
.cta .i { width: 20px; height: 20px; stroke-width: 1.5; }

/* Icon-only button (always give it a title / aria-label). */
.icon-btn {
  display: inline-grid; place-items: center; width: 30px; height: 30px; padding: 0;
  border: 0; border-radius: 4px; background: none; color: var(--ink-2); cursor: pointer;
}
.icon-btn:hover:not(:disabled) { background: var(--bg-2); color: var(--ink); }
.icon-btn:disabled { opacity: 0.3; cursor: default; }
.icon-btn .i { width: 17px; height: 17px; }

/* Dialog: a centered panel over a dimmed page (native <dialog>, so focus
   stays inside and Escape closes it); full screen on phones. Shared by the
   admin (Send a ticket, QA T11; covers, U23), which puts its own palette on
   top, and the clients' Send favorites (QA P2). */
.dialog {
  width: min(560px, calc(100vw - 32px)); max-height: calc(100vh - 48px); padding: 0;
  border: 1px solid var(--line); border-radius: 8px; background: var(--bg); color: var(--ink); box-shadow: var(--shadow);
}
.dialog::backdrop { background: rgb(0 0 0 / 0.35); }
.dialog[open] { animation: dialog-in var(--dur-drawer) var(--ease); }
@keyframes dialog-in { from { opacity: 0; transform: translateY(12px); } }
.dialog form { display: flex; flex-direction: column; max-height: inherit; }
.dialog-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 18px 8px; }
.dialog-head h2 { margin: 0; font: 400 24px/1.2 var(--serif); }
.dialog-body { padding: 4px 18px 8px; overflow-y: auto; }
.dialog-body > p:first-child { margin-bottom: 14px; }
.dialog-foot { display: flex; justify-content: flex-end; gap: 8px; padding: 12px 18px 16px; }
.dialog-note:empty { display: none; }
.dialog-note { padding: 0 18px; }
@media (max-width: 600px) {
  .dialog { width: 100vw; max-width: none; height: 100dvh; max-height: none; margin: 0; border: 0; border-radius: 0; }
  .dialog form { height: 100%; }
  .dialog-body { flex: 1; }
}

/* Tooltip: any element with data-tip. One shared bubble (tips.js) shows on
   hover or keyboard focus after a short delay, never on touch (except an
   .info-tip, which a tap toggles), and sits above clipping parents. */
.tip {
  position: fixed; z-index: 1000; max-width: 240px; padding: 6px 9px; border-radius: 4px;
  background: var(--ink); color: var(--bg); font: 400 0.75rem/1.4 var(--sans); white-space: pre-line;
  box-shadow: var(--shadow); pointer-events: none; opacity: 0; transition: opacity 0.12s var(--ease);
}
.tip.on { opacity: 1; }

/* Info button: a small (i) with a tip, next to a label. */
.info-tip {
  display: inline-grid; place-items: center; width: 22px; height: 22px; padding: 0; vertical-align: middle;
  border: 0; border-radius: 50%; background: none; color: var(--ink-3); cursor: default;
}
.info-tip:hover { color: var(--ink); }
.info-tip .i { width: 16px; height: 16px; }

/* Switch: a real checkbox, styled. <input type="checkbox" class="switch" role="switch"> */
input[type="checkbox"].switch {
  -webkit-appearance: none; appearance: none; position: relative; flex: none;
  width: 34px; height: 20px; margin: 0; border-radius: 99px; background: var(--line);
  cursor: pointer; transition: background 0.15s;
}
input[type="checkbox"].switch::after {
  content: ""; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; border-radius: 50%;
  background: #fff; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); transition: transform 0.15s;
}
input[type="checkbox"].switch:checked { background: var(--accent); }
input[type="checkbox"].switch:checked::after { transform: translateX(14px); }
input[type="checkbox"].switch:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Segmented control: real radios, one visible choice.
   <div class="seg" role="radiogroup"><label><input type="radio" name="x" value="a"><span>A</span></label>…</div>
   As wide as its choices wherever it sits (a grid or flex column would
   otherwise stretch the track and leave an empty strip); .fill spans the
   full width with equal choices, to line up with full-width fields. */
.seg { display: inline-flex; flex-wrap: wrap; width: fit-content; max-width: 100%; padding: 2px; gap: 2px; border-radius: 5px; background: var(--bg-2); }
.seg.fill { display: flex; width: 100%; }
.seg.fill label { flex: 1 1 0; text-align: center; }
.seg label { position: relative; margin: 0; cursor: pointer; }
.seg input { position: absolute; opacity: 0; width: 1px; height: 1px; pointer-events: none; }
.seg span {
  display: block; padding: 5px 12px; border-radius: 4px; color: var(--ink-3);
  font-size: 0.8125rem; line-height: 1.5; white-space: nowrap; transition: background 0.15s, color 0.15s;
}
.seg label:hover span { color: var(--ink); }
.seg input:checked + span { background: var(--surface); color: var(--ink); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.14); }
.seg input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Menu (dropdown panel): .menu > a | button, .menu-label, hr. An item may
   carry a line of detail (<small>); aria-disabled="true" = can't now (its
   detail says why), still focusable so it's read out. */
.menu {
  min-width: 200px; padding: 6px; background: var(--surface); border: 1px solid var(--line);
  border-radius: 6px; box-shadow: var(--shadow);
}
.menu :is(a, button):where(:not(.btn, .icon-btn)) {
  display: flex; align-items: center; width: 100%; gap: 10px; padding: 8px 10px; border: 0; border-radius: 4px;
  background: none; color: var(--ink); font: inherit; font-size: 0.875rem; text-align: left; text-decoration: none; cursor: pointer;
}
.menu :is(a, button):where(:not(.btn, .icon-btn)):hover, .menu :is(a, button):where(:not(.btn, .icon-btn)):focus-visible { background: var(--bg-2); outline: none; }
.menu :is(a, button) b { display: block; font-weight: 500; }
.menu :is(a, button) small { display: block; color: var(--ink-3); font-size: 0.75rem; line-height: 1.4; }
.menu [aria-disabled="true"] { cursor: default; }
.menu [aria-disabled="true"] b { color: var(--ink-3); }
.menu .menu-label { padding: 8px 10px 4px; color: var(--ink-3); font-size: 0.75rem; }
.menu .menu-row { padding: 2px 6px 6px; }
.menu hr { border: 0; border-top: 1px solid var(--line); margin: 6px 0; }

/* Tabs: a row of real tabs over their panels (core.js bindTablist/showTab:
   role="tab" buttons with aria-controls, ←/→ move between them). The chosen
   one is underlined in ink. Scrolls sideways rather than wrapping. */
.tabs { display: flex; gap: 2px; overflow-x: auto; border-bottom: 1px solid var(--line); scrollbar-width: none; }
.tabs [role="tab"] {
  flex: none; margin-bottom: -1px; padding: 8px 9px 9px; border: 0; border-bottom: 2px solid transparent;
  background: none; color: var(--ink-3); font: inherit; font-size: 0.8125rem; cursor: pointer;
}
.tabs [role="tab"]:hover { color: var(--ink); }
.tabs [role="tab"][aria-selected="true"] { color: var(--ink); border-bottom-color: var(--ink); }
.tabs [role="tab"]:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* Summary row: a setting people rarely change, shown as its name and current
   value; Change opens its controls in place (core.js bindSummaries).
   <div class="sum"><button class="sum-head" aria-expanded aria-controls>
     <span class="sum-what"><b>Name</b><span class="sum-val">Off</span></span>
     <span class="sum-change">Change</span></button>
   <div class="sum-body" hidden>…</div></div> */
.sum { border-top: 1px solid var(--line); }
.sum:last-child { border-bottom: 1px solid var(--line); }
.sum-head {
  display: flex; align-items: center; gap: 12px; width: 100%; padding: 11px 0; border: 0;
  background: none; color: var(--ink); font: inherit; text-align: left; cursor: pointer;
}
.sum-what { flex: 1; min-width: 0; }
.sum-what b { display: block; font-weight: 500; }
.sum-val { color: var(--ink-3); font-size: 0.8125rem; }
.sum-change { color: var(--ink-2); font-size: 0.8125rem; text-decoration: underline; text-underline-offset: 3px; text-decoration-color: var(--line); }
.sum-head:hover .sum-change { text-decoration-color: currentColor; }
.sum-body { padding: 0 0 12px; }
/* Plain variant (.sum.plain; admin Site settings, QA U45): no lines, spaced
   rows; the open one sits on a light wash (core.js openSummary adds .open).
   An optional small picture (.sum-pic) can sit before Change. */
.sum.plain, .sum.plain:last-child { border: 0; border-radius: 6px; transition: background var(--dur-drawer) var(--ease); }
.sum.plain .sum-head { min-height: 60px; padding: 10px 12px; border-radius: 6px; }
.sum.plain.open { background: var(--bg-2); }
.sum.plain .sum-body { padding: 0 12px 18px; }
@media (hover: hover) { .sum.plain:not(.open) .sum-head:hover { background: var(--bg-2); } }
.sum-change { flex: none; }
.sum-pic { flex: none; display: flex; }
.sum-pic img { width: 40px; height: 40px; object-fit: cover; border-radius: 3px; background: var(--photo-bg); }

/* List field (listfield.js mountListField): an "add" field (Enter adds), then
   the entries as hairline rows: grip, the text (a quiet field, edited in
   place), and move/remove tools that show on hover or focus (always on touch
   screens). The rows' order is the list's order. */
.list-field { display: grid; }
.list-field .lf-add { width: 100%; }
.list-field .lf-msg { margin-top: 6px; color: var(--danger); font-size: 0.75rem; }
.list-field .lf-list { list-style: none; margin: 10px 0 0; padding: 0; border-top: 1px solid var(--line); }
.list-field .lf-row {
  display: flex; align-items: center; gap: 2px; min-height: 42px; padding: 2px 0;
  border-bottom: 1px solid var(--line); transition: background-color 0.15s var(--ease);
}
.list-field .lf-row:hover, .list-field .lf-row:focus-within { background: color-mix(in srgb, var(--bg-2) 60%, transparent); }
.list-field .lf-grip { display: inline-grid; place-items: center; width: 26px; height: 34px; color: var(--ink-3); cursor: grab; touch-action: none; }
.list-field .lf-grip:hover { color: var(--ink); }
.list-field .lf-grip .i { width: 16px; height: 16px; }
.list-field .lf-row .lf-text { flex: 1; min-width: 0; height: 34px; padding: 0 8px; border-color: transparent; background: none; }
.list-field .lf-row .lf-text:hover:not(:focus) { border-color: var(--line); }
.list-field .lf-row .lf-text:focus { background: var(--surface); }
.list-field .lf-tools { display: inline-flex; gap: 2px; padding-right: 2px; opacity: 0; transition: opacity 0.15s var(--ease); }
.list-field .lf-row:hover .lf-tools, .list-field .lf-row:focus-within .lf-tools { opacity: 1; }
@media (hover: none) { .list-field .lf-tools { opacity: 1; } }
.list-field .lf-row.dragging:not(.lifted) { opacity: 0.4; }
/* A row being dragged by touch (bindListDrag) lifts and follows the finger. */
.lifted { position: relative; z-index: 5; background: var(--surface); box-shadow: var(--shadow); }
.list-field .lf-row.drop-before { box-shadow: inset 0 2px 0 var(--accent); }
.list-field .lf-row.drop-after { box-shadow: inset 0 -2px 0 var(--accent); }
.list-field .lf-empty { padding: 12px 0; border-bottom: 1px solid var(--line); color: var(--ink-3); font-size: 0.8125rem; }
.list-field .lf-live { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }

/* ---- Form fields (design-system component) ----------------------------
   Shared interaction states for every text-like field (input, textarea,
   select). Pages own size/padding/border; these own hover, focus, disabled.
   The :is()/:not() selector is deliberately specific enough to win over
   page-level `input[type=text]` rules. */
:is(input:not([type=checkbox],[type=radio],[type=color],[type=file],[type=range],[type=submit],[type=button],[type=hidden]), textarea, select) {
  transition: border-color 0.2s var(--ease), box-shadow 0.2s var(--ease);
}
:is(input:not([type=checkbox],[type=radio],[type=color],[type=file],[type=range],[type=submit],[type=button],[type=hidden]), textarea, select):hover:not(:focus):not(:disabled) {
  border-color: var(--ink-3);
}
:is(input:not([type=checkbox],[type=radio],[type=color],[type=file],[type=range],[type=submit],[type=button],[type=hidden]), textarea, select):focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
}
:is(input, textarea, select):disabled { opacity: 0.5; cursor: default; }
/* Touch screens: fields at 16px at least. iOS zooms the page in on any field
   with smaller text when it's focused, and keeps that zoom after leaving it. */
@media (pointer: coarse) {
  :is(input:not([type=checkbox],[type=radio],[type=color],[type=file],[type=range],[type=submit],[type=button],[type=hidden]), textarea, select) { font-size: max(16px, 1em) !important; } /* beats any page rule */
}
input[type=checkbox], input[type=radio] { accent-color: var(--ink); }

/* ---- Select -----------------------------------------------------------
   Native <select> with the OS arrow replaced by an inset chevron, so the caret
   never touches the border. Pages set padding/border; this owns the arrow. */
select {
  -webkit-appearance: none; appearance: none;
  padding-right: 42px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23716c63' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 14px center;
  background-size: 16px 16px;
  cursor: pointer;
  text-overflow: ellipsis;
}
select::-ms-expand { display: none; }

/* ---- Buttons / links -------------------------------------------------- */
.link-underline {
  display: inline-flex;
  align-items: center;
  gap: 0.6em;
  font-size: 0.85rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding-bottom: 3px;
  border-bottom: 1px solid currentColor;
  transition: gap 0.4s var(--ease), opacity 0.3s var(--ease);
}
.link-underline:hover { gap: 1.1em; }
.link-underline .arrow { transition: transform 0.4s var(--ease); }
.link-underline:hover .arrow { transform: translateX(4px); }

/* =========================================================================
   HOME + INDEX PAGES (masthead, page headers, tile grids)
   ========================================================================= */
/* Inner-page header — mirrors the home masthead rhythm, plus clearance for the
   fixed nav bar (--nav-h). */
.page-header {
  padding-top: calc(var(--nav-h) + clamp(20px, 4vh, 48px));
  padding-bottom: clamp(24px, 4vh, 44px);
}
.page-header h1, .cover h1 {
  font-family: var(--serif); font-weight: 300;
  font-size: clamp(2rem, 5vw, 3.5rem); letter-spacing: -0.02em; line-height: 1.05;
}
.page-header p { margin-top: 18px; color: var(--ink-2); max-width: 48ch; }

/* About page (QA U33): headshot beside the text on wide screens (never
   cropped: the photo keeps its own shape), stacked on phones. */
.about { padding-bottom: var(--space-section); }
.about.has-photo {
  display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: clamp(28px, 5vw, 72px); align-items: start;
}
.about-photo img { display: block; width: 100%; height: auto; }
.about-body { max-width: 62ch; color: var(--ink-2); font-size: 1.0625rem; line-height: 1.7; }
.about-body p + p { margin-top: 1.1em; }
@media (max-width: 700px) {
  .about.has-photo { grid-template-columns: 1fr; gap: 28px; }
}

/* Home header — masthead above the grid (home page has no nav bar). */
.home-intro {
  padding-top: clamp(48px, 8vh, 96px);
  padding-bottom: clamp(28px, 5vh, 56px);
}
.home-title {
  font-family: var(--serif); font-weight: 300;
  font-size: clamp(2.4rem, 7vw, 5rem); letter-spacing: -0.02em; line-height: 1;
}
/* The logo in place of the name (site.headerLogo): about the name's height,
   never wider than the page. A fixed height, so nothing shifts as it loads. */
.home-logo {
  display: block; width: auto; height: clamp(56px, 8vw, 104px);
  max-width: min(100%, 560px); object-fit: contain; object-position: left center;
}
.home-sub {
  margin-top: 16px; color: var(--ink-2);
  font-size: clamp(1rem, 1.4vw, 1.15rem); max-width: 56ch;
}

.collection-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--gap);
  padding-bottom: var(--space-section);
}
/* Same side gutter as the photo galleries (half the page gutter) on larger
   screens; phones stay edge to edge. */
@media (min-width: 701px) { .collection-grid { margin-inline: calc(var(--gutter) / 2); } }
@media (max-width: 900px) { .collection-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 560px) {
  .collection-grid { grid-template-columns: 1fr; gap: 0; }
}
/* The home page's Static tiles (QA U40): Thumbnail size and Spacing hang off
   attributes only home.js sets, so category pages, /collections, and
   /shortlist keep the rules above. Regular + Regular adds nothing (today's).
   Large tiles: 2 across from 701px, 1 below. Large spacing: the wider gap
   everywhere, phones too, and a page edge never tighter than the gap; the
   tiles no longer touch the window there, so titles line up with them. */
@media (min-width: 701px) { .collection-grid[data-size="large"] { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 700px) { .collection-grid[data-size="large"] { grid-template-columns: 1fr; } }
.collection-grid[data-spacing="large"] { gap: var(--gap-large); margin-inline: max(calc(var(--gutter) / 2), var(--gap-large)); }
@media (max-width: 700px) { .collection-grid[data-spacing="large"] .card-foot { padding-inline: 0; } }
.collection-card { display: block; }
.collection-card .card-media {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius);
  aspect-ratio: 4 / 5;   /* fixed ratio keeps every tile the same height */
  background: var(--bg-2);
}
.collection-card .card-media img {
  width: 100%; height: 100%; object-fit: cover;
  transition: transform 1.1s var(--ease), filter 0.6s var(--ease);
}
@media (hover: hover) { .collection-card:hover .card-media img { transform: scale(1.05); } }
.card-foot {
  display: flex; align-items: baseline; justify-content: space-between;
  /* Closer to its own photo than to the next row's, so it reads as that photo's title. */
  gap: 16px; margin-top: 12px; padding-bottom: clamp(28px, 3.2vw, 44px);
}
.card-foot h3 { font-family: var(--serif); font-weight: 400; font-size: 1.35rem; }
/* A client gallery's "Closed" / "Open until …" (P8, /shortlist). */
.card-note { flex: none; font-size: 0.85rem; color: var(--ink-3); }
/* Phones: tiles run edge to edge, so the title keeps the page's side margin. */
@media (max-width: 700px) { .card-foot { padding-inline: var(--gutter); } }

/* =========================================================================
   COLLECTION PAGE
   ========================================================================= */
/* Starts below the fixed nav bar so the top of the cover isn't hidden under it. */
.collection-hero { position: relative; height: 78svh; min-height: 480px; overflow: hidden; margin-top: var(--nav-h); }
.collection-hero img { width: 100%; height: 100%; object-fit: cover; object-position: var(--focus-d, 50% 50%); }
/* The cover's focus point (QA U23): set per cover by the collection page. */
@media (max-width: 700px) { .collection-hero img { object-position: var(--focus-m, var(--focus-d, 50% 50%)); } }
.collection-hero::after {  /* light scrims: nav at the top, breadcrumb at the bottom */
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(180deg, rgba(0,0,0,0.1), rgba(0,0,0,0) 30%, rgba(0,0,0,0) 75%, rgba(0,0,0,0.3));
}
.collection-hero .caption {
  position: absolute; left: 0; bottom: 0; z-index: 1;
  width: 100%; color: var(--on-photo); padding: var(--gutter);
}
.collection-hero .caption .crumb,
.page-header .crumb {
  display: inline-block; margin-bottom: 12px;
  font-size: 0.75rem; letter-spacing: 0.14em; text-transform: uppercase;
  color: color-mix(in srgb, var(--on-photo) 85%, transparent); opacity: 0.9;
}
.collection-hero .caption .crumb:hover { opacity: 1; }
.collection-hero .caption .crumb { margin-bottom: 0; } /* the only thing on the banner */
.page-header .crumb { color: var(--ink-3); opacity: 1; } /* --ink-3 is the lightest AA-safe gray: never fade it */
.page-header .meta, .cover .meta {
  margin-top: 10px; font-size: 0.8rem; letter-spacing: 0.2em; text-transform: uppercase; color: var(--ink-3);
}
.page-header :is(.open-until, .closed-on) { margin-top: 8px; font-size: 0.9rem; color: var(--ink-3); }
/* No banner: the intro lines up under the left-aligned header, not centered. */
.page-header + .collection-intro { padding-top: 0; max-width: var(--container); }
.page-header + .collection-intro p { max-width: 60ch; }
.page-header .crumb:hover { color: var(--ink); }

.collection-intro { padding-block: clamp(40px, 8vh, 96px); max-width: 60ch; }
/* No description: keep the banner off the photos by exactly one gallery gap. */
/* Title + date sit under the banner: tight, no nav clearance needed there. */
.collection-hero + .page-header, .cover + .page-header { padding-block: 15px; }
.cover + .page-header:empty { display: none; } /* nothing left for it to hold (the line is added later by the page) */
.cover + .page-header:empty + .collection-intro { padding-top: clamp(40px, 8vh, 96px); } /* the intro then follows the cover itself */

/* ---- Cover designs (QA U41): Divider, Journal, Stamp, Outline ------------
   `.cover[data-design]` holds the photo and the title with the date (one
   text block); Classic and None are the rules above. Which cover shows where
   is the <picture>'s doing (Desktop above 700px, Phone at 700px and below).
   Divider and Outline crop around the cover's focus point; Journal and Stamp
   show the cover whole, sized from its shape (--cover-ar, width / height). */
.cover { position: relative; margin-top: var(--nav-h); }
.cover picture { display: block; }
.cover img { display: block; }
.cover .crumb {
  display: inline-block; font-size: 0.75rem; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--ink-3); /* the lightest AA-safe gray: never fade it */
}
@media (hover: hover) { .cover .crumb:hover { color: var(--ink); } }
.cover-text { display: grid; justify-items: center; align-content: center; gap: 14px; text-align: center; }
.cover-text .meta, .cover-frame .meta { margin-top: 0; }

.cover[data-design="divider"] { display: grid; grid-template-columns: 1fr 1fr; height: 78svh; min-height: 480px; }
.cover[data-design="divider"] picture { overflow: hidden; background: var(--photo-bg); }
.cover[data-design="divider"] img,
.cover[data-design="outline"] img { width: 100%; height: 100%; object-fit: cover; object-position: var(--focus-d, 50% 50%); }
.cover[data-design="divider"] .cover-text { padding: 32px var(--gutter); }

.cover[data-design="journal"] {
  display: grid; grid-template-columns: 5fr 7fr; align-items: end; gap: clamp(24px, 4vw, 56px);
  padding-top: clamp(20px, 4vh, 48px); padding-bottom: 8px;
}
.cover[data-design="journal"] .cover-text { justify-items: start; text-align: left; padding-bottom: 6px; }
.cover[data-design="journal"] img { margin-left: auto; width: min(100%, calc(78svh * var(--cover-ar))); height: auto; }

.cover[data-design="stamp"] { display: grid; justify-items: center; gap: 22px; padding-top: clamp(20px, 4vh, 48px); text-align: center; }
/* The picture holds the size (not the image), so its box exists before the photo loads. */
.cover[data-design="stamp"] picture { width: min(640px, 100%, calc(60svh * var(--cover-ar))); }
.cover[data-design="stamp"] img { width: 100%; height: auto; }

.cover[data-design="outline"] { height: 78svh; min-height: 480px; overflow: hidden; }
.cover[data-design="outline"] picture { height: 100%; }
.cover[data-design="outline"]::before {  /* a light shade on the whole photo... */
  content: ""; position: absolute; inset: 0; z-index: 1; background: rgba(0, 0, 0, 0.18);
}
.cover[data-design="outline"]::after {  /* ...and Classic's scrims for the breadcrumb */
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(180deg, rgba(0,0,0,0.1), rgba(0,0,0,0) 30%, rgba(0,0,0,0) 75%, rgba(0,0,0,0.3));
}
.cover-frame {  /* the title and date, in a thin frame with a darker wash */
  position: absolute; z-index: 2; left: 50%; top: 50%; transform: translate(-50%, -50%);
  display: grid; justify-items: center; gap: 14px; text-align: center;
  width: max-content; max-width: min(calc(100% - 2 * var(--gutter)), 900px);
  padding: clamp(22px, 4vw, 34px) clamp(18px, 5vw, 56px);
  border: 1px solid var(--on-photo); background: var(--photo-shade); color: var(--on-photo);
}
.cover-frame :is(h1, .meta) { color: var(--on-photo); overflow-wrap: anywhere; }
.cover .caption { position: absolute; left: 0; bottom: 0; z-index: 2; width: 100%; padding: var(--gutter); }
.cover .caption .crumb { color: color-mix(in srgb, var(--on-photo) 85%, transparent); }
@media (hover: hover) { .cover .caption .crumb:hover { color: var(--on-photo); } }

@media (max-width: 700px) {
  .cover[data-design="divider"] {  /* the photo on the top 62% of the first screen, the title in the rest */
    grid-template-columns: 1fr; height: auto; min-height: calc(100svh - var(--nav-h));
    grid-template-rows: max(280px, calc((100svh - var(--nav-h)) * 0.62)) 1fr;
  }
  .cover[data-design="divider"] img,
  .cover[data-design="outline"] img { object-position: var(--focus-m, var(--focus-d, 50% 50%)); }
  .cover[data-design="journal"] { grid-template-columns: 1fr; gap: 22px; padding-top: 28px; }
  .cover[data-design="journal"] img { width: 100%; }
  .cover[data-design="stamp"] picture { width: 72vw; }
  .cover-frame { width: calc(100% - 2 * var(--gutter)); }
}
.collection-intro p { font-size: 1.1rem; color: var(--ink-2); }

/* Galleries (gallery.js) — photos are NEVER cropped. Each tile gets its
   photo's own aspect-ratio inline; JS arranges tiles into justified rows
   (.g-row, "grid") or balanced columns (.g-col, "masonry"). The gap is the
   same on every screen size (--g, or --gap-large), which the layout math
   reads back as the resolved column-gap. */
.gallery {
  --g: var(--gap);
  /* Side gutter (half the page gutter) so photos don't touch the window edge.
     Margin, not padding: gallery.js lays out against clientWidth. */
  margin-inline: calc(var(--gutter) / 2);
  display: flex; flex-direction: column; gap: var(--g);
  padding-bottom: var(--space-section);
}
/* Spacing: Large (QA U40). The page edge is never tighter than the space
   between photos, so it grows to match. */
.gallery[data-spacing="large"] {
  --g: var(--gap-large);
  margin-inline: max(calc(var(--gutter) / 2), var(--g));
}
.gallery[data-layout="masonry"] { flex-direction: row; align-items: flex-start; }
.g-row { display: flex; gap: var(--g); }
.g-row--capped { justify-content: center; }  /* a lone tall photo isn't stretched */
.g-col { display: flex; flex-direction: column; gap: var(--g); flex: none; }
/* Positioned tiles (gallery.js mountTiles `placed`, home Vertical): each tile
   at its spot, so the page order can be the order on screen. The script sets
   the height, so the space below is a margin. */
.gallery[data-placed] { display: block; position: relative; padding-bottom: 0; margin-bottom: var(--space-section); }
.gallery[data-placed] > * { position: absolute; }
/* Home Vertical and Horizontal tiles (QA U40): the collection tiles in the
   galleries' layout, each cover whole (its shape is set inline), the title
   on one line ending in an ellipsis (the link's name is still the whole
   title), aligned with its photo on phones too. */
.gallery .collection-card { min-width: 0; }
.gallery .card-foot h3 { min-width: 0; line-height: 1.25; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
@media (max-width: 700px) { .gallery .card-foot { padding-inline: 0; } }
.photo {
  margin: 0;                 /* reset default <figure> margin */
  position: relative;
  overflow: hidden;
  border-radius: var(--radius);
  background: var(--bg-2);
  cursor: pointer;
  min-width: 0;
}
/* The tile's button: invisible, over the whole photo (the download link sits
   above it). */
.photo-open {
  position: absolute; inset: 0; z-index: 1; width: 100%; height: 100%;
  margin: 0; padding: 0; border: 0; background: none; cursor: pointer;
}
.photo img {
  width: 100%; height: 100%; object-fit: cover; display: block; /* box == photo ratio → no crop */
  transition: opacity 0.4s var(--ease);
}
.photo .dl {
  position: absolute; right: 12px; bottom: 12px;
  width: 40px; height: 40px; display: grid; place-items: center;
  background: rgba(255,255,255,0.92); color: var(--ink);
  border-radius: 50%; opacity: 0; transform: translateY(6px);
  transition: opacity 0.3s var(--ease), transform 0.3s var(--ease);
  backdrop-filter: blur(6px);
}
.photo .dl svg { width: 18px; height: 18px; }
.photo .dl { z-index: 2; cursor: pointer; }
/* Hover reveals only on devices with a real pointer. On touch screens a tap
   that reveals content counts as a "hover" first (iOS then needs a second tap),
   so there the tile's download button is hidden and one tap opens the
   lightbox, which has its own download button. */
@media (hover: hover) {
  .photo:hover img { opacity: 0.9; }
  .photo .dl:hover { background: #fff; }
  .photo:hover .dl,
  .photo:focus-within .dl { opacity: 1; transform: translateY(0); } /* keyboard users can reach it */
  .photo:hover .photo-label { opacity: 1; transform: none; }
}
@media (hover: none) {
  .photo .dl { display: none; }
}
/* Hover title (collection name on the starred home grid). */
.photo-label {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 1;
  padding: 40px 16px 14px;
  font-family: var(--serif); font-size: 1rem; letter-spacing: 0.01em; color: #fff;
  background: linear-gradient(180deg, transparent, rgba(0,0,0,0.45));
  opacity: 0; transform: translateY(6px); pointer-events: none;
  transition: opacity 0.3s var(--ease), transform 0.3s var(--ease);
}
@media (hover: none) { .photo-label { opacity: 1; transform: none; } } /* always shown on touch */
.photo:has(.photo-label) .dl { z-index: 2; }

/* Lightbox — full-bleed viewer with a thumbnail rail on the right. */
.lightbox {
  position: fixed; inset: 0; z-index: 100;
  background: rgba(12, 12, 13, 0.98);
  display: none; opacity: 0; transition: opacity 0.35s var(--ease);
}
.lightbox.open { display: flex; opacity: 1; }

.lb-main {
  position: relative; flex: 1; min-width: 0;
  display: flex; flex-direction: column;
  padding: clamp(24px, 4vw, 56px);
}
.lb-stage { flex: 1; min-height: 0; display: flex; align-items: center; justify-content: center; position: relative; }
.lb-stage img { transition: opacity 0.25s var(--ease); }
.lb-stage.loading img { opacity: 0.35; }   /* previous photo dims while the next loads */
.lb-spinner {
  position: absolute; left: 50%; top: 50%; width: 30px; height: 30px; margin: -15px 0 0 -15px;
  border: 2px solid rgba(255,255,255,0.25); border-top-color: #fff; border-radius: 50%;
  opacity: 0; transition: opacity 0.2s var(--ease); pointer-events: none;
  animation: lb-spin 0.8s linear infinite;
}
.lb-stage.loading .lb-spinner { opacity: 1; transition-delay: 0.15s; } /* skip it for instant loads */
@keyframes lb-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .lb-spinner { animation: none; } }
.lb-main { touch-action: pan-y pinch-zoom; } /* horizontal swipes are ours */
.lb-stage img {
  max-width: 100%; max-height: 100%; object-fit: contain;
  box-shadow: 0 30px 80px -30px rgba(0,0,0,0.8);
}
.lb-footer {
  display: flex; align-items: center; justify-content: center; gap: 18px;
  padding-top: 16px; min-height: 28px;
}
.lb-caption { color: rgba(255,255,255,0.72); font-size: 0.85rem; letter-spacing: 0.02em; cursor: default; } /* plain text: no I-beam over the time */
.lb-caption .lb-count { color: rgba(255,255,255,0.62); margin-left: 12px; }

.lb-close, .lb-nav {
  position: absolute; z-index: 2; color: #fff; background: none; border: none;
  line-height: 1; opacity: 0.65; transition: opacity 0.25s var(--ease); cursor: pointer;
}
.lb-close:hover, .lb-nav:hover { opacity: 1; }
.lb-close { top: 14px; right: 26px; font-size: 2rem; padding: 8px; }
.lb-nav { top: 50%; transform: translateY(-50%); font-size: 2.4rem; padding: 12px; }
.lb-prev { left: 4px; }
.lb-next { right: 4px; }

.lb-download, .lb-share {
  width: 40px; height: 40px; display: grid; place-items: center; flex: none;
  color: #fff; background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.3);
  border-radius: 50%; transition: background 0.25s var(--ease);
}
.lb-share { padding: 0; cursor: pointer; }
.lb-download:hover { background: rgba(255,255,255,0.22); }
@media (hover: hover) { .lb-share:hover { background: rgba(255,255,255,0.22); } }
.lb-download svg, .lb-share svg { width: 18px; height: 18px; }
.lb-status { color: rgba(255,255,255,0.85); font-size: 0.85rem; cursor: default; }
.lb-status:empty { display: none; }

/* Right-side thumbnail rail */
.lb-rail {
  flex: none; width: 120px; overflow-y: auto; overscroll-behavior: contain;
  padding: 72px 16px 20px; display: flex; flex-direction: column; gap: 10px;
  background: rgba(0, 0, 0, 0.28); border-left: 1px solid rgba(255,255,255,0.06);
}
.lb-rail::-webkit-scrollbar { width: 6px; }
.lb-rail::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.18); border-radius: 3px; }
.lb-rail button {
  flex: none; padding: 0; border: 2px solid transparent; border-radius: 2px;
  background: none; cursor: pointer; overflow: hidden; aspect-ratio: 3 / 2;
}
.lb-rail button img { width: 100%; height: 100%; object-fit: cover; display: block;
  opacity: 0.5; transition: opacity 0.2s var(--ease); }
.lb-rail button:hover img { opacity: 0.85; }
.lb-rail button.active { border-color: var(--accent); }
.lb-rail button.active img { opacity: 1; }

@media (max-width: 700px) {
  .lb-rail { display: none; }
  .lb-main { padding: 60px 8px 20px; }
  .lb-close { right: 10px; }
}

/* =========================================================================
   VIDEOS (QA P14): in galleries and the viewer
   A video tile is a photo tile with a round play mark and its length; until
   it has a cover it draws a film frame. In the viewer (always dark) the cover
   carries a big Play button, or the reason there is nothing to play.
   ========================================================================= */
.photo .play-mark {
  position: absolute; inset: 0; margin: auto; width: 56px; height: 56px; border-radius: 50%;
  display: grid; place-items: center; pointer-events: none;
  background: color-mix(in srgb, var(--bg) 85%, transparent); color: var(--ink);
}
.photo .play-mark .i { width: 24px; height: 24px; fill: currentColor; stroke: none; margin-left: 3px; }
.photo .video-length {
  position: absolute; left: 0; right: 0; bottom: 0; padding: 18px 10px 8px; pointer-events: none;
  font-size: 0.75rem; font-weight: 500; font-variant-numeric: tabular-nums; color: var(--on-photo);
  background: linear-gradient(to top, var(--photo-band), transparent);
}
/* (The starred grid's hover title owns the bottom edge: the length moves up.) */
.photo:has(.photo-label) .video-length {
  bottom: auto; top: 0; padding: 8px 10px 18px;
  background: linear-gradient(to bottom, var(--photo-band), transparent);
}
.photo-blank, .card-blank { width: 100%; height: 100%; display: grid; place-items: center; color: var(--ink-3); pointer-events: none; } /* marks never take the pointer: the tile's own button/link does */
.photo-blank .i, .card-blank .i { width: 32px; height: 32px; }
.photo-blank ~ .play-mark { top: auto; bottom: 34px; width: 44px; height: 44px; }
@media (max-width: 700px) { .photo .play-mark { width: 44px; height: 44px; } }

.lb-play {
  position: absolute; inset: 0; z-index: 1; margin: auto; width: 76px; height: 76px; padding: 0;
  display: grid; place-items: center; border: 0; border-radius: 50%; cursor: pointer;
  background: rgba(255,255,255,0.9); color: rgba(12,12,13,0.98); transition: transform 0.25s var(--ease);
}
.lb-play .i { width: 30px; height: 30px; fill: currentColor; stroke: none; margin-left: 4px; }
@media (hover: hover) { .lb-play:hover { transform: scale(1.06); } }
.lb-blank {
  width: min(100%, 640px); max-height: 100%; display: grid; place-items: center;
  background: rgba(255,255,255,0.06); color: rgba(255,255,255,0.62);
}
.lb-blank .i { width: 32px; height: 32px; }
.lb-stage video { width: 100%; height: 100%; object-fit: contain; } /* the whole picture, never cropped */
.lb-vnote {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 1; padding: 0 16px 8px;
  display: flex; flex-direction: column; align-items: center; gap: 10px; text-align: center;
}
.lb-vnote p { margin: 0; padding: 6px 12px; color: rgba(255,255,255,0.92); font-size: 0.9rem; background: rgba(0,0,0,0.6); }
.lb-vdl {
  display: inline-flex; align-items: center; gap: 8px; padding: 8px 14px; font-size: 0.875rem; color: #fff;
  background: rgba(0,0,0,0.6); border: 1px solid rgba(255,255,255,0.4); border-radius: 4px;
}
.lb-vdl svg { width: 18px; height: 18px; }
@media (hover: hover) { .lb-vdl:hover { background: rgba(255,255,255,0.22); } }
.lb-stage.video .lb-spinner { display: none; }
.lb-rail button { position: relative; }
.lb-rail-blank { width: 100%; height: 100%; display: grid; place-items: center; color: rgba(255,255,255,0.62); background: rgba(255,255,255,0.06); pointer-events: none; }
.lb-rail-blank .i { width: 22px; height: 22px; }
.lb-mini {
  position: absolute; right: 4px; bottom: 4px; width: 18px; height: 18px; border-radius: 50%;
  display: grid; place-items: center; background: rgba(255,255,255,0.9); color: rgba(12,12,13,0.98);
  pointer-events: none;
}
.lb-mini .i { width: 10px; height: 10px; fill: currentColor; stroke: none; margin-left: 1px; }
@media (max-width: 700px) { .lb-play { width: 64px; height: 64px; } }

/* =========================================================================
   FAVORITES (QA P2): an invited client's hearts, notes, and bar
   The heart is a real button on a round chip, so it reads on any photo and in
   any site color: outline = not a favorite, a filled heart in the site's
   accent, on a light chip, = a favorite. Always visible (phones have no hover). The bar sits at the
   bottom of the gallery page; the Send dialog is the shared `.dialog`.
   ========================================================================= */
.heart {
  position: relative; display: inline-grid; place-items: center; flex: none;
  width: 44px; height: 44px; padding: 0; border: 0; border-radius: 50%;
  background: color-mix(in srgb, var(--ink) 62%, transparent); color: var(--bg);
  cursor: pointer; transition: background 0.2s var(--ease);
}
.heart > .i { width: 22px; height: 22px; transition: fill 0.2s var(--ease); }
.heart[aria-pressed="true"] { background: var(--bg); color: var(--accent); }
.heart[aria-pressed="true"] > .i { fill: currentColor; }
.heart[aria-disabled="true"] { cursor: default; }
.heart-note { position: absolute; top: 5px; right: 5px; display: grid; place-items: center; }
.heart-note .i { width: 13px; height: 13px; }
@media (hover: hover) {
  .heart:not([aria-disabled="true"]):hover { background: color-mix(in srgb, var(--ink) 80%, transparent); }
  .heart[aria-pressed="true"]:not([aria-disabled="true"]):hover { background: var(--bg-2); }
}
.photo .heart { position: absolute; right: 6px; bottom: 6px; z-index: 2; }
/* The tile's download link (pointer devices) moves over to make room. */
.photo:has(.heart) .dl { right: 58px; }

.picks-bar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 80;
  display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px 24px;
  padding: 10px var(--gutter) calc(10px + env(safe-area-inset-bottom));
  background: var(--bg); border-top: 1px solid var(--line);
}
body.has-picks-bar { padding-bottom: 84px; }
.picks-text { min-width: 0; }
.picks-count, .picks-msg { margin: 0; }
.picks-count { color: var(--ink-2); }
.picks-count strong { color: var(--ink); font-weight: 500; }
.picks-msg { font-size: 0.85rem; color: var(--ink-2); }
.picks-msg:empty { display: none; }
.picks-msg.err { color: var(--danger); }
.picks-actions { display: flex; gap: 8px; }
.picks-bar .btn { height: 44px; padding: 0 18px; }
.picks-bar .btn[aria-pressed="true"] { background: var(--bg-2); border-color: var(--ink); }
@media (max-width: 600px) {
  .picks-bar { flex-direction: column; align-items: stretch; gap: 8px; }
  .picks-actions .btn { flex: 1; }
  body.has-picks-bar { padding-bottom: 130px; }
}

/* In the viewer: the heart beside the download, the file number beside the
   count, and the note under them (white tints like the rest of the viewer,
   which is dark in both themes). */
.lb-footer { flex-wrap: wrap; }
.lb-picks { display: contents; }
.lb-picks .heart[aria-pressed="true"] { background: var(--bg); border-color: var(--bg); color: var(--accent); }
.lb-picks .heart { background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.3); color: #fff; }
@media (hover: hover) { .lb-picks .heart:not([aria-disabled="true"], [aria-pressed="true"]):hover { background: rgba(255,255,255,0.22); } }
.lb-filenum { margin-left: 12px; color: rgba(255,255,255,0.72); }
.lb-note { flex-basis: 100%; display: flex; flex-direction: column; align-items: center; gap: 8px; text-align: center; }
.lb-note-text { margin: 0; max-width: 60ch; color: rgba(255,255,255,0.85); font-size: 0.9rem; white-space: pre-line; }
.lb-note-btn {
  display: inline-flex; align-items: center; gap: 7px; min-height: 44px; padding: 0 14px;
  border: 1px solid rgba(255,255,255,0.3); border-radius: 4px; background: none; color: #fff; font: inherit; font-size: 0.875rem; cursor: pointer;
}
.lb-note-btn .i { width: 16px; height: 16px; }
@media (hover: hover) { .lb-note-btn:hover { background: rgba(255,255,255,0.12); } }
.lb-note-actions { display: flex; gap: 8px; }
.lb-note textarea {
  width: min(560px, 100%); padding: 10px 12px; border: 1px solid rgba(255,255,255,0.3); border-radius: 4px;
  background: rgba(255,255,255,0.08); color: #fff; font: inherit; resize: vertical;
}
.lb-note textarea::placeholder { color: rgba(255,255,255,0.6); }

/* The Send dialog's own bits. */
.send-dialog p { margin: 0 0 10px; }
.send-dialog .btn { height: 44px; padding: 0 18px; }
.send-dialog label { display: block; margin-bottom: 6px; color: var(--ink-2); }
.send-dialog textarea {
  width: 100%; min-height: 96px; padding: 10px 12px; border: 1px solid var(--line); border-radius: 4px;
  background: var(--surface); color: var(--ink); font: inherit; resize: vertical;
}
.send-dialog .dialog-note { color: var(--danger); }

/* =========================================================================
   PAGE NOT FOUND (404.html; content from notfound.js)
   Masthead (logo or name), then the words on the left and the site's photo
   on the right, running off the right edge and sitting on the bottom. The
   photo multiplies onto the page, so a white background disappears into it.
   ========================================================================= */
.nf-page { min-height: 100vh; min-height: 100svh; display: flex; flex-direction: column; }
.nf-root { flex: 1; display: flex; flex-direction: column; }
.nf-mast { display: flex; justify-content: center; padding: clamp(28px, 5vh, 52px) var(--gutter) 0; }
.site-mark { font-family: var(--serif); font-size: clamp(1.4rem, 2.4vw, 2rem); font-weight: 400; letter-spacing: 0.06em; line-height: 1; }
.site-mark img { height: clamp(44px, 6vw, 72px); width: auto; }
.nf {
  flex: 1; display: grid; grid-template-columns: minmax(0, 1fr); align-items: center; column-gap: 24px;
  padding-block: clamp(40px, 8vh, 96px);
  padding-inline: clamp(20px, 7.5vw, 144px) var(--gutter);
}
.nf.has-photo { grid-template-columns: minmax(0, 5fr) minmax(0, 6fr); padding-right: 0; padding-bottom: 0; }
.nf-code { font-size: clamp(6.5rem, 15vw, 14rem); font-weight: 300; line-height: 0.8; letter-spacing: -0.03em; }
.nf-title { margin-top: 0.3em; font-size: clamp(2.25rem, 4.6vw, 4.25rem); font-weight: 400; line-height: 1.05; letter-spacing: -0.02em; }
.nf-line { margin-top: 1.4em; color: var(--ink-2); font-size: clamp(0.95rem, 1.3vw, 1.25rem); letter-spacing: 0.18em; }
.nf .cta { margin-top: clamp(32px, 5vh, 56px); }
.nf-photo { align-self: end; }
.nf-photo img {
  width: 100%; height: auto; max-height: 78vh; max-height: 78svh;
  object-fit: contain; object-position: right bottom; mix-blend-mode: multiply;
}
@media (max-width: 700px) {
  .nf, .nf.has-photo { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto 1fr; align-items: start; row-gap: 40px; padding-inline: var(--gutter); }
  .nf.has-photo { padding-right: 0; }
  .nf-text { padding-right: var(--gutter); }
}

/* =========================================================================
   CONTACT
   ========================================================================= */
.contact { padding-block: var(--space-section); }
[data-contact] { white-space: pre-line; } /* editable wording keeps its line breaks */
.contact-lead h2 {
  font-family: var(--serif); font-weight: 300;
  font-size: clamp(1.8rem, 4vw, 3rem); letter-spacing: -0.01em; margin-bottom: 16px;
}
.contact-lead p { color: var(--ink-2); max-width: 40ch; }
.contact-form { display: grid; gap: 16px; }
.field-row { display: grid; gap: 16px; grid-template-columns: 1fr 1fr; }
@media (max-width: 560px) { .field-row { grid-template-columns: 1fr; } }
.field { display: grid; gap: 6px; }
.field span {
  font-size: 0.7rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-3);
}
.contact-form input,
.contact-form select,
.contact-form textarea,
.lock-form input {
  width: 100%; padding: 12px 14px; border: 1px solid var(--line); border-radius: 6px;
  font: inherit; background-color: #fff; color: var(--ink);
}
.contact-form select { padding-right: 42px; }
.contact-form textarea { resize: vertical; min-height: 96px; }
.btn-primary {
  justify-self: start; padding: 13px 26px; border: none; border-radius: 6px;
  background: var(--ink); color: #fff; font-size: 0.82rem; letter-spacing: 0.06em;
  display: inline-flex; align-items: center; gap: 0.5em; transition: opacity 0.25s var(--ease);
}
.btn-primary:hover { opacity: 0.85; }
.btn-primary:disabled { opacity: 0.4; }
.form-note { font-size: 0.85rem; }
/* Contact honeypot: off-screen (not display:none, which some bots skip). */
.hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }
.form-note.ok { color: #2f7d4f; }
.form-note.err { color: #b23a2e; }

/* =========================================================================
   PASSWORD-PROTECTED GALLERY — lock screen (collection.js showLock)
   ========================================================================= */
.lock { padding-bottom: var(--space-section); }
.lock-form { display: grid; gap: 16px; max-width: 420px; }
.lock-form > p { color: var(--ink-2); }
.lock-form > p a { color: var(--ink); text-decoration: underline; }
/* Password field with a Show/Hide button inside it. */
.field .pw-field { /* undo the `.field span` label style */
  position: relative; display: block;
  font-size: inherit; letter-spacing: normal; text-transform: none; color: inherit;
}
.pw-field input { padding-right: 72px; }
.pw-toggle {
  position: absolute; right: 6px; top: 50%; transform: translateY(-50%);
  padding: 6px 10px; border: 0; background: none; cursor: pointer;
  font-size: 0.7rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-3);
}
.pw-toggle:hover { color: var(--ink); }

/* Invited-client galleries and /shortlist (QA P1) use the same screen:
   its link starts at the left, and the sign-in notes keep to its spacing. */
.lock .cta { justify-self: start; }
.lock-form > :is(.auth-hint, .auth-alt) { margin: 0; }
.lock-form > .auth-hint { color: var(--ink-3); }

/* ---- Signing in by email (signin.js: the admin's /login and clients'
   /shortlist; QA T3, P1): notes under the form, text-style buttons, and
   the code box (big, spaced digits, one width so they line up). */
.auth-alt { margin: 18px 0 0; font-size: 0.875rem; color: var(--ink-2); }
.auth-alt a { color: var(--ink-2); text-decoration: underline; }
.auth-msg { margin: 14px 0 0; color: var(--danger); min-height: 1.4em; }
.auth-msg:empty { margin: 0; }
.auth-note { margin: 14px 0 0; color: var(--ink-2); }
.auth-note:empty { display: none; }
.auth-hint { margin: 14px 0 0; color: var(--ink-3); font-size: 0.875rem; }
.auth-row { display: flex; flex-wrap: wrap; gap: 6px 18px; }
.auth-link { font: inherit; color: var(--ink-2); background: none; border: 0; padding: 0; text-decoration: underline; cursor: pointer; }
.auth-link:hover, .auth-alt a:hover { color: var(--ink); }
.auth-link:disabled { color: var(--ink-3); cursor: default; }
/* input[type=text] so it outranks admin.css `.app input[type=text]` (loaded
   later, `font: inherit`) and `.lock-form input`; on touch screens it also
   outranks the 16px floor above (24px clears it). */
:is(.app, .lock-form) input[type=text].auth-code { font-size: 1.5rem; letter-spacing: 0.2em; font-variant-numeric: tabular-nums; text-align: center; }
@media (pointer: coarse) {
  :is(.app, .lock-form) input[type=text].auth-code { font-size: max(16px, 1.5rem) !important; }
}

/* =========================================================================
   FOOTER
   ========================================================================= */
.site-foot {
  border-top: 1px solid var(--line);
  padding-block: 22px;
}
.foot-grid {
  display: flex; align-items: center; justify-content: space-between;
  gap: 18px; flex-wrap: wrap;
}
.foot-mark { font-family: var(--serif); font-size: 1.05rem; font-weight: 500; letter-spacing: 0.01em; }
.foot-mark .foot-logo { height: 26px; width: auto; display: block; }
.foot-links { display: flex; gap: 22px; font-size: 0.76rem; letter-spacing: 0.06em; text-transform: uppercase; }
.foot-fine { margin-top: 12px; color: var(--ink-3); font-size: 0.72rem; width: 100%; }

/* =========================================================================
   MOTION — reveal on scroll (progressive enhancement)
   ========================================================================= */
[data-reveal] {
  opacity: 0;
  transform: translateY(28px);
  transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease);
  transition-delay: var(--reveal-delay, 0s);
}
[data-reveal].in { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  * { animation: none !important; transition-duration: 0.01ms !important; }
  [data-reveal] { opacity: 1; transform: none; }
  html { scroll-behavior: auto; }
}

/* Preview of unpublished changes (ui.js showPreviewBar). */
.preview-bar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 90;
  display: flex; align-items: center; justify-content: space-between; gap: 12px 20px; flex-wrap: wrap;
  padding: 12px var(--gutter); background: var(--ink); color: #fff; font-size: 0.85rem;
}
.preview-actions { display: flex; gap: 8px; }
.preview-bar button {
  padding: 8px 14px; border-radius: 6px; border: 1px solid rgba(255, 255, 255, 0.5);
  background: none; color: #fff; font: inherit; cursor: pointer;
}
.preview-bar .preview-publish { background: #fff; color: var(--ink); border-color: #fff; }
.preview-bar button:hover { opacity: 0.85; }
.preview-bar button:disabled { opacity: 0.5; cursor: default; }
body.has-preview-bar { padding-bottom: 72px; }

/* Couldn't load the site data (ui.js showLoadError). Inner pages: clear the nav. */
.load-error .btn-primary { margin-top: 18px; }
main > .load-error { padding-top: calc(var(--nav-h) + 80px); }

/* Empty / loading states */
.state {
  padding: 80px 0; text-align: center; color: var(--ink-3);
  grid-column: 1 / -1;  /* span the full grid when used inside one */
}

/* Cover design picker (U41): picture choices. A group of real radios, each a
   label with a small drawing (aria-hidden) and its name under it. The
   drawing's contents are the page's own (admin.css draws the cover designs). */
.pick { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px 8px; min-width: 0; }
.pick label { position: relative; display: grid; gap: 5px; margin: 0; cursor: pointer; }
.pick input { position: absolute; opacity: 0; width: 1px; height: 1px; pointer-events: none; }
.pick .art { position: relative; display: block; aspect-ratio: 4 / 3; overflow: hidden; border-radius: 4px; background: var(--surface); box-shadow: 0 0 0 1px var(--line); transition: box-shadow 0.15s var(--ease); }
.pick .name { color: var(--ink-2); font-size: 0.8125rem; text-align: center; }
.pick input:checked + .art { box-shadow: 0 0 0 2px var(--accent); }
.pick input:checked ~ .name { color: var(--ink); font-weight: 500; }
.pick input:focus-visible + .art { outline: 2px solid var(--accent); outline-offset: 3px; }
@media (hover: hover) {
  .pick label:hover .art { box-shadow: 0 0 0 1px var(--ink-3); }
  .pick label:hover input:checked + .art { box-shadow: 0 0 0 2px var(--accent); }
}
.pick.lg { grid-template-columns: repeat(3, minmax(0, 150px)); gap: 14px 12px; }

/* Proofs and Finals (P4), the client's page: two sections ("Your photos",
   then "Proofs"), each a heading with a quiet line under it
   (.gallery-note) and, for the finals, Download all. .gallery-note is the
   quiet line under a section heading: a clock and the words in --ink-3;
   .warn (the 14-day notices and the daily-limit line) puts --ink on the
   --bg-2 wash with the clock in --danger, so it reads as a notice on any
   site color without a colored border. Tokens only: light and dark follow. */
.set-sec { padding: 0; }
.set-head { display: flex; align-items: flex-end; justify-content: space-between; flex-wrap: wrap; gap: 14px 24px; padding: 0 var(--gutter) 16px; }
.set-sec > .set-head:last-child { padding-bottom: var(--space-section); } /* a section with no grid ends the page */
.set-head h2 { font: 400 clamp(1.5rem, 3vw, 2rem)/1.15 var(--serif); letter-spacing: -0.01em; }
.gallery-note { display: flex; align-items: flex-start; gap: 8px; margin: 8px 0 0; color: var(--ink-3); font-size: 0.9rem; line-height: 1.45; }
.gallery-note .i { flex: none; width: 16px; height: 16px; margin-top: 2px; }
.gallery-note.warn { width: fit-content; max-width: 60ch; padding: 9px 12px; border-radius: 4px; background: var(--bg-2); color: var(--ink); }
.gallery-note.warn .i { color: var(--danger); }
.gallery-note a { color: inherit; text-decoration: underline; text-underline-offset: 3px; }
.dl-all { position: relative; display: grid; justify-items: end; gap: 8px; }
.dl-all .btn { height: 44px; padding: 0 18px; text-decoration: none; }
.dl-all .menu { position: absolute; top: 50px; right: 0; z-index: 4; width: 280px; }
.dl-all .gallery-note { margin-top: 0; }
.dl-all .gallery-note.warn { max-width: 34ch; }
@media (max-width: 600px) {
  .set-head { display: grid; justify-content: stretch; }
  .dl-all { justify-items: stretch; }
  .dl-all .btn { width: 100%; }
  .dl-all .menu { left: 0; right: 0; width: auto; }
  .dl-all .gallery-note.warn { max-width: none; }
}
