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

/* ---- Design tokens ---------------------------------------------------- */
:root {
  /* Brand colors, from manifest.site.theme (set at runtime; QA T7, U59).
     --brand-primary/-secondary/-accent are the photographer's colors as
     stored: --accent (focus rings, field borders, the quote's rule, hearts,
     checks) is the primary, which the page's head replaces with the same color
     lifted to 3:1 on its page when it falls short (palette.js accentVars).
     The "Use your colors" roles (QA U59) are separate
     variables, written only while that switch is on (html[data-palette]),
     each already checked for contrast by palette.js: --palette-main /
     --palette-on-main (filled buttons), --palette-hover / --palette-on-hover
     (a button under the pointer), --palette-line (link lines and arrows, the
     solid menu bar's line), --palette-accent (the tinted band, step 5).
     Words, headings, photos, the footer, and the menu's background are never
     colored. 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 */
  --bg-veil:     rgba(250, 249, 247, 0.9); /* the scrolled nav bar: the page color, a little see-through (QA U42) */
  --on-ink:      #fff;      /* text on an --ink fill (the primary button) */
  --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) */
  --frame-w: 15px;    /* Frame: the even border around each tile (QA U63); a plain length, gallery.js reads it back */
  --space-section: clamp(56px, 9vh, 120px);
  --space-heading: clamp(20px, 2.6vw, 36px); /* a section's heading to its photos (QA U56) */
  --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;
    --bg-veil: rgba(21, 21, 21, 0.9); --on-ink: #151515;
    --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;
  --bg-veil: rgba(21, 21, 21, 0.9); --on-ink: #151515;
  --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: var(--bg-veil);
  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; }
/* On a dark page (a dark look) the solid bar and the footer sit on dark, where
   the default (dark) logo vanishes: show the light logo, else a white copy of
   the default (QA U42). */
:root[data-theme="dark"] .nav.has-logo.has-logo-light.solid .wordmark-img--default { display: none; }
:root[data-theme="dark"] .nav.has-logo.has-logo-light.solid .wordmark-img--light { display: block; }
:root[data-theme="dark"] .nav.has-logo:not(.has-logo-light).solid .wordmark-img--default { filter: brightness(0) invert(1); }
/* 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); }
/* "Use your colors" (QA U59): on the solid bar only (over a photo the bar
   blends and would invert the color), the line is the Main color. */
html[data-palette] .nav.solid .nav-links a::after { background: var(--palette-line); }

/* 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; }

/* Popover: a tip that can hold a link, so it stays open while the pointer is
   on it (the .tip bubble can't). Sits under its .pop-wrap; .danger = a reason
   something is off. Admin only, wired by hand (collections.js, QA U55). */
.pop-wrap { position: relative; }
.pop {
  position: absolute; left: 0; right: 0; top: calc(100% + 4px); z-index: 20; padding: 6px 9px; border-radius: 4px;
  background: var(--surface); border: 1px solid var(--line); color: var(--ink-2); font: 400 0.75rem/1.4 var(--sans);
  box-shadow: var(--shadow);
}
.pop.danger { color: var(--danger); border-color: var(--danger); }
.pop a { color: inherit; text-decoration: underline; }
.pop a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 2px; }

/* 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;
}
/* An icon-only choice (the Opening's text alignment): the icon centered, the same height as a word. */
.seg span:has(> .i:only-child) { display: grid; place-items: center; min-height: calc(1lh + 10px); }
.seg label:hover span { color: var(--ink); }
.seg input[aria-disabled="true"] + span { color: var(--ink-3); opacity: 0.55; }
.seg label:has(input[aria-disabled="true"]) { cursor: default; }
.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); }
/* A red line under a disabled item that says why, with a link (QA U55). */
.menu .menu-note { margin: -6px 0 2px; padding: 0 10px 8px; font-size: 0.75rem; line-height: 1.4; }
.menu .menu-note.danger { color: var(--danger); }
.menu .menu-note a, .menu .menu-note a:hover { display: inline; width: auto; padding: 0; background: none; color: inherit; font: inherit; text-decoration: underline; }
.menu .menu-note a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 2px; }
.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 */
}
/* Multi-line boxes grow with their content (QA U58) up to 60% of the window,
   then scroll. Each page keeps its own min-height as the starting size.
   Browsers without field-sizing get the same from autogrow.js. */
textarea { field-sizing: content; max-height: 60vh; resize: none; }
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);
}
/* Its arrow is the icon (icon("arrow-right")), about the size of the words. */
.link-underline .i { width: 1.2em; height: 1.2em; stroke-width: 1.5; transition: transform 0.4s var(--ease); }
@media (hover: hover) {
  .link-underline:hover { gap: 1.1em; }
  .link-underline:hover .i { transform: translateX(4px); }
}
/* "Use your colors" (QA U59): the line and its arrow take the Main color; the words stay put. */
html[data-palette] .link-underline { border-bottom-color: var(--palette-line); }
html[data-palette] .link-underline .i { color: var(--palette-line); }

/* =========================================================================
   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; }
/* Under the headshot (QA U52): the contact link, then the social links, quiet
   like the footer's. On tokens, so it reads on dark pages too. */
.about-links { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 22px; margin-top: 22px; font-size: 0.9rem; }
.about-body .about-links { margin-top: 32px; }
.about-contact { color: var(--ink); text-decoration: underline; text-underline-offset: 4px; text-decoration-color: var(--accent); }
.about-social { display: flex; flex-wrap: wrap; gap: 6px 18px; }
.about-social a { color: var(--ink-2); }
@media (hover: hover) {
  .about-contact:hover, .about-social a:hover { color: var(--accent); }
}
.about-body p + p { margin-top: 1.1em; }
@media (max-width: 700px) {
  .about.has-photo { grid-template-columns: 1fr; gap: 28px; }
}

/* The home page's sections (QA U56, homesections.js). With no Opening the
   menu bar shows from the top, so the first section starts below it. */
.home-sections:not(:has(.home-intro)) { padding-top: var(--nav-h); }

/* 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;
}

/* The Opening with photos (QA U56): Full photo, Photo and text, Side by side.
   Like a collection's banner, its photos crop around their focus point
   (--focus, set per photo by homesections.js); with no photo it's the
   masthead above. */
.home-open { margin-bottom: var(--space-section); }
.home-open-photo { display: block; width: 100%; object-fit: cover; object-position: var(--focus, 50% 50%); background: var(--photo-bg); }
.home-open-action { margin-top: 28px; }

/* Full photo: the name bottom left, on the photo shade (the Outline cover's
   rule, so white text stays AA on any photo; tests/cover.test.js). */
.home-open[data-layout="full"] { position: relative; height: 92svh; min-height: 520px; overflow: hidden; }
.home-open[data-layout="full"] .home-open-photo { position: absolute; inset: 0; height: 100%; }
.home-open[data-layout="full"] .home-intro {
  position: absolute; z-index: 2; left: var(--gutter); bottom: var(--gutter);
  width: max-content; max-width: min(calc(100% - 2 * var(--gutter)), 900px);
  padding: clamp(18px, 3vw, 32px) clamp(18px, 3.4vw, 40px);
  background: var(--photo-shade); color: var(--on-photo);
}
.home-open[data-layout="full"] :is(.home-title, .home-sub) { color: var(--on-photo); overflow-wrap: anywhere; }

/* Photo and text: the photo on the left, the words beside it. */
.home-open[data-layout="split"] {
  display: grid; grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); align-items: center;
  gap: clamp(24px, 5vw, 80px); padding-right: var(--gutter);
}
.home-open[data-layout="split"] .home-open-photo { height: 88svh; min-height: 480px; }
.home-open[data-layout="split"] .home-intro { padding: 0; }

/* Side by side: the name above the photos, edge to edge. */
.home-open-photos { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); gap: var(--gap); }
.home-open[data-layout="trio"] .home-open-photo { height: auto; aspect-ratio: 4 / 5; max-height: 82svh; }
@media (min-width: 701px) { .home-open-photos { margin-inline: calc(var(--gutter) / 2); } }

@media (max-width: 700px) {
  .home-open[data-layout="split"] { grid-template-columns: 1fr; padding-right: 0; gap: 0; }
  .home-open[data-layout="split"] .home-open-photo { height: auto; min-height: 0; aspect-ratio: 4 / 5; }
  .home-open[data-layout="split"] .home-intro { padding: 32px var(--gutter) 0; }
  /* Phones show the first photo; the others are lazy, so they're never fetched. */
  .home-open-photos { grid-auto-flow: row; }
  .home-open-photos .home-open-photo:not(:first-child) { display: none; }
}

/* Text alignment (site.homeSections align; left is the default), set on the
   section so the title, logo, subheading, and button move together; the sub's box (max 56ch) and the
   logo are pushed with auto margins. */
.home-sec[data-align="center"] { text-align: center; }
.home-sec[data-align="right"] { text-align: right; }
.home-sec[data-align="center"] :is(.home-sub, .home-logo) { margin-inline: auto; }
.home-sec[data-align="right"] :is(.home-sub, .home-logo) { margin-inline-start: auto; }
.home-sec[data-align="center"] .home-logo { object-position: center; }
.home-sec[data-align="right"] .home-logo { object-position: right center; }
/* Full photo: the block sits along the bottom at the chosen side. */
.home-open[data-layout="full"][data-align="center"] .home-intro { left: 50%; transform: translateX(-50%); }
.home-open[data-layout="full"][data-align="right"] .home-intro { left: auto; right: var(--gutter); }

.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; } }
@media (max-width: 700px) { :root { --frame-w: 10px; } }
/* The home page's tiles have no frame on phones (owner, QA U63): its width
   is 0 there, so the layout math (gallery.js reads it back) counts none too.
   Collections and category pages keep theirs. */
@media (max-width: 700px) { [data-frame="home"] { --frame-w: 0px; } }
/* Frame on Static tiles (QA U63; home.js sets `data-frame` and `--frame-color`
   on the grid): drawn over the tile's edge, inside the 4:5 crop, so the
   tiles keep their size and the hover zoom stays under it. */
.collection-grid[data-frame] .card-media::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  box-shadow: inset 0 0 0 var(--frame-w) var(--frame-color);
}
.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%; }
.home-open[data-layout="full"]::before,
.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));
}
/* Frame (QA U63): padding in the color around each photo, which keeps its own
   shape (gallery.js counts the padding in the layout, so nothing is cropped
   and the frame is the same width on every side). The tile's shape is the
   photo's `--ar`, not the tile's own. A tile zooming on hover would cover its
   frame, so a framed tile doesn't zoom. */
.gallery[data-frame] :is(.photo, .card-media) { padding: var(--frame-w); background: var(--frame-color); }
.gallery[data-frame] :is(.photo > img, .photo > .photo-blank, .card-media > img, .card-media > .card-blank) { height: auto; aspect-ratio: var(--ar); }
@media (hover: hover) { .gallery[data-frame] .collection-card:hover .card-media img { transform: none; } }
.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);
}
/* The tile's small round controls (QA P16): the heart, this download, and the
   select-mode check are all 26px circles; the heart and download at the top
   left (the heart first), the check at the bottom right. An invisible ::before
   grows each one's hit area to 44px without growing its look. */
.photo .dl {
  position: absolute; left: 10px; top: 10px;
  width: 26px; height: 26px; display: grid; place-items: center;
  background: color-mix(in srgb, var(--surface) 92%, transparent); color: var(--ink);
  border-radius: 50%; opacity: 0; transform: translateY(-4px);
  transition: opacity 0.3s var(--ease), transform 0.3s var(--ease);
  backdrop-filter: blur(6px);
}
.photo .dl svg { width: 16px; height: 16px; }
.photo .dl { z-index: 2; cursor: pointer; }
.photo .dl::before, .photo .heart::before { content: ""; position: absolute; inset: -9px; }
/* 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: var(--surface); }
  .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; left: 10px; top: 10px; z-index: 3; width: 26px; height: 26px; }
.photo .heart > .i { width: 16px; height: 16px; }
.photo .heart-note { top: -4px; right: -4px; width: 14px; height: 14px; border-radius: 50%; background: var(--bg); color: var(--ink); }
.photo .heart-note .i { width: 9px; height: 9px; }
/* The tile's download link (pointer devices) sits to the heart's right. */
.photo:has(.heart) .dl { left: 48px; }

/* The page's bottom action bar, shared (QA P16): the favorites bar is
   class="page-bar picks-bar", the selection bar class="page-bar select-bar".
   Only one is on screen at a time (selecting hides the favorites bar). */
.page-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);
}
.page-bar[hidden] { display: none; }
.page-bar .btn { height: 44px; padding: 0 18px; }
/* Quiet words instead of boxes (select mode, QA P16): .link-btn is an underlined
   entry point beside a Download all; .text-btn is a bar's Choose or Cancel;
   .hit grows a smaller button's touch target to 44px with an invisible ::before. */
.link-btn {
  display: inline-flex; align-items: center; min-height: 44px; padding: 0 4px; border: 0; background: none;
  color: var(--ink-2); font: inherit; font-size: 0.9rem; text-decoration: underline; text-underline-offset: 4px;
  text-decoration-color: var(--line); cursor: pointer;
}
.text-btn {
  display: inline-flex; align-items: center; gap: 4px; height: 44px; padding: 0 8px; border: 0; background: none;
  color: var(--ink-2); font: inherit; font-size: 0.875rem; cursor: pointer;
}
.text-btn .i { width: 14px; height: 14px; }
@media (hover: hover) {
  .link-btn:hover { color: var(--ink); text-decoration-color: var(--ink-3); }
  .text-btn:hover { color: var(--ink); }
}
.hit { position: relative; }
.hit::before { content: ""; position: absolute; left: 0; right: 0; top: 50%; height: 44px; min-width: 44px; transform: translateY(-50%); }
.bar-text { min-width: 0; }
.bar-text p { margin: 0; color: var(--ink-2); }
.bar-text strong { color: var(--ink); font-weight: 500; }
.bar-msg { font-size: 0.85rem; }
.bar-msg:empty { display: none; }
.bar-msg.err { color: var(--danger); }
.bar-actions { position: relative; display: flex; gap: 8px; }
/* The selection bar floats over the photos, centered, with the design system's
   corner radius (.btn's); the Choose menu opens above it. */
.page-bar.select-bar {
  left: 50%; right: auto; bottom: calc(16px + env(safe-area-inset-bottom)); transform: translateX(-50%);
  width: max-content; max-width: calc(100% - 24px); flex-direction: row; flex-wrap: nowrap; justify-content: flex-start; gap: 4px;
  padding: 4px; background: var(--surface); border: 1px solid var(--line); border-radius: 4px; box-shadow: var(--shadow);
}
.select-bar .bar-text { padding: 0 6px; }
.select-bar .bar-text p { line-height: 1.3; }
.select-bar [data-line] { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 8px; white-space: nowrap; }
.select-bar [data-line] small { color: var(--ink-3); font-size: 0.85rem; }
.select-bar .bar-actions { position: static; align-items: center; gap: 2px; }
.page-bar.select-bar .btn.small { height: 30px; padding: 0 12px; margin-left: 4px; }
.select-bar .menu { position: absolute; right: 0; bottom: calc(100% + 8px); width: 260px; }
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[aria-pressed="true"] { background: var(--bg-2); border-color: var(--ink); }
@media (max-width: 600px) {
  .page-bar { flex-direction: column; align-items: stretch; gap: 8px; }
  .picks-actions .btn, .bar-actions .btn { flex: 1; }
  .page-bar.select-bar { flex-direction: row; align-items: center; gap: 0; }
  .select-bar .bar-text { padding: 0 4px; }
  .select-bar [data-line] { flex-direction: column; gap: 0; }
  .select-bar .text-btn { padding: 0 6px; }
  .page-bar.select-bar .btn.small { flex: none; padding: 0 10px; }
  .select-bar .btn-word { display: none; } /* the icons fit; the words don't (aria-labels keep the names) */
  .select-bar .text-btn[data-cancel] { min-width: 44px; justify-content: center; }
  .select-bar [data-cancel] .i { width: 20px; height: 20px; }
  body.has-picks-bar { padding-bottom: 130px; }
}

/* Select mode (QA P16): the grid's host carries data-selecting. The tile's own
   button is the toggle (aria-pressed); the hover download steps aside and the
   heart stays (a proof can be both); a small check dot, the heart's size, sits
   at the bottom right: an empty ring until the photo is chosen, then filled with the accent. */
.pick-check {
  position: absolute; right: 10px; bottom: 10px; z-index: 2; display: grid; place-items: center;
  width: 26px; height: 26px; border-radius: 50%; pointer-events: none;
  background: color-mix(in srgb, var(--ink) 30%, transparent); color: var(--on-accent, var(--bg));
  box-shadow: inset 0 0 0 2px var(--bg);
  transition: background 0.2s var(--ease);
}
.pick-check[hidden] { display: none; }
.pick-check .i { width: 16px; height: 16px; opacity: 0; transition: opacity 0.15s var(--ease); }
[data-selecting] .photo .dl { display: none; }
[data-selecting] .photo:has(.photo-open[aria-pressed="true"]) .pick-check { background: var(--accent); }
[data-selecting] .photo:has(.photo-open[aria-pressed="true"]) .pick-check .i { opacity: 1; stroke-width: 2.5; }
[data-selecting] .photo:has(.photo-open[aria-pressed="true"])::after {
  content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  box-shadow: inset 0 0 0 2px var(--accent);
}
@media (hover: hover) { [data-selecting] .photo:hover img { opacity: 1; } }
/* A tile that can't be chosen (a video proof) stays as it is, quieter. */
[data-selecting] .photo:has(.photo-open[aria-disabled="true"]) img { opacity: 0.45; }

/* 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: none; min-height: 84px;
}
.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: none;
}
.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 { flex: 1; display: flex; flex-direction: column; } /* fills the screen above the footer (body is a 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, .home-sec-heading {
  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; }
/* Home "Text and a button" sections (QA U56 step 5): the contact block's look
   for any words; Centered centers the block, the quote, and the link, and
   Right sets them against the right edge. */
.home-text { padding-block: var(--space-section); }
.home-text-body { white-space: pre-line; } /* keeps the line breaks that were typed */
.home-quote { margin: 28px 0 0; max-width: 40ch; }
.home-quote blockquote { margin: 0; padding-inline-start: 20px; border-inline-start: 1px solid var(--accent); }
.home-quote p { font-family: var(--serif); font-weight: 300; font-size: clamp(1.15rem, 2.2vw, 1.5rem); line-height: 1.4; color: var(--ink); white-space: pre-line; }
.home-quote figcaption { margin: 10px 0 0; padding-inline-start: 20px; color: var(--ink-3); font-size: 0.9rem; }
.home-text-action { margin-top: 22px; }
/* A Tinted section (QA U59 step 5): a band in the Accent color (--palette-accent,
   already checked for words and link lines) running edge to edge behind the
   section, whose own padding gives it room. The shadow and the clip draw the
   band past the column without adding width, so nothing scrolls sideways.
   Muted words use --ink-2, the shade the band was checked against. */
.home-sec[data-tint] { background: var(--palette-accent); box-shadow: 0 0 0 100vmax var(--palette-accent); clip-path: inset(0 -100vmax); }
.home-sec[data-tint] :is(.home-quote figcaption, .contact-lead p) { color: var(--ink-2); }
/* A Galleries or Featured photos heading (owner, 2026-10-22): the Text
   section's heading size, room before the photos, and lined up with the
   photos' left edge (--edge: their side gutter). The section before it
   already ends with --space-section. Phones run Static tiles edge to edge, so
   there it lines up with the tiles' titles instead. */
.home-sec-heading { --edge: calc(var(--gutter) / 2); margin: 0 var(--edge) var(--space-heading); }
.home-sec:has(> [data-spacing="large"]) > .home-sec-heading { --edge: max(calc(var(--gutter) / 2), var(--gap-large)); }
@media (max-width: 700px) {
  .home-sec:has(> .collection-grid:not([data-spacing="large"])) > .home-sec-heading { --edge: var(--gutter); }
}
/* Featured photos (QA U56 step 6). Row: one even row, each photo at its own
   shape, never cropped. Each grows by its shape (--r, width / height) from a
   zero start, so all are the same height with no measuring; the pairs vanish
   on wide screens and make the two-across rows on phones. Gallery uses the
   galleries' engine (.collection-grid, .gallery). */
/* The same side gutter as a gallery's at every width (owner, 2026-10-22). */
.featured-row { display: flex; gap: var(--gap); margin-inline: calc(var(--gutter) / 2); padding-bottom: var(--space-section); }
.featured-pair { display: contents; }
.featured-row .photo { flex: var(--r) 1 0px; aspect-ratio: var(--r); }
@media (max-width: 700px) {
  .featured-row { display: block; }
  .featured-pair { display: flex; gap: var(--gap); margin-bottom: var(--gap); }
  .featured-pair .photo:only-child { flex: 0 0 calc(50% - var(--gap) / 2); }
}
.home-text[data-layout="center"] { text-align: center; }
.home-text[data-layout="center"] :is(.contact-lead p, .home-quote) { margin-inline: auto; }
.home-text[data-layout="center"] .home-quote blockquote { padding-inline-start: 0; border-inline-start: 0; }
.home-text[data-layout="center"] .home-quote figcaption { padding-inline-start: 0; }
/* Right: the same blocks pushed to the right edge, the quote's rule on its right. */
.home-text[data-layout="right"] { text-align: right; }
.home-text[data-layout="right"] :is(.contact-lead p, .home-quote) { margin-inline-start: auto; }
.home-text[data-layout="right"] .home-quote blockquote { padding-inline: 0 20px; border-inline-start: 0; border-inline-end: 1px solid var(--accent); }
.home-text[data-layout="right"] .home-quote figcaption { padding-inline: 0 20px; }
/* Home About section (QA U56 step 7): the About page's headshot (never
   cropped) beside its title, first paragraph, and link; Stacked puts the photo
   above centered words. Phones stack with the photo first at a modest size. */
.home-about { display: grid; grid-template-columns: minmax(0, 4fr) minmax(0, 6fr); gap: clamp(28px, 5vw, 72px); align-items: center; padding-block: var(--space-section); }
.home-about:not(:has(.home-about-photo)) { grid-template-columns: minmax(0, 1fr); }
.home-about-photo img { display: block; width: 100%; height: auto; }
.home-about-text p { max-width: 52ch; }
.home-about[data-layout="stacked"] { grid-template-columns: minmax(0, 1fr); justify-items: center; text-align: center; }
.home-about[data-layout="stacked"] .home-about-photo { width: min(100%, 320px); }
.home-about[data-layout="stacked"] .home-about-text p { margin-inline: auto; }
@media (max-width: 700px) {
  .home-about { grid-template-columns: minmax(0, 1fr); gap: 24px; }
  .home-about-photo { width: min(100%, 280px); }
}
.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: var(--surface); color: var(--ink);
}
.contact-form select { padding-right: 42px; }
.contact-form textarea { min-height: 96px; }
.btn-primary {
  justify-self: start; padding: 13px 26px; border: none; border-radius: 6px;
  background: var(--ink); color: var(--on-ink); 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; }
/* Its arrow is the icon (icon("arrow-right")), about the size of the old text one. */
.btn-primary .i { width: 1.2em; height: 1.2em; stroke-width: 1.5; }
.btn-primary:disabled { opacity: 0.4; }
/* "Use your colors" (QA U59): the button wears the Main color, its words the
   computed text color, and the Hover color under the pointer. Off, none of
   this matches (no html[data-palette]). */
html[data-palette] .btn-primary { background: var(--palette-main); color: var(--palette-on-main); }
@media (hover: hover) {
  html[data-palette] .btn-primary:hover:not(:disabled) { background: var(--palette-hover); color: var(--palette-on-hover); opacity: 1; }
}
.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: var(--ok); }
.form-note.err { color: var(--danger); }

/* =========================================================================
   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; }
.page-header .auth-hint { margin-top: 8px; color: var(--ink-3); }
.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
   ========================================================================= */
/* Short pages (sign-in, 404, a small gallery) rest the footer at the bottom
   of the screen (QA B48): public pages are a column at least a screen tall. */
body:has(> .site-foot) { min-height: 100vh; min-height: 100dvh; display: flex; flex-direction: column; }
.site-foot {
  margin-top: auto;
  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-mark .foot-logo--light { display: none; } /* shown on dark pages only, below */
:root[data-theme="dark"] .foot-mark.has-logo-light .foot-logo--default { display: none; }
:root[data-theme="dark"] .foot-mark.has-logo-light .foot-logo--light { display: block; }
:root[data-theme="dark"] .foot-mark:not(.has-logo-light) .foot-logo { filter: brightness(0) invert(1); }
.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; }
/* The first Download all with Select beside it (select mode, QA P16), and the line that replaces them while choosing. */
.dl-row { display: grid; grid-auto-flow: column; gap: 12px; align-items: center; }
.dl-all .sel-hint { margin: 0; color: var(--ink-2); font-size: 0.9rem; min-height: 44px; display: flex; align-items: center; }
.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-row { grid-template-columns: 1fr auto; }
  .dl-row > .btn:first-child { width: 100%; }
  .dl-all .menu { left: 0; right: 0; width: auto; }
  .dl-all .gallery-note.warn { max-width: none; }
}
